diff --git a/.all-contributorsrc b/.all-contributorsrc index f57081a2f..a8e7fafee 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -628,6 +628,69 @@ "contributions": [ "bug" ] + }, + { + "login": "ZakaryaCH", + "name": "ZakaryaCH", + "avatar_url": "https://avatars.githubusercontent.com/u/45012800?v=4", + "profile": "https://github.com/ZakaryaCH", + "contributions": [ + "bug" + ] + }, + { + "login": "jboeijenga", + "name": "Jasper Boeijenga", + "avatar_url": "https://avatars.githubusercontent.com/u/1516438?v=4", + "profile": "https://github.com/jboeijenga", + "contributions": [ + "code" + ] + }, + { + "login": "mrfelton", + "name": "Tom Kirkpatrick", + "avatar_url": "https://avatars.githubusercontent.com/u/200251?v=4", + "profile": "https://kirkdesigns.co.uk", + "contributions": [ + "code" + ] + }, + { + "login": "Machul84126", + "name": "Machul", + "avatar_url": "https://avatars.githubusercontent.com/u/128227109?v=4", + "profile": "https://github.com/Machul84126", + "contributions": [ + "bug" + ] + }, + { + "login": "ugostack", + "name": "ugostack", + "avatar_url": "https://avatars.githubusercontent.com/u/81093728?v=4", + "profile": "https://github.com/ugostack", + "contributions": [ + "code" + ] + }, + { + "login": "jdebarochez", + "name": "Jean de Barochez", + "avatar_url": "https://avatars.githubusercontent.com/u/3436890?v=4", + "profile": "https://github.com/jdebarochez", + "contributions": [ + "code" + ] + }, + { + "login": "cog06", + "name": "cog06", + "avatar_url": "https://avatars.githubusercontent.com/u/44521955?v=4", + "profile": "https://github.com/cog06", + "contributions": [ + "bug" + ] } ], "contributorsPerLine": 7, diff --git a/.astro/content-assets.mjs b/.astro/content-assets.mjs deleted file mode 100644 index 2b8b8234b..000000000 --- a/.astro/content-assets.mjs +++ /dev/null @@ -1 +0,0 @@ -export default new Map(); \ No newline at end of file diff --git a/.astro/content-modules.mjs b/.astro/content-modules.mjs deleted file mode 100644 index 2b8b8234b..000000000 --- a/.astro/content-modules.mjs +++ /dev/null @@ -1 +0,0 @@ -export default new Map(); \ No newline at end of file diff --git a/.astro/content.d.ts b/.astro/content.d.ts deleted file mode 100644 index c0082cc81..000000000 --- a/.astro/content.d.ts +++ /dev/null @@ -1,199 +0,0 @@ -declare module 'astro:content' { - export interface RenderResult { - Content: import('astro/runtime/server/index.js').AstroComponentFactory; - headings: import('astro').MarkdownHeading[]; - remarkPluginFrontmatter: Record; - } - interface Render { - '.md': Promise; - } - - export interface RenderedContent { - html: string; - metadata?: { - imagePaths: Array; - [key: string]: unknown; - }; - } -} - -declare module 'astro:content' { - type Flatten = T extends { [K: string]: infer U } ? U : never; - - export type CollectionKey = keyof AnyEntryMap; - export type CollectionEntry = Flatten; - - export type ContentCollectionKey = keyof ContentEntryMap; - export type DataCollectionKey = keyof DataEntryMap; - - type AllValuesOf = T extends any ? T[keyof T] : never; - type ValidContentEntrySlug = AllValuesOf< - ContentEntryMap[C] - >['slug']; - - export type ReferenceDataEntry< - C extends CollectionKey, - E extends keyof DataEntryMap[C] = string, - > = { - collection: C; - id: E; - }; - export type ReferenceContentEntry< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}) = string, - > = { - collection: C; - slug: E; - }; - export type ReferenceLiveEntry = { - collection: C; - id: string; - }; - - /** @deprecated Use `getEntry` instead. */ - export function getEntryBySlug< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}), - >( - collection: C, - // Note that this has to accept a regular string too, for SSR - entrySlug: E, - ): E extends ValidContentEntrySlug - ? Promise> - : Promise | undefined>; - - /** @deprecated Use `getEntry` instead. */ - export function getDataEntryById( - collection: C, - entryId: E, - ): Promise>; - - export function getCollection>( - collection: C, - filter?: (entry: CollectionEntry) => entry is E, - ): Promise; - export function getCollection( - collection: C, - filter?: (entry: CollectionEntry) => unknown, - ): Promise[]>; - - export function getLiveCollection( - collection: C, - filter?: LiveLoaderCollectionFilterType, - ): Promise< - import('astro').LiveDataCollectionResult, LiveLoaderErrorType> - >; - - export function getEntry< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}), - >( - entry: ReferenceContentEntry, - ): E extends ValidContentEntrySlug - ? Promise> - : Promise | undefined>; - export function getEntry< - C extends keyof DataEntryMap, - E extends keyof DataEntryMap[C] | (string & {}), - >( - entry: ReferenceDataEntry, - ): E extends keyof DataEntryMap[C] - ? Promise - : Promise | undefined>; - export function getEntry< - C extends keyof ContentEntryMap, - E extends ValidContentEntrySlug | (string & {}), - >( - collection: C, - slug: E, - ): E extends ValidContentEntrySlug - ? Promise> - : Promise | undefined>; - export function getEntry< - C extends keyof DataEntryMap, - E extends keyof DataEntryMap[C] | (string & {}), - >( - collection: C, - id: E, - ): E extends keyof DataEntryMap[C] - ? string extends keyof DataEntryMap[C] - ? Promise | undefined - : Promise - : Promise | undefined>; - export function getLiveEntry( - collection: C, - filter: string | LiveLoaderEntryFilterType, - ): Promise, LiveLoaderErrorType>>; - - /** Resolve an array of entry references from the same collection */ - export function getEntries( - entries: ReferenceContentEntry>[], - ): Promise[]>; - export function getEntries( - entries: ReferenceDataEntry[], - ): Promise[]>; - - export function render( - entry: AnyEntryMap[C][string], - ): Promise; - - export function reference( - collection: C, - ): import('astro/zod').ZodEffects< - import('astro/zod').ZodString, - C extends keyof ContentEntryMap - ? ReferenceContentEntry> - : ReferenceDataEntry - >; - // Allow generic `string` to avoid excessive type errors in the config - // if `dev` is not running to update as you edit. - // Invalid collection names will be caught at build time. - export function reference( - collection: C, - ): import('astro/zod').ZodEffects; - - type ReturnTypeOrOriginal = T extends (...args: any[]) => infer R ? R : T; - type InferEntrySchema = import('astro/zod').infer< - ReturnTypeOrOriginal['schema']> - >; - - type ContentEntryMap = { - - }; - - type DataEntryMap = { - - }; - - type AnyEntryMap = ContentEntryMap & DataEntryMap; - - type ExtractLoaderTypes = T extends import('astro/loaders').LiveLoader< - infer TData, - infer TEntryFilter, - infer TCollectionFilter, - infer TError - > - ? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError } - : { data: never; entryFilter: never; collectionFilter: never; error: never }; - type ExtractDataType = ExtractLoaderTypes['data']; - type ExtractEntryFilterType = ExtractLoaderTypes['entryFilter']; - type ExtractCollectionFilterType = ExtractLoaderTypes['collectionFilter']; - type ExtractErrorType = ExtractLoaderTypes['error']; - - type LiveLoaderDataType = - LiveContentConfig['collections'][C]['schema'] extends undefined - ? ExtractDataType - : import('astro/zod').infer< - Exclude - >; - type LiveLoaderEntryFilterType = - ExtractEntryFilterType; - type LiveLoaderCollectionFilterType = - ExtractCollectionFilterType; - type LiveLoaderErrorType = ExtractErrorType< - LiveContentConfig['collections'][C]['loader'] - >; - - export type ContentConfig = typeof import("../src/content.config.mjs"); - export type LiveContentConfig = never; -} diff --git a/.astro/data-store.json b/.astro/data-store.json deleted file mode 100644 index 5036073c0..000000000 --- a/.astro/data-store.json +++ /dev/null @@ -1 +0,0 @@ -[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.15.3","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false}}"] \ No newline at end of file diff --git a/.astro/settings.json b/.astro/settings.json deleted file mode 100644 index 0afe29342..000000000 --- a/.astro/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "_variables": { - "lastUpdateCheck": 1763653789860 - } -} \ No newline at end of file diff --git a/.astro/types.d.ts b/.astro/types.d.ts deleted file mode 100644 index f964fe0cf..000000000 --- a/.astro/types.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/.changeset/config.json b/.changeset/config.json index 8639ae567..46c604cd8 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,8 +4,12 @@ "commit": false, "fixed": [], "linked": [], - "access": "restricted", + "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "privatePackages": { + "version": false, + "tag": false + }, + "ignore": ["@eventcatalog/dsl-playground"] } diff --git a/.changeset/quiet-otters-shine.md b/.changeset/quiet-otters-shine.md new file mode 100644 index 000000000..b43aa98f5 --- /dev/null +++ b/.changeset/quiet-otters-shine.md @@ -0,0 +1,5 @@ +--- +"@eventcatalog/core": patch +--- + +fix(core): serve proper favicons instead of the logo image diff --git a/.claude/agents/code-review-uncommitted.md b/.claude/agents/code-review-uncommitted.md new file mode 100644 index 000000000..4f90ff5b9 --- /dev/null +++ b/.claude/agents/code-review-uncommitted.md @@ -0,0 +1,103 @@ +--- +name: code-review-uncommitted +description: "Use this agent when you want to review uncommitted changes in your working directory to ensure they follow project conventions, are maintainable, and could be simplified without changing behavior. This agent should be called before committing code to catch issues early.\\n\\nExamples:\\n\\n\\nContext: The user has just finished writing a new feature and wants to ensure quality before committing.\\nuser: \"I've finished implementing the new event filtering feature, can you review my changes?\"\\nassistant: \"I'll use the code-review-uncommitted agent to review your uncommitted changes and ensure they follow project conventions.\"\\n\\n\\n\\n\\nContext: The user wants a pre-commit review of their work.\\nuser: \"Review my code before I commit\"\\nassistant: \"Let me launch the code-review-uncommitted agent to analyze your uncommitted changes for convention compliance and simplification opportunities.\"\\n\\n\\n\\n\\nContext: The user has been coding for a while and wants to check their work proactively.\\nuser: \"I've been working on this for a few hours, let's see if there are any issues\"\\nassistant: \"I'll use the code-review-uncommitted agent to review all your uncommitted changes and identify any convention violations or simplification opportunities.\"\\n\\n" +model: opus +color: purple +--- + +You are an expert code reviewer with deep expertise in software maintainability, clean code principles, and long-term codebase health. Your role is to review uncommitted changes and ensure they align with existing project conventions while identifying opportunities for simplification. + +## Your Primary Objectives + +1. **Convention Compliance**: Ensure all changes follow the established patterns and conventions already present in the codebase +2. **Code Simplification**: Identify opportunities to simplify code without altering its behavior +3. **Long-term Maintainability**: Flag code that may become problematic to maintain over time + +## Review Process + +### Step 1: Gather Context +- Run `git diff` to see all uncommitted changes +- Run `git diff --cached` to see staged changes +- Examine the CLAUDE.md file and any project-specific configuration for coding standards +- Look at surrounding code in modified files to understand existing patterns + +### Step 2: Convention Analysis +For each changed file, verify: +- **Naming conventions**: Variables, functions, classes, files follow existing patterns +- **Code structure**: Organization matches similar code elsewhere in the project +- **Import/export patterns**: Consistent with the codebase style +- **Error handling**: Follows established error handling patterns +- **TypeScript usage**: Proper typing, type guards, and strict mode compliance +- **Theming**: Uses CSS variables instead of hardcoded colors (use `--ec-*` variables) +- **Formatting**: Code should be formatted according to project standards + +### Step 3: Simplification Opportunities +Identify code that can be simplified: +- **Redundant code**: Duplicate logic that could be extracted +- **Complex conditionals**: Nested if/else that could be flattened or use early returns +- **Verbose patterns**: Code that could use more concise language features +- **Over-engineering**: Abstractions that add complexity without clear benefit +- **Dead code**: Unused variables, unreachable code paths, commented-out code + +### Step 4: Maintainability Assessment +Evaluate long-term health: +- **Readability**: Will another developer understand this in 6 months? +- **Testability**: Is the code structured for easy testing? +- **Coupling**: Are dependencies appropriate and minimal? +- **Single Responsibility**: Does each function/component do one thing well? +- **Documentation**: Are complex logic sections adequately commented? + +## Output Format + +Provide your review in this structure: + +### Summary +Brief overview of the changes and overall assessment. + +### Convention Issues +List each convention violation with: +- File and line reference +- Description of the issue +- How it should be corrected (with code example if helpful) + +### Simplification Suggestions +List each simplification opportunity with: +- File and line reference +- Current code snippet +- Suggested simplified version +- Explanation of why this is better + +### Maintainability Concerns +List any long-term concerns with: +- Description of the concern +- Potential future impact +- Recommended approach + +### Positive Observations +Note things done well to reinforce good practices. + +## Important Guidelines + +- **Never suggest changes that alter behavior** - simplification must be functionally equivalent +- **Prioritize issues by impact** - focus on significant problems, not nitpicks +- **Provide actionable feedback** - every issue should have a clear resolution path +- **Respect existing patterns** - even if you'd prefer different conventions, consistency matters more +- **Consider context** - a quick fix may warrant different standards than core architecture +- **Be constructive** - frame feedback as improvements, not criticisms + +## Project-Specific Context + +For this EventCatalog project: +- Use `pnpm run format` for formatting +- Follow strict TypeScript typing +- Use CSS variables (`--ec-*`) for theming, never hardcoded colors or `dark:` variants +- Use ES modules with explicit imports/exports +- Follow patterns in existing codebase structure + +## Self-Verification + +Before finalizing your review: +1. Confirm you've checked all uncommitted changes +2. Verify each suggestion maintains identical behavior +3. Ensure recommendations align with existing codebase conventions +4. Check that feedback is specific and actionable diff --git a/.claude/agents/docs.md b/.claude/agents/docs.md new file mode 100644 index 000000000..b5184703f --- /dev/null +++ b/.claude/agents/docs.md @@ -0,0 +1,150 @@ +--- +name: docs-updater +description: Use this agent when the user asks to write, update, or generate documentation for a feature or code change. This includes requests like 'write docs for this feature', 'update the documentation', 'document these changes', or 'add docs for what I just built'. The agent analyzes recent code changes and intelligently updates or creates EventCatalog documentation pages.\n\nExamples:\n\n\nContext: User has just implemented a new visualization feature for EventCatalog.\nuser: "Write docs for this feature"\nassistant: "I'll use the docs-updater agent to analyze your recent code changes and update the EventCatalog documentation accordingly."\n\nSince the user wants documentation written for their recent feature work, use the Task tool to launch the docs-updater agent which will compare the current code against the previous commit, identify the changes, and update or create relevant documentation pages.\n\n\n\n\nContext: User has added a new component to the EventCatalog codebase.\nuser: "Can you document the changes I made?"\nassistant: "I'll launch the docs-updater agent to review your changes and create appropriate documentation."\n\nThe user is asking for documentation of their changes. Use the Task tool to launch the docs-updater agent to analyze the diff, understand what was changed, and update the relevant documentation in the EventCatalog docs.\n\n\n\n\nContext: User has refactored an existing feature and needs docs updated.\nuser: "Update the docs to reflect my refactoring"\nassistant: "I'll use the docs-updater agent to identify what changed in your refactoring and update the documentation to match."\n\nSince the user has refactored code and needs documentation updated, use the Task tool to launch the docs-updater agent which will detect the changes and modify existing documentation pages to reflect the new implementation.\n\n +model: sonnet +color: cyan +--- + +You are an expert technical documentation specialist with deep knowledge of EventCatalog's architecture, documentation patterns, and writing style. Your role is to analyze code changes and create or update documentation that seamlessly integrates with the existing EventCatalog docs. + +You are not verbose + +The title of the page should be a word or a 2-3 word phrase + +The description should be one short line, should not start with "The", should avoid repeating the title of the page, should be 5-10 words long + +Chunks of text should not be more than 3 sentences long + +The section titles are short with only the first letter of the word capitalized + +The section titles are in the imperative mood + +The section titles should not repeat the term used in the page title, for example, if the page title is "Models", avoid using a section title like "Add new models". This might be unavoidable in some cases, but try to avoid it. + +If the feature is a paid for feature use admonition to let the user that the feature is paid for, typically at the top of the page, but sometimes this is unavoidable and you should place it where it makes sense. + +when you reference localhost use port 3000 for the port number of running catalogs. + +never use em-dash. + +## Your Core Responsibilities + +1. **Analyze Code Changes**: Compare the current state against the previous commit to understand exactly what was modified, added, or removed. + +2. **Assess Documentation Impact**: Determine which documentation pages need updating and whether new pages should be created. + +3. **Maintain Consistent Voice**: Match the existing tone, style, and formatting conventions found throughout the EventCatalog documentation. + +4. **Strategic Page Placement**: Prefer updating existing pages when the content fits logically. Only create new pages when the feature is substantial enough to warrant its own documentation or doesn't fit naturally into existing pages. + +## Documentation Location + +All documentation lives in **`eventcatalog/website-2`** (a sibling directory to this repo, at `../../website-2` relative to the eventcatalog package, or `/Users/dboyne/Dev/eventcatalog/website-2`). Docs are Docusaurus markdown/MDX under `website-2/docs/`. **Never edit the legacy `website/` directory.** + +## Workflow + +### Step 1: Determine the Release Version + +The version that the new feature ships in is required for the `` component. Resolve it in this order: + +1. If the user told you the release type (patch / minor / major) or an explicit version, use that. +2. Otherwise read the current version from `packages/core/package.json` in this repo and bump it according to the release type the user specified. If the user did not specify, ask before writing docs — do not guess. +3. Convention: bug fixes = patch, new feature/additive = minor, breaking = major. + +### Step 2: Understand the Changes +- Use `git diff HEAD~1` to see what changed in the most recent commit +- If more context is needed, examine additional commits with `git log --oneline -10` and `git diff ..HEAD` +- Read the changed files thoroughly to understand the feature's purpose and implementation +- Note: only `@eventcatalog/core` changes need user-facing docs. SDK / CLI / playground / language-server changes generally do not belong in `website-2/docs`. + +### Step 3: Survey Existing Documentation +- Explore `eventcatalog/website-2/docs` to understand the documentation structure +- Read existing documentation pages to absorb the writing style, tone, and formatting patterns +- Identify pages that cover related topics where new content might fit +- Note the markdown conventions, heading structures, and code example patterns used + +### Step 4: Plan Documentation Updates +- List which existing pages should be updated and why +- Determine if a new page is necessary (only for substantial, standalone features) +- Outline what content needs to be added or modified + +### Step 5: Write Documentation +- Match the existing documentation's: + - Tone (professional but approachable, clear and concise) + - Heading hierarchy and structure + - Code example formatting + - Use of admonitions, tips, or warnings +- Include practical code examples when relevant +- Explain both the 'what' and the 'why' of features +- Link to related documentation pages when appropriate +- Apply the `` component (see section below) to any new or changed feature + +### Step 6: Validate Changes +- Ensure new content flows naturally with existing content +- Verify all code examples are accurate and follow project conventions +- Check that formatting is consistent with other pages +- Confirm every documented addition is annotated with the correct `` version + +## Documentation Location Guidelines + +- **Component documentation**: Look for existing component docs and add to them +- **Configuration options**: Update relevant configuration documentation +- **New features**: Consider if they extend an existing feature (update that page) or are entirely new (may warrant new page) +- **Bug fixes**: Usually don't require documentation unless they change behavior +- **API changes**: Update API reference documentation + +## The `` Component + +Every new feature or notable change you document **must** be marked with `` so readers know which release introduced it. + +### Import (once per file) + +```mdx +import AddedIn from '@site/src/components/MDX/AddedIn'; +``` + +If the page already imports it, do not duplicate the import. + +### Usage + +Place the tag directly under the feature heading it applies to, before any prose: + +```mdx +## Customize the landing page + + + +EventCatalog provides a landing page for... +``` + +- Use the full release version (e.g. `2.37.1`, `3.15.0`), never `latest` or a range. +- For a brand new page, place a single `` near the top, under the page title intro. +- For an existing page that gains a new section/option, scope the `` to that section only — do not bump the page-level marker. +- A single page can contain multiple `` tags at different versions; that is expected and correct. +- Bug fixes generally do not need `` (and usually don't need docs). + +## IMAGES + +- If you need to add an image just use the italic _PLACE_HOLDER_IMAGE_ for the image. + +## Quality Standards + +- Never use placeholder text - all content must be complete and accurate +- Code examples must be syntactically correct and follow the project's TypeScript/Astro conventions +- Explanations should be clear enough for developers unfamiliar with the codebase +- Document edge cases and important considerations +- Include any relevant CSS variable usage following the theming guidelines (use `--ec-*` variables, never hardcoded colors) + +## Output Format + +When updating documentation: +1. Clearly state which files you're modifying and why +2. Show the relevant changes in context +3. Explain your reasoning for page placement decisions + +When creating new pages: +1. Explain why a new page is warranted +2. Describe where in the documentation structure it belongs +3. Create the complete page content + +Remember: Your goal is to make the documentation feel like it was always there - seamlessly integrated, professionally written, and genuinely helpful to EventCatalog users. diff --git a/.claude/commands/audit.md b/.claude/commands/audit.md new file mode 100644 index 000000000..abcbb20d3 --- /dev/null +++ b/.claude/commands/audit.md @@ -0,0 +1,13 @@ +--- +description: Audit the uncommited changes in the current branch +allowed-tools: Bash(git status:*), Bash(git diff:*), Bash(git diff --staged:*), Bash(git branch:*), Bash(git rev-parse:*), Bash(git switch:*), Bash(git checkout:*), Bash(git add:*), Bash(git commit:*), Bash(git restore:*) +--- + +You will audit the uncommited changes in the current branch and make sure they follow these patterns: + +Goal: +- Code should be maintainable think about your future self and your team. +- We prefer functional programming over object-oriented programming. +- Code should be performant and efficient. + +User hint (may be empty): $ARGUMENTS diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md new file mode 100644 index 000000000..09faabf88 --- /dev/null +++ b/.claude/commands/commit.md @@ -0,0 +1,45 @@ +--- +description: Create a semantic commit (EventCatalog conventions), auto-branch if needed +allowed-tools: Bash(git status:*), Bash(git diff:*), Bash(git diff --staged:*), Bash(git branch:*), Bash(git rev-parse:*), Bash(git switch:*), Bash(git checkout:*), Bash(git add:*), Bash(git commit:*), Bash(git restore:*) +--- + +You are in a git repository that uses EventCatalog-style semantic commit messages. + +Goal: +- Create a single commit with message format: `(): ` where scope is optional. +- Message must be lowercase. +- Allowed types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `misc`. +- Pick a scope only if it clearly applies (short, kebab-case). Examples: `core`, `content-docs`, `theme-classic`. + +User hint (may be empty): $ARGUMENTS + +Workflow: +1) Run: + - `git status` + - `git diff` + - `git diff --staged` +2) If there are both unrelated changes and the user hint clearly targets one thing, keep the commit focused: + - stage only the relevant files/lines + - leave unrelated changes unstaged +3) Decide the semantic commit message: + - Choose the best `type` from the allowed list based on the actual diff + - Choose `scope` only if unambiguous + - Write a short present-tense, imperative `subject` in lowercase (no trailing period) +4) Branch handling: + - Determine current branch with git + - If on `main` (or `master`) OR in detached HEAD, create and switch to a new branch + - Branch name format: `/-` + - lowercase, kebab-case + - example: `feat/core-add-schema-registry-sidebar` + - if no scope: `fix-update-docs-links` +5) Before committing: + - Show the exact commit message you plan to use + - Show the exact branch name (if creating one) + - Then proceed without asking follow-up questions +6) Stage changes (only what belongs in this commit) using `git add ...`. +7) Create the commit using the exact semantic message. + +Constraints: +- Do not include “wip”. +- Do not invent changes not present in the diff. +- If there are no changes to commit, say so and stop. diff --git a/.claude/commands/issue.md b/.claude/commands/issue.md new file mode 100644 index 000000000..97ad827fa --- /dev/null +++ b/.claude/commands/issue.md @@ -0,0 +1,39 @@ +--- +description: Create a GitHub issue in this project +allowed-tools: Bash(gh issue create:*), Bash(gh label list:*) +--- + +You are creating a GitHub issue for the EventCatalog project using the GitHub CLI (`gh`). + +Goal: +- Create a clear, concise GitHub issue based on the user's request. +- Keep it simple and to the point. + +User request: $ARGUMENTS + +Workflow: +1) Parse the user's request to understand: + - What the issue is about + - Whether it's a bug, feature request, enhancement, or documentation need +2) Fetch available labels with `gh label list` to see what labels exist. +3) Create the issue with `gh issue create`: + - Write a clear, concise title (imperative mood, lowercase) + - Write a focused body that captures the requirement without fluff + - Add appropriate labels based on issue type: + - Bug: `bug` + - Feature: `enhancement` + - Documentation: `documentation` + - Add other relevant labels if they exist and apply + +Issue format: +- Title: Short, descriptive, imperative (e.g., "add support for X", "fix Y when Z") +- Body: + - Brief description of what is needed + - Context if necessary (1-2 sentences max) + - No verbose templates or unnecessary sections + +Constraints: +- Do not add sections that aren't needed (no "steps to reproduce" for features, etc.) +- Keep the body under 200 words unless complexity requires more +- Do not ask follow-up questions - use your judgment based on the request +- After creating, output the issue URL diff --git a/.claude/commands/pr-description.md b/.claude/commands/pr-description.md new file mode 100644 index 000000000..69ede283d --- /dev/null +++ b/.claude/commands/pr-description.md @@ -0,0 +1,107 @@ +--- +description: Generate a PR description markdown file summarizing branch changes +allowed-tools: Bash(git status:*), Bash(git diff:*), Bash(git log:*), Bash(git branch:*), Bash(git rev-parse:*), Bash(git merge-base:*), Bash(git show:*), Bash(mkdir:*), Read, Write, Glob +--- + +You are a helpful assistant that generates clear, user-friendly PR descriptions. + +Goal: +- Analyze the changes in the current branch compared to the main branch +- Generate a well-structured markdown file summarizing the PR +- Save the file to `pr-descriptions/` folder with a descriptive filename + +User hint (may be empty): $ARGUMENTS + +Workflow: + +1) **Gather Information** + Run these commands to understand the changes: + - `git branch --show-current` - get current branch name + - `git merge-base HEAD main` - find common ancestor with main + - `git log main..HEAD --oneline` - list commits in this branch + - `git diff main...HEAD --stat` - file change summary + - `git diff main...HEAD` - full diff for analysis + +2) **Analyze the Changes** + Review all changes and identify: + - The primary purpose/feature of this PR + - Key files and components modified + - Any new dependencies or configurations + - Potential breaking changes (API changes, removed features, renamed exports, config changes) + - Testing considerations + +3) **Create the pr-descriptions Directory** + - Run `mkdir -p pr-descriptions` to ensure the folder exists + +4) **Generate the PR Description File** + Create a markdown file with this structure: + + ```markdown + # PR: [Brief Title Based on Changes] + + **Branch:** `[branch-name]` + **Date:** [YYYY-MM-DD] + **Commits:** [number of commits] + + ## What This PR Does + + [2-4 sentences describing the purpose and goals of this PR. Focus on the "why" - what problem does this solve or what feature does it add?] + + ## Changes Overview + + ### Files Changed + - [List key files/directories modified with brief notes] + + ### Key Changes + - [Bullet points of the main changes, written for humans to understand] + - [Focus on what changed, not line-by-line diffs] + + ## How It Works + + [Explain the implementation approach. How does the new code work? What patterns or approaches were used? Include relevant technical details that reviewers should understand.] + + ## Breaking Changes + + [List any breaking changes, or state "None" if there are no breaking changes] + + - **[Change]**: [Description of what broke and how to migrate] + + ## Testing + + - [ ] Unit tests added/updated + - [ ] Manual testing performed + - [ ] [Other relevant testing notes] + + ## Screenshots/Examples + + [If applicable, note where screenshots could be added or include code examples] + + ## Checklist + + - [ ] Code follows project conventions + - [ ] Documentation updated (if needed) + - [ ] No console errors or warnings introduced + - [ ] Reviewed for security implications + + ## Additional Notes + + [Any other context, related issues, follow-up work needed, or notes for reviewers] + ``` + +5) **Filename Convention** + Save the file as: `pr-descriptions/[branch-name]-[YYYY-MM-DD].md` + - Replace `/` in branch names with `-` + - Example: `pr-descriptions/feat-core-add-mcp-server-2025-01-12.md` + +6) **Output** + After creating the file: + - Show the full path to the generated file + - Display a brief summary of what was documented + +Constraints: +- Write in clear, simple language that non-technical stakeholders can understand +- Be honest about breaking changes - don't hide them +- If there are no changes compared to main, say so and stop +- Focus on substance over formatting - the content matters more than looking pretty +- Use present tense ("Adds feature" not "Added feature") +- Keep bullet points concise but informative diff --git a/.claude/commands/update-docs.md b/.claude/commands/update-docs.md new file mode 100644 index 000000000..0c36ea859 --- /dev/null +++ b/.claude/commands/update-docs.md @@ -0,0 +1,57 @@ +--- +description: Update EventCatalog docs in website-2 based on uncommitted changes (or the previous commit) using the docs-updater agent +allowed-tools: Bash(git status:*), Bash(git diff:*), Bash(git diff --staged:*), Bash(git log:*), Bash(git show:*), Bash(git rev-parse:*), Bash(git branch:*), Read, Agent +--- + +You are coordinating a documentation update for EventCatalog. The actual writing is delegated to the `docs-updater` agent (defined in `.claude/agents/docs.md`). Your job is to gather change context, then dispatch that agent with a complete brief. + +User hint (may be empty, may contain release type like "minor" / "patch" / "major" or an explicit version): $ARGUMENTS + +## Workflow + +### 1) Decide which changes to document + +Run `git status` and `git diff --stat` first. + +- If there are **uncommitted changes** (staged or unstaged) → document those. +- If the working tree is clean → document the **previous commit** (`HEAD`). Use `git show HEAD` and `git log -1`. +- If both apply (uncommitted + the user clearly meant the last commit), ask the user which to use before proceeding. + +Capture the actual diff with one of: +- `git diff` and `git diff --staged` (uncommitted) +- `git show HEAD` (previous commit) + +Only `@eventcatalog/core` (under `packages/core/`) changes need user-facing docs. If the diff is purely SDK / CLI / playground / language-server / tests / formatting, tell the user there's nothing to document and stop. + +### 2) Resolve the release version + +The `docs-updater` agent will tag new content with ``, so it needs an exact version. + +- Read the current version from `packages/core/package.json`. +- If the user passed a release type in `$ARGUMENTS` (patch / minor / major) or an explicit version, compute / use that. +- If no release type was given, **ask the user** which bump to use before dispatching the agent. Do not guess. + +### 3) Dispatch the docs-updater agent + +Launch a single `Agent` call with `subagent_type: "docs-updater"`. The agent has no memory of this conversation — your prompt must be self-contained. Include: + +- The resolved target version (e.g. `3.33.0`) and the release type. +- A summary of what changed (feature description, not a raw diff dump) — point at file paths and line numbers so the agent can read them itself. +- Whether you're documenting uncommitted changes or `HEAD`, and how to reproduce the diff (the exact `git` command). +- A reminder that docs live in `eventcatalog/website-2/docs` (never `/website`) and that every new/changed feature must carry an `` marker with the resolved version. +- Any user hint from `$ARGUMENTS` that's relevant (e.g. "this is a paid feature", "ship under the Scale plan section"). + +Keep the brief tight — the agent will do its own exploration of `website-2`. + +### 4) Report back + +After the agent returns, summarise to the user in 2–3 lines: +- Which doc files were created or updated. +- The version stamped via ``. +- Anything the agent flagged as unresolved (missing screenshots, ambiguous placement, etc.). + +## Constraints + +- Never edit docs yourself in this command — always delegate to the agent so its conventions (tone, headings, `` rules) are applied. +- Never invent a version. If unsure, ask. +- If the diff has no user-facing impact (refactor, internal tests, formatting), say so and skip the agent dispatch. diff --git a/.claude/skills/copy-editing b/.claude/skills/copy-editing new file mode 120000 index 000000000..b204bdb97 --- /dev/null +++ b/.claude/skills/copy-editing @@ -0,0 +1 @@ +../../.agents/skills/copy-editing \ No newline at end of file diff --git a/.claude/skills/copywriting b/.claude/skills/copywriting new file mode 120000 index 000000000..214f2185c --- /dev/null +++ b/.claude/skills/copywriting @@ -0,0 +1 @@ +../../.agents/skills/copywriting \ No newline at end of file diff --git a/.claude/skills/frontend-design/SKILL.md b/.claude/skills/frontend-design/SKILL.md new file mode 100644 index 000000000..600b6db41 --- /dev/null +++ b/.claude/skills/frontend-design/SKILL.md @@ -0,0 +1,42 @@ +--- +name: frontend-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics. +license: Complete terms in LICENSE.txt +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. \ No newline at end of file diff --git a/.claude/skills/grill-me/SKILL.md b/.claude/skills/grill-me/SKILL.md new file mode 100644 index 000000000..f19dd749b --- /dev/null +++ b/.claude/skills/grill-me/SKILL.md @@ -0,0 +1,10 @@ +--- +name: grill-me +description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time. + +If a question can be answered by exploring the codebase, explore the codebase instead. \ No newline at end of file diff --git a/.claude/skills/skill-creator/SKILL.md b/.claude/skills/skill-creator/SKILL.md new file mode 100644 index 000000000..b3b699497 --- /dev/null +++ b/.claude/skills/skill-creator/SKILL.md @@ -0,0 +1,357 @@ +--- +name: skill-creator +description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations. +license: Complete terms in LICENSE.txt +--- + +# Skill Creator + +This skill provides guidance for creating effective skills. + +## About Skills + +Skills are modular, self-contained packages that extend Claude's capabilities by providing +specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific +domains or tasks—they transform Claude from a general-purpose agent into a specialized agent +equipped with procedural knowledge that no model can fully possess. + +### What Skills Provide + +1. Specialized workflows - Multi-step procedures for specific domains +2. Tool integrations - Instructions for working with specific file formats or APIs +3. Domain expertise - Company-specific knowledge, schemas, business logic +4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks + +## Core Principles + +### Concise is Key + +The context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request. + +**Default assumption: Claude is already very smart.** Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" and "Does this paragraph justify its token cost?" + +Prefer concise examples over verbose explanations. + +### Set Appropriate Degrees of Freedom + +Match the level of specificity to the task's fragility and variability: + +**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach. + +**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior. + +**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed. + +Think of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom). + +### Anatomy of a Skill + +Every skill consists of a required SKILL.md file and optional bundled resources: + +``` +skill-name/ +├── SKILL.md (required) +│ ├── YAML frontmatter metadata (required) +│ │ ├── name: (required) +│ │ ├── description: (required) +│ │ └── compatibility: (optional, rarely needed) +│ └── Markdown instructions (required) +└── Bundled Resources (optional) + ├── scripts/ - Executable code (Python/Bash/etc.) + ├── references/ - Documentation intended to be loaded into context as needed + └── assets/ - Files used in output (templates, icons, fonts, etc.) +``` + +#### SKILL.md (required) + +Every SKILL.md consists of: + +- **Frontmatter** (YAML): Contains `name` and `description` fields (required), plus optional fields like `license`, `metadata`, and `compatibility`. Only `name` and `description` are read by Claude to determine when the skill triggers, so be clear and comprehensive about what the skill is and when it should be used. The `compatibility` field is for noting environment requirements (target product, system packages, etc.) but most skills don't need it. +- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all). + +#### Bundled Resources (optional) + +##### Scripts (`scripts/`) + +Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten. + +- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed +- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks +- **Benefits**: Token efficient, deterministic, may be executed without loading into context +- **Note**: Scripts may still need to be read by Claude for patching or environment-specific adjustments + +##### References (`references/`) + +Documentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking. + +- **When to include**: For documentation that Claude should reference while working +- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications +- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides +- **Benefits**: Keeps SKILL.md lean, loaded only when Claude determines it's needed +- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md +- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files. + +##### Assets (`assets/`) + +Files not intended to be loaded into context, but rather used within the output Claude produces. + +- **When to include**: When the skill needs files that will be used in the final output +- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography +- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified +- **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context + +#### What to Not Include in a Skill + +A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including: + +- README.md +- INSTALLATION_GUIDE.md +- QUICK_REFERENCE.md +- CHANGELOG.md +- etc. + +The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion. + +### Progressive Disclosure Design Principle + +Skills use a three-level loading system to manage context efficiently: + +1. **Metadata (name + description)** - Always in context (~100 words) +2. **SKILL.md body** - When skill triggers (<5k words) +3. **Bundled resources** - As needed by Claude (Unlimited because scripts can be executed without reading into context window) + +#### Progressive Disclosure Patterns + +Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them. + +**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files. + +**Pattern 1: High-level guide with references** + +```markdown +# PDF Processing + +## Quick start + +Extract text with pdfplumber: +[code example] + +## Advanced features + +- **Form filling**: See [FORMS.md](FORMS.md) for complete guide +- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods +- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns +``` + +Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed. + +**Pattern 2: Domain-specific organization** + +For Skills with multiple domains, organize content by domain to avoid loading irrelevant context: + +``` +bigquery-skill/ +├── SKILL.md (overview and navigation) +└── reference/ + ├── finance.md (revenue, billing metrics) + ├── sales.md (opportunities, pipeline) + ├── product.md (API usage, features) + └── marketing.md (campaigns, attribution) +``` + +When a user asks about sales metrics, Claude only reads sales.md. + +Similarly, for skills supporting multiple frameworks or variants, organize by variant: + +``` +cloud-deploy/ +├── SKILL.md (workflow + provider selection) +└── references/ + ├── aws.md (AWS deployment patterns) + ├── gcp.md (GCP deployment patterns) + └── azure.md (Azure deployment patterns) +``` + +When the user chooses AWS, Claude only reads aws.md. + +**Pattern 3: Conditional details** + +Show basic content, link to advanced content: + +```markdown +# DOCX Processing + +## Creating documents + +Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md). + +## Editing documents + +For simple edits, modify the XML directly. + +**For tracked changes**: See [REDLINING.md](REDLINING.md) +**For OOXML details**: See [OOXML.md](OOXML.md) +``` + +Claude reads REDLINING.md or OOXML.md only when the user needs those features. + +**Important guidelines:** + +- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md. +- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing. + +## Skill Creation Process + +Skill creation involves these steps: + +1. Understand the skill with concrete examples +2. Plan reusable skill contents (scripts, references, assets) +3. Initialize the skill (run init_skill.py) +4. Edit the skill (implement resources and write SKILL.md) +5. Package the skill (run package_skill.py) +6. Iterate based on real usage + +Follow these steps in order, skipping only if there is a clear reason why they are not applicable. + +### Step 1: Understanding the Skill with Concrete Examples + +Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill. + +To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback. + +For example, when building an image-editor skill, relevant questions include: + +- "What functionality should the image-editor skill support? Editing, rotating, anything else?" +- "Can you give some examples of how this skill would be used?" +- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?" +- "What would a user say that should trigger this skill?" + +To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness. + +Conclude this step when there is a clear sense of the functionality the skill should support. + +### Step 2: Planning the Reusable Skill Contents + +To turn concrete examples into an effective skill, analyze each example by: + +1. Considering how to execute on the example from scratch +2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly + +Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows: + +1. Rotating a PDF requires re-writing the same code each time +2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill + +Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows: + +1. Writing a frontend webapp requires the same boilerplate HTML/React each time +2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill + +Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows: + +1. Querying BigQuery requires re-discovering the table schemas and relationships each time +2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill + +To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets. + +### Step 3: Initializing the Skill + +At this point, it is time to actually create the skill. + +Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step. + +When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable. + +Usage: + +```bash +scripts/init_skill.py --path +``` + +The script: + +- Creates the skill directory at the specified path +- Generates a SKILL.md template with proper frontmatter and TODO placeholders +- Creates example resource directories: `scripts/`, `references/`, and `assets/` +- Adds example files in each directory that can be customized or deleted + +After initialization, customize or remove the generated SKILL.md and example files as needed. + +### Step 4: Edit the Skill + +When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively. + +#### Learn Proven Design Patterns + +Consult these helpful guides based on your skill's needs: + +- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic +- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns + +These files contain established best practices for effective skill design. + +#### Start with Reusable Skill Contents + +To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`. + +Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion. + +Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them. + +#### Update SKILL.md + +**Writing Guidelines:** Always use imperative/infinitive form. + +##### Frontmatter + +Write the YAML frontmatter with `name` and `description`: + +- `name`: The skill name +- `description`: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill. + - Include both what the Skill does and specific triggers/contexts for when to use it. + - Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Claude. + - Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks" + +Do not include any other fields in YAML frontmatter. + +##### Body + +Write instructions for using the skill and its bundled resources. + +### Step 5: Packaging a Skill + +Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements: + +```bash +scripts/package_skill.py +``` + +Optional output directory specification: + +```bash +scripts/package_skill.py ./dist +``` + +The packaging script will: + +1. **Validate** the skill automatically, checking: + + - YAML frontmatter format and required fields + - Skill naming conventions and directory structure + - Description completeness and quality + - File organization and resource references + +2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension. + +If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again. + +### Step 6: Iterate + +After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed. + +**Iteration workflow:** + +1. Use the skill on real tasks +2. Notice struggles or inefficiencies +3. Identify how SKILL.md or bundled resources should be updated +4. Implement changes and test again \ No newline at end of file diff --git a/.claude/skills/skill-creator/references/output-patterns.md b/.claude/skills/skill-creator/references/output-patterns.md new file mode 100644 index 000000000..073ddda5f --- /dev/null +++ b/.claude/skills/skill-creator/references/output-patterns.md @@ -0,0 +1,82 @@ +# Output Patterns + +Use these patterns when skills need to produce consistent, high-quality output. + +## Template Pattern + +Provide templates for output format. Match the level of strictness to your needs. + +**For strict requirements (like API responses or data formats):** + +```markdown +## Report structure + +ALWAYS use this exact template structure: + +# [Analysis Title] + +## Executive summary +[One-paragraph overview of key findings] + +## Key findings +- Finding 1 with supporting data +- Finding 2 with supporting data +- Finding 3 with supporting data + +## Recommendations +1. Specific actionable recommendation +2. Specific actionable recommendation +``` + +**For flexible guidance (when adaptation is useful):** + +```markdown +## Report structure + +Here is a sensible default format, but use your best judgment: + +# [Analysis Title] + +## Executive summary +[Overview] + +## Key findings +[Adapt sections based on what you discover] + +## Recommendations +[Tailor to the specific context] + +Adjust sections as needed for the specific analysis type. +``` + +## Examples Pattern + +For skills where output quality depends on seeing examples, provide input/output pairs: + +```markdown +## Commit message format + +Generate commit messages following these examples: + +**Example 1:** +Input: Added user authentication with JWT tokens +Output: +``` +feat(auth): implement JWT-based authentication + +Add login endpoint and token validation middleware +``` + +**Example 2:** +Input: Fixed bug where dates displayed incorrectly in reports +Output: +``` +fix(reports): correct date formatting in timezone conversion + +Use UTC timestamps consistently across report generation +``` + +Follow this style: type(scope): brief description, then detailed explanation. +``` + +Examples help Claude understand the desired style and level of detail more clearly than descriptions alone. diff --git a/.claude/skills/skill-creator/references/workflows.md b/.claude/skills/skill-creator/references/workflows.md new file mode 100644 index 000000000..a350c3cc8 --- /dev/null +++ b/.claude/skills/skill-creator/references/workflows.md @@ -0,0 +1,28 @@ +# Workflow Patterns + +## Sequential Workflows + +For complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md: + +```markdown +Filling a PDF form involves these steps: + +1. Analyze the form (run analyze_form.py) +2. Create field mapping (edit fields.json) +3. Validate mapping (run validate_fields.py) +4. Fill the form (run fill_form.py) +5. Verify output (run verify_output.py) +``` + +## Conditional Workflows + +For tasks with branching logic, guide Claude through decision points: + +```markdown +1. Determine the modification type: + **Creating new content?** → Follow "Creation workflow" below + **Editing existing content?** → Follow "Editing workflow" below + +2. Creation workflow: [steps] +3. Editing workflow: [steps] +``` \ No newline at end of file diff --git a/.claude/skills/skill-creator/scripts/init_skill.py b/.claude/skills/skill-creator/scripts/init_skill.py new file mode 100644 index 000000000..c544fc725 --- /dev/null +++ b/.claude/skills/skill-creator/scripts/init_skill.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +""" +Skill Initializer - Creates a new skill from template + +Usage: + init_skill.py --path + +Examples: + init_skill.py my-new-skill --path skills/public + init_skill.py my-api-helper --path skills/private + init_skill.py custom-skill --path /custom/location +""" + +import sys +from pathlib import Path + + +SKILL_TEMPLATE = """--- +name: {skill_name} +description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.] +--- + +# {skill_title} + +## Overview + +[TODO: 1-2 sentences explaining what this skill enables] + +## Structuring This Skill + +[TODO: Choose the structure that best fits this skill's purpose. Common patterns: + +**1. Workflow-Based** (best for sequential processes) +- Works well when there are clear step-by-step procedures +- Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing" +- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2... + +**2. Task-Based** (best for tool collections) +- Works well when the skill offers different operations/capabilities +- Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text" +- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2... + +**3. Reference/Guidelines** (best for standards or specifications) +- Works well for brand guidelines, coding standards, or requirements +- Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features" +- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage... + +**4. Capabilities-Based** (best for integrated systems) +- Works well when the skill provides multiple interrelated features +- Example: Product Management with "Core Capabilities" → numbered capability list +- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature... + +Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations). + +Delete this entire "Structuring This Skill" section when done - it's just guidance.] + +## [TODO: Replace with the first main section based on chosen structure] + +[TODO: Add content here. See examples in existing skills: +- Code samples for technical skills +- Decision trees for complex workflows +- Concrete examples with realistic user requests +- References to scripts/templates/references as needed] + +## Resources + +This skill includes example resource directories that demonstrate how to organize different types of bundled resources: + +### scripts/ +Executable code (Python/Bash/etc.) that can be run directly to perform specific operations. + +**Examples from other skills:** +- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation +- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing + +**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations. + +**Note:** Scripts may be executed without loading into context, but can still be read by Claude for patching or environment adjustments. + +### references/ +Documentation and reference material intended to be loaded into context to inform Claude's process and thinking. + +**Examples from other skills:** +- Product management: `communication.md`, `context_building.md` - detailed workflow guides +- BigQuery: API reference documentation and query examples +- Finance: Schema documentation, company policies + +**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Claude should reference while working. + +### assets/ +Files not intended to be loaded into context, but rather used within the output Claude produces. + +**Examples from other skills:** +- Brand styling: PowerPoint template files (.pptx), logo files +- Frontend builder: HTML/React boilerplate project directories +- Typography: Font files (.ttf, .woff2) + +**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output. + +--- + +**Any unneeded directories can be deleted.** Not every skill requires all three types of resources. +""" + +EXAMPLE_SCRIPT = '''#!/usr/bin/env python3 +""" +Example helper script for {skill_name} + +This is a placeholder script that can be executed directly. +Replace with actual implementation or delete if not needed. + +Example real scripts from other skills: +- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields +- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images +""" + +def main(): + print("This is an example script for {skill_name}") + # TODO: Add actual script logic here + # This could be data processing, file conversion, API calls, etc. + +if __name__ == "__main__": + main() +''' + +EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title} + +This is a placeholder for detailed reference documentation. +Replace with actual reference content or delete if not needed. + +Example real reference docs from other skills: +- product-management/references/communication.md - Comprehensive guide for status updates +- product-management/references/context_building.md - Deep-dive on gathering context +- bigquery/references/ - API references and query examples + +## When Reference Docs Are Useful + +Reference docs are ideal for: +- Comprehensive API documentation +- Detailed workflow guides +- Complex multi-step processes +- Information too lengthy for main SKILL.md +- Content that's only needed for specific use cases + +## Structure Suggestions + +### API Reference Example +- Overview +- Authentication +- Endpoints with examples +- Error codes +- Rate limits + +### Workflow Guide Example +- Prerequisites +- Step-by-step instructions +- Common patterns +- Troubleshooting +- Best practices +""" + +EXAMPLE_ASSET = """# Example Asset File + +This placeholder represents where asset files would be stored. +Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed. + +Asset files are NOT intended to be loaded into context, but rather used within +the output Claude produces. + +Example asset files from other skills: +- Brand guidelines: logo.png, slides_template.pptx +- Frontend builder: hello-world/ directory with HTML/React boilerplate +- Typography: custom-font.ttf, font-family.woff2 +- Data: sample_data.csv, test_dataset.json + +## Common Asset Types + +- Templates: .pptx, .docx, boilerplate directories +- Images: .png, .jpg, .svg, .gif +- Fonts: .ttf, .otf, .woff, .woff2 +- Boilerplate code: Project directories, starter files +- Icons: .ico, .svg +- Data files: .csv, .json, .xml, .yaml + +Note: This is a text placeholder. Actual assets can be any file type. +""" + + +def title_case_skill_name(skill_name): + """Convert hyphenated skill name to Title Case for display.""" + return ' '.join(word.capitalize() for word in skill_name.split('-')) + + +def init_skill(skill_name, path): + """ + Initialize a new skill directory with template SKILL.md. + + Args: + skill_name: Name of the skill + path: Path where the skill directory should be created + + Returns: + Path to created skill directory, or None if error + """ + # Determine skill directory path + skill_dir = Path(path).resolve() / skill_name + + # Check if directory already exists + if skill_dir.exists(): + print(f"❌ Error: Skill directory already exists: {skill_dir}") + return None + + # Create skill directory + try: + skill_dir.mkdir(parents=True, exist_ok=False) + print(f"✅ Created skill directory: {skill_dir}") + except Exception as e: + print(f"❌ Error creating directory: {e}") + return None + + # Create SKILL.md from template + skill_title = title_case_skill_name(skill_name) + skill_content = SKILL_TEMPLATE.format( + skill_name=skill_name, + skill_title=skill_title + ) + + skill_md_path = skill_dir / 'SKILL.md' + try: + skill_md_path.write_text(skill_content) + print("✅ Created SKILL.md") + except Exception as e: + print(f"❌ Error creating SKILL.md: {e}") + return None + + # Create resource directories with example files + try: + # Create scripts/ directory with example script + scripts_dir = skill_dir / 'scripts' + scripts_dir.mkdir(exist_ok=True) + example_script = scripts_dir / 'example.py' + example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name)) + example_script.chmod(0o755) + print("✅ Created scripts/example.py") + + # Create references/ directory with example reference doc + references_dir = skill_dir / 'references' + references_dir.mkdir(exist_ok=True) + example_reference = references_dir / 'api_reference.md' + example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title)) + print("✅ Created references/api_reference.md") + + # Create assets/ directory with example asset placeholder + assets_dir = skill_dir / 'assets' + assets_dir.mkdir(exist_ok=True) + example_asset = assets_dir / 'example_asset.txt' + example_asset.write_text(EXAMPLE_ASSET) + print("✅ Created assets/example_asset.txt") + except Exception as e: + print(f"❌ Error creating resource directories: {e}") + return None + + # Print next steps + print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}") + print("\nNext steps:") + print("1. Edit SKILL.md to complete the TODO items and update the description") + print("2. Customize or delete the example files in scripts/, references/, and assets/") + print("3. Run the validator when ready to check the skill structure") + + return skill_dir + + +def main(): + if len(sys.argv) < 4 or sys.argv[2] != '--path': + print("Usage: init_skill.py --path ") + print("\nSkill name requirements:") + print(" - Kebab-case identifier (e.g., 'my-data-analyzer')") + print(" - Lowercase letters, digits, and hyphens only") + print(" - Max 64 characters") + print(" - Must match directory name exactly") + print("\nExamples:") + print(" init_skill.py my-new-skill --path skills/public") + print(" init_skill.py my-api-helper --path skills/private") + print(" init_skill.py custom-skill --path /custom/location") + sys.exit(1) + + skill_name = sys.argv[1] + path = sys.argv[3] + + print(f"🚀 Initializing skill: {skill_name}") + print(f" Location: {path}") + print() + + result = init_skill(skill_name, path) + + if result: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/skill-creator/scripts/package_skill.py b/.claude/skills/skill-creator/scripts/package_skill.py new file mode 100644 index 000000000..5cd36cb16 --- /dev/null +++ b/.claude/skills/skill-creator/scripts/package_skill.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Skill Packager - Creates a distributable .skill file of a skill folder + +Usage: + python utils/package_skill.py [output-directory] + +Example: + python utils/package_skill.py skills/public/my-skill + python utils/package_skill.py skills/public/my-skill ./dist +""" + +import sys +import zipfile +from pathlib import Path +from quick_validate import validate_skill + + +def package_skill(skill_path, output_dir=None): + """ + Package a skill folder into a .skill file. + + Args: + skill_path: Path to the skill folder + output_dir: Optional output directory for the .skill file (defaults to current directory) + + Returns: + Path to the created .skill file, or None if error + """ + skill_path = Path(skill_path).resolve() + + # Validate skill folder exists + if not skill_path.exists(): + print(f"❌ Error: Skill folder not found: {skill_path}") + return None + + if not skill_path.is_dir(): + print(f"❌ Error: Path is not a directory: {skill_path}") + return None + + # Validate SKILL.md exists + skill_md = skill_path / "SKILL.md" + if not skill_md.exists(): + print(f"❌ Error: SKILL.md not found in {skill_path}") + return None + + # Run validation before packaging + print("🔍 Validating skill...") + valid, message = validate_skill(skill_path) + if not valid: + print(f"❌ Validation failed: {message}") + print(" Please fix the validation errors before packaging.") + return None + print(f"✅ {message}\n") + + # Determine output location + skill_name = skill_path.name + if output_dir: + output_path = Path(output_dir).resolve() + output_path.mkdir(parents=True, exist_ok=True) + else: + output_path = Path.cwd() + + skill_filename = output_path / f"{skill_name}.skill" + + # Create the .skill file (zip format) + try: + with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Walk through the skill directory + for file_path in skill_path.rglob('*'): + if file_path.is_file(): + # Calculate the relative path within the zip + arcname = file_path.relative_to(skill_path.parent) + zipf.write(file_path, arcname) + print(f" Added: {arcname}") + + print(f"\n✅ Successfully packaged skill to: {skill_filename}") + return skill_filename + + except Exception as e: + print(f"❌ Error creating .skill file: {e}") + return None + + +def main(): + if len(sys.argv) < 2: + print("Usage: python utils/package_skill.py [output-directory]") + print("\nExample:") + print(" python utils/package_skill.py skills/public/my-skill") + print(" python utils/package_skill.py skills/public/my-skill ./dist") + sys.exit(1) + + skill_path = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + print(f"📦 Packaging skill: {skill_path}") + if output_dir: + print(f" Output directory: {output_dir}") + print() + + result = package_skill(skill_path, output_dir) + + if result: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/skill-creator/scripts/quick_validate.py b/.claude/skills/skill-creator/scripts/quick_validate.py new file mode 100644 index 000000000..ed8e1dddc --- /dev/null +++ b/.claude/skills/skill-creator/scripts/quick_validate.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Quick validation script for skills - minimal version +""" + +import sys +import os +import re +import yaml +from pathlib import Path + +def validate_skill(skill_path): + """Basic validation of a skill""" + skill_path = Path(skill_path) + + # Check SKILL.md exists + skill_md = skill_path / 'SKILL.md' + if not skill_md.exists(): + return False, "SKILL.md not found" + + # Read and validate frontmatter + content = skill_md.read_text() + if not content.startswith('---'): + return False, "No YAML frontmatter found" + + # Extract frontmatter + match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) + if not match: + return False, "Invalid frontmatter format" + + frontmatter_text = match.group(1) + + # Parse YAML frontmatter + try: + frontmatter = yaml.safe_load(frontmatter_text) + if not isinstance(frontmatter, dict): + return False, "Frontmatter must be a YAML dictionary" + except yaml.YAMLError as e: + return False, f"Invalid YAML in frontmatter: {e}" + + # Define allowed properties + ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata', 'compatibility'} + + # Check for unexpected properties (excluding nested keys under metadata) + unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES + if unexpected_keys: + return False, ( + f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. " + f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}" + ) + + # Check required fields + if 'name' not in frontmatter: + return False, "Missing 'name' in frontmatter" + if 'description' not in frontmatter: + return False, "Missing 'description' in frontmatter" + + # Extract name for validation + name = frontmatter.get('name', '') + if not isinstance(name, str): + return False, f"Name must be a string, got {type(name).__name__}" + name = name.strip() + if name: + # Check naming convention (kebab-case: lowercase with hyphens) + if not re.match(r'^[a-z0-9-]+$', name): + return False, f"Name '{name}' should be kebab-case (lowercase letters, digits, and hyphens only)" + if name.startswith('-') or name.endswith('-') or '--' in name: + return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens" + # Check name length (max 64 characters per spec) + if len(name) > 64: + return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters." + + # Extract and validate description + description = frontmatter.get('description', '') + if not isinstance(description, str): + return False, f"Description must be a string, got {type(description).__name__}" + description = description.strip() + if description: + # Check for angle brackets + if '<' in description or '>' in description: + return False, "Description cannot contain angle brackets (< or >)" + # Check description length (max 1024 characters per spec) + if len(description) > 1024: + return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters." + + # Validate compatibility field if present (optional) + compatibility = frontmatter.get('compatibility', '') + if compatibility: + if not isinstance(compatibility, str): + return False, f"Compatibility must be a string, got {type(compatibility).__name__}" + if len(compatibility) > 500: + return False, f"Compatibility is too long ({len(compatibility)} characters). Maximum is 500 characters." + + return True, "Skill is valid!" + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python quick_validate.py ") + sys.exit(1) + + valid, message = validate_skill(sys.argv[1]) + print(message) + sys.exit(0 if valid else 1) \ No newline at end of file diff --git a/.claude/skills/spec-change/SKILL.md b/.claude/skills/spec-change/SKILL.md new file mode 100644 index 000000000..8451a70f4 --- /dev/null +++ b/.claude/skills/spec-change/SKILL.md @@ -0,0 +1,87 @@ +--- +name: spec-change +description: Update the EventCatalog DSL specification across all locations — Langium grammar, specification markdown docs, playground examples, and tests. Use when making any change to DSL syntax, adding new DSL features, modifying grammar rules, or updating DSL documentation. Triggers on requests like "add X to the DSL", "update the grammar", "change DSL syntax for Y", or "spec change". +--- + +# DSL Specification Change + +Apply a DSL specification change across all required locations. See [references/spec-files.md](references/spec-files.md) for all file paths. + +## Workflow + +### 1. Understand the change +- Parse the user's request to identify the DSL feature being added/modified +- Identify affected grammar rules, spec files, and examples + +### 2. Update the Langium grammar +- Read `packages/language-server/src/ec.langium` +- Locate and update the relevant grammar rules +- Follow Langium conventions + +### 3. Add/update tests +- Add tests in `packages/language-server/src/test/` (or `__tests__/`) +- Cover: valid syntax, invalid syntax, edge cases, backwards compatibility +- Use vitest. Test pattern: + +```typescript +describe('Feature Name', () => { + it('should parse valid syntax', () => { + const dsl = `...valid DSL...`; + const result = parseDsl(dsl); + expect(result.parseErrors).toHaveLength(0); + }); + + it('should reject invalid syntax', () => { + const dsl = `...invalid DSL...`; + const result = parseDsl(dsl); + expect(result.parseErrors.length).toBeGreaterThan(0); + }); +}); +``` + +- Run: `pnpm --filter @eventcatalog/language-server run test --run` + +### 4. Update specification markdown +- See [references/spec-files.md](references/spec-files.md) for the full file list +- Update examples, EBNF sections, syntax tables, and reference sections in all affected files + +### 5. Update playground examples and completions +- Read `packages/playground/src/examples.ts` +- Update ALL relevant examples to demonstrate the new syntax consistently +- Add new example if the feature warrants one +- Read `packages/playground/src/monaco/ec-completion.ts` +- Update annotation suggestions and keyword completions to match the grammar changes + +### 6. Update SDK DSL functions and tests +- Check `packages/sdk/src/dsl/` for functions that generate or parse DSL syntax +- Check `packages/sdk/src/test/dsl.test.ts` for DSL-related tests +- Ensure any DSL output or parsing reflects the grammar changes made above + +### 7. Verify the Langium grammar matches the spec +- After any spec change, read `packages/language-server/src/ec.langium` and confirm the grammar is consistent with what the spec now describes +- If they diverge, flag it to the user — the grammar may need updating too +- Run: `pnpm --filter @eventcatalog/language-server run test --run` + +### 8. Format and verify +- Run `pnpm run format` +- Run tests again to confirm everything passes + +## Rules +- **Grammar first** — always update grammar before markdown/examples +- **Test coverage** — every grammar change MUST have tests +- **Be thorough** — update ALL affected files, not just some +- **Consistency** — ensure syntax is consistent across ALL playground examples +- **EBNF sync** — keep EBNF notation in spec markdown in sync with Langium grammar +- **Note breaking changes** — flag if changes break existing syntax + +## Output Format +1. State what's changing and why +2. Show grammar changes +3. Show test additions/updates and run results +4. List all spec files updated +5. Show playground example updates +6. Confirm formatting completed + +**IMPORTANT:** If you discover issues or inconsistencies in the specification that are outside the scope of the current change, do NOT fix them without first verifying with the user. Only make changes directly related to the requested spec change. + +Ask clarifying questions if you need to know more details. diff --git a/.claude/skills/spec-change/references/spec-files.md b/.claude/skills/spec-change/references/spec-files.md new file mode 100644 index 000000000..e016c6cea --- /dev/null +++ b/.claude/skills/spec-change/references/spec-files.md @@ -0,0 +1,49 @@ +# DSL Specification File Locations + +## Grammar +- `packages/language-server/src/ec.langium` — Langium grammar definition + +## Tests +- `packages/language-server/src/test/` or `packages/language-server/__tests__/` — Test directory +- Framework: vitest +- Run: `pnpm --filter @eventcatalog/language-server run test --run` + +## Specification Markdown +| File | Content | +|------|---------| +| `00-overview.md` | High-level overview | +| `01-domain.md` | Domain syntax | +| `02-service.md` | Service syntax | +| `03-event.md` | Event syntax | +| `04-command.md` | Command syntax | +| `05-query.md` | Query syntax | +| `06-channel.md` | Channel syntax | +| `07-entity.md` | Entity syntax | +| `08-container.md` | Container syntax | +| `09-data-product.md` | Data product syntax | +| `14-relationships.md` | Relationship/pointer syntax | +| `15-versioning.md` | Version syntax | +| `17-examples.md` | Example updates | +| `18-grammar.md` | Grammar reference (EBNF) | +| `19-visualizer.md` | Visualizer syntax | + +Path: `packages/language-server/specification/` + +## SDK DSL Functions +- `packages/sdk/src/dsl/` — DSL generation/parsing functions +- `packages/sdk/src/test/dsl.test.ts` — DSL-related tests + +## Playground Examples +- `packages/playground/src/examples.ts` + +## Playground Monaco Completions +- `packages/playground/src/monaco/ec-completion.ts` — Autocomplete suggestions for annotations and keywords in the playground editor + +## Language Server Completions +- `packages/language-server/src/ec-completion-provider.ts` — Autocomplete suggestions for the language server (KNOWN_ANNOTATIONS, keyword sets) + +## DSL Conventions +- Version syntax: `@` prefix (e.g., `resource@1.0.0`) +- Lists: comma-separated (e.g., `to ChannelA, ChannelB`) +- Optional blocks: `{...}` only when needed +- Prefer inline syntax over blocks diff --git a/.claude/skills/vercel-react-best-practices b/.claude/skills/vercel-react-best-practices new file mode 120000 index 000000000..e567923b3 --- /dev/null +++ b/.claude/skills/vercel-react-best-practices @@ -0,0 +1 @@ +../../.agents/skills/vercel-react-best-practices \ No newline at end of file diff --git a/.claude/skills/visualiser-performance/SKILL.md b/.claude/skills/visualiser-performance/SKILL.md new file mode 100644 index 000000000..c78c8e321 --- /dev/null +++ b/.claude/skills/visualiser-performance/SKILL.md @@ -0,0 +1,142 @@ +--- +name: visualiser-performance +description: React Flow performance rules and review checklist for the @eventcatalog/visualiser package. Automatically applies when making changes to any file under packages/visualiser/. Use this skill to audit, review, or implement visualiser code with performance in mind. +globs: + - packages/visualiser/**/*.tsx + - packages/visualiser/**/*.ts +--- + +# Visualiser Performance Rules + +When modifying any code in `packages/visualiser/`, follow these rules to avoid React Flow performance regressions. A single unoptimized line can cause all nodes to re-render on every drag tick, dropping FPS from 60 to 2. + +## Rule 1: Never pass unstable references to `` props + +All props on `` must be referentially stable: + +- **Objects/arrays**: Define outside the component or wrap in `useMemo` with stable deps +- **Functions**: Wrap in `useCallback` with stable deps +- **NEVER** pass anonymous functions (`onClick={() => {}}`) or inline objects directly + +```tsx +// BAD - anonymous function causes ALL nodes to re-render on every state change + {}} /> + +// GOOD +const handleNodeClick = useCallback(() => {}, []); + +``` + +`nodeTypes` and `edgeTypes` must be memoized with `useMemo(() => ..., [])` or defined outside the component. These are currently correct in `NodeGraph.tsx`. + +## Rule 2: Never depend on `nodes`/`edges` arrays for structural data + +The `nodes` and `edges` arrays from `useNodesState`/`useEdgesState` get new references on every position change (drag). If you put them in a `useMemo`/`useEffect` dependency array, that code runs on every drag tick. + +**Pattern: Use stable structural keys** + +When you only care about which nodes exist (not their positions), derive a stable key using `useRef`: + +```tsx +// Stable key - only changes when nodes are added/removed +const nodeIdsKeyRef = useRef(""); +const computedKey = nodes.map((n) => n.id).join(","); +if (computedKey !== nodeIdsKeyRef.current) { + nodeIdsKeyRef.current = computedKey; +} +const nodeIdsKey = nodeIdsKeyRef.current; + +// Now use nodeIdsKey instead of nodes in deps +const searchNodes = useMemo(() => nodes, [nodeIdsKey]); +``` + +For edges, include source/target in the key: +```tsx +const edgeKey = edges.map((e) => `${e.source}-${e.target}`).join(","); +``` + +**Never do this:** +```tsx +// BAD - runs on every drag tick +useEffect(() => { /* expensive work */ }, [nodes, edges]); + +// BAD - filter runs on every position change +const selected = useMemo(() => nodes.filter(n => n.selected), [nodes]); +``` + +## Rule 3: Always wrap custom nodes and edges in `memo()` + +Every custom node and edge component MUST be wrapped in `React.memo`. This is the single most impactful optimization — it prevents node content from re-rendering during drag even if parent state changes. + +```tsx +// GOOD +export default memo(function MyNode(props: NodeProps) { + return
...
; +}); +``` + +All current node components (`ServiceNode`, `EventNode`, `CommandNode`, `QueryNode`, `ChannelNode`, `DataNode`, `ViewNode`, `ActorNode`, `NoteNode`, `ExternalSystem`, `Custom`, `Entity`, `Step`, `Domain`, `Flow`, `DataProduct`, `User`) are correctly wrapped. + +All edge components (`AnimatedMessageEdge`, `MultilineEdgeLabel`, `FlowEdge`) are correctly wrapped. + +**Do not break this pattern when adding new node or edge types.** + +## Rule 4: Memoize heavy sub-components inside nodes + +If a node renders complex sub-components (data grids, forms, SVG animations), wrap those in `memo()` too. This prevents the inner content from re-rendering even when the node itself re-renders. + +```tsx +// Sub-components with static or rarely-changing props should be memoized +const GlowHandle = memo(function GlowHandle({ side }: { side: "left" | "right" }) { + return
; +}); +``` + +Currently memoized sub-components: `GlowHandle` (in ServiceNode, EventNode, CommandNode, QueryNode), `MiniEnvelope`, `ServiceMessageFlow` (in ServiceNode). + +## Rule 5: Avoid `useStore` selectors that return new references + +If using ReactFlow's `useStore` (or any Zustand store), never return arrays/objects that get recreated on every state change: + +```tsx +// BAD - new array reference on every state update +const selected = useStore(state => state.nodes.filter(n => n.selected)); + +// GOOD - extract primitive values, or use useShallow +const selectedIds = useStore( + state => state.nodes.filter(n => n.selected).map(n => n.id) +); +// With useShallow for object/array returns +import { useShallow } from 'zustand/react/shallow'; +const [a, b] = useStore(useShallow(state => [state.a, state.b])); +``` + +## Checklist for PR review + +When reviewing visualiser changes, verify: + +- [ ] No anonymous functions or inline objects passed to `` props +- [ ] No `useMemo`/`useEffect`/`useCallback` with `nodes` or `edges` in deps (use structural keys instead) +- [ ] New custom nodes/edges are wrapped in `memo()` +- [ ] Heavy sub-components inside nodes are wrapped in `memo()` +- [ ] No `useStore` selectors returning unstable references +- [ ] `nodeTypes`/`edgeTypes` remain memoized with empty deps + +## Key files + +| File | What to check | +|------|--------------| +| `src/components/NodeGraph.tsx` | ReactFlow props, structural keys, legend computation | +| `src/components/StepWalkthrough.tsx` | Effect dependencies use stable keys | +| `src/components/VisualiserSearch.tsx` | Search filtering uses stable node snapshot | +| `src/components/FocusMode/FocusModeContent.tsx` | Focus graph calculation deps | +| `src/nodes/*/` | All node components wrapped in memo() | +| `src/edges/*/` | All edge components wrapped in memo() | + +## Reference + +Based on: "The Ultimate Guide to Optimize React Flow Project Performance" by Lukasz Jazwa. Key benchmarks from that article (100 nodes): +- Anonymous function on ReactFlow prop: 60 FPS -> 10 FPS (default), 2 FPS (heavy) +- Node depending on full nodes array via useStore: 60 FPS -> 12 FPS +- Adding React.memo to nodes: recovers to 50-60 FPS even with non-optimal parent +- Memoizing heavy node content: recovers to 60 FPS stable diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 1e0b6cd75..000000000 --- a/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules/ -.changeset/ -.github/ -.vscode/ -images/ - -**/dist/ diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 000000000..b8106cdfb --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +node scripts/sync-core-docs.mjs +git add packages/core/docs diff --git a/.github/workflows/notify-discord.yml b/.github/workflows/notify-discord.yml new file mode 100644 index 000000000..112ed5e57 --- /dev/null +++ b/.github/workflows/notify-discord.yml @@ -0,0 +1,53 @@ +name: Discord Release Notification + +on: + workflow_dispatch: # Allow manual triggering for testing + release: + types: + - published + +jobs: + notify: + runs-on: ubuntu-latest + # Run on manual trigger OR when a core package release is published + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'release' && + startsWith(github.event.release.tag_name, '@eventcatalog/core@')) + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Get latest release info (manual trigger only) + if: github.event_name == 'workflow_dispatch' + id: release + env: + GH_TOKEN: ${{ github.token }} + run: | + # Fetch the latest @eventcatalog/core release specifically + RELEASE_JSON=$(gh api repos/${{ github.repository }}/releases \ + --jq '[.[] | select(.tag_name | startswith("@eventcatalog/core@"))] | first') + + # Extract fields + echo "tag=$(echo "$RELEASE_JSON" | jq -r '.tag_name')" >> $GITHUB_OUTPUT + echo "name=$(echo "$RELEASE_JSON" | jq -r '.name')" >> $GITHUB_OUTPUT + echo "url=$(echo "$RELEASE_JSON" | jq -r '.html_url')" >> $GITHUB_OUTPUT + + # Body needs special handling for multiline + echo "body<> $GITHUB_OUTPUT + echo "$RELEASE_JSON" | jq -r '.body' >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Send Discord notification + run: node packages/core/scripts/notify-discord-release.js + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} + RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || steps.release.outputs.tag }} + RELEASE_NAME: ${{ github.event_name == 'release' && github.event.release.name || steps.release.outputs.name }} + RELEASE_BODY: ${{ github.event_name == 'release' && github.event.release.body || steps.release.outputs.body }} + RELEASE_URL: ${{ github.event_name == 'release' && github.event.release.html_url || steps.release.outputs.url }} diff --git a/.github/workflows/redeploy-eventcatalog-examples.yml b/.github/workflows/redeploy-eventcatalog-examples.yml new file mode 100644 index 000000000..ba613af80 --- /dev/null +++ b/.github/workflows/redeploy-eventcatalog-examples.yml @@ -0,0 +1,47 @@ +name: Redeploy EventCatalog Examples on core release +on: + workflow_run: + workflows: ["Release"] + types: + - completed +jobs: + redeploy-finance: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_commit.message, 'Version Packages') + steps: + - name: Redeploy EventCatalog Finance Example Catalog + run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_FINANCE_EXAMPLE_CATALOG_URL }} + redeploy-healthcare: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_commit.message, 'Version Packages') + steps: + - name: Redeploy EventCatalog Healthcare Example Catalog + run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_HEALTHCARE_EXAMPLE_CATALOG_URL }} + redeploy-demo: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_commit.message, 'Version Packages') + steps: + - name: Redeploy EventCatalog Demo (FlowMart) Example Catalog + run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_DEMO_FLOWMART_CATALOG_URL }} + redeploy-saas: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_commit.message, 'Version Packages') + steps: + - name: Redeploy EventCatalog Demo (SaaS) Example Catalog + run: curl -f -X POST "$VERCEL_DEPLOY_HOOK_URL" + env: + VERCEL_DEPLOY_HOOK_URL: ${{ secrets.VERCEL_DEPLOY_HOOK_SAAS_EXAMPLE_CATALOG_URL }} \ No newline at end of file diff --git a/.github/workflows/release-vscode.yml b/.github/workflows/release-vscode.yml new file mode 100644 index 000000000..a627d4977 --- /dev/null +++ b/.github/workflows/release-vscode.yml @@ -0,0 +1,60 @@ +name: Release VS Code Extension +on: + workflow_dispatch: + inputs: + version: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + if: github.repository == 'event-catalog/eventcatalog' + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Use Node.js 24.x + uses: actions/setup-node@v4 + with: + node-version: 24.x + cache: 'pnpm' + + - name: Install deps + run: pnpm i + + - name: Bump version + working-directory: packages/vscode-extension + run: npm version ${{ inputs.version }} --no-git-tag-version + + - name: Build all packages + run: pnpm run build:bin + + - name: Publish to VS Code Marketplace + working-directory: packages/vscode-extension + run: npx @vscode/vsce publish --no-dependencies + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + + - name: Get version + id: version + working-directory: packages/vscode-extension + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + + - name: Commit version bump + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add packages/vscode-extension/package.json + git commit -m "chore: release vscode extension v${{ steps.version.outputs.version }}" + git push diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b8ff674e..2150974cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,12 +26,16 @@ jobs: with: node-version: 24.x cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' - name: Install deps run: pnpm i + - name: Build all packages + run: pnpm run build:bin + - name: Create Release Pull Request or Publish to npm - uses: changesets/action@master + uses: changesets/action@v1 with: publish: pnpm run release env: diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index dd6531e0f..7f2847d2a 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -8,8 +8,8 @@ on: - main jobs: - build-cli: - name: Build cli + build-and-test: + name: Build and Test All Packages runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -22,24 +22,64 @@ jobs: cache: 'pnpm' - name: Installation run: pnpm i - - name: Test + - name: Turbo Cache + uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ github.sha }} + restore-keys: | + turbo-${{ runner.os }}- + - name: Test All Packages run: pnpm run test:ci - - name: Build cli + - name: Build All Packages run: pnpm run build:bin - - name: Package + - name: Package SDK + run: pnpm pack + working-directory: packages/sdk + - name: Package CLI + run: pnpm pack + working-directory: packages/cli + - name: Package Core + run: pnpm pack + working-directory: packages/core + - name: Package Linter run: pnpm pack - - name: Rename package artifact - run: mv *.tgz package.tgz # Rename to a consistent name - - name: Upload package artifact + working-directory: packages/linter + - name: Package Connectors + run: pnpm pack + working-directory: packages/connectors + - name: Package Visualiser + run: pnpm pack + working-directory: packages/visualiser + - name: Package Language Server + run: pnpm pack + working-directory: packages/language-server + - name: Rename package artifacts + run: | + mv packages/sdk/*.tgz sdk-package.tgz + mv packages/cli/*.tgz cli-package.tgz + mv packages/core/*.tgz core-package.tgz + mv packages/linter/*.tgz linter-package.tgz + mv packages/connectors/*.tgz connectors-package.tgz + mv packages/visualiser/*.tgz visualiser-package.tgz + mv packages/language-server/*.tgz language-server-package.tgz + - name: Upload package artifacts uses: actions/upload-artifact@v4 with: - name: eventcatalog-core-package-artifact - path: package.tgz + name: eventcatalog-packages-artifact + path: | + sdk-package.tgz + cli-package.tgz + core-package.tgz + linter-package.tgz + connectors-package.tgz + visualiser-package.tgz + language-server-package.tgz build-eventcatalog: name: Build EventCatalog timeout-minutes: 10 - needs: build-cli + needs: build-and-test strategy: matrix: os: [ubuntu-latest, windows-latest] @@ -50,13 +90,24 @@ jobs: uses: actions/setup-node@v4 with: node-version: 24.x - - name: Download package artifact + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }}-${{ hashFiles('examples/default/package-lock.json') }} + restore-keys: npm-${{ runner.os }}- + - name: Download package artifacts uses: actions/download-artifact@v4 with: - name: eventcatalog-core-package-artifact - - name: Install @eventcatalog-core + name: eventcatalog-packages-artifact + - name: Install @eventcatalog packages working-directory: examples/default/ - run: npm install ../../package.tgz + run: npm install ../../sdk-package.tgz ../../core-package.tgz ../../linter-package.tgz ../../connectors-package.tgz ../../visualiser-package.tgz - name: Build EventCatalog working-directory: examples/default/ run: npx eventcatalog build + - name: Check build size + if: matrix.os == 'ubuntu-latest' + run: node packages/core/scripts/check-build-size.js + env: + SIZE_THRESHOLD: 5 diff --git a/.gitignore b/.gitignore index 326a6a93e..0ada19382 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # build output dist/ +# turborepo +.turbo/ + # dependencies node_modules/ @@ -22,14 +25,19 @@ pnpm-debug.log* # EventCatalog files **/.eventcatalog-core/ +**/.eventcatalog/ /examples/large-catalog/* -eventcatalog/pnpm-lock.yaml -eventcatalog/public/pagefind +packages/core/eventcatalog/pnpm-lock.yaml +packages/core/eventcatalog/public/pagefind +/packages/core/eventcatalog/src/pages/api/[...auth].ts .vscode/* git-push.sh +# local-only Claude skill (Discord webhook automation) +.claude/skills/release-discord-summary/ + /test-results/ /playwright-report/ /blob-report/ @@ -37,10 +45,39 @@ git-push.sh src/__tests__/example-catalog-dependencies/dependencies -eventcatalog/public/ai +**/__tests__/catalog/ + +packages/core/eventcatalog/public/ai examples/default/public/ai +examples/default/eventcatalog.chat.js +examples/default/exports/ **/[...auth].ts +.astro/ + dev-scripts/ -examples/e-commerce \ No newline at end of file +examples/e-commerce + +.claude/agents/eventcatalog-blog-writer.md +pr-descriptions/ +.claude/commands/ship.md +.claude/skills/release-discord-summary/SKILL.md +.claude/skills/release-discord-summary/scripts/post-to-discord.sh + + +PRD.md +progress.txt +ralph-once.sh +terms/ + +notes/* +PERFORMANCE-ANALYSIS.md + + +.agents/ +ec-test/ + +docs/superpowers/* +.worktrees/ +.pnpm-store/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 3686cbaff..000000000 --- a/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -dist -node_modules -.astro/ -dist -eventcatalog.config.js -examples/ - diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..7e3e8ffad --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,54 @@ +# Repository Guidelines + +## Project Structure & Module Organization +This repository is a `pnpm` + Turborepo monorepo. Packages live in `packages/*`: +- `packages/core`: main EventCatalog app (Astro + React) and local scripts. +- `packages/cli`, `packages/sdk`, `packages/linter`, `packages/language-server`, `packages/visualiser`, `packages/create-eventcatalog`. +- `packages/playground`: local DSL playground app. + +Examples and integration checks live in `examples/` (notably `examples/default`). Shared release metadata is in `.changeset/`. + +## Build, Test, and Development Commands +Run from repo root unless noted: +- `pnpm i`: install workspace dependencies. +- `pnpm build:bin`: build distributable artifacts across packages. +- `pnpm build`: run full Turbo build graph. +- `pnpm test`: run package tests. +- `pnpm test:ci`: CI-style test run used by GitHub Actions. +- `pnpm format` / `pnpm format:diff`: format or check formatting. +- `pnpm start:catalog`: run local catalog via `@eventcatalog/core`. +- `pnpm --filter @eventcatalog/ run diff --git a/eventcatalog/src/components/EnvironmentDropdown.tsx b/eventcatalog/src/components/EnvironmentDropdown.tsx deleted file mode 100644 index 8cb37ae5e..000000000 --- a/eventcatalog/src/components/EnvironmentDropdown.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; - -interface Environment { - name: string; - url: string; - description?: string; - shortName?: string; -} - -interface EnvironmentDropdownProps { - environments: Environment[]; -} - -export const EnvironmentDropdown: React.FC = ({ environments }) => { - const [isOpen, setIsOpen] = useState(false); - const [currentEnvironment, setCurrentEnvironment] = useState(null); - const dropdownRef = useRef(null); - - useEffect(() => { - // Check if current URL matches any environment - const currentUrl = window.location.origin; - const matchedEnv = environments.find((env) => { - // Normalize URLs for comparison - const envUrl = new URL(env.url).origin; - return envUrl === currentUrl; - }); - setCurrentEnvironment(matchedEnv || null); - }, [environments]); - - useEffect(() => { - // Handle click outside - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsOpen(false); - } - }; - - // Handle escape key - const handleEscape = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - setIsOpen(false); - } - }; - - if (isOpen) { - document.addEventListener('click', handleClickOutside); - document.addEventListener('keydown', handleEscape); - } - - return () => { - document.removeEventListener('click', handleClickOutside); - document.removeEventListener('keydown', handleEscape); - }; - }, [isOpen]); - - const toggleDropdown = (e: React.MouseEvent) => { - e.stopPropagation(); - setIsOpen(!isOpen); - }; - - return ( -
- - -
- ); -}; diff --git a/eventcatalog/src/components/Grids/DomainGrid.tsx b/eventcatalog/src/components/Grids/DomainGrid.tsx deleted file mode 100644 index e71a18fc2..000000000 --- a/eventcatalog/src/components/Grids/DomainGrid.tsx +++ /dev/null @@ -1,390 +0,0 @@ -import { useState, useMemo, useEffect } from 'react'; -import { - ServerIcon, - EnvelopeIcon, - RectangleGroupIcon, - Squares2X2Icon, - QueueListIcon, - CircleStackIcon, -} from '@heroicons/react/24/outline'; -import { buildUrlWithParams, buildUrl } from '@utils/url-builder'; -import type { CollectionEntry } from 'astro:content'; -import { type CollectionMessageTypes } from '@types'; -import { getCollectionStyles } from './utils'; -import { SearchBar } from './components'; -import { BoxIcon } from 'lucide-react'; - -export interface ExtendedDomain extends CollectionEntry<'domains'> { - sends: CollectionEntry[]; - receives: CollectionEntry[]; - services: CollectionEntry<'services'>[]; - domains: CollectionEntry<'domains'>[]; -} - -interface DomainGridProps { - domains: ExtendedDomain[]; - embeded: boolean; -} - -export default function DomainGrid({ domains, embeded }: DomainGridProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [isMultiColumn, setIsMultiColumn] = useState(false); - - useEffect(() => { - if (typeof window !== 'undefined') { - const saved = localStorage.getItem('EventCatalog:ArchitectureColumnLayout'); - if (saved !== null) { - setIsMultiColumn(saved === 'multi'); - } - } - }, []); - - const toggleColumnLayout = () => { - const newValue = !isMultiColumn; - setIsMultiColumn(newValue); - if (typeof window !== 'undefined') { - localStorage.setItem('EventCatalog:ArchitectureColumnLayout', newValue ? 'multi' : 'single'); - } - }; - - const filteredDomains = useMemo(() => { - let result = [...domains]; - - // Filter by search query - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (domain) => - domain.data.name?.toLowerCase().includes(query) || - domain.data.summary?.toLowerCase().includes(query) || - domain.data.services?.some((service: any) => service.data.name.toLowerCase().includes(query)) || - domain.sends?.some((message: any) => message.data.name.toLowerCase().includes(query)) || - domain.receives?.some((message: any) => message.data.name.toLowerCase().includes(query)) - ); - } - - // Sort by name by default - result.sort((a, b) => (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id)); - - return result; - }, [domains, searchQuery]); - - return ( -
- {/* Breadcrumb */} - - -
-
-
-

- Domains ({filteredDomains.length}) -

-

Browse and manage domains in your event-driven architecture

-
- -
- - -
-
-
- -
- {filteredDomains.map((domain) => ( - s.data.id).join(','), - domainId: domain.data.id, - domainName: domain.data.name, - })} - className="group hover:bg-orange-100 border-2 border-orange-400/50 bg-yellow-50 rounded-lg shadow-sm hover:shadow-lg transition-all duration-200 overflow-hidden" - > -
-
-
- -

- {domain.data.name || domain.data.id} -

-
- - v{domain.data.version} - -
- -

- {domain.data.summary || No summary available} -

- -
-
- -
-

{domain.data.domains?.length || 0} Subdomains

-
-
-
- -
-

{domain.data.services?.length || 0} Services

-
-
-
- -
-

- {(domain.sends?.length || 0) + (domain.receives?.length || 0)} Messages -

-
-
- {domain.data.entities && domain.data.entities.length > 0 && ( -
- -
-

{domain.data.entities?.length} Entities

-
-
- )} -
- -
- {/* Subdomains and there services */} - {domain.data.domains?.slice(0, 2).map((subdomain: any) => ( -
-
-
- -

- {subdomain.data.name || subdomain.data.id} (Subdomain) -

-
- v{subdomain.data.version} -
- -
-
- -
-

{subdomain.data.services?.length || 0} Services

-
-
-
- -
-

- {(subdomain.sends?.length || 0) + (subdomain.receives?.length || 0)} Messages -

-
-
-
-
- ))} - - {/* Services and their messages */} - {domain.data.services?.slice(0, 2).map((service: any) => ( -
-
-
- -

{service.data.name || service.data.id}

-
- v{service.data.version} -
- -
-
-
- {service.data.receives?.slice(0, 3).map((message: any) => { - const { Icon, color } = getCollectionStyles(message.collection); - return ( -
-
- -
- {message.id} -
- ); - })} - {service.data.receives && service.data.receives.length > 3 && ( -
-

+ {service.data.receives.length - 3} more

-
- )} - {!service.data.receives?.length && ( -
-

No messages received

-
- )} -
-
- -
-
-
-
- -
-

{service.data.name || service.data.id}

-

v{service.data.version}

-
-
-
-
-
- -
-
- {service.data.sends?.slice(0, 3).map((message: any) => { - const { Icon, color } = getCollectionStyles(message.collection); - return ( -
-
- -
- - {message.id} -
- ); - })} - {service.data.sends && service.data.sends.length > 3 && ( -
-

+ {service.data.sends.length - 3} more

-
- )} - {!service.data.sends?.length && ( -
-

No messages sent

-
- )} -
-
-
- - {/* Container lists at the bottom */} - {((service.data.readsFrom && service.data.readsFrom.length > 0) || - (service.data.writesTo && service.data.writesTo.length > 0)) && ( -
- {/* Reads From */} - {service.data.readsFrom && service.data.readsFrom.length > 0 && ( -
-
- -

Reads from

-
-
- {service.data.readsFrom.slice(0, 3).map((container: any) => ( - - - {container.id} - - ))} - {service.data.readsFrom.length > 3 && ( - - + {service.data.readsFrom.length - 3} more - - )} -
-
- )} - - {/* Writes To */} - {service.data.writesTo && service.data.writesTo.length > 0 && ( -
-
- -

Writes to

-
-
- {service.data.writesTo.slice(0, 3).map((container: any) => ( - - - {container.id} - - ))} - {service.data.writesTo.length > 3 && ( - - + {service.data.writesTo.length - 3} more - - )} -
-
- )} -
- )} -
- ))} - {domain.data.domains && domain.data.domains.length > 2 && ( -
-
-
- -

+{domain.data.domains.length - 2} more subdomains

-
-
-
- )} - {domain.data.services && domain.data.services.length > 2 && ( -
-
-
- -

+{domain.data.services.length - 2} more services

-
-
-
- )} -
-
-
- ))} -
- - {filteredDomains.length === 0 && ( -
-

No domains found matching your criteria

-
- )} -
- ); -} diff --git a/eventcatalog/src/components/Grids/MessageGrid.tsx b/eventcatalog/src/components/Grids/MessageGrid.tsx deleted file mode 100644 index 97c77eab3..000000000 --- a/eventcatalog/src/components/Grids/MessageGrid.tsx +++ /dev/null @@ -1,577 +0,0 @@ -import { useState, useMemo, useEffect } from 'react'; -import { EnvelopeIcon, ChevronRightIcon, ServerIcon, CircleStackIcon } from '@heroicons/react/24/outline'; -import { RectangleGroupIcon } from '@heroicons/react/24/outline'; -import { buildUrl, buildUrlWithParams } from '@utils/url-builder'; -import type { CollectionEntry } from 'astro:content'; -import type { CollectionMessageTypes } from '@types'; -import { getCollectionStyles } from './utils'; -import { SearchBar, TypeFilters, Pagination } from './components'; - -interface MessageGridProps { - messages: CollectionEntry[]; - containers?: CollectionEntry<'containers'>[]; - embeded: boolean; - isVisualiserEnabled: boolean; -} - -interface GroupedMessages { - all?: CollectionEntry[]; - sends?: CollectionEntry[]; - receives?: CollectionEntry[]; -} - -export default function MessageGrid({ messages, embeded, containers, isVisualiserEnabled }: MessageGridProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [urlParams, setUrlParams] = useState<{ - serviceId?: string; - serviceName?: string; - domainId?: string; - domainName?: string; - } | null>(null); - const [currentPage, setCurrentPage] = useState(1); - const [selectedTypes, setSelectedTypes] = useState([]); - const [producerConsumerFilter, setProducerConsumerFilter] = useState<'all' | 'no-producers' | 'no-consumers'>('all'); - const ITEMS_PER_PAGE = 15; - - // Effect to sync URL params with state - useEffect(() => { - const params = new URLSearchParams(window.location.search); - const serviceId = params.get('serviceId') || undefined; - const serviceName = params.get('serviceName') ? decodeURIComponent(params.get('serviceName')!) : undefined; - const domainId = params.get('domainId') || undefined; - const domainName = params.get('domainName') || undefined; - setUrlParams({ - serviceId, - serviceName, - domainId, - domainName, - }); - }, []); - - const filteredAndSortedMessages = useMemo(() => { - if (urlParams === null) return []; - - let result = [...messages]; - - // Filter by message type - if (selectedTypes.length > 0) { - result = result.filter((message) => selectedTypes.includes(message.collection)); - } - - // Apply producer/consumer filters - if (producerConsumerFilter === 'no-producers') { - result = result.filter((message) => !message.data.producers || message.data.producers.length === 0); - } else if (producerConsumerFilter === 'no-consumers') { - result = result.filter((message) => !message.data.consumers || message.data.consumers.length === 0); - } - - // Filter by service ID or name if present - if (urlParams.serviceId) { - result = result.filter( - (message) => - message.data.producers?.some( - (producer: any) => producer.id === urlParams.serviceId && !producer.id.includes('/versioned/') - ) || - message.data.consumers?.some( - (consumer: any) => consumer.id === urlParams.serviceId && !consumer.id.includes('/versioned/') - ) - ); - } - - // Filter by search query - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (message) => - message.data.name?.toLowerCase().includes(query) || - message.data.summary?.toLowerCase().includes(query) || - message.data.producers?.some((producer: any) => producer.data.id?.toLowerCase().includes(query)) || - message.data.consumers?.some((consumer: any) => consumer.data.id?.toLowerCase().includes(query)) - ); - } - - // Sort by name by default - result.sort((a, b) => a.data.name.localeCompare(b.data.name)); - - return result; - }, [messages, searchQuery, urlParams, selectedTypes, producerConsumerFilter]); - - // Add totalPages calculation - const totalPages = useMemo(() => { - if (urlParams?.serviceId || urlParams?.domainId) return 1; - return Math.ceil(filteredAndSortedMessages.length / ITEMS_PER_PAGE); - }, [filteredAndSortedMessages.length, urlParams]); - - // Add paginatedMessages calculation - const paginatedMessages = useMemo(() => { - if (urlParams?.serviceId || urlParams?.domainId) { - return filteredAndSortedMessages; - } - - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - return filteredAndSortedMessages.slice(startIndex, startIndex + ITEMS_PER_PAGE); - }, [filteredAndSortedMessages, currentPage, urlParams]); - - // Reset pagination when search query or filters change - useEffect(() => { - setCurrentPage(1); - }, [searchQuery, selectedTypes]); - - // Group messages by sends/receives when a service is selected - const groupedMessages = useMemo(() => { - if (!urlParams?.serviceId) return { all: filteredAndSortedMessages }; - - const serviceIdentifier = urlParams.serviceId; - const sends = filteredAndSortedMessages.filter((message) => - message.data.producers?.some((producer: any) => producer.id === serviceIdentifier) - ); - const receives = filteredAndSortedMessages.filter((message) => - message.data.consumers?.some((consumer: any) => consumer.id === serviceIdentifier) - ); - - return { sends, receives }; - }, [filteredAndSortedMessages, urlParams]); - - // Get the containers that are referenced by the service - const serviceContainersReferenced = useMemo(() => { - if (!urlParams?.serviceId || !containers) return { writesTo: [], readsFrom: [] }; - return { - writesTo: containers.filter((container) => - container.data.servicesThatWriteToContainer?.some((service: any) => service.data.id === urlParams.serviceId) - ), - readsFrom: containers.filter((container) => - container.data.servicesThatReadFromContainer?.some((service: any) => service.data.id === urlParams.serviceId) - ), - }; - }, [containers, urlParams]); - - const renderTypeFilters = () => { - return ( -
-
- selectedTypes.includes(m.collection)).length} - /> -
- -
-
- - {producerConsumerFilter !== 'all' && ( - - )} -
-
-
- ); - }; - - const renderMessageGrid = (messages: CollectionEntry[]) => ( - - ); - - const renderPaginationControls = () => { - if (totalPages <= 1 || urlParams?.serviceName || urlParams?.domainId) return null; - - return ( - - ); - }; - - return ( -
- {/* Breadcrumb */} - - - {/* Title Section */} -
-
-
-
-

- {urlParams?.domainName ? `Messages in ${urlParams.serviceName}` : 'All Messages'} -

-
-

- {urlParams?.domainName - ? `Browse messages in the ${urlParams.serviceName} service` - : 'Browse and discover messages in your event-driven architecture'} -

-
- -
- -
-
-
- -
- {/* Results count and top pagination */} -
- {renderTypeFilters()} - {renderPaginationControls()} -
-
- - {filteredAndSortedMessages.length > 0 && ( -
- {urlParams?.domainName && ( - <> - - - )} - -
- {urlParams?.serviceName ? ( - <> - {/*
*/} - {/* Service Title */} -
- -

{urlParams.serviceName}

-
- {isVisualiserEnabled && ( - - View in visualizer - - )} - - Read documentation - -
-
-
- {/* Left Column - Receives Messages & Reads From Containers */} -
- {/* Receives Messages Section */} -
-
-

- - Receives ({groupedMessages.receives?.length || 0}) -

-
- {groupedMessages.receives && groupedMessages.receives.length > 0 ? ( - renderMessageGrid(groupedMessages.receives) - ) : ( -
-

No messages

-
- )} -
- - {/* Reads From Containers - Only show if containers exist */} - {serviceContainersReferenced.readsFrom && serviceContainersReferenced.readsFrom.length > 0 && ( -
-
-

- - Reads from ({serviceContainersReferenced.readsFrom.length}) -

-
-
- {serviceContainersReferenced.readsFrom.map((container: CollectionEntry<'containers'>) => ( - -
- -

- {container.data.name} -

-
- {container.data.summary && ( -

{container.data.summary}

- )} -
- ))} -
- {/* Arrow from Reads From to Service */} -
-
-
-
-
- )} -
- - {/* Arrow from Receives to Service */} -
-
-
-
- - {/* Service Information (Center) */} -
-
- -

{urlParams.serviceName}

- - {/* Quick Stats Grid */} -
-
-
{groupedMessages.receives?.length || 0}
-
Receives
-
-
-
{groupedMessages.sends?.length || 0}
-
Sends
-
- {serviceContainersReferenced.readsFrom && serviceContainersReferenced.readsFrom.length > 0 && ( -
-
- {serviceContainersReferenced.readsFrom.length} -
-
Reads from
-
- )} - {serviceContainersReferenced.writesTo && serviceContainersReferenced.writesTo.length > 0 && ( -
-
- {serviceContainersReferenced.writesTo.length} -
-
Writes to
-
- )} -
-
-
- - {/* Arrow from Service to Sends */} -
-
-
-
- - {/* Right Column - Sends Messages & Writes To Containers */} -
- {/* Sends Messages Section */} -
-
-

- - Sends ({groupedMessages.sends?.length || 0}) -

-
- {groupedMessages.sends && groupedMessages.sends.length > 0 ? ( - renderMessageGrid(groupedMessages.sends) - ) : ( -
-

No messages

-
- )} -
- - {/* Writes To Containers - Only show if containers exist */} - {serviceContainersReferenced.writesTo && serviceContainersReferenced.writesTo.length > 0 && ( -
- {/* Arrow from Service to Writes To */} -
-
-
-
-
-

- - Writes to ({serviceContainersReferenced.writesTo.length}) -

-
-
- {serviceContainersReferenced.writesTo.map((container: CollectionEntry<'containers'>) => ( - -
- -

- {container.data.name} -

-
- {container.data.summary && ( -

{container.data.summary}

- )} -
- ))} -
-
- )} -
-
- - ) : ( - <> - {renderMessageGrid(paginatedMessages)} -
{renderPaginationControls()}
- - )} -
-
- )} - - {filteredAndSortedMessages.length === 0 && ( -
-

No messages found matching your criteria

-
- )} -
- ); -} diff --git a/eventcatalog/src/components/Grids/ServiceGrid.tsx b/eventcatalog/src/components/Grids/ServiceGrid.tsx deleted file mode 100644 index 85f3b649c..000000000 --- a/eventcatalog/src/components/Grids/ServiceGrid.tsx +++ /dev/null @@ -1,540 +0,0 @@ -import { useState, useMemo, useEffect, memo } from 'react'; -import { ServerIcon, ChevronRightIcon, Squares2X2Icon, QueueListIcon, CircleStackIcon } from '@heroicons/react/24/outline'; -import { RectangleGroupIcon } from '@heroicons/react/24/outline'; -import { buildUrl, buildUrlWithParams } from '@utils/url-builder'; -import type { CollectionEntry } from 'astro:content'; -import type { CollectionMessageTypes } from '@types'; -import { getCollectionStyles } from './utils'; -import { SearchBar, TypeFilters, Pagination } from './components'; -import type { ExtendedDomain } from './DomainGrid'; -import { BoxIcon } from 'lucide-react'; - -// Message component for reuse -const Message = memo(({ message, collection }: { message: any; collection: string }) => { - const { Icon, color } = getCollectionStyles(message.collection); - return ( - -
- -
- {message.data.name} -
- ); -}); - -// Messages Container component -const MessagesContainer = memo( - ({ messages, type, selectedTypes }: { messages: any[]; type: 'receives' | 'sends'; selectedTypes: string[] }) => { - const bgColor = type === 'receives' ? 'blue' : 'green'; - const MAX_MESSAGES_DISPLAYED = 4; - - const filteredMessages = messages?.filter( - (message: any) => selectedTypes.length === 0 || selectedTypes.includes(message.collection) - ); - - const messagesToShow = filteredMessages?.slice(0, MAX_MESSAGES_DISPLAYED); - const remainingMessagesCount = filteredMessages ? filteredMessages.length - MAX_MESSAGES_DISPLAYED : 0; - - return ( -
-
- {messagesToShow?.map((message: any) => ( - - ))} - {remainingMessagesCount > 0 && ( -
-

+ {remainingMessagesCount} more

-
- )} - {(!messages?.length || - (selectedTypes.length > 0 && !messages?.some((message: any) => selectedTypes.includes(message.collection)))) && ( -
-

- {selectedTypes.length > 0 - ? `Service does not ${type} ${selectedTypes.join(' or ')}` - : `Service does not ${type} any messages`} -

-
- )} -
-
- ); - } -); - -// Service Card component -const ServiceCard = memo(({ service, urlParams, selectedTypes }: { service: any; urlParams: any; selectedTypes: string[] }) => { - return ( - -
-
-
- -

- {service.data.name || service.data.id} (v{service.data.version}) -

-
-
- - {service.data.summary &&

{service.data.summary}

} - - {!urlParams?.serviceName && ( -
- - -
-
-
-
- -
-

{service.data.name || service.data.id}

-

v{service.data.version}

-
-
-
-
-
- - -
- )} - - {/* Container lists at the bottom */} - {((service.data.readsFrom && service.data.readsFrom.length > 0) || - (service.data.writesTo && service.data.writesTo.length > 0)) && ( -
- {/* Reads From */} - {service.data.readsFrom && service.data.readsFrom.length > 0 && ( -
-
- -

Reads from

-
-
- {service.data.readsFrom.slice(0, 3).map((container: any) => ( - - - {container.data.name} - - ))} - {service.data.readsFrom.length > 3 && ( - - + {service.data.readsFrom.length - 3} more - - )} -
-
- )} - - {/* Writes To */} - {service.data.writesTo && service.data.writesTo.length > 0 && ( -
-
- -

Writes to

-
-
- {service.data.writesTo.slice(0, 3).map((container: any) => ( - - - {container.data.name} - - ))} - {service.data.writesTo.length > 3 && ( - - + {service.data.writesTo.length - 3} more - - )} -
-
- )} -
- )} -
- - ); -}); - -// Domain Section component -const DomainSection = memo( - ({ - domain, - services, - urlParams, - selectedTypes, - isMultiColumn, - isVisualiserEnabled, - }: { - domain: any; - services: any[]; - urlParams: any; - selectedTypes: string[]; - isMultiColumn: boolean; - isVisualiserEnabled: boolean; - }) => { - const subdomains = domain.data.domains || []; - const allSubDomainServices = subdomains.map((subdomain: any) => subdomain.data.services || []).flat(); - - const servicesWithoutSubdomains = services.filter((service) => { - return !allSubDomainServices.some((s: any) => s.id === service.data.id); - }); - - return ( -
- {servicesWithoutSubdomains.length > 0 && ( -
- {servicesWithoutSubdomains.map((service) => ( - - ))} -
- )} - - {subdomains.map((subdomainRef: any) => { - const subdomain = domain.data.domains?.find((d: any) => d.data.id === subdomainRef.data.id); - if (!subdomain) return null; - - const subdomainServices = services.filter((service) => - subdomain.data.services?.some((s: any) => s.id === service.data.id) - ); - - if (subdomainServices.length === 0) return null; - - return ( -
-
-
- -

{subdomain.data.name} (Subdomain)

-
-
- {isVisualiserEnabled && ( - - View in visualizer - - )} - - Read documentation - -
-
- - {/* Entities */} - {subdomain.data.entities && subdomain.data.entities.length > 0 && ( -
-
- -

Entities

-
-
- {subdomain.data.entities.map((entity: any) => ( - - - {entity.id} - - ))} -
-
- )} - -
- {subdomainServices.map((service) => ( - - ))} -
-
- ); - })} -
- ); - } -); - -interface ServiceGridProps { - services: CollectionEntry<'services'>[]; - domains: ExtendedDomain[]; - embeded: boolean; - isVisualiserEnabled: boolean; -} - -// Main ServiceGrid component -export default function ServiceGrid({ services, domains, embeded, isVisualiserEnabled }: ServiceGridProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [currentPage, setCurrentPage] = useState(1); - const [selectedTypes, setSelectedTypes] = useState([]); - const [isMultiColumn, setIsMultiColumn] = useState(false); - const ITEMS_PER_PAGE = 16; - const [urlParams, setUrlParams] = useState<{ - serviceIds?: string[]; - domainId?: string; - domainName?: string; - serviceName?: string; - } | null>(null); - - useEffect(() => { - const params = new URLSearchParams(window.location.search); - setUrlParams({ - serviceIds: params.get('serviceIds')?.split(',').filter(Boolean), - domainId: params.get('domainId') || undefined, - domainName: params.get('domainName') || undefined, - serviceName: params.get('serviceName') || undefined, - }); - }, []); - - useEffect(() => { - if (typeof window !== 'undefined') { - const saved = localStorage.getItem('EventCatalog:ServiceColumnLayout'); - if (saved !== null) { - setIsMultiColumn(saved === 'multi'); - } - } - }, []); - - const toggleColumnLayout = () => { - const newValue = !isMultiColumn; - setIsMultiColumn(newValue); - if (typeof window !== 'undefined') { - localStorage.setItem('EventCatalog:ServiceColumnLayout', newValue ? 'multi' : 'single'); - } - }; - - const filteredAndSortedServices = useMemo(() => { - if (urlParams === null) return []; - - let result = [...services]; - - if (urlParams.serviceIds?.length) { - result = result.filter( - (service) => urlParams.serviceIds?.includes(service.data.id) && !service.data.id.includes('/versioned/') - ); - } - - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (service) => - service.data.name?.toLowerCase().includes(query) || - service.data.summary?.toLowerCase().includes(query) || - service.data.sends?.some((message: any) => message.data.name.toLowerCase().includes(query)) || - service.data.receives?.some((message: any) => message.data.name.toLowerCase().includes(query)) - ); - } - - if (selectedTypes.length > 0) { - result = result.filter((service) => { - const hasMatchingSends = service.data.sends?.some((message: any) => selectedTypes.includes(message.collection)); - const hasMatchingReceives = service.data.receives?.some((message: any) => selectedTypes.includes(message.collection)); - return hasMatchingSends || hasMatchingReceives; - }); - } - - result.sort((a, b) => (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id)); - return result; - }, [services, searchQuery, urlParams, selectedTypes]); - - const paginatedServices = useMemo(() => { - if (urlParams?.domainId || urlParams?.serviceIds?.length) { - return filteredAndSortedServices; - } - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - return filteredAndSortedServices.slice(startIndex, startIndex + ITEMS_PER_PAGE); - }, [filteredAndSortedServices, currentPage, urlParams]); - - const totalPages = useMemo(() => { - if (urlParams?.domainId || urlParams?.serviceIds?.length) return 1; - return Math.ceil(filteredAndSortedServices.length / ITEMS_PER_PAGE); - }, [filteredAndSortedServices.length, urlParams]); - - useEffect(() => { - setCurrentPage(1); - }, [searchQuery, selectedTypes]); - - return ( -
- {/* Breadcrumb */} - - - {/* Title Section */} -
-
-
-
-

- {urlParams?.domainId ? `${urlParams.domainName} Architecture` : 'All Services'} -

-
-

- {urlParams?.domainId - ? `Browse services and messages in the ${urlParams.domainId} domain` - : 'Browse and discover services in your event-driven architecture'} -

-
- -
- - -
-
-
- -
- {/* Results count and pagination */} -
- -
- {urlParams?.domainId || urlParams?.serviceIds?.length ? ( - - Showing {filteredAndSortedServices.length} services in the {urlParams.domainId} domain - - ) : ( - - Showing {(currentPage - 1) * ITEMS_PER_PAGE + 1} to{' '} - {Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedServices.length)} of {filteredAndSortedServices.length}{' '} - services - - )} -
- {!(urlParams?.domainId || urlParams?.serviceIds?.length) && ( - - )} -
-
- - {filteredAndSortedServices.length > 0 && ( -
- {urlParams?.domainName ? ( - domains - .filter((domain: ExtendedDomain) => domain.data.id === urlParams.domainId) - .map((domain: ExtendedDomain) => ( - - )) - ) : ( -
- {paginatedServices.map((service) => ( - - ))} -
- )} -
- )} - - {filteredAndSortedServices.length === 0 && ( -
-

- {selectedTypes.length > 0 - ? `No services found that ${selectedTypes.length > 1 ? 'handle' : 'handles'} ${selectedTypes.join(' or ')} messages` - : 'No services found matching your criteria'} -

-
- )} - - {/* Bottom pagination */} - {!(urlParams?.domainId || urlParams?.serviceIds?.length) && ( -
- -
- )} -
- ); -} diff --git a/eventcatalog/src/components/Grids/components.tsx b/eventcatalog/src/components/Grids/components.tsx deleted file mode 100644 index 7050fa476..000000000 --- a/eventcatalog/src/components/Grids/components.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React from 'react'; -import { - MagnifyingGlassIcon, - ChevronLeftIcon, - ChevronRightIcon, - ChevronDoubleLeftIcon, - ChevronDoubleRightIcon, -} from '@heroicons/react/24/outline'; -import type { CollectionMessageTypes } from '@types'; -import { getCollectionStyles, type PaginationProps, type SearchBarProps, type TypeFilterProps } from './utils'; - -export function SearchBar({ searchQuery, onSearchChange, placeholder, totalResults, totalItems }: SearchBarProps) { - return ( -
-
-
-
- onSearchChange(e.target.value)} - className="block w-full rounded-lg border-0 py-2.5 pl-10 pr-4 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6" - /> - {searchQuery && ( -
- -
- )} -
- {searchQuery && totalResults !== undefined && totalItems !== undefined && ( -
- - Found {totalResults} of{' '} - {totalItems} - - ESC to clear -
- )} -
- ); -} - -export function TypeFilters({ selectedTypes, onTypeChange, filteredCount, totalCount }: TypeFilterProps) { - const types: CollectionMessageTypes[] = ['events', 'commands', 'queries']; - - return ( -
- {types.map((type) => { - const { color, Icon } = getCollectionStyles(type); - const isSelected = selectedTypes.includes(type); - return ( - - ); - })} - {selectedTypes.length > 0 && ( - - )} -
- ); -} - -export function Pagination({ currentPage, totalPages, totalItems, itemsPerPage, onPageChange }: PaginationProps) { - if (totalPages <= 1) return null; - - return ( -
-
- - -
-
-
-

- Showing {(currentPage - 1) * itemsPerPage + 1} to{' '} - {Math.min(currentPage * itemsPerPage, totalItems)} of{' '} - {totalItems} results -

-
-
- -
-
-
- ); -} diff --git a/eventcatalog/src/components/Header.astro b/eventcatalog/src/components/Header.astro deleted file mode 100644 index eb0519b4c..000000000 --- a/eventcatalog/src/components/Header.astro +++ /dev/null @@ -1,234 +0,0 @@ ---- -import catalog from '@utils/eventcatalog-config/catalog'; -import Search from '@components/Search/Search.astro'; -import { buildUrl } from '@utils/url-builder'; -import { showEventCatalogBranding, showCustomBranding } from '@utils/feature'; -import { getSession } from 'auth-astro/server'; -import { isAuthEnabled, isSSR } from '@utils/feature'; -import { EnvironmentDropdown } from './EnvironmentDropdown'; - -let session = null; -if (isAuthEnabled()) { - session = await getSession(Astro.request); -} - -const logo = { - src: ('/' + (catalog?.logo?.src || 'logo.png')).replace(/^\/+/, '/'), - alt: catalog?.logo?.alt || 'Event Catalog', - text: catalog?.logo?.text || 'EventCatalog', -}; - -const repositoryUrl = catalog?.repositoryUrl || 'https://github.com/event-catalog/eventcatalog'; ---- - - - -
- - - - - diff --git a/eventcatalog/src/components/Lists/CustomSideBarSectionList.astro b/eventcatalog/src/components/Lists/CustomSideBarSectionList.astro deleted file mode 100644 index 1983be736..000000000 --- a/eventcatalog/src/components/Lists/CustomSideBarSectionList.astro +++ /dev/null @@ -1,55 +0,0 @@ ---- -import { buildUrl } from '@utils/url-builder'; -import { getCollection } from 'astro:content'; -import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util'; -import PillListFlat from './PillListFlat'; -import { resourceToCollectionMap } from '@utils/collections/util'; -interface Props { - section?: { - title?: string; - limit?: number; - items: { - id: string; - type: string; - version?: string; - }[]; - }; -} - -const { section } = Astro.props; -const title = section?.title || 'Custom Section'; -const limit = section?.limit || 10; -const sectionItems = section?.items || []; - -// Array to store resolved related resources -const resolvedResources = []; - -// Fetch related resources -for (const resource of sectionItems) { - try { - // Use type assertion to ensure TypeScript understands this is a valid collection name - const collectionName = resourceToCollectionMap[resource.type as keyof typeof resourceToCollectionMap]; - - // Use getEntry instead of getCollection for single item lookup by ID - const allItemsInCollection = await getCollection(collectionName); - // @ts-ignore - const entryToMatchVersion = getItemsFromCollectionByIdAndSemverOrLatest(allItemsInCollection, resource.id, resource.version); - const entry = entryToMatchVersion[0]; - - if (entry) { - resolvedResources.push(entry); - } - } catch (error) { - console.error(`Failed to fetch related resource: ${resource.id} of type ${resource.type}`, error); - } -} - -const sectionList = resolvedResources.map((p: any) => ({ - label: `${p.data.name}`, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); ---- - - diff --git a/eventcatalog/src/components/Lists/OwnersList.tsx b/eventcatalog/src/components/Lists/OwnersList.tsx deleted file mode 100644 index ffbb4cbea..000000000 --- a/eventcatalog/src/components/Lists/OwnersList.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'; -import { ChevronDownIcon } from '@heroicons/react/20/solid'; -import { UserGroupIcon, UserIcon } from '@heroicons/react/24/outline'; - -interface Props { - title: string; - owners: { - label: string; - badge?: string; - type: string; - avatarUrl?: string; - href: string; - }[]; - emptyMessage: string; -} - -const OwnersList = ({ title, owners, emptyMessage }: Props) => { - return ( -
-
- - - {title} - - - - - - -
-
-
- ); -}; - -export default OwnersList; diff --git a/eventcatalog/src/components/Lists/PillListFlat.styles.css b/eventcatalog/src/components/Lists/PillListFlat.styles.css deleted file mode 100644 index 81033879e..000000000 --- a/eventcatalog/src/components/Lists/PillListFlat.styles.css +++ /dev/null @@ -1,8 +0,0 @@ -.tooltip { - visibility: hidden; - position: absolute; - } - .has-tooltip:hover .tooltip { - visibility: visible; - z-index: 100; - } \ No newline at end of file diff --git a/eventcatalog/src/components/Lists/ProtocolList.tsx b/eventcatalog/src/components/Lists/ProtocolList.tsx deleted file mode 100644 index 108c61acc..000000000 --- a/eventcatalog/src/components/Lists/ProtocolList.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'; -import { ChevronDownIcon } from '@heroicons/react/20/solid'; -import { getIconForProtocol } from '@utils/protocols'; - -import './PillListFlat.styles.css'; - -interface Props { - title: string; - color: string; - icon?: any; - pills: { - label: string; - badge?: string; - href?: string; - tag?: string; - color?: string; - collection?: string; - description?: string; - icon?: string; - }[]; - emptyMessage?: string; -} - -const ProtocolList = ({ title, pills, emptyMessage, color = 'gray', ...props }: Props) => { - return ( -
-
- - - {title} - - - - - - -
-
- ); -}; - -export default ProtocolList; diff --git a/eventcatalog/src/components/Lists/RepositoryList.astro b/eventcatalog/src/components/Lists/RepositoryList.astro deleted file mode 100644 index 282b9d854..000000000 --- a/eventcatalog/src/components/Lists/RepositoryList.astro +++ /dev/null @@ -1,37 +0,0 @@ ---- -import { ScrollText, Workflow, FileDownIcon, Code, Link } from 'lucide-react'; - -interface Props { - repository?: string; - language?: string; -} - -const { repository, language } = Astro.props; ---- - -
- Repository -
    - { - repository && ( -
  • - - - {repository} - -
  • - ) - } - { - language && ( -
  • -
    - - {language} -
    -
  • - ) - } -
- -
diff --git a/eventcatalog/src/components/Lists/SpecificationsList.astro b/eventcatalog/src/components/Lists/SpecificationsList.astro deleted file mode 100644 index de23978c4..000000000 --- a/eventcatalog/src/components/Lists/SpecificationsList.astro +++ /dev/null @@ -1,67 +0,0 @@ ---- -import type { CollectionTypes } from '@types'; -import { buildUrl } from '@utils/url-builder'; -import type { CollectionEntry } from 'astro:content'; -import { getSpecificationsForService } from '@utils/collections/services'; -import type { Service } from '@utils/collections/services'; -interface Props { - collectionItem: CollectionEntry; -} - -const { collectionItem } = Astro.props; - -const specVersions = collectionItem.data.specifications || {}; -const numberOfSpecifications = Object.keys(specVersions).length; - -const specs = getSpecificationsForService(collectionItem as Service); - -const openAPISpecifications = specs.filter((spec) => spec.type === 'openapi'); -const asyncAPISpecifications = specs.filter((spec) => spec.type === 'asyncapi'); -const graphQLSpecifications = specs.filter((spec) => spec.type === 'graphql'); ---- - -
- Specifications ({numberOfSpecifications}) - { - openAPISpecifications.length > 0 && - openAPISpecifications.map((spec) => ( - - - {spec.name} - - )) - } - { - asyncAPISpecifications.length > 0 && - asyncAPISpecifications.map((spec) => ( - - - {spec.name} - - )) - } - { - graphQLSpecifications.length > 0 && - graphQLSpecifications.map((spec) => ( - - - {spec.name} - - )) - } -
diff --git a/eventcatalog/src/components/Lists/VersionList.astro b/eventcatalog/src/components/Lists/VersionList.astro deleted file mode 100644 index da7a076c9..000000000 --- a/eventcatalog/src/components/Lists/VersionList.astro +++ /dev/null @@ -1,50 +0,0 @@ ---- -import type { CollectionTypes } from '@types'; -import { buildUrl } from '@utils/url-builder'; -import type { CollectionEntry } from 'astro:content'; -import { HistoryIcon } from 'lucide-react'; - -interface Props { - title?: string; - versions: string[]; - collectionItem: CollectionEntry; -} - -const { versions, collectionItem, title } = Astro.props; -const currentPath = Astro.url.pathname; ---- - -
- - {title || `Versions (${collectionItem.data.versions?.length})`} - - -
-
- - diff --git a/eventcatalog/src/components/MDX/Accordion/Accordion.tsx b/eventcatalog/src/components/MDX/Accordion/Accordion.tsx deleted file mode 100644 index 08442aa8a..000000000 --- a/eventcatalog/src/components/MDX/Accordion/Accordion.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'; -import { MinusIcon } from '@heroicons/react/16/solid'; -import { PlusIcon } from '@heroicons/react/24/outline'; -import { useEffect } from 'react'; - -declare global { - interface Window { - renderDiagrams?: (graphs: HTMLCollectionOf) => void; - renderPlantUML?: (graphs: HTMLCollectionOf) => void; - } -} - -export default function Example({ title, children }: any) { - return ( -
- - {({ open }) => { - useEffect(() => { - if (open) { - const graphs = document.getElementsByClassName('mermaid'); - const plantUML = document.getElementsByClassName('plantuml'); - if (graphs.length > 0 && window.renderDiagrams) { - window.renderDiagrams(graphs); - } - if (plantUML.length > 0 && window.renderPlantUML) { - window.renderPlantUML(plantUML); - } - } - }, [open]); - - return ( -
- - {title} - - {open ? ( - - - -
{children}
-
-
- ); - }} -
-
- ); -} diff --git a/eventcatalog/src/components/MDX/Accordion/AccordionGroup.astro b/eventcatalog/src/components/MDX/Accordion/AccordionGroup.astro deleted file mode 100644 index 12a8d23f4..000000000 --- a/eventcatalog/src/components/MDX/Accordion/AccordionGroup.astro +++ /dev/null @@ -1,14 +0,0 @@ ---- - ---- - -
- -
- - diff --git a/eventcatalog/src/components/MDX/Admonition.tsx b/eventcatalog/src/components/MDX/Admonition.tsx deleted file mode 100644 index db9233b33..000000000 --- a/eventcatalog/src/components/MDX/Admonition.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { InformationCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'; - -const getConfigurationByType = (type: string) => { - switch (type) { - case 'danger': - return { color: 'red', icon: ExclamationTriangleIcon, title: 'Danger' }; - case 'alert': - return { color: 'red', icon: ExclamationTriangleIcon, title: 'Alert' }; - case 'warning': - return { color: 'yellow', icon: ExclamationTriangleIcon, title: 'Warning' }; - default: - return { color: 'indigo', icon: InformationCircleIcon, title: 'Info' }; - } -}; - -interface AdmonitionProps { - children: React.ReactNode; - type?: string; - className?: string; - title?: string; -} - -export default function Admonition({ children, type = 'info', className = '', title }: AdmonitionProps) { - const config = getConfigurationByType(type); - const Icon = config.icon; - - return ( -
-
-
-
-
{children}
-
-
- ); -} diff --git a/eventcatalog/src/components/MDX/Attachments.astro b/eventcatalog/src/components/MDX/Attachments.astro deleted file mode 100644 index 92033ecbf..000000000 --- a/eventcatalog/src/components/MDX/Attachments.astro +++ /dev/null @@ -1,158 +0,0 @@ ---- -import type { ComponentType } from 'react'; -import * as Icons from '@heroicons/react/24/outline'; -import * as SolidIcons from '@heroicons/react/24/solid'; -import * as LucideIcons from 'lucide-react'; - -interface AttachmentObject { - url: string; - title?: string; - type?: string; - description?: string; - summary?: string; - icon?: string; -} - -type Attachment = string | AttachmentObject; - -interface Props { - title?: string; - description?: string; - columns?: number; - data: { - attachments: Attachment[]; - }; -} - -const { title = 'Attachments', description, columns = 2, ...props } = Astro.props; -const { attachments } = props.data; - -function getIconForAttachment(attachment: AttachmentObject): ComponentType<{ className?: string }> | null { - // If custom icon is provided, try to find it - if (attachment.icon) { - const customIcon = - (LucideIcons as any)[attachment.icon] || (Icons as any)[attachment.icon] || (SolidIcons as any)[attachment.icon]; - if (customIcon) return customIcon; - } - - // Default to link icon for all attachments - return Icons.LinkIcon; -} - -function normalizeAttachment(attachment: Attachment): AttachmentObject { - if (typeof attachment === 'string') { - // Extract filename from URL for title - const urlParts = attachment.split('/'); - const filename = urlParts[urlParts.length - 1]; - const title = filename.includes('.') ? filename.split('.')[0] : filename || 'Link'; - - return { - url: attachment, - title: title.replace(/[-_]/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()), - }; - } - return attachment; -} - -function isExternalUrl(url: string): boolean { - return url.startsWith('http://') || url.startsWith('https://'); -} - -const normalizedAttachments = attachments.map(normalizeAttachment); - -// Group attachments by type -const groupedAttachments = normalizedAttachments.reduce( - (groups, attachment) => { - const type = attachment.type || 'Other'; - if (!groups[type]) { - groups[type] = []; - } - groups[type].push(attachment); - return groups; - }, - {} as Record -); - -const sortedGroups = Object.entries(groupedAttachments).sort(([a], [b]) => { - if (a === 'Other') return 1; - if (b === 'Other') return -1; - return a.localeCompare(b); -}); ---- - -
- {title &&

{title}

} - - {description &&

{description}

} - - { - normalizedAttachments.length === 0 ? ( -
No attachments available.
- ) : ( -
- {sortedGroups.map(([groupType, groupAttachments], index) => ( -
-

- {groupType} ({groupAttachments.length}) -

- -
- ))} -
- ) - } -
- - diff --git a/eventcatalog/src/components/MDX/ChannelInformation/ChannelInformation.tsx b/eventcatalog/src/components/MDX/ChannelInformation/ChannelInformation.tsx deleted file mode 100644 index a48c2f3f8..000000000 --- a/eventcatalog/src/components/MDX/ChannelInformation/ChannelInformation.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import type { CollectionEntry } from 'astro:content'; -import { getIconForProtocol } from '@utils/protocols'; - -const ChannelParameters = (data: CollectionEntry<'channels'>['data']) => { - return ( -
-
-

Channel information

-
- -
-

- {data.address && ( -

- Address: {data.address} -
- )} - {data.protocols && data.protocols.length > 0 && ( -
- {data.protocols.length > 1 ? 'Protocols:' : 'Protocol:'} -
    - {data.protocols.map((protocol) => { - const IconComponent = getIconForProtocol(protocol.toLowerCase()); - return ( -
  • - {IconComponent && } - {protocol} -
  • - ); - })} -
-
- )} -

-
- - {data.parameters && Object.keys(data.parameters).length > 0 && ( -
- - - - - - - - - - - {Object.entries(data.parameters).map(([param, details]) => ( - - - - - - - ))} - -
ParameterDescriptionOptionsDefault
{param}{details.description}{details.enum ? details.enum.join(', ') : 'N/A'}{details.default || 'N/A'}
-
- )} -
- ); -}; - -export default ChannelParameters; diff --git a/eventcatalog/src/components/MDX/Design/Design.astro b/eventcatalog/src/components/MDX/Design/Design.astro deleted file mode 100644 index cecb1a557..000000000 --- a/eventcatalog/src/components/MDX/Design/Design.astro +++ /dev/null @@ -1,70 +0,0 @@ ---- -const { file: designPath, filePath, search = true, title } = Astro.props; -import fs from 'fs'; -import { getAbsoluteFilePathForAstroFile } from '@utils/files'; -import Admonition from '@components/MDX/Admonition'; -import NodeGraph from '../NodeGraph/NodeGraph'; - -import { isVisualiserEnabled } from '@utils/feature'; - -let design: any; -let id = 'design'; -let maxHeight = 30; - -try { - const fileName = designPath.endsWith('.ecstudio') ? designPath : `${designPath}.ecstudio`; - const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName); - const designFile = fs.readFileSync(pathToSpec, 'utf8'); - design = JSON.parse(designFile); -} catch (error) { - console.error(`Error reading design file: ${error}`); -} ---- - -{ - !design && ( - -
- {``} failed to load - - Tried to load design file: {designPath}. Make sure you have this design file defined in your project. - -
-
- ) -} - -{ - design && ( -
-
- -
- -
-
- ) -} - - diff --git a/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro b/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro deleted file mode 100644 index 486aa4cbf..000000000 --- a/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +++ /dev/null @@ -1,81 +0,0 @@ ---- -import { getDomains } from '@utils/collections/domains'; -import { getNodesAndEdges } from '@utils/node-graphs/domain-entity-map.ts'; -import Admonition from '@components/MDX/Admonition'; -import NodeGraph from '../NodeGraph/NodeGraph'; -import { getVersionFromCollection } from '@utils/collections/versions'; -import { getServices } from '@utils/collections/services'; - -const { id, version = 'latest', maxHeight, includeKey = true, entities, collection = 'domains', ...rest } = Astro.props; -let resource = null; - -let resourceId = id; - -// If the user did not provide an id, we need to use the id from the rest props (the collection) -const isAstroGeneratedId = id.match(/-\d+\.\d+\.\d+$/); -if (isAstroGeneratedId) { - resourceId = rest.data.id; -} - -if (collection === 'domains') { - const domains = await getDomains(); - const domainCollection = getVersionFromCollection(domains, resourceId, version) || []; - resource = domainCollection[0]; -} else if (collection === 'services') { - const services = await getServices(); - const serviceCollection = getVersionFromCollection(services, resourceId, version) || []; - resource = serviceCollection[0]; -} else { - throw new Error(`Invalid collection: ${collection}`); -} - -const { nodes, edges } = await getNodesAndEdges({ - id: resourceId, - version: resource?.data?.version, - ...(entities ? { entities } : {}), // Pass entities if provided - type: collection, -}); ---- - -{ - !resource && ( - -
- {``} failed to load - - Tried to load {collection} id: {id} with version {version}. Make sure you have this {collection} defined in your - project. - -
-
- ) -} - -
-
- -
- -
- - diff --git a/eventcatalog/src/components/MDX/EntityPropertiesTable/EntityPropertiesTable.astro b/eventcatalog/src/components/MDX/EntityPropertiesTable/EntityPropertiesTable.astro deleted file mode 100644 index 4cc865141..000000000 --- a/eventcatalog/src/components/MDX/EntityPropertiesTable/EntityPropertiesTable.astro +++ /dev/null @@ -1,106 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; -import Admonition from '../Admonition'; - -// Define the shape for a single property used in this component -type EntityProperty = { - name: string; - type: string; - required?: boolean; - description?: string; - enum?: string[]; - items?: { - type: string; - }; -}; - -// Expects a CollectionEntry for 'entities'. -// The actual properties structure might differ slightly from the base type, -// so we handle it below. -export interface Props extends CollectionEntry<'entities'> {} - -const { data, collection } = Astro.props; -// Cast the properties to our expected type for use in the template -const properties = data?.properties as EntityProperty[] | undefined; -const isComponentEnabled = collection === 'entities'; ---- - -{ - !isComponentEnabled && ( - -
- - {``} component is not supported for resources of type {collection}. - - This component is only supported for services and domains. -
-
- ) -} - -{ - isComponentEnabled && properties && properties.length > 0 ? ( -
- - - - - - - - - - - {properties.map((prop) => ( - - - - - - - ))} - -
- Name - - Type - - Required - - Description -
- {prop.name} - - {prop.type === 'array' && prop.items ? ( - - array<{prop.items.type}> - - ) : ( - {prop.type} - )} - {prop.enum && ( -
- Enum: -
    - {prop.enum.map((enumValue: string) => ( -
  • - {enumValue} -
  • - ))} -
-
- )} -
- {prop.required ? ( - Required - ) : ( - Optional - )} - {prop.description || No description provided.}
-
- ) : ( - -

There are no properties defined for this entity.

-
- ) -} diff --git a/eventcatalog/src/components/MDX/File.tsx b/eventcatalog/src/components/MDX/File.tsx deleted file mode 100644 index f5e19520f..000000000 --- a/eventcatalog/src/components/MDX/File.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -const File = ({ file, catalog, title }: any) => { - try { - const exists = fs.existsSync(path.join(catalog.filePath, file)); - - if (exists) { - const text = fs.readFileSync(path.join(catalog.filePath, file), 'utf-8'); - return ( -
-
-            
-
- {title && {title}} - {!title && {file}} -
-
-                
-                  
-
{text}
-
-
-
-
- -
-
-
-
- ); - } else { - return
Tried to load file from {path.join(catalog.filePath, file)}, but no file can be found
; - } - } catch (error) { - console.log('Failed to load file', error); - return null; - } -}; - -export default File; diff --git a/eventcatalog/src/components/MDX/Flow/Flow.astro b/eventcatalog/src/components/MDX/Flow/Flow.astro deleted file mode 100644 index 50db3c17b..000000000 --- a/eventcatalog/src/components/MDX/Flow/Flow.astro +++ /dev/null @@ -1,67 +0,0 @@ ---- -import { getFlows } from '@utils/collections/flows'; -import { getNodesAndEdges } from '@utils/node-graphs/flows-node-graph'; -import Admonition from '@components/MDX/Admonition'; -import NodeGraph from '../NodeGraph/NodeGraph'; -import { getVersionFromCollection } from '@utils/collections/versions'; -import { isVisualiserEnabled } from '@utils/feature'; - -const { id, version = 'latest', maxHeight, includeKey = true, mode = 'simple', walkthrough = true, search = true } = Astro.props; - -// Find the flow for the given id and version -const flows = await getFlows(); -const flowCollection = getVersionFromCollection(flows, id, version) || []; -const flow = flowCollection[0]; - -// const flow = flows.find((flow) => flow.data.id === id && flow.data.version === version); - -const { nodes, edges } = await getNodesAndEdges({ - id: id, - version: flow.data.version, - mode: mode, -}); ---- - -{ - !flow && ( - -
- {``} failed to load - - Tried to load flow id: {id} with version {version}. Make sure you have this flow defined in your project. - -
-
- ) -} - -
-
- -
- -
- - diff --git a/eventcatalog/src/components/MDX/Link/Link.astro b/eventcatalog/src/components/MDX/Link/Link.astro deleted file mode 100644 index bdc140b1d..000000000 --- a/eventcatalog/src/components/MDX/Link/Link.astro +++ /dev/null @@ -1,7 +0,0 @@ ---- -import { buildUrl } from '@utils/url-builder'; - -const { href } = Astro.props; ---- - - diff --git a/eventcatalog/src/components/MDX/NodeGraph/DownloadButton.tsx b/eventcatalog/src/components/MDX/NodeGraph/DownloadButton.tsx deleted file mode 100644 index 2f6e07d6d..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/DownloadButton.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Panel, useReactFlow, getNodesBounds, getViewportForBounds } from '@xyflow/react'; -import { toPng } from 'html-to-image'; -import { DocumentArrowDownIcon } from '@heroicons/react/24/outline'; - -function downloadImage(dataUrl: string, filename?: string) { - const a = document.createElement('a'); - - a.setAttribute('download', `${filename || 'eventcatalog'}.png`); - a.setAttribute('href', dataUrl); - a.click(); -} - -const imageWidth = 1024; -const imageHeight = 768; - -function DownloadButton({ filename, addPadding = true }: { filename?: string; addPadding?: boolean }) { - const { getNodes } = useReactFlow(); - const onClick = () => { - const nodesBounds = getNodesBounds(getNodes()); - const width = imageWidth > nodesBounds.width ? imageWidth : nodesBounds.width; - const height = imageHeight > nodesBounds.height ? imageHeight : nodesBounds.height; - const viewport = getViewportForBounds(nodesBounds, width, height, 0.5, 2, 0); - - // Hide the button - // @ts-ignore - document.getElementById('download-visual').style.display = 'none'; - // @ts-ignore - document.querySelector('.react-flow__controls').style.display = 'none'; - - // @ts-ignore - toPng(document.querySelector('.react-flow__viewport'), { - backgroundColor: '#f1f1f1', - width, - height, - style: { - width, - height, - transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, - }, - }).then((dataUrl: string) => { - downloadImage(dataUrl, filename); - // @ts-ignore - document.getElementById('download-visual').style.display = 'block'; - // @ts-ignore - document.querySelector('.react-flow__controls').style.display = 'block'; - }); - }; - - return ( -
- -
- ); -} - -export default DownloadButton; diff --git a/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx b/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx deleted file mode 100644 index 34dbc1851..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useMemo } from 'react'; -import { BaseEdge, getBezierPath } from '@xyflow/react'; - -const AnimatedMessageEdge = ({ - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - data, - label = '', - markerEnd, - markerStart, -}: any) => { - const [edgePath, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - - const messageColor = useMemo( - () => (collection: string) => { - switch (collection) { - case 'events': - return 'orange'; - case 'commands': - return 'blue'; - case 'queries': - return 'green'; - default: - return 'gray'; - } - }, - [] - ); - - const collection = data?.message?.collection; - const opacity = data?.opacity ?? 1; - const customColor = data?.customColor || messageColor(collection ?? 'default'); - const warning = data?.warning; - - // For each customColor (string or array of strings), we need to create the animated nodes - const customColors = Array.isArray(customColor) ? customColor : [customColor]; - - const randomDelay = useMemo(() => Math.random() * 1, []); - - const animatedNodes = customColors.map((color, index) => { - // Stagger the animations so multiple colored nodes are visible - const delay = randomDelay + index * 0.3; - return ( - - - - - - - - ); - }); - - // Label can be spit using \n to create multiple lines - const lines = String(label ?? '').split('\n'); - - return ( - // @ts-ignore - <> - - {/* Circle Icon */} - {animatedNodes} - {/* - */} - - {/* Text element */} - - {lines.map((line, i) => ( - - {line} - - ))} - - - - ); -}; - -export default AnimatedMessageEdge; diff --git a/eventcatalog/src/components/MDX/NodeGraph/Edges/FlowEdge.tsx b/eventcatalog/src/components/MDX/NodeGraph/Edges/FlowEdge.tsx deleted file mode 100644 index c51d27bdc..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Edges/FlowEdge.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { useMemo } from 'react'; -import { BaseEdge, EdgeLabelRenderer, getBezierPath, type EdgeProps as XYFlowEdgeProps } from '@xyflow/react'; - -interface EdgeData { - message?: { - collection?: string; - }; - opacity?: number; - animated?: boolean; -} - -interface CustomEdgeProps extends Omit { - data?: EdgeData; -} - -export default function CustomEdge({ - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - style = {}, - markerEnd, - label, - labelStyle, - data, -}: CustomEdgeProps) { - const [edgePath, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - sourcePosition, - targetX, - targetY, - targetPosition, - }); - - const randomDelay = useMemo(() => Math.random() * 1, []); - const collection = data?.message?.collection; - const opacity = data?.opacity ?? 1; - - const messageColor = useMemo( - () => (collection: string) => { - switch (collection) { - case 'events': - return 'orange'; - case 'commands': - return 'blue'; - case 'queries': - return 'green'; - default: - return 'gray'; - } - }, - [] - ); - - return ( - <> - - {data?.animated && ( - - - - - - - - )} - {label && ( - -
- {label} -
-
- )} - - ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Edges/MultilineEdgeLabel.tsx b/eventcatalog/src/components/MDX/NodeGraph/Edges/MultilineEdgeLabel.tsx deleted file mode 100644 index 71d2ad2c9..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Edges/MultilineEdgeLabel.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { type EdgeProps, getBezierPath } from '@xyflow/react'; - -export default function MultilineEdgeLabel(props: EdgeProps) { - const { - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - label, - markerStart, // <-- forward these - markerEnd, - style, - selected, - } = props; - - const [edgePath, labelX, labelY] = getBezierPath({ - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - }); - - const lines = String(label ?? '').split('\n'); - - return ( - <> - - - {/* Optional: bigger hitbox for hover/selection */} - - - {lines.map((line, i) => ( - - {line} - - ))} - - - ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro b/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro deleted file mode 100644 index d87c26d2e..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +++ /dev/null @@ -1,162 +0,0 @@ ---- -import NodeGraphNew from './NodeGraph'; -import { getNodesAndEdges as getNodesAndEdgesForService } from '@utils/node-graphs/services-node-graph'; -import { - getNodesAndEdgesForCommands, - getNodesAndEdgesForEvents, - getNodesAndEdgesForQueries, -} from '@utils/node-graphs/message-node-graph'; -import { - getNodesAndEdges as getNodesAndEdgesForDomain, - getNodesAndEdgesForDomainContextMap, -} from '@utils/node-graphs/domains-node-graph'; -import { getNodesAndEdges as getNodesAndEdgesForDomainEntityMap } from '@utils/node-graphs/domain-entity-map'; -import { getDomainsCanvasData } from '@utils/node-graphs/domains-canvas'; -import { getNodesAndEdges as getNodesAndEdgesForFlows } from '@utils/node-graphs/flows-node-graph'; -import { buildUrl } from '@utils/url-builder'; -import { getVersionFromCollection } from '@utils/collections/versions'; -import { pageDataLoader } from '@utils/page-loaders/page-data-loader'; -import { getNodesAndEdges as getNodesAndEdgesForContainer } from '@utils/node-graphs/container-node-graph'; -import config from '@config'; - -interface Props { - id: string; - collection: string; - title?: string; - version: string; - mode: 'full' | 'simple'; - linkTo?: 'docs' | 'visualiser'; - href?: { - label: string; - url: string; - }; - linksToVisualiser?: boolean; - showSearch?: boolean; - showLegend?: boolean; - zoomOnScroll?: boolean; -} - -const { - id, - collection, - title, - mode = 'simple', - linkTo = 'docs', - version, - href, - linksToVisualiser, - showSearch = true, - showLegend = true, - zoomOnScroll = false, -} = Astro.props; - -let nodes = [], - edges = []; - -const getNodesAndEdgesFunctions = { - services: getNodesAndEdgesForService, - events: getNodesAndEdgesForEvents, - commands: getNodesAndEdgesForCommands, - queries: getNodesAndEdgesForQueries, - domains: getNodesAndEdgesForDomain, - flows: getNodesAndEdgesForFlows, - containers: getNodesAndEdgesForContainer, -}; - -let links: { label: string; url: string }[] = []; - -if (collection in getNodesAndEdgesFunctions) { - const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesFunctions[ - collection as keyof typeof getNodesAndEdgesFunctions - ]({ - id, - version, - mode, - channelRenderMode: config.visualiser?.channels?.renderMode === 'single' ? 'single' : 'flat', - }); - - nodes = fetchedNodes; - edges = fetchedEdges; - - if (mode === 'full') { - // Try and get the list of versions for the rendered item - try { - const allItems = await pageDataLoader[collection as keyof typeof pageDataLoader](); - const versions = getVersionFromCollection(allItems, id, version); - - const item = versions[0]; - const listOfVersions = item.data.versions || []; - - // Order by version - listOfVersions.sort((a, b) => b.localeCompare(a)); - - if (listOfVersions.length > 1) { - links = listOfVersions.map((version) => ({ - label: `${item.data.name} v${version}`, - url: buildUrl(`/visualiser/${collection}/${id}/${version}`), - selected: version === version, - })); - } - } catch (error) { - links = []; - } - } -} - -if (collection === 'domain-context-map') { - const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesForDomainContextMap({}); - nodes = fetchedNodes; - edges = fetchedEdges; -} - -if (collection === 'domains-canvas') { - const { domainNodes, messageNodes, edges: fetchedEdges } = await getDomainsCanvasData(); - nodes = [...domainNodes, ...messageNodes]; - edges = fetchedEdges; -} - -if (collection === 'domains-entities') { - const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesForDomainEntityMap({ - id, - version, - }); - nodes = fetchedNodes; - edges = fetchedEdges; -} - -if (collection === 'services-containers') { - const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesForService({ - id, - version, - renderMessages: false, - mode: 'full', - }); - nodes = fetchedNodes; - edges = fetchedEdges; -} ---- - -
- -
- - diff --git a/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx b/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx deleted file mode 100644 index 337a0b589..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +++ /dev/null @@ -1,866 +0,0 @@ -import { useEffect, useMemo, useState, useCallback, useRef } from 'react'; -import { createPortal } from 'react-dom'; -import { - ReactFlow, - Background, - ConnectionLineType, - Controls, - Panel, - ReactFlowProvider, - useNodesState, - useEdgesState, - type Edge, - type Node, - useReactFlow, - getNodesBounds, - getViewportForBounds, - type NodeTypes, -} from '@xyflow/react'; -import '@xyflow/react/dist/style.css'; -import { ExternalLink, HistoryIcon } from 'lucide-react'; -import { toPng } from 'html-to-image'; -import { DocumentArrowDownIcon } from '@heroicons/react/24/outline'; -// Nodes and edges -import ServiceNode from './Nodes/Service'; -import FlowNode from './Nodes/Flow'; -import EventNode from './Nodes/Event'; -import EntityNode from './Nodes/Entity'; -import QueryNode from './Nodes/Query'; -import UserNode from './Nodes/User'; -import StepNode from './Nodes/Step'; -import CommandNode from './Nodes/Command'; -import ExternalSystemNode from './Nodes/ExternalSystem'; -import DomainNode from './Nodes/Domain'; -import AnimatedMessageEdge from './Edges/AnimatedMessageEdge'; -import MultilineEdgeLabel from './Edges/MultilineEdgeLabel'; -import FlowEdge from './Edges/FlowEdge'; -import CustomNode from './Nodes/Custom'; -import DataNode from './Nodes/Data'; -import ViewNode from './Nodes/View'; -import ActorNode from './Nodes/Actor'; -import ExternalSystemNode2 from './Nodes/ExternalSystem2'; -import { Note as NoteNode } from '@eventcatalog/visualizer'; - -import type { CollectionEntry } from 'astro:content'; -import { navigate } from 'astro:transitions/client'; -import type { CollectionTypes } from '@types'; -import { buildUrl } from '@utils/url-builder'; -import ChannelNode from './Nodes/Channel'; -import { CogIcon } from '@heroicons/react/20/solid'; -import { useEventCatalogVisualiser } from 'src/hooks/eventcatalog-visualizer'; -import VisualiserSearch, { type VisualiserSearchRef } from './VisualiserSearch'; -import StepWalkthrough from './StepWalkthrough'; -import StudioModal from './StudioModal'; -interface Props { - nodes: any; - edges: any; - title?: string; - subtitle?: string; - includeBackground?: boolean; - includeControls?: boolean; - linkTo: 'docs' | 'visualiser'; - includeKey?: boolean; - linksToVisualiser?: boolean; - links?: { label: string; url: string }[]; - mode?: 'full' | 'simple'; - showFlowWalkthrough?: boolean; - showSearch?: boolean; - zoomOnScroll?: boolean; - designId?: string; - isStudioModalOpen?: boolean; - setIsStudioModalOpen?: (isOpen: boolean) => void; -} - -const getVisualiserUrlForCollection = (collectionItem: CollectionEntry) => { - return buildUrl(`/visualiser/${collectionItem.collection}/${collectionItem.data.id}/${collectionItem.data.version}`); -}; - -const NodeGraphBuilder = ({ - nodes: initialNodes, - edges: initialEdges, - title, - includeBackground = true, - linkTo = 'docs', - includeKey = true, - linksToVisualiser = false, - links = [], - mode = 'full', - showFlowWalkthrough = true, - showSearch = true, - zoomOnScroll = false, - isStudioModalOpen, - setIsStudioModalOpen = () => {}, -}: Props) => { - const nodeTypes = useMemo( - () => - ({ - service: ServiceNode, - services: ServiceNode, - flow: FlowNode, - flows: FlowNode, - event: EventNode, - events: EventNode, - channel: ChannelNode, - channels: ChannelNode, - query: QueryNode, - queries: QueryNode, - command: CommandNode, - commands: CommandNode, - domain: DomainNode, - domains: DomainNode, - step: StepNode, - user: UserNode, - custom: CustomNode, - externalSystem: ExternalSystemNode, - 'external-system': ExternalSystemNode2, - entity: EntityNode, - entities: EntityNode, - data: DataNode, - view: ViewNode, - actor: ActorNode, - note: (props: any) => , - }) as unknown as NodeTypes, - [] - ); - const edgeTypes = useMemo( - () => ({ - animated: AnimatedMessageEdge, - multiline: MultilineEdgeLabel, - 'flow-edge': FlowEdge, - }), - [] - ); - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const [animateMessages, setAnimateMessages] = useState(false); - const [activeStepIndex, setActiveStepIndex] = useState(null); - // const [isStudioModalOpen, setIsStudioModalOpen] = useState(false); - - // Check if there are channels to determine if we need the visualizer functionality - const hasChannels = useMemo(() => initialNodes.some((node: any) => node.type === 'channels'), [initialNodes]); - const { hideChannels, toggleChannelsVisibility } = useEventCatalogVisualiser({ - nodes, - edges, - setNodes, - setEdges, - skipProcessing: !hasChannels, // Pass flag to skip processing when no channels - }); - const { fitView, getNodes, toObject } = useReactFlow(); - const searchRef = useRef(null); - const reactFlowWrapperRef = useRef(null); - const scrollableContainerRef = useRef(null); - - const resetNodesAndEdges = useCallback(() => { - setNodes((nds) => - nds.map((node) => { - node.style = { ...node.style, opacity: 1 }; - return { ...node, animated: animateMessages }; - }) - ); - setEdges((eds) => - eds.map((edge) => { - edge.style = { ...edge.style, opacity: 1 }; - edge.labelStyle = { ...edge.labelStyle, opacity: 1 }; - return { ...edge, data: { ...edge.data, opacity: 1, animated: animateMessages }, animated: animateMessages }; - }) - ); - }, [setNodes, setEdges, animateMessages]); - - const handleNodeClick = useCallback( - (_: any, node: Node) => { - if (linksToVisualiser) { - if (node.type === 'events' || node.type === 'commands') { - navigate(getVisualiserUrlForCollection(node.data.message as CollectionEntry)); - } - if (node.type === 'services') { - navigate(getVisualiserUrlForCollection(node.data.service as CollectionEntry<'services'>)); - } - return; - } - - resetNodesAndEdges(); - - const connectedNodeIds = new Set(); - connectedNodeIds.add(node.id); - - const updatedEdges = edges.map((edge) => { - if (edge.source === node.id || edge.target === node.id) { - connectedNodeIds.add(edge.source); - connectedNodeIds.add(edge.target); - return { - ...edge, - data: { ...edge.data, opacity: 1, animated: animateMessages }, - style: { ...edge.style, opacity: 1 }, - labelStyle: { ...edge.labelStyle, opacity: 1 }, - animated: true, - }; - } - return { - ...edge, - data: { ...edge.data, opacity: 0.1, animated: animateMessages }, - style: { ...edge.style, opacity: 0.1 }, - labelStyle: { ...edge.labelStyle, opacity: 0.1 }, - animated: animateMessages, - }; - }); - - const updatedNodes = nodes.map((n) => { - if (connectedNodeIds.has(n.id)) { - return { ...n, style: { ...n.style, opacity: 1 } }; - } - return { ...n, style: { ...n.style, opacity: 0.1 } }; - }); - - setNodes(updatedNodes); - setEdges(updatedEdges); - - // Fit the clicked node and its connected nodes into view - fitView({ - padding: 0.2, - duration: 800, - nodes: updatedNodes.filter((n) => connectedNodeIds.has(n.id)), - }); - }, - [nodes, edges, setNodes, setEdges, resetNodesAndEdges, fitView] - ); - - const toggleAnimateMessages = () => { - setAnimateMessages(!animateMessages); - localStorage.setItem('EventCatalog:animateMessages', JSON.stringify(!animateMessages)); - }; - - // animate messages, between views - useEffect(() => { - const storedAnimateMessages = localStorage.getItem('EventCatalog:animateMessages'); - if (storedAnimateMessages !== null) { - setAnimateMessages(storedAnimateMessages === 'true'); - } - }, []); - - useEffect(() => { - setEdges((eds) => - eds.map((edge) => ({ - ...edge, - animated: animateMessages, - type: edge.type === 'flow-edge' || edge.type === 'multiline' ? edge.type : animateMessages ? 'animated' : 'default', - data: { ...edge.data, animateMessages, animated: animateMessages }, - })) - ); - }, [animateMessages]); - - useEffect(() => { - setTimeout(() => { - fitView({ duration: 800 }); - }, 150); - }, []); - - // Handle scroll wheel events to forward to page when no modifier keys are pressed - // Only when zoomOnScroll is disabled - // This is a fix for when we embed node graphs into pages, and users are scrolling the documentation pages - // We dont want REACT FLOW to swallow the scroll events, so we forward them to the parent page - useEffect(() => { - // Skip scroll handling if zoomOnScroll is enabled - if (zoomOnScroll) return; - - // Cache the scrollable container on mount (expensive operation done once) - const findScrollableContainer = (): HTMLElement | null => { - // Try specific known selectors first (fast) - const selectors = [ - '.docs-layout .overflow-y-auto', - '.overflow-y-auto', - '[style*="overflow-y:auto"]', - '[style*="overflow-y: auto"]', - ]; - - for (const selector of selectors) { - const element = document.querySelector(selector) as HTMLElement; - if (element) return element; - } - - return null; - }; - - // Find and cache the scrollable container once - if (!scrollableContainerRef.current) { - scrollableContainerRef.current = findScrollableContainer(); - } - - const handleWheel = (event: WheelEvent) => { - // Only forward scroll if no modifier keys are pressed - if (!event.ctrlKey && !event.shiftKey && !event.metaKey) { - event.preventDefault(); - - const scrollableContainer = scrollableContainerRef.current; - - if (scrollableContainer) { - scrollableContainer.scrollBy({ - top: event.deltaY, - left: event.deltaX, - behavior: 'instant', - }); - } else { - // Fallback to window scroll - window.scrollBy({ - top: event.deltaY, - left: event.deltaX, - behavior: 'instant', - }); - } - } - }; - - const wrapper = reactFlowWrapperRef.current; - if (wrapper) { - wrapper.addEventListener('wheel', handleWheel, { passive: false }); - return () => { - wrapper.removeEventListener('wheel', handleWheel); - }; - } - }, [zoomOnScroll]); - - const handlePaneClick = useCallback(() => { - setIsSettingsOpen(false); - searchRef.current?.hideSuggestions(); - resetNodesAndEdges(); - fitView({ duration: 800 }); - }, [resetNodesAndEdges, fitView]); - - const handleNodeSelect = useCallback( - (node: Node) => { - handleNodeClick(null, node); - }, - [handleNodeClick] - ); - - const handleSearchClear = useCallback(() => { - resetNodesAndEdges(); - fitView({ duration: 800 }); - }, [resetNodesAndEdges, fitView]); - - const downloadImage = useCallback((dataUrl: string, filename?: string) => { - const a = document.createElement('a'); - a.setAttribute('download', `${filename || 'eventcatalog'}.png`); - a.setAttribute('href', dataUrl); - a.click(); - }, []); - - const openStudioModal = () => { - setIsStudioModalOpen(true); - }; - - const handleExportVisual = useCallback(() => { - const imageWidth = 1024; - const imageHeight = 768; - const nodesBounds = getNodesBounds(getNodes()); - const width = imageWidth > nodesBounds.width ? imageWidth : nodesBounds.width; - const height = imageHeight > nodesBounds.height ? imageHeight : nodesBounds.height; - const viewport = getViewportForBounds(nodesBounds, width, height, 0.5, 2, 0); - - // Hide settings panel and controls during export - setIsSettingsOpen(false); - const controls = document.querySelector('.react-flow__controls') as HTMLElement; - if (controls) controls.style.display = 'none'; - - toPng(document.querySelector('.react-flow__viewport') as HTMLElement, { - backgroundColor: '#f1f1f1', - width, - height, - style: { - width: width.toString(), - height: height.toString(), - transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, - }, - }).then((dataUrl: string) => { - downloadImage(dataUrl, title); - // Restore controls - if (controls) controls.style.display = 'block'; - }); - }, [getNodes, downloadImage, title]); - - const handleLegendClick = useCallback( - (collectionType: string, groupId?: string) => { - const updatedNodes = nodes.map((node: Node) => { - // Check if the groupId is set first - if (groupId && node.data.group && node.data.group?.id === groupId) { - return { ...node, style: { ...node.style, opacity: 1 } }; - } else { - if (node.type === collectionType) { - return { ...node, style: { ...node.style, opacity: 1 } }; - } - } - return { ...node, style: { ...node.style, opacity: 0.1 } }; - }); - - const updatedEdges = edges.map((edge) => { - return { - ...edge, - data: { ...edge.data, opacity: 0.1 }, - style: { ...edge.style, opacity: 0.1 }, - labelStyle: { ...edge.labelStyle, opacity: 0.1 }, - animated: animateMessages, - }; - }); - - setNodes(updatedNodes); - setEdges(updatedEdges); - - fitView({ - padding: 0.2, - duration: 800, - nodes: updatedNodes.filter((node) => node.type === collectionType), - }); - }, - [nodes, edges, setNodes, setEdges, fitView] - ); - - const getNodesByCollectionWithColors = useCallback((nodes: Node[]) => { - const colorClasses = { - events: 'bg-orange-600', - services: 'bg-pink-600', - flows: 'bg-teal-600', - commands: 'bg-blue-600', - queries: 'bg-green-600', - channels: 'bg-gray-600', - externalSystem: 'bg-pink-600', - actor: 'bg-yellow-500', - step: 'bg-gray-700', - data: 'bg-blue-600', - }; - - let legendForDomains: { [key: string]: { count: number; colorClass: string; groupId: string } } = {}; - - // Find any groups - const domainGroups = [ - ...new Set( - nodes.filter((node) => node.data.group && node.data.group?.type === 'Domain').map((node) => node.data.group?.id) - ), - ]; - - domainGroups.forEach((groupId) => { - const group = nodes.filter((node) => node.data.group && node.data.group?.id === groupId); - legendForDomains[`${groupId} (Domain)`] = { count: group.length, colorClass: 'bg-yellow-600', groupId }; - }); - - const legendForNodes = nodes.reduce( - (acc: { [key: string]: { count: number; colorClass: string; groupId?: string } }, node) => { - const collection = node.type; - if (collection) { - if (acc[collection]) { - acc[collection].count += 1; - } else { - acc[collection] = { count: 1, colorClass: colorClasses[collection as keyof typeof colorClasses] || 'bg-black' }; - } - } - return acc; - }, - {} - ); - - return { ...legendForDomains, ...legendForNodes }; - }, []); - - const legend = getNodesByCollectionWithColors(nodes); - - const handleStepChange = useCallback( - (nodeId: string | null, highlightPaths?: string[], shouldZoomOut?: boolean) => { - if (nodeId === null) { - // Reset all nodes and edges - resetNodesAndEdges(); - setActiveStepIndex(null); - - // If shouldZoomOut is true, fit the entire view - if (shouldZoomOut) { - setTimeout(() => { - fitView({ duration: 800, padding: 0.1 }); - }, 100); - } - return; - } - - const activeNode = nodes.find((node: Node) => node.id === nodeId); - if (!activeNode) return; - - // Create set of highlighted nodes and edges - const highlightedNodeIds = new Set(); - const highlightedEdgeIds = new Set(); - - // Add current node - highlightedNodeIds.add(activeNode.id); - - // Add incoming edges and their source nodes - edges.forEach((edge: Edge) => { - if (edge.target === activeNode.id) { - highlightedEdgeIds.add(edge.id); - highlightedNodeIds.add(edge.source); - } - }); - - // Add outgoing edges - if (highlightPaths) { - // Highlight all possible paths when at a fork - highlightPaths.forEach((pathId) => { - const [source, target] = pathId.split('-'); - edges.forEach((edge: Edge) => { - if (edge.source === source && edge.target === target) { - highlightedEdgeIds.add(edge.id); - highlightedNodeIds.add(edge.target); - } - }); - }); - } else { - // Highlight all outgoing edges normally - edges.forEach((edge: Edge) => { - if (edge.source === activeNode.id) { - highlightedEdgeIds.add(edge.id); - highlightedNodeIds.add(edge.target); - } - }); - } - - // Update nodes - const updatedNodes = nodes.map((node: Node) => { - if (highlightedNodeIds.has(node.id)) { - return { ...node, style: { ...node.style, opacity: 1 } }; - } - return { ...node, style: { ...node.style, opacity: 0.2 } }; - }); - - // Update edges - const updatedEdges = edges.map((edge: Edge) => { - if (highlightedEdgeIds.has(edge.id)) { - return { - ...edge, - data: { ...edge.data, opacity: 1, animated: true }, - style: { ...edge.style, opacity: 1, strokeWidth: 3 }, - labelStyle: { ...edge.labelStyle, opacity: 1 }, - animated: true, - }; - } - return { - ...edge, - data: { ...edge.data, opacity: 0.2, animated: false }, - style: { ...edge.style, opacity: 0.2, strokeWidth: 2 }, - labelStyle: { ...edge.labelStyle, opacity: 0.2 }, - animated: false, - }; - }); - - setNodes(updatedNodes); - setEdges(updatedEdges); - - // Fit view to active node - fitView({ - padding: 0.4, - duration: 800, - nodes: [activeNode], - }); - }, - [nodes, edges, setNodes, setEdges, resetNodesAndEdges, fitView] - ); - - // Check if this is a flow visualization by checking if edges use flow-edge type - const isFlowVisualization = edges.some((edge: Edge) => edge.type === 'flow-edge'); - - return ( -
- - -
-
-
- -
- - {title && ( - - {title} - - )} -
- {mode === 'full' && showSearch && ( -
- -
- )} -
- {links.length > 0 && ( -
-
- - - - - - - - - -
-
- )} -
- - {isSettingsOpen && ( -
-

Visualizer Settings

-
-
-
- - -
-

Animate events, queries and commands.

-
- {hasChannels && ( -
-
- - -
-

Show or hide channels in the visualizer.

-
- )} -
- - -
-
-
- )} - {includeBackground && } - {includeBackground && } - {isFlowVisualization && showFlowWalkthrough && ( - - - - )} - {includeKey && ( - -
-
    - {Object.entries(legend).map(([key, { count, colorClass, groupId }]) => ( -
  • handleLegendClick(key, groupId)} - > - - - {key} ({count}) - -
  • - ))} -
-
-
- )} -
- setIsStudioModalOpen(false)} /> -
- ); -}; - -interface NodeGraphProps { - id: string; - title?: string; - href?: string; - hrefLabel?: string; - nodes: Node[]; - edges: Edge[]; - linkTo: 'docs' | 'visualiser'; - includeKey?: boolean; - footerLabel?: string; - linksToVisualiser?: boolean; - links?: { label: string; url: string }[]; - mode?: 'full' | 'simple'; - portalId?: string; - showFlowWalkthrough?: boolean; - showSearch?: boolean; - zoomOnScroll?: boolean; - designId?: string; -} - -const NodeGraph = ({ - id, - nodes, - edges, - title, - href, - linkTo = 'docs', - hrefLabel = 'Open in visualizer', - includeKey = true, - footerLabel, - linksToVisualiser = false, - links = [], - mode = 'full', - portalId, - showFlowWalkthrough = true, - showSearch = true, - zoomOnScroll = false, - designId, -}: NodeGraphProps) => { - const [elem, setElem] = useState(null); - const [showFooter, setShowFooter] = useState(true); - const [isStudioModalOpen, setIsStudioModalOpen] = useState(false); - - const openStudioModal = useCallback(() => { - setIsStudioModalOpen(true); - }, []); - - const containerToRenderInto = portalId || `${id}-portal`; - - useEffect(() => { - // @ts-ignore - setElem(document.getElementById(containerToRenderInto)); - }, []); - - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const embed = urlParams.get('embed'); - if (embed === 'true') { - setShowFooter(false); - } - }, []); - - if (!elem) return null; - - return ( -
- {createPortal( - - - - {showFooter && ( - - )} - , - elem - )} -
- ); -}; - -export default NodeGraph; diff --git a/eventcatalog/src/components/MDX/NodeGraph/NodeGraphPortal.tsx b/eventcatalog/src/components/MDX/NodeGraph/NodeGraphPortal.tsx deleted file mode 100644 index 11fa070bb..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/NodeGraphPortal.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const NodeGraphPortal = (props: any) => { - return ( -
- {/* {props.title} */} -
- ); -}; - -export default NodeGraphPortal; diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Actor.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Actor.tsx deleted file mode 100644 index 36e67c420..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Actor.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Handle, Position, type XYPosition } from '@xyflow/react'; - -import { nodeComponents } from '@eventcatalog/visualizer'; -const ActorComponent = nodeComponents.actor; - -interface Data { - data: { - actor: { - name: string; - summary: string; - }; - mode: 'simple' | 'full'; - }; - type: 'actor'; - id: string; - position: XYPosition; -} - -export default function ActorNode(props: Data) { - const componentData = { - ...props, - data: { - ...props.data, - name: props.data.actor.name, - summary: props.data.actor.summary, - }, - }; - - return ( -
- - - -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Channel.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Channel.tsx deleted file mode 100644 index 851c687ee..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Channel.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Handle, Position } from '@xyflow/react'; -import * as ContextMenu from '@radix-ui/react-context-menu'; -import { buildUrl } from '@utils/url-builder'; - -import { nodeComponents, type ChannelNode } from '@eventcatalog/visualizer'; -const ChannelComponent = nodeComponents.channel; - -export default function ChannelNode(props: ChannelNode) { - // @ts-ignore - const { id, version } = props.data.channel; - - return ( - - -
- - - -
-
- - - - Read documentation - - - - - - Read changelog - - - - -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Command.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Command.tsx deleted file mode 100644 index bbea54a67..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Command.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Handle, Position } from '@xyflow/react'; -import MessageContextMenu from './MessageContextMenu'; - -import { nodeComponents, type CommandNode } from '@eventcatalog/visualizer'; -const CommandComponent = nodeComponents.command; - -export default function CommandNode(props: CommandNode) { - return ( - -
- - - -
-
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Custom.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Custom.tsx deleted file mode 100644 index d8f15c08b..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Custom.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { Handle } from '@xyflow/react'; -import * as Icons from '@heroicons/react/24/solid'; -import type { ComponentType } from 'react'; -import * as ContextMenu from '@radix-ui/react-context-menu'; -import * as Tooltip from '@radix-ui/react-tooltip'; - -type MenuItem = { - label: string; - url?: string; -}; - -interface Data { - title: string; - label: string; - bgColor: string; - color: string; - mode: 'simple' | 'full'; - step: { id: string; title: string; summary: string; name: string; actor: { name: string } }; - showTarget?: boolean; - showSource?: boolean; - custom: { - icon?: string; - type?: string; - title?: string; - summary?: string; - url?: string; - color?: string; - properties?: Record; - menu?: MenuItem[]; - height?: number; - }; -} - -function classNames(...classes: any) { - return classes.filter(Boolean).join(' '); -} - -export default function UserNode({ data, sourcePosition, targetPosition }: any) { - const { mode, step, custom: customProps } = data as Data; - - const { - color = 'blue', - title = 'Custom', - icon = 'UserIcon', - type = 'custom', - summary = '', - url = '', - properties = {}, - menu = [], - height = 5, - } = customProps; - - const IconComponent: ComponentType<{ className?: string }> | undefined = Icons[icon as keyof typeof Icons]; - - const { actor: { name } = {} } = step; - - const isLongType = type && type.length > 10; - const displayType = isLongType ? `${type.substring(0, 10)}...` : type; - - return ( - - -
-
- - {mode === 'full' && ( - - - - - {displayType} - - - {isLongType && ( - - - {type} - - - - )} - - - )} -
-
- {targetPosition && } - {sourcePosition && } - - {(!summary || mode !== 'full') && ( -
- {title} -
- )} - - {summary && mode === 'full' && ( -
-
- {title} -
- {mode === 'full' && ( -
-
- {summary} -
- {properties && ( -
- {Object.entries(properties).map(([key, value]) => ( - - {key}:{' '} - {typeof value === 'string' && value.startsWith('http') ? ( - - {value} - - ) : ( - value - )} - - ))} -
- )} -
- )} -
- )} -
-
-
- {menu?.length > 0 && ( - - - {menu?.map((item) => { - return ( - - {item.label} - - ); - })} - - - )} -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Data.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Data.tsx deleted file mode 100644 index a9d11fdbc..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Data.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Handle, Position } from '@xyflow/react'; - -import { nodeComponents, type DataNode } from '@eventcatalog/visualizer'; -import * as ContextMenu from '@radix-ui/react-context-menu'; -import { buildUrl } from '@utils/url-builder'; -const DataComponent = nodeComponents.data; - -interface Data { - data: { - data: { - id: string; - version: string; - name: string; - }; - }; -} - -export default function DataNode(props: Data) { - const { id, version, name } = props.data.data; - - return ( - - -
- - - -
-
- - - - Read documentation - - - - - Read changelog - - - - -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Domain.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Domain.tsx deleted file mode 100644 index 3bfaba453..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Domain.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import type { CollectionEntry } from 'astro:content'; -import { Handle, useReactFlow, useOnSelectionChange, Position } from '@xyflow/react'; -import * as ContextMenu from '@radix-ui/react-context-menu'; -import { buildUrl } from '@utils/url-builder'; -import { getIcon } from '@utils/badges'; -import { useState } from 'react'; - -interface Data { - mode: 'simple' | 'full'; - domain: CollectionEntry<'domains'>; -} - -export default function DomainNode({ data, id: nodeId }: any) { - const { mode, domain } = data as Data; - const reactFlow = useReactFlow(); - const [highlightedServices, setHighlightedServices] = useState>(new Set()); - - const { id, version, name, services = [], styles } = domain.data; - const { icon = 'RectangleGroupIcon' } = styles || {}; - - const Icon = getIcon(icon); - const ServerIcon = getIcon('ServerIcon'); - - // Listen for selection changes to highlight connected services - useOnSelectionChange({ - onChange: ({ nodes: selectedNodes }) => { - if (selectedNodes.length === 0) { - setHighlightedServices(new Set()); - return; - } - - const selectedNode = selectedNodes[0]; - if (!selectedNode) { - setHighlightedServices(new Set()); - return; - } - - // Get all edges - const edges = reactFlow.getEdges(); - const connectedServiceIds = new Set(); - - // Find services connected to the selected node - edges.forEach((edge) => { - if (edge.source === selectedNode.id || edge.target === selectedNode.id) { - // Check if this edge connects to our domain - if (edge.source === nodeId && edge.sourceHandle) { - // Extract service ID from sourceHandle (format: "serviceId-source") - const serviceId = edge.sourceHandle.replace('-source', ''); - connectedServiceIds.add(serviceId); - } - if (edge.target === nodeId && edge.targetHandle) { - // Extract service ID from targetHandle (format: "serviceId-target") - const serviceId = edge.targetHandle.replace('-target', ''); - connectedServiceIds.add(serviceId); - } - } - }); - - setHighlightedServices(connectedServiceIds); - }, - }); - - return ( - - -
-
- {Icon && } -
- {name} - v{version} -
-
- {mode === 'full' && services.length > 0 && ( -
- {services.map((service: any, index: number) => { - const isHighlighted = highlightedServices.has(service.data.id); - - return ( - - -
- - -
-
- {ServerIcon && } -
- {service.data.name || service.data.id} -
-
- v{service.data.version} -
-
-
- - - - (window.location.href = buildUrl(`/docs/services/${service.data.id}/${service.data.version}`)) - } - > - View Service Documentation - - - (window.location.href = buildUrl(`/visualiser/services/${service.data.id}/${service.data.version}`)) - } - > - View Service Visualizer - - - -
- ); - })} -
- )} -
-
- - - (window.location.href = buildUrl(`/docs/domains/${id}/${version}`))} - > - View Domain Documentation - - (window.location.href = buildUrl(`/visualiser/domains/${id}/${version}`))} - > - View Domain Visualizer - - - -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx deleted file mode 100644 index 24b5c1620..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { CubeIcon } from '@heroicons/react/16/solid'; -import type { CollectionEntry } from 'astro:content'; -import { Handle, Position } from '@xyflow/react'; -import { getIcon } from '@utils/badges'; -import * as ContextMenu from '@radix-ui/react-context-menu'; -import { buildUrl } from '@utils/url-builder'; -import { useState } from 'react'; - -interface Data { - title: string; - label: string; - bgColor: string; - color: string; - mode: 'simple' | 'full'; - entity: CollectionEntry<'entities'>; - showTarget?: boolean; - showSource?: boolean; - externalToDomain?: boolean; - domainName?: string; - domainId?: string; - group?: { - type: string; - value: string; - }; -} - -function classNames(...classes: any) { - return classes.filter(Boolean).join(' '); -} - -export default function EntityNode({ data, sourcePosition, targetPosition }: any) { - const { mode, entity, externalToDomain, domainName } = data as Data; - const { name, version, properties = [], aggregateRoot, styles, sidebar } = entity.data; - - const { node: { color = 'blue', label } = {}, icon = 'CubeIcon' } = styles || {}; - - const Icon = getIcon(icon); - - const [hoveredProperty, setHoveredProperty] = useState(null); - - return ( - - -
- {/* Table Header */} -
-
- {Icon && } - {name} - {aggregateRoot && AR} -
- {/* {externalToDomain && domainName && ( */} -
from {domainName} domain
- {/* )} */} - {mode === 'full' &&
v{version}
} -
- - {/* Properties Table */} - {properties.length > 0 ? ( -
- {properties.map((property: any, index: number) => { - const propertyKey = `${property.name}-${index}`; - const isHovered = hoveredProperty === propertyKey; - return ( -
property.description && setHoveredProperty(propertyKey)} - onMouseLeave={() => setHoveredProperty(null)} - > - {/* Target handle */} - - - {/* Source handle */} - - - {/* Property content */} -
-
- {property.name} - {property.required && *} -
- {property.type} -
- - {/* Reference indicator */} - {property.references && ( -
-
-
- )} - - {/* Property Tooltip */} - {isHovered && property.description && ( -
-
{property.description}
-
-
- )} -
- ); - })} -
- ) : ( -
No properties defined
- )} - - {/* Main node handles (if no properties) */} - {properties.length === 0 && ( - <> - {targetPosition && } - {sourcePosition && } - - )} -
-
- - - - Read documentation - - - -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Event.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Event.tsx deleted file mode 100644 index 82209f580..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Event.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Handle } from '@xyflow/react'; -import MessageContextMenu from './MessageContextMenu'; -import { Position } from '@xyflow/react'; - -// Import from properly installed package -import { nodeComponents, type EventNode } from '@eventcatalog/visualizer'; -const EventComponent = nodeComponents.event; - -export default function EventNode(props: EventNode) { - return ( - -
- - - -
-
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem.tsx deleted file mode 100644 index e642fd4f3..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { ServerIcon } from '@heroicons/react/16/solid'; -import { GlobeAmericasIcon } from '@heroicons/react/20/solid'; -import type { CollectionEntry } from 'astro:content'; -import { Handle } from '@xyflow/react'; - -interface Data { - label: string; - bgColor: string; - color: string; - mode: 'simple' | 'full'; - step: { id: string; title: string; summary: string; externalSystem: { name: string; summary?: string; url?: string } }; - showTarget?: boolean; - showSource?: boolean; -} - -function classNames(...classes: any) { - return classes.filter(Boolean).join(' '); -} - -export default function ExternalSystemNode({ data, sourcePosition, targetPosition }: any) { - const { mode, step } = data as Data; - const { externalSystem } = step; - const { name, summary, url } = externalSystem; - - return ( -
-
- - {mode === 'full' && ( - - External - - )} -
-
- {targetPosition && } - {sourcePosition && } -
-
- {name} - {mode === 'simple' && ( -
- External System -
- )} -
-
- {mode === 'full' && ( -
-
- {summary} -
- - {url && ( -
- - URL:{' '} - - {url} - - -
- )} -
- )} -
-
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem2.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem2.tsx deleted file mode 100644 index 02935da16..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem2.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Handle, Position } from '@xyflow/react'; - -import { nodeComponents, type ExternalSystemNode } from '@eventcatalog/visualizer'; -const ExternalSystemComponent = nodeComponents.externalSystem; - -export default function ExternalSystemNode(props: ExternalSystemNode) { - return ( -
- - - -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Flow.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Flow.tsx deleted file mode 100644 index 606d4406c..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Flow.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import type { CollectionEntry } from 'astro:content'; -import { Handle } from '@xyflow/react'; -import * as ContextMenu from '@radix-ui/react-context-menu'; -import { buildUrl } from '@utils/url-builder'; -import { getIcon } from '@utils/badges'; - -interface Data { - label: string; - bgColor: string; - color: string; - mode: 'simple' | 'full'; - flow: CollectionEntry<'flows'>; - showTarget?: boolean; - showSource?: boolean; -} - -function classNames(...classes: any) { - return classes.filter(Boolean).join(' '); -} - -export default function FlowNode({ data, sourcePosition, targetPosition }: any) { - const { mode, flow } = data as Data; - - const { id, version, owners = [], name, styles } = flow.data; - const { node: { color = 'teal', label } = {}, icon = 'QueueListIcon' } = styles || {}; - - const Icon = getIcon(icon); - const nodeLabel = label || flow?.data?.sidebar?.badge || 'Flow'; - const fontSize = nodeLabel.length > 10 ? '7px' : '9px'; - - return ( - - -
-
- {Icon && } - {mode === 'full' && ( - - {nodeLabel} - - )} -
-
- {targetPosition && } - {sourcePosition && } -
- {name} -
- v{version} - {mode === 'simple' && ( - {nodeLabel} - )} -
-
- {mode === 'full' && ( -
-
- {flow.data.summary} -
- -
- - Owners: {owners.length} - -
-
- )} -
-
-
- - - - Read documentation - - - View in visualiser - - - - - Read changelog - - - - -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx deleted file mode 100644 index 2575ced29..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as ContextMenu from '@radix-ui/react-context-menu'; -import { buildUrl } from '@utils/url-builder'; -import type { CollectionMessageTypes } from '@types'; -interface Data { - message: { - id: string; - version: string; - name: string; - schemaPath: string; - }; - messageType: CollectionMessageTypes; - children: React.ReactNode; -} - -export default function MessageContextMenu(data: Data) { - const { message, messageType, children } = data; - const { id, version, name, schemaPath } = message; - - if (!id) return null; - - return ( - - {children} - - - - Read documentation - - - {schemaPath && ( - - - Download schema - - - )} - - - Read changelog - - - - - - ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Query.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Query.tsx deleted file mode 100644 index fdd3d05ad..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Query.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Handle } from '@xyflow/react'; -import MessageContextMenu from './MessageContextMenu'; -import { Position } from '@xyflow/react'; - -import { nodeComponents, type QueryNode } from '@eventcatalog/visualizer'; -const QueryComponent = nodeComponents.query; - -export default function QueryNode(props: QueryNode) { - return ( - -
- - - -
-
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx deleted file mode 100644 index cb33973ac..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { Handle } from '@xyflow/react'; -import * as ContextMenu from '@radix-ui/react-context-menu'; -import { buildUrl } from '@utils/url-builder'; -import { Position } from '@xyflow/react'; - -import { nodeComponents, type ServiceNode } from '@eventcatalog/visualizer'; -const ServiceComponent = nodeComponents.service; - -export default function ServiceNode(props: ServiceNode) { - const { id, version, specifications, repository } = props.data.service as any; - - let asyncApiFiles = Array.isArray(specifications) ? specifications?.filter((spec) => spec.type === 'asyncapi') : ([] as any); - let openApiFiles = Array.isArray(specifications) ? specifications?.filter((spec) => spec.type === 'openapi') : ([] as any); - - if (!Array.isArray(specifications) && specifications?.asyncapiPath) { - asyncApiFiles.push({ path: specifications.asyncapiPath, type: 'asyncapi', name: 'AsyncAPI' }); - } - - if (!Array.isArray(specifications) && specifications?.openapiPath) { - openApiFiles.push({ path: specifications.openapiPath, type: 'openapi', name: 'OpenAPI' }); - } - - // Add filename on asyncApiFiles and openApiFiles - asyncApiFiles = asyncApiFiles.map((file: any) => { - return { - ...file, - filename: file.path.split('/').pop()?.split('.').shift(), - }; - }); - openApiFiles = openApiFiles.map((file: any) => { - return { - ...file, - filename: file.path.split('/').pop()?.split('.').shift(), - name: file.name, - }; - }); - - const repositoryUrl = repository?.url; - - return ( - - -
- - - -
-
- - - - Read documentation - - - {asyncApiFiles.length > 0 && - asyncApiFiles.map((file: any) => ( - - - View AsyncAPI specification {file.name ? `(${file.name})` : ''} - - - ))} - {openApiFiles.length > 0 && - openApiFiles.map((file: any) => ( - - - View OpenAPI specification {file.name ? `(${file.name})` : ''} - - - ))} - {asyncApiFiles.length > 0 && openApiFiles.length > 0 && } - {repositoryUrl && ( - <> - - - View code repository - - - - - )} - - - Read changelog - - - - -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Step.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/Step.tsx deleted file mode 100644 index 4243ed82f..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/Step.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Handle } from '@xyflow/react'; - -interface Data { - title: string; - label: string; - bgColor: string; - color: string; - mode: 'simple' | 'full'; - step: { id: string; title: string; summary: string }; - showTarget?: boolean; - showSource?: boolean; -} - -function classNames(...classes: any) { - return classes.filter(Boolean).join(' '); -} - -export default function StepNode({ data, sourcePosition, targetPosition }: any) { - const { mode, step } = data as Data; - - const { title, summary } = step; - - return ( -
-
- {mode === 'full' && ( - - Step - - )} -
-
- {targetPosition && } - {sourcePosition && } - - {!summary && ( -
- {title} -
- )} - - {summary && ( -
-
- {title} -
- {mode === 'full' && ( -
-
- {summary} -
-
- )} -
- )} -
-
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/User.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/User.tsx deleted file mode 100644 index b153f36a9..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/User.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { UserIcon } from '@heroicons/react/20/solid'; -import { Handle } from '@xyflow/react'; - -interface Data { - title: string; - label: string; - bgColor: string; - color: string; - mode: 'simple' | 'full'; - step: { id: string; title: string; summary: string; name: string; actor: { name: string } }; - showTarget?: boolean; - showSource?: boolean; -} - -function classNames(...classes: any) { - return classes.filter(Boolean).join(' '); -} - -export default function UserNode({ data, sourcePosition, targetPosition }: any) { - const { mode, step, showTarget = true, showSource = true } = data as Data; - - const { summary, actor: { name } = {} } = step; - - return ( -
-
- - {mode === 'full' && ( - - ACTOR - - )} -
-
- {targetPosition && } - {sourcePosition && } - - {(!summary || mode !== 'full') && ( -
- {name} - {mode === 'simple' && ( -
- Event -
- )} -
- )} - - {summary && mode === 'full' && ( -
-
- {name} -
- {mode === 'full' && ( -
-
- {summary} -
-
- )} -
- )} -
-
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/Nodes/View.tsx b/eventcatalog/src/components/MDX/NodeGraph/Nodes/View.tsx deleted file mode 100644 index 98ff862e4..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/Nodes/View.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Handle, Position } from '@xyflow/react'; - -import { nodeComponents, type ViewNode } from '@eventcatalog/visualizer'; -const ViewComponent = nodeComponents.view; - -export default function ViewNode(props: ViewNode) { - return ( -
- - - -
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx b/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx deleted file mode 100644 index 21b9c1b0f..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; -import type { Node, Edge } from '@xyflow/react'; - -interface NodeData { - step?: { - title?: string; - summary?: string; - }; - service?: { - data?: { - name?: string; - summary?: string; - }; - }; - message?: { - data?: { - name?: string; - summary?: string; - }; - }; - flow?: { - data?: { - name?: string; - }; - }; - custom?: { - title?: string; - label?: string; - summary?: string; - }; - actor?: { - label?: string; - }; - externalSystem?: { - label?: string; - }; -} - -interface CustomNode { - id: string; - data: NodeData; -} - -interface StepWalkthroughProps { - nodes: CustomNode[]; - edges: Edge[]; - isFlowVisualization: boolean; - onStepChange: (nodeId: string | null, highlightPaths?: string[], shouldZoomOut?: boolean) => void; - mode?: 'full' | 'simple'; -} - -interface PathOption { - targetId: string; - label?: string; - targetNode: CustomNode; -} - -export default function StepWalkthrough({ - nodes, - edges, - isFlowVisualization, - onStepChange, - mode = 'full', -}: StepWalkthroughProps) { - const [currentNodeId, setCurrentNodeId] = useState(null); - const [pathHistory, setPathHistory] = useState([]); - const [currentStepIndex, setCurrentStepIndex] = useState(-1); // -1 means not started - const [availablePaths, setAvailablePaths] = useState([]); - const [selectedPathIndex, setSelectedPathIndex] = useState(0); - const [startNodeId, setStartNodeId] = useState(null); - - useEffect(() => { - if (isFlowVisualization && nodes.length > 0) { - // Find the starting node (node with no incoming edges) - const incomingEdgeMap = new Map(); - nodes.forEach((node: CustomNode) => incomingEdgeMap.set(node.id, 0)); - - edges.forEach((edge: Edge) => { - if (incomingEdgeMap.has(edge.target)) { - incomingEdgeMap.set(edge.target, (incomingEdgeMap.get(edge.target) || 0) + 1); - } - }); - - const startNodes = nodes.filter((node: CustomNode) => incomingEdgeMap.get(node.id) === 0); - if (startNodes.length > 0 && !startNodeId) { - const firstStartNode = startNodes[0]; - setStartNodeId(firstStartNode.id); - } - } - }, [nodes, edges, isFlowVisualization, startNodeId]); - - useEffect(() => { - if (currentNodeId) { - // Find available paths from current node - const outgoingEdges = edges.filter((edge: Edge) => edge.source === currentNodeId); - const paths: PathOption[] = outgoingEdges.map((edge: Edge) => { - const targetNode = nodes.find((n: CustomNode) => n.id === edge.target); - return { - targetId: edge.target, - label: edge.label as string | undefined, - targetNode: targetNode!, - }; - }); - setAvailablePaths(paths); - setSelectedPathIndex(0); - } else { - setAvailablePaths([]); - } - }, [currentNodeId, nodes, edges]); - - const handleNextStep = useCallback(() => { - if (currentStepIndex === -1) { - // Start the walkthrough - if (startNodeId) { - setPathHistory([startNodeId]); - setCurrentNodeId(startNodeId); - setCurrentStepIndex(0); - onStepChange(startNodeId); - } - } else if (availablePaths.length > 0) { - // Move to the selected path - const selectedPath = availablePaths[selectedPathIndex]; - const newHistory = [...pathHistory, selectedPath.targetId]; - setPathHistory(newHistory); - setCurrentNodeId(selectedPath.targetId); - setCurrentStepIndex((prev) => prev + 1); - - // Highlight the selected path - const allPaths = availablePaths.map((p) => `${currentNodeId}-${p.targetId}`); - onStepChange(selectedPath.targetId, allPaths); - } - }, [currentStepIndex, startNodeId, availablePaths, selectedPathIndex, currentNodeId, onStepChange]); - - const handlePreviousStep = useCallback(() => { - if (currentStepIndex > 0) { - // Go back to previous step - const newIndex = currentStepIndex - 1; - const prevNodeId = pathHistory[newIndex]; - setCurrentNodeId(prevNodeId); - setCurrentStepIndex(newIndex); - onStepChange(prevNodeId); - } else if (currentStepIndex === 0) { - // Go back to the start (no selection) - setCurrentNodeId(null); - setCurrentStepIndex(-1); - onStepChange(null); - } - }, [currentStepIndex, pathHistory, onStepChange]); - - const handlePathSelection = useCallback((index: number) => { - setSelectedPathIndex(index); - }, []); - - const handleFinish = useCallback(() => { - setCurrentNodeId(null); - setCurrentStepIndex(-1); - setPathHistory([]); - onStepChange(null, [], true); // Pass true to indicate full reset with zoom out - }, [onStepChange]); - - if (!isFlowVisualization || nodes.length === 0 || mode !== 'full') { - return null; - } - - const getCurrentStepInfo = () => { - if (currentStepIndex === -1) { - return { title: 'Walk through business flow', description: 'Step through the flow to understand the business process' }; - } - - const currentNode = nodes.find((n: CustomNode) => n.id === currentNodeId); - if (!currentNode) return { title: 'Unknown step', description: '' }; - - let stepNumber = currentStepIndex + 1; - let title = `Step ${stepNumber}`; - let description = ''; - - // Get node information based on type - check step data first, then type-specific data - if (currentNode.data.step?.title) { - title += `: ${currentNode.data.step.title}`; - } else if (currentNode.data.service?.data?.name) { - title += `: ${currentNode.data.service.data.name}`; - } else if (currentNode.data.message?.data?.name) { - title += `: ${currentNode.data.message.data.name}`; - } else if (currentNode.data.flow?.data?.name) { - title += `: ${currentNode.data.flow.data.name}`; - } else if (currentNode.data.custom?.title) { - title += `: ${currentNode.data.custom.title}`; - } else if (currentNode.data.custom?.label) { - title += `: ${currentNode.data.custom.label}`; - } else if (currentNode.data.actor?.label) { - title += `: ${currentNode.data.actor.label}`; - } else if (currentNode.data.externalSystem?.label) { - title += `: ${currentNode.data.externalSystem.label}`; - } - - // Get description - check step data first, then type-specific data - if (currentNode.data.step?.summary) { - description = currentNode.data.step.summary; - } else if (currentNode.data.service?.data?.summary) { - description = currentNode.data.service.data.summary; - } else if (currentNode.data.message?.data?.summary) { - description = currentNode.data.message.data.summary; - } else if (currentNode.data.custom?.summary) { - description = currentNode.data.custom.summary; - } - - return { title, description }; - }; - - const { title, description } = getCurrentStepInfo(); - - return ( -
-
-

{title}

- {description &&

{description}

} -
- - {/* Show path options when there are multiple paths */} - {currentNodeId && availablePaths.length > 1 && ( -
- - -
- )} - -
- {currentStepIndex === -1 ? ( - // Initial state - show only Start button on the right - <> -
- - - ) : ( - // In walkthrough - show Previous on left, Next on right (only if paths available) - <> - - - {availablePaths.length > 0 ? ( - - ) : ( - - )} - - )} -
-
- ); -} diff --git a/eventcatalog/src/components/MDX/NodeGraph/StudioModal.tsx b/eventcatalog/src/components/MDX/NodeGraph/StudioModal.tsx deleted file mode 100644 index 241952f9f..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/StudioModal.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import React, { useState, useCallback } from 'react'; -import * as Dialog from '@radix-ui/react-dialog'; -import { CheckIcon, ClipboardIcon, ExternalLinkIcon } from 'lucide-react'; -import { useReactFlow } from '@xyflow/react'; -import { exportNodeGraphForStudio } from '@utils/node-graphs/export-node-graph'; - -interface StudioModalProps { - isOpen: boolean; - onClose: () => void; -} - -const StudioModal: React.FC = ({ isOpen, onClose }) => { - const [copySuccess, setCopySuccess] = useState(false); - - const { toObject } = useReactFlow(); - - const handleCopyToClipboard = useCallback(async () => { - const visualizerData = toObject(); - const studioData = exportNodeGraphForStudio(visualizerData); - - try { - await navigator.clipboard.writeText(JSON.stringify(studioData, null, 2)); - setCopySuccess(true); - setTimeout(() => setCopySuccess(false), 2000); - } catch (error) { - console.error('Failed to copy to clipboard:', error); - // Fallback for older browsers - const textarea = document.createElement('textarea'); - textarea.value = JSON.stringify(studioData, null, 2); - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - setCopySuccess(true); - setTimeout(() => setCopySuccess(false), 2000); - } - }, []); - - const handleOpenStudio = () => { - window.open( - 'https://app.eventcatalog.studio/playground?import=true&utm_source=eventcatalog&utm_medium=referral&utm_campaign=playground-import', - '_blank' - ); - onClose(); - }; - - return ( - - - - - Open in EventCatalog Studio - - - Import your diagram into{' '} - - EventCatalog Studio - {' '} - to create designs from your visualization of your architecture. - - -
-
-

Step 1: Copy diagram

-

Copy your diagram data to your clipboard.

- -
- -
-

Step 2: Open EventCatalog Studio

-

- Go to EventCatalog Studio and import your design using the "Import from EventCatalog" button. -

- - -

- Don't worry, none of your data is stored by EventCatalog Studio, everything is local to your browser. -

-
-
- -
- - - -
-
-
-
- ); -}; - -export default StudioModal; diff --git a/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx b/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx deleted file mode 100644 index 5c71d2dbe..000000000 --- a/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useState, useCallback, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; -import type { Node } from '@xyflow/react'; - -// Define interfaces for different node data structures -interface MessageData { - name: string; - version?: string; -} - -interface ServiceData { - name: string; - version?: string; -} - -interface DomainData { - name: string; - version?: string; -} - -interface EntityData { - name: string; - version?: string; -} - -interface NodeDataContent extends Record { - message?: { - data: MessageData; - }; - service?: { - data: ServiceData; - }; - domain?: { - data: DomainData; - }; - entity?: { - data: EntityData; - }; - name?: string; - version?: string; -} - -// Extend the Node type with our custom data structure -type CustomNode = Node; - -interface VisualiserSearchProps { - nodes: CustomNode[]; - onNodeSelect: (node: CustomNode) => void; - onClear: () => void; - onPaneClick?: () => void; -} - -export interface VisualiserSearchRef { - hideSuggestions: () => void; -} - -const VisualiserSearch = forwardRef( - ({ nodes, onNodeSelect, onClear, onPaneClick }, ref) => { - const [searchQuery, setSearchQuery] = useState(''); - const [filteredSuggestions, setFilteredSuggestions] = useState([]); - const [showSuggestions, setShowSuggestions] = useState(false); - const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1); - const searchInputRef = useRef(null); - const containerRef = useRef(null); - - const hideSuggestions = useCallback(() => { - setShowSuggestions(false); - setSelectedSuggestionIndex(-1); - }, []); - - useImperativeHandle( - ref, - () => ({ - hideSuggestions, - }), - [hideSuggestions] - ); - - const getNodeDisplayName = useCallback((node: CustomNode) => { - const name = - node.data?.message?.data?.name || - node.data?.service?.data?.name || - node.data?.domain?.data?.name || - node.data?.entity?.data?.name || - node.data?.name || - node.id; - const version = - node.data?.message?.data?.version || - node.data?.service?.data?.version || - node.data?.domain?.data?.version || - node.data?.entity?.data?.version || - node.data?.version; - return version ? `${name} (v${version})` : name; - }, []); - - const getNodeTypeColorClass = useCallback((nodeType: string) => { - const colorClasses: { [key: string]: string } = { - events: 'bg-orange-600 text-white', - services: 'bg-pink-600 text-white', - flows: 'bg-teal-600 text-white', - commands: 'bg-blue-600 text-white', - queries: 'bg-green-600 text-white', - channels: 'bg-gray-600 text-white', - domains: 'bg-yellow-500 text-white', - externalSystem: 'bg-pink-600 text-white', - actor: 'bg-yellow-500 text-white', - step: 'bg-gray-700 text-white', - user: 'bg-yellow-500 text-white', - custom: 'bg-gray-500 text-white', - }; - return colorClasses[nodeType] || 'bg-gray-100 text-gray-700'; - }, []); - - const handleSearchChange = useCallback( - (event: React.ChangeEvent) => { - const query = event.target.value; - setSearchQuery(query); - - if (query.length > 0) { - const filtered = nodes.filter((node) => { - const nodeName = getNodeDisplayName(node); - return nodeName.toLowerCase().includes(query.toLowerCase()); - }); - setFilteredSuggestions(filtered); - setShowSuggestions(true); - setSelectedSuggestionIndex(-1); - } else { - setFilteredSuggestions(nodes); - setShowSuggestions(true); - setSelectedSuggestionIndex(-1); - } - }, - [nodes, getNodeDisplayName] - ); - - const handleSearchFocus = useCallback(() => { - if (searchQuery.length === 0) { - setFilteredSuggestions(nodes); - } - setShowSuggestions(true); - setSelectedSuggestionIndex(-1); - }, [nodes, searchQuery]); - - const handleSuggestionClick = useCallback( - (node: CustomNode) => { - setSearchQuery(getNodeDisplayName(node)); - setShowSuggestions(false); - onNodeSelect(node); - }, - [onNodeSelect, getNodeDisplayName] - ); - - const handleSearchKeyDown = useCallback( - (event: React.KeyboardEvent) => { - if (!showSuggestions || filteredSuggestions.length === 0) return; - - switch (event.key) { - case 'ArrowDown': - event.preventDefault(); - setSelectedSuggestionIndex((prev) => (prev < filteredSuggestions.length - 1 ? prev + 1 : 0)); - break; - case 'ArrowUp': - event.preventDefault(); - setSelectedSuggestionIndex((prev) => (prev > 0 ? prev - 1 : filteredSuggestions.length - 1)); - break; - case 'Enter': - event.preventDefault(); - if (selectedSuggestionIndex >= 0) { - handleSuggestionClick(filteredSuggestions[selectedSuggestionIndex]); - } - break; - case 'Escape': - setShowSuggestions(false); - setSelectedSuggestionIndex(-1); - break; - } - }, - [showSuggestions, filteredSuggestions, selectedSuggestionIndex, handleSuggestionClick] - ); - - const clearSearch = useCallback(() => { - setSearchQuery(''); - setShowSuggestions(false); - setFilteredSuggestions([]); - setSelectedSuggestionIndex(-1); - onClear(); - if (searchInputRef.current) { - searchInputRef.current.focus(); - } - }, [onClear]); - - // Close suggestions when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (containerRef.current && !containerRef.current.contains(event.target as any)) { - setShowSuggestions(false); - setSelectedSuggestionIndex(-1); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - return ( -
-
- - {searchQuery && ( - - )} -
- {showSuggestions && filteredSuggestions.length > 0 && ( -
- {filteredSuggestions.map((node, index) => { - const nodeName = getNodeDisplayName(node); - const nodeType = node.type || 'unknown'; - return ( -
handleSuggestionClick(node)} - className={`px-4 py-2 cursor-pointer flex items-center justify-between hover:bg-gray-100 ${ - index === selectedSuggestionIndex ? 'bg-purple-50' : '' - }`} - > - {nodeName} - {nodeType} -
- ); - })} -
- )} -
- ); - } -); - -VisualiserSearch.displayName = 'VisualiserSearch'; - -export default VisualiserSearch; diff --git a/eventcatalog/src/components/MDX/ResourceGroupTable/ResourceGroupTable.client.tsx b/eventcatalog/src/components/MDX/ResourceGroupTable/ResourceGroupTable.client.tsx deleted file mode 100644 index c7a5a1867..000000000 --- a/eventcatalog/src/components/MDX/ResourceGroupTable/ResourceGroupTable.client.tsx +++ /dev/null @@ -1,408 +0,0 @@ -import { getColorAndIconForCollection } from '@utils/collections/icons'; -import { buildUrl } from '@utils/url-builder'; -import { useState, useMemo, useCallback, memo } from 'react'; - -type Resource = { - id: string; - name: string; - version: string; - collection: string; - type: string; - summary?: string; - description?: string; - owners?: any[]; - tags?: string[]; -}; - -type ResourceGroupTableProps = { - resources: Resource[]; - limit?: number; - showTags?: boolean; - showOwners?: boolean; - title: string; - subtitle?: string; - description: string; -}; - -type ResourceType = 'service' | 'event' | 'query' | 'command' | 'domain' | 'flow' | 'channel' | 'user' | 'team' | null; - -const ResourceRow = memo( - ({ resource, showTags, showOwners }: { resource: Resource; showTags?: boolean; showOwners?: boolean }) => { - const { color, Icon } = getColorAndIconForCollection(resource.collection); - const url = buildUrl(`/docs/${resource.collection}/${resource.id}/${resource.version}`); - let type = resource.collection.slice(0, -1); - type = type === 'querie' ? 'query' : type; - - const tags = resource.tags || []; - const owners = resource.owners || []; - - return ( - - - -
- - {resource.name} -
- - -
- -
- -
-
- - {IconComponent && } -
-
- - -
-

- {title} -

-

- {description} -

-
-
diff --git a/eventcatalog/src/components/MDX/Tiles/Tiles.astro b/eventcatalog/src/components/MDX/Tiles/Tiles.astro deleted file mode 100644 index 31ab25801..000000000 --- a/eventcatalog/src/components/MDX/Tiles/Tiles.astro +++ /dev/null @@ -1,10 +0,0 @@ ---- -const { title, columns = 2 } = Astro.props; ---- - -
- {title &&

{title}

} -
- -
-
diff --git a/eventcatalog/src/components/MDX/components.tsx b/eventcatalog/src/components/MDX/components.tsx deleted file mode 100644 index 81d850edc..000000000 --- a/eventcatalog/src/components/MDX/components.tsx +++ /dev/null @@ -1,72 +0,0 @@ -// React components -import Schema from '@components/MDX/Schema.astro'; -import File from '@components/MDX/File'; -import Accordion from '@components/MDX/Accordion/Accordion.astro'; -import AccordionGroup from '@components/MDX/Accordion/AccordionGroup.astro'; -import Flow from '@components/MDX/Flow/Flow.astro'; -import EntityMap from '@components/MDX/EntityMap/EntityMap.astro'; -import Tiles from '@components/MDX/Tiles/Tiles.astro'; -import Tile from '@components/MDX/Tiles/Tile.astro'; -import Steps from '@components/MDX/Steps/Steps.astro'; -import Step from '@components/MDX/Steps/Step.astro'; -import Admonition from '@components/MDX/Admonition'; -import OpenAPI from '@components/MDX/OpenAPI/OpenAPI.astro'; -import AsyncAPI from '@components/MDX/AsyncAPI/AsyncAPI.astro'; -import ChannelInformation from '@components/MDX/ChannelInformation/ChannelInformation'; -import Attachments from '@components/MDX/Attachments.astro'; -import MessageTable from '@components/MDX/MessageTable/MessageTable.astro'; -import ResourceGroupTable from '@components/MDX/ResourceGroupTable/ResourceGroupTable.astro'; -import EntityPropertiesTable from '@components/MDX/EntityPropertiesTable/EntityPropertiesTable.astro'; -import Tabs from '@components/MDX/Tabs/Tabs.astro'; -import TabItem from '@components/MDX/Tabs/TabItem.astro'; -import ResourceLink from '@components/MDX/ResourceLink/ResourceLink.astro'; -import Link from '@components/MDX/Link/Link.astro'; -import Miro from '@components/MDX/Miro/Miro.astro'; -import Lucid from '@components/MDX/Lucid/Lucid.astro'; -import DrawIO from '@components/MDX/DrawIO/DrawIO.astro'; -import FigJam from '@components/MDX/FigJam/FigJam.astro'; -import Design from '@components/MDX/Design/Design.astro'; -import MermaidFileLoader from '@components/MDX/MermaidFileLoader/MermaidFileLoader.astro'; -// Portals: required for server/client components -import NodeGraphPortal from '@components/MDX/NodeGraph/NodeGraphPortal'; -import SchemaViewerPortal from '@components/MDX/SchemaViewer/SchemaViewerPortal'; -import { jsx } from 'astro/jsx-runtime'; -import RemoteSchema from '@components/MDX/RemoteSchema.astro'; - -const components = (props: any) => { - return { - Attachments: (mdxProp: any) => jsx(Attachments, { ...props, ...mdxProp }), - Accordion, - AccordionGroup, - Admonition, - AsyncAPI, - ChannelInformation: (mdxProp: any) => ChannelInformation({ ...props.data, ...mdxProp }), - Design: (mdxProp: any) => jsx(Design, { ...props, ...mdxProp }), - File: (mdxProp: any) => File({ ...props, ...mdxProp }), - RemoteSchema, - Flow, - Link: (mdxProp: any) => jsx(Link, { ...props, ...mdxProp }), - MessageTable: (mdxProp: any) => jsx(MessageTable, { ...props, ...mdxProp }), - EntityPropertiesTable: (mdxProp: any) => jsx(EntityPropertiesTable, { ...props, ...mdxProp }), - NodeGraph: (mdxProp: any) => jsx(NodeGraphPortal, { ...props.data, ...mdxProp, props, mdxProp }), - EntityMap: (mdxProp: any) => jsx(EntityMap, { ...props, ...mdxProp }), - OpenAPI, - ResourceGroupTable: (mdxProp: any) => jsx(ResourceGroupTable, { ...props, ...mdxProp }), - ResourceLink: (mdxProp: any) => jsx(ResourceLink, { ...props, ...mdxProp }), - Schema: (mdxProp: any) => jsx(Schema, { ...props, ...mdxProp }), - SchemaViewer: (mdxProp: any) => SchemaViewerPortal({ ...props.data, ...mdxProp }), - Step, - Steps, - TabItem, - Tabs, - Tile, - Tiles, - Miro: (mdxProp: any) => jsx(Miro, { ...props, ...mdxProp }), - Lucid: (mdxProp: any) => jsx(Lucid, { ...props, ...mdxProp }), - DrawIO: (mdxProp: any) => jsx(DrawIO, { ...props, ...mdxProp }), - FigJam: (mdxProp: any) => jsx(FigJam, { ...props, ...mdxProp }), - MermaidFileLoader: (mdxProp: any) => jsx(MermaidFileLoader, { ...props, ...mdxProp }), - }; -}; - -export default components; diff --git a/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx b/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx deleted file mode 100644 index 29feb0e35..000000000 --- a/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { ChevronUpIcon, ChevronDownIcon, ClipboardDocumentIcon } from '@heroicons/react/24/outline'; -import type { SchemaItem } from './types'; - -interface ApiAccessSectionProps { - message: SchemaItem; - isExpanded: boolean; - onToggle: () => void; - onCopy: (content: string, id: string) => void; - copiedId: string | null; - apiAccessEnabled?: boolean; -} - -export default function ApiAccessSection({ - message, - isExpanded, - onToggle, - onCopy, - copiedId, - apiAccessEnabled = false, -}: ApiAccessSectionProps) { - // Generate API path based on collection type - let apiPath = ''; - if (message.collection === 'services') { - const specType = message.specType || 'openapi'; - apiPath = `/api/schemas/services/${message.data.id}/${message.data.version}/${specType}`; - } else { - apiPath = `/api/schemas/${message.collection}/${message.data.id}/${message.data.version}`; - } - - const curlCommand = typeof window !== 'undefined' ? `curl -X GET "${window.location.origin}${apiPath}"` : ''; - - return ( -
- - - {isExpanded && ( -
- {apiAccessEnabled ? ( - <> -

Access this schema programmatically via API

-
-
- GET - -
- {apiPath} -
-

Example:

- {curlCommand} -
-
- - ) : ( -
-
-
- - - -
-
-

Upgrade to Scale

-

- Access your schemas programmatically via API. Perfect for CI/CD pipelines, automation, and integrations. -

- - Start 14-day free trial - - - - -
-
-
- )} -
- )} -
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx b/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx deleted file mode 100644 index 7c6f7cd2a..000000000 --- a/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { ArrowsPointingOutIcon } from '@heroicons/react/24/outline'; -import type { VersionDiff } from './types'; - -interface DiffViewerProps { - diffs: VersionDiff[]; - onOpenFullscreen?: () => void; - apiAccessEnabled?: boolean; -} - -export default function DiffViewer({ diffs, onOpenFullscreen, apiAccessEnabled = false }: DiffViewerProps) { - if (diffs.length === 0) return null; - - return ( -
-
-
-

Version History

-

- {apiAccessEnabled - ? `Showing ${diffs.length} version comparison${diffs.length !== 1 ? 's' : ''}` - : 'Compare schema versions side-by-side'} -

-
- {onOpenFullscreen && apiAccessEnabled && ( - - )} -
- {apiAccessEnabled ? ( -
- {diffs.map((diff, index) => ( -
-
-
-
- v{diff.newerVersion} - - v{diff.olderVersion} -
- - {index === 0 ? 'Latest change' : `${index + 1} version${index + 1 !== 1 ? 's' : ''} ago`} - -
-
-
-
-
-
- ))} -
- ) : ( -
-
-
- - - -
-

Upgrade to Scale

-

- Compare schema versions side-by-side with visual diffs. Track breaking changes, see exactly what changed between - versions, and maintain better schema governance. -

- - Start 14-day free trial - - - - -
-
- )} -
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx b/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx deleted file mode 100644 index 833e65838..000000000 --- a/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +++ /dev/null @@ -1,740 +0,0 @@ -import { useState, useEffect, useRef, useMemo } from 'react'; - -interface JSONSchemaViewerProps { - schema: any; - title?: string; - maxHeight?: string; - expand?: boolean | string; - search?: boolean | string; - id?: string; - onOpenFullscreen?: () => void; -} - -interface SchemaPropertyProps { - name: string; - details: any; - isRequired: boolean; - level: number; - isListItem?: boolean; - expand: boolean; -} - -// Helper function to count properties recursively -function countProperties(obj: any): number { - if (!obj || typeof obj !== 'object') return 0; - - let count = 0; - if (obj.properties) { - count += Object.keys(obj.properties).length; - Object.values(obj.properties).forEach((prop: any) => { - count += countProperties(prop); - }); - } - if (obj.items) { - count += countProperties(obj.items); - } - if (obj._isRootArrayItem && obj._rootArraySchema?.items) { - // Don't double count - } - return count; -} - -// Schema processing functions -function mergeAllOfSchemas(schemaWithProcessor: any): any { - const { processSchema: processor, ...schema } = schemaWithProcessor; - if (!schema.allOf) return schema; - - const mergedSchema: { - type: string; - properties: Record; - required: string[]; - description?: string; - [key: string]: any; - } = { - type: schema.type || 'object', - properties: {}, - required: [], - description: schema.description, - }; - - // Copy base schema properties first (excluding allOf) - Object.keys(schema).forEach((key) => { - if (key !== 'allOf' && key !== 'properties' && key !== 'required' && key !== 'description') { - mergedSchema[key] = schema[key]; - } - }); - - // Copy base properties if they exist - if (schema.properties) { - mergedSchema.properties = { ...schema.properties }; - } - if (schema.required) { - mergedSchema.required = [...schema.required]; - } - - schema.allOf.forEach((subSchema: any) => { - const processedSubSchema = processor ? processor(subSchema) : subSchema; - - if (processedSubSchema.properties) { - mergedSchema.properties = { - ...mergedSchema.properties, - ...processedSubSchema.properties, - }; - } - if (processedSubSchema.required) { - mergedSchema.required = [...new Set([...mergedSchema.required, ...processedSubSchema.required])]; - } - if (processedSubSchema.description && !mergedSchema.description) { - mergedSchema.description = processedSubSchema.description; - } - - Object.keys(processedSubSchema).forEach((key) => { - if (key !== 'properties' && key !== 'required' && key !== 'description' && key !== 'type') { - if (!mergedSchema[key]) { - mergedSchema[key] = processedSubSchema[key]; - } - } - }); - }); - - return mergedSchema; -} - -function processSchema(schema: any, rootSchema?: any): any { - if (!schema) return schema; - - const root = rootSchema || schema; - - // Handle $ref - if (schema.$ref) { - const refPath = schema.$ref; - let resolvedSchema = null; - let defName = ''; - - // Try draft-7 style first: #/definitions/ - if (refPath.startsWith('#/definitions/')) { - defName = refPath.replace('#/definitions/', ''); - if (root.definitions && root.definitions[defName]) { - resolvedSchema = root.definitions[defName]; - } - } - // Try 2020-12 style: #/$defs/ - else if (refPath.startsWith('#/$defs/')) { - defName = refPath.replace('#/$defs/', ''); - if (root.$defs && root.$defs[defName]) { - resolvedSchema = root.$defs[defName]; - } - } - // Try other common patterns - else if (refPath.startsWith('#/components/schemas/')) { - defName = refPath.replace('#/components/schemas/', ''); - if (root.components && root.components.schemas && root.components.schemas[defName]) { - resolvedSchema = root.components.schemas[defName]; - } - } - - if (resolvedSchema) { - const processedSchema = processSchema(resolvedSchema, root); - return { - ...processedSchema, - _refPath: refPath, - _refName: defName, - _originalRef: schema.$ref, - }; - } - - return { - type: 'string', - description: `Reference to ${refPath} (definition not found in root schema)`, - title: defName || refPath.split('/').pop(), - _refPath: refPath, - _refName: defName, - _refNotFound: true, - }; - } - - if (schema.allOf) { - return mergeAllOfSchemas({ ...schema, processSchema: (s: any) => processSchema(s, root) }); - } - - if (schema.oneOf) { - const processedVariants = schema.oneOf.map((variant: any) => { - const processedVariant = processSchema(variant, root); - return { - title: processedVariant.title || variant.title || 'Unnamed Variant', - required: processedVariant.required || variant.required || [], - properties: processedVariant.properties || {}, - ...processedVariant, - }; - }); - - const allProperties: Record = {}; - processedVariants.forEach((variant: any) => { - if (variant.properties) { - Object.assign(allProperties, variant.properties); - } - }); - - return { - ...schema, - type: schema.type || 'object', - properties: { - ...(schema.properties || {}), - ...allProperties, - }, - variants: processedVariants, - }; - } - - // Process nested schemas in properties - if (schema.properties) { - const processedProperties: Record = {}; - Object.entries(schema.properties).forEach(([key, prop]: [string, any]) => { - processedProperties[key] = processSchema(prop, root); - }); - schema = { ...schema, properties: processedProperties }; - } - - // Process array items - if (schema.type === 'array' && schema.items) { - schema = { ...schema, items: processSchema(schema.items, root) }; - } - - return schema; -} - -// SchemaProperty component -const SchemaProperty = ({ name, details, isRequired, level, isListItem = false, expand }: SchemaPropertyProps) => { - const [isExpanded, setIsExpanded] = useState(expand); - const contentId = useRef(`prop-${name}-${level}-${Math.random().toString(36).substring(2, 7)}`).current; - - useEffect(() => { - setIsExpanded(expand); - }, [expand]); - - const hasNestedProperties = details.type === 'object' && details.properties && Object.keys(details.properties).length > 0; - const hasArrayItems = details.type === 'array' && details.items; - const hasArrayItemProperties = - hasArrayItems && - ((details.items.type === 'object' && details.items.properties) || - details.items.allOf || - details.items.oneOf || - details.items.$ref); - const isCollapsible = hasNestedProperties || hasArrayItemProperties; - - const indentationClass = `pl-${level * 3}`; - - return ( -
-
- {isCollapsible ? ( - - ) : ( -
- )} - -
-
-
- {name} - - {details.type} - {details.type === 'array' && details.items?.type ? `[${details.items.type}]` : ''} - {details.format ? `<${details.format}>` : ''} - {details._refPath && → {details._refName || details._refPath}} - {details._refNotFound && ❌ ref not found} - {details.const !== undefined && ( - - constant: {details.const} - - )} - -
- {isRequired && required} -
- - {details.description &&

{details.description}

} - {details.title && details.title !== details.description && ( -

Title: {details.title}

- )} - -
- {details.pattern && ( -
- Match pattern: {details.pattern} -
- )} - {details.minimum !== undefined && ( -
- Minimum: {details.minimum} -
- )} - {details.maximum !== undefined && ( -
- Maximum: {details.maximum} -
- )} - {details.minLength !== undefined && ( -
- Min length: {details.minLength} -
- )} - {details.maxLength !== undefined && ( -
- Max length: {details.maxLength} -
- )} - {details.enum && ( -
- Allowed values: - {details.enum.map((val: any, idx: number) => ( - - {' '} - {val} - - ))} -
- )} -
- - {(hasNestedProperties || hasArrayItems) && ( -
- {hasNestedProperties && - details.properties && - Object.entries(details.properties).map(([nestedName, nestedDetails]: [string, any]) => ( - - ))} - - {hasArrayItemProperties && ( -
- Item Details: - {details.items.properties && - Object.entries(details.items.properties).map(([itemPropName, itemPropDetails]: [string, any]) => ( - - ))} - {(details.items.allOf || details.items.oneOf || details.items.$ref) && !details.items.properties && ( -
- Complex array item schema detected. The properties should be processed by the parent SchemaViewer. -
- )} -
- )} -
- )} -
-
-
- ); -}; - -// Main JSONSchemaViewer component -export default function JSONSchemaViewer({ - schema, - title, - maxHeight, - expand = false, - search = true, - id, - onOpenFullscreen, -}: JSONSchemaViewerProps) { - // Convert string props to booleans (MDX passes strings) - const expandBool = expand === true || expand === 'true'; - const searchBool = search !== false && search !== 'false'; - - const [searchQuery, setSearchQuery] = useState(''); - const [expandAll, setExpandAll] = useState(expandBool); - const [selectedVariantIndex, setSelectedVariantIndex] = useState(0); - const [currentMatches, setCurrentMatches] = useState([]); - const [currentMatchIndex, setCurrentMatchIndex] = useState(-1); - const searchInputRef = useRef(null); - const propertiesContainerRef = useRef(null); - - const processedSchema = useMemo(() => processSchema(schema), [schema]); - - // Handle root-level array schemas - const { displaySchema, isRootArray } = useMemo(() => { - let display = processedSchema; - let isArray = false; - - if (processedSchema.type === 'array' && processedSchema.items) { - isArray = true; - if (processedSchema.items.type === 'object' && processedSchema.items.properties) { - display = { - ...processedSchema.items, - description: processedSchema.description || processedSchema.items.description, - _isRootArrayItem: true, - _rootArraySchema: processedSchema, - }; - } else if (processedSchema.items.allOf || processedSchema.items.oneOf || processedSchema.items.$ref) { - display = { - ...processSchema(processedSchema.items), - description: processedSchema.description || processedSchema.items.description, - _isRootArrayItem: true, - _rootArraySchema: processedSchema, - }; - } - } - - return { displaySchema: display, isRootArray: isArray }; - }, [processedSchema]); - - const { description, properties, required = [], variants } = displaySchema; - const totalProperties = useMemo(() => countProperties(displaySchema), [displaySchema]); - - // Search functionality - useEffect(() => { - if (!propertiesContainerRef.current) return; - - const propertyContainers = propertiesContainerRef.current.querySelectorAll('.property-container'); - const matches: HTMLElement[] = []; - - if (searchQuery === '') { - // Reset search - propertyContainers.forEach((container) => { - container.classList.remove('search-match', 'search-no-match', 'search-current-match', 'search-dimmed'); - const nameEl = container.querySelector('.font-semibold'); - if (nameEl) { - nameEl.innerHTML = nameEl.textContent || ''; - } - }); - setCurrentMatches([]); - setCurrentMatchIndex(-1); - return; - } - - const query = searchQuery.toLowerCase().trim(); - - propertyContainers.forEach((container) => { - const nameEl = container.querySelector('.font-semibold'); - if (!nameEl) return; - - const propName = (nameEl.textContent || '').toLowerCase(); - - if (propName.includes(query)) { - container.classList.add('search-match'); - container.classList.remove('search-dimmed'); - matches.push(container as HTMLElement); - - // Highlight the search term - const regex = new RegExp(`(${query})`, 'gi'); - nameEl.innerHTML = (nameEl.textContent || '').replace(regex, '$1'); - - // Expand parent containers and remove dimming from them - let parent = container.parentElement; - while (parent && parent !== propertiesContainerRef.current) { - if (parent.classList.contains('nested-content') && parent.classList.contains('hidden')) { - const parentPropertyContainer = parent.closest('.property-container'); - if (parentPropertyContainer) { - const toggleBtn = parentPropertyContainer.querySelector('.property-toggle'); - if (toggleBtn && toggleBtn.getAttribute('aria-expanded') === 'false') { - (toggleBtn as HTMLButtonElement).click(); - } - } - } - // Remove dimming from parent property containers so they're fully visible - if (parent.classList.contains('property-container')) { - parent.classList.remove('search-dimmed'); - } - parent = parent.parentElement; - } - } else { - container.classList.remove('search-match', 'search-current-match'); - container.classList.add('search-dimmed'); - nameEl.innerHTML = nameEl.textContent || ''; - } - }); - - setCurrentMatches(matches); - if (matches.length > 0) { - setCurrentMatchIndex(0); - matches[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); - } else { - setCurrentMatchIndex(-1); - } - }, [searchQuery]); - - // Update match highlighting - useEffect(() => { - currentMatches.forEach((match, index) => { - if (index === currentMatchIndex) { - match.classList.add('search-current-match'); - } else { - match.classList.remove('search-current-match'); - } - }); - - if (currentMatchIndex >= 0 && currentMatches[currentMatchIndex]) { - currentMatches[currentMatchIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - }, [currentMatchIndex, currentMatches]); - - const handleExpandAll = () => { - setExpandAll(true); - }; - - const handleCollapseAll = () => { - setExpandAll(false); - }; - - const handlePrevMatch = () => { - if (currentMatchIndex > 0) { - setCurrentMatchIndex(currentMatchIndex - 1); - } - }; - - const handleNextMatch = () => { - if (currentMatchIndex < currentMatches.length - 1) { - setCurrentMatchIndex(currentMatchIndex + 1); - } - }; - - if (!schema) { - return ( -
-

Unable to parse JSON schema

-
- ); - } - - const containerStyle = maxHeight - ? { - maxHeight: maxHeight.includes('px') ? maxHeight : `${maxHeight}px`, - minHeight: '15em', - } - : {}; - - // Use h-full when no maxHeight (SchemaExplorer context), otherwise size based on content (MDX context) - const heightClass = maxHeight ? '' : 'h-full'; - const overflowClass = maxHeight ? 'overflow-hidden' : ''; - - return ( -
- {/* Toolbar */} - {searchBool && ( -
-
-
- setSearchQuery(e.target.value)} - placeholder="Search properties..." - className="w-full px-3 py-1.5 pr-20 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - if (e.shiftKey) { - handlePrevMatch(); - } else { - handleNextMatch(); - } - } - }} - /> -
- - -
-
-
- {onOpenFullscreen && ( - - )} - - -
- {totalProperties} {totalProperties === 1 ? 'property' : 'properties'} -
-
-
- {searchQuery && ( -
- {currentMatches.length > 0 - ? `${currentMatchIndex + 1} of ${currentMatches.length} ${currentMatches.length === 1 ? 'match' : 'matches'}` - : 'No properties found'} -
- )} -
- )} - - {/* Content */} -
- {isRootArray && ( -
-
- Array Schema - array[object] -
-

- This schema defines an array of objects. Each item in the array has the properties shown below. -

-
- )} - {description &&

{description}

} - - {variants && ( -
-
- (one of) - -
-
- )} - - {properties ? ( -
- {Object.entries(properties).map(([name, details]: [string, any]) => ( - - ))} -
- ) : !isRootArray ? ( -

Schema does not contain any properties.

- ) : ( -
-
-

- This array contains items of type:{' '} - {processedSchema.items?.type || 'unknown'} -

- {processedSchema.items?.description && ( -

{processedSchema.items.description}

- )} -
-
- )} - - {searchQuery && currentMatches.length === 0 && ( -
-
- - - -

No properties match your search

-

Try a different search term or clear the search to see all properties

-
-
- )} -
- - -
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx b/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx deleted file mode 100644 index 77d7e3ab3..000000000 --- a/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/24/outline'; -import { UserIcon, UserGroupIcon } from '@heroicons/react/20/solid'; -import type { SchemaItem, Owner } from './types'; - -interface OwnersSectionProps { - message: SchemaItem; - isExpanded: boolean; - onToggle: () => void; -} - -export default function OwnersSection({ message, isExpanded, onToggle }: OwnersSectionProps) { - const owners = message.data.owners || []; - - if (owners.length === 0) return null; - - return ( -
- - - {isExpanded && ( -
-
- {owners.map((owner: Owner, idx: number) => { - const Icon = owner.type === 'users' ? UserIcon : UserGroupIcon; - return ( - -
- -
- {owner.name} -
- ); - })} -
-
- )} -
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/Pagination.tsx b/eventcatalog/src/components/SchemaExplorer/Pagination.tsx deleted file mode 100644 index 3e8a8ecbb..000000000 --- a/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +++ /dev/null @@ -1,33 +0,0 @@ -interface PaginationProps { - currentPage: number; - totalPages: number; - onPageChange: (page: number) => void; -} - -export default function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) { - if (totalPages <= 1) return null; - - return ( -
-
- - - Page {currentPage} of {totalPages} - - -
-
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx b/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx deleted file mode 100644 index 0312eb88a..000000000 --- a/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/24/outline'; -import { ServerIcon } from '@heroicons/react/20/solid'; -import { buildUrl } from '@utils/url-builder'; -import type { SchemaItem, Producer, Consumer } from './types'; - -interface ProducersConsumersSectionProps { - message: SchemaItem; - isExpanded: boolean; - onToggle: () => void; -} - -export default function ProducersConsumersSection({ message, isExpanded, onToggle }: ProducersConsumersSectionProps) { - const producers = message.data.producers || []; - const consumers = message.data.consumers || []; - const totalCount = producers.length + consumers.length; - - if (totalCount === 0) return null; - - return ( -
- - - {isExpanded && ( -
- {producers.length > 0 && ( -
-

Producers ({producers.length})

-
- {producers.map((producer: Producer, idx: number) => ( - -
- -
- {producer.id} - v{producer.version} -
- ))} -
-
- )} - {consumers.length > 0 && ( -
-

Consumers ({consumers.length})

-
- {consumers.map((consumer: Consumer, idx: number) => ( - -
- -
- {consumer.id} - v{consumer.version} -
- ))} -
-
- )} -
- )} -
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx b/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx deleted file mode 100644 index 87efe2759..000000000 --- a/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import * as Dialog from '@radix-ui/react-dialog'; -import { XMarkIcon, ArrowsPointingOutIcon, ClipboardDocumentIcon } from '@heroicons/react/24/outline'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneLight as syntaxHighlighterStyle } from 'react-syntax-highlighter/dist/cjs/styles/prism'; -import { getLanguageForHighlight } from './utils'; -import type { SchemaItem } from './types'; - -interface SchemaCodeModalProps { - isOpen: boolean; - onOpenChange: (open: boolean) => void; - message: SchemaItem; - onCopy: () => void; - isCopied: boolean; -} - -export default function SchemaCodeModal({ isOpen, onOpenChange, message, onCopy, isCopied }: SchemaCodeModalProps) { - if (!message.schemaContent) return null; - - return ( - - - - - {/* Header */} -
-
- -
- {message.data.name} - - v{message.data.version} · {getLanguageForHighlight(message.schemaExtension).toUpperCase()} - -
-
-
- - - - -
-
- - {/* Content */} -
- - {message.schemaContent} - -
- - {/* Footer */} -
- - - -
-
-
-
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx b/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx deleted file mode 100644 index bf6b6086a..000000000 --- a/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { ClipboardDocumentIcon, ArrowsPointingOutIcon } from '@heroicons/react/24/outline'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { oneLight as syntaxHighlighterStyle } from 'react-syntax-highlighter/dist/cjs/styles/prism'; -import { buildUrl } from '@utils/url-builder'; -import JSONSchemaViewer from './JSONSchemaViewer'; -import AvroSchemaViewer from './AvroSchemaViewer'; -import { getLanguageForHighlight } from './utils'; -import type { SchemaItem } from './types'; - -interface SchemaContentViewerProps { - message: SchemaItem; - onCopy: () => void; - isCopied: boolean; - viewMode: 'code' | 'schema' | 'diff'; - parsedSchema: any; - parsedAvroSchema?: any; - onOpenFullscreen?: () => void; - showRequired?: boolean; -} - -export default function SchemaContentViewer({ - message, - onCopy, - isCopied, - viewMode, - parsedSchema, - parsedAvroSchema, - showRequired = false, - onOpenFullscreen, -}: SchemaContentViewerProps) { - if (!message.schemaContent) { - return ( -
-

No schema content available

-
- ); - } - - // Render schema viewer based on schema type - if (viewMode === 'schema') { - if (parsedAvroSchema) { - return ; - } - if (parsedSchema) { - return ; - } - } - - return ( -
-
- {message.collection === 'services' && - (() => { - const specType = message.specType || 'openapi'; - const specFilename = message.specFilenameWithoutExtension || 'schema'; - - // Determine the URL path segment based on spec type - let urlSegment = 'spec'; - if (specType === 'asyncapi') { - urlSegment = 'asyncapi'; - } else if (specType === 'graphql') { - urlSegment = 'graphql'; - } - - const specUrl = buildUrl(`/docs/services/${message.data.id}/${message.data.version}/${urlSegment}/${specFilename}`); - - return ( - - - - - View Spec - - ); - })()} - {onOpenFullscreen && ( - - )} - -
- - {message.schemaContent} - -
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx b/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx deleted file mode 100644 index badcb44ec..000000000 --- a/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { ClipboardDocumentIcon, ArrowDownTrayIcon, CodeBracketIcon, TableCellsIcon } from '@heroicons/react/24/outline'; -import { buildUrl } from '@utils/url-builder'; -import { getCollectionStyles } from '@components/Grids/utils'; -import { getSchemaTypeLabel } from './utils'; -import type { SchemaItem } from './types'; - -interface SchemaDetailsHeaderProps { - message: SchemaItem; - availableVersions: SchemaItem[]; - selectedVersion: string | null; - onVersionChange: (version: string) => void; - onCopy: () => void; - onDownload: () => void; - isCopied: boolean; - schemaViewMode: 'code' | 'schema' | 'diff'; - onViewModeChange: (mode: 'code' | 'schema' | 'diff') => void; - hasParsedSchema: boolean; - hasDiffs: boolean; - diffCount: number; -} - -export default function SchemaDetailsHeader({ - message, - availableVersions, - selectedVersion, - onVersionChange, - onCopy, - onDownload, - isCopied, - schemaViewMode, - onViewModeChange, - hasParsedSchema, - hasDiffs, - diffCount, -}: SchemaDetailsHeaderProps) { - const { color, Icon } = getCollectionStyles(message.collection); - const hasMultipleVersions = availableVersions.length > 1; - - return ( -
-
-
-
- - - {message.data.name} - - {hasMultipleVersions ? ( - - ) : ( - v{message.data.version} - )} -
-
- - {message.collection} - - - {(() => { - const ext = message.schemaExtension?.toLowerCase(); - if ( - ext === 'openapi' || - ext === 'asyncapi' || - ext === 'graphql' || - ext === 'avro' || - ext === 'json' || - ext === 'proto' - ) { - // Map json extension to json-schema icon - const iconName = ext === 'json' ? 'json-schema' : ext; - const iconPath = buildUrl(`/icons/${iconName}.svg`, true); - return ( - <> - {`${ext} - {getSchemaTypeLabel(message.schemaExtension)} - - ); - } - return getSchemaTypeLabel(message.schemaExtension); - })()} - -
- {message.data.summary &&

{message.data.summary}

} -
-
- - {/* Action Buttons */} -
- {/* View Mode Toggle */} -
- - {hasParsedSchema && ( - - )} - {hasDiffs && ( - - )} -
- - - - - View Docs → - -
-
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx b/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx deleted file mode 100644 index 6f120afd0..000000000 --- a/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { useState, useMemo } from 'react'; -import * as Diff from 'diff'; -import { html } from 'diff2html'; -import 'diff2html/bundles/css/diff2html.min.css'; -import SchemaDetailsHeader from './SchemaDetailsHeader'; -import ApiAccessSection from './ApiAccessSection'; -import OwnersSection from './OwnersSection'; -import ProducersConsumersSection from './ProducersConsumersSection'; -import SchemaContentViewer from './SchemaContentViewer'; -import DiffViewer from './DiffViewer'; -import VersionHistoryModal from './VersionHistoryModal'; -import SchemaCodeModal from './SchemaCodeModal'; -import SchemaViewerModal from './SchemaViewerModal'; -import { copyToClipboard, downloadSchema } from './utils'; -import type { SchemaItem, VersionDiff } from './types'; - -interface SchemaDetailsPanelProps { - message: SchemaItem; - availableVersions: SchemaItem[]; - selectedVersion: string | null; - onVersionChange: (version: string) => void; - apiAccessEnabled?: boolean; -} - -export default function SchemaDetailsPanel({ - message, - availableVersions, - selectedVersion, - onVersionChange, - apiAccessEnabled = false, -}: SchemaDetailsPanelProps) { - const [copiedId, setCopiedId] = useState(null); - const [schemaViewMode, setSchemaViewMode] = useState<'code' | 'schema' | 'diff'>('code'); - const [apiAccessExpanded, setApiAccessExpanded] = useState(false); - const [ownersExpanded, setOwnersExpanded] = useState(false); - const [producersConsumersExpanded, setProducersConsumersExpanded] = useState(false); - const [isDiffModalOpen, setIsDiffModalOpen] = useState(false); - const [isCodeModalOpen, setIsCodeModalOpen] = useState(false); - const [isSchemaViewerModalOpen, setIsSchemaViewerModalOpen] = useState(false); - - const hasMultipleVersions = availableVersions.length > 1; - - // Generate diffs between all consecutive versions - const allDiffs: VersionDiff[] = useMemo(() => { - const diffs: VersionDiff[] = []; - if (!hasMultipleVersions) return diffs; - - for (let i = 0; i < availableVersions.length - 1; i++) { - const newerVersion = availableVersions[i]; - const olderVersion = availableVersions[i + 1]; - - if (newerVersion.schemaContent && olderVersion.schemaContent) { - const diff = Diff.createTwoFilesPatch( - `v${olderVersion.data.version}`, - `v${newerVersion.data.version}`, - olderVersion.schemaContent, - newerVersion.schemaContent, - '', - '', - { context: 3 } - ); - - const diffHtml = html(diff, { - drawFileList: false, - matching: 'lines', - outputFormat: 'side-by-side', - }); - - diffs.push({ - newerVersion: newerVersion.data.version, - olderVersion: olderVersion.data.version, - diffHtml, - newerContent: newerVersion.schemaContent, - olderContent: olderVersion.schemaContent, - }); - } - } - return diffs; - }, [availableVersions, hasMultipleVersions]); - - // Check if this is a JSON schema - const parsedSchema = useMemo(() => { - const isJSONSchema = - message.schemaExtension?.toLowerCase() === 'json' && message.schemaContent && message.schemaContent.trim() !== ''; - if (!isJSONSchema) return null; - - try { - const parsed = JSON.parse(message.schemaContent ?? ''); - // Check if it's actually a JSON Schema (has properties or $schema field) - if (!parsed.properties && !parsed.$schema && !parsed.type) { - return null; - } - return parsed; - } catch { - return null; - } - }, [message.schemaContent, message.schemaExtension]); - - // Check if this is an Avro schema - const parsedAvroSchema = useMemo(() => { - const ext = message.schemaExtension?.toLowerCase(); - const isAvroSchema = (ext === 'avro' || ext === 'avsc') && message.schemaContent && message.schemaContent.trim() !== ''; - if (!isAvroSchema) return null; - - try { - const parsed = JSON.parse(message.schemaContent ?? ''); - // Check if it's actually an Avro Schema (has type field, typically "record") - if (!parsed.type) { - return null; - } - return parsed; - } catch { - return null; - } - }, [message.schemaContent, message.schemaExtension]); - - const handleCopy = async () => { - if (!message.schemaContent) return; - const success = await copyToClipboard(message.schemaContent); - if (success) { - setCopiedId(message.data.id); - setTimeout(() => setCopiedId(null), 2000); - } - }; - - const handleCopyCustom = async (content: string, id: string) => { - const success = await copyToClipboard(content); - if (success) { - setCopiedId(id); - setTimeout(() => setCopiedId(null), 2000); - } - }; - - const handleDownload = () => { - if (!message.schemaContent) return; - downloadSchema(message.schemaContent, message.data.id, message.schemaExtension || 'json'); - }; - - const isCopied = copiedId === message.data.id; - - return ( -
- {/* Header */} - 0} - diffCount={allDiffs.length} - /> - - {/* API Access Section - Always show, but content changes based on Scale access */} - setApiAccessExpanded(!apiAccessExpanded)} - onCopy={handleCopyCustom} - copiedId={copiedId} - apiAccessEnabled={apiAccessEnabled} - /> - - {/* Producers and Consumers Section - Only show for messages (not services) */} - {message.collection !== 'services' && ( - setProducersConsumersExpanded(!producersConsumersExpanded)} - /> - )} - - {/* Owners Section */} - setOwnersExpanded(!ownersExpanded)} /> - - {/* Schema Content - Takes full remaining height */} -
- {schemaViewMode === 'diff' && allDiffs.length > 0 ? ( - setIsDiffModalOpen(true)} apiAccessEnabled={apiAccessEnabled} /> - ) : ( - setIsCodeModalOpen(true) - : schemaViewMode === 'schema' && (parsedSchema || parsedAvroSchema) - ? () => setIsSchemaViewerModalOpen(true) - : undefined - } - /> - )} -
- - {/* Version History Modal */} - - - {/* Schema Code Modal */} - - - {/* Schema Viewer Modal */} - -
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx b/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx deleted file mode 100644 index 7cbb4cdcc..000000000 --- a/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +++ /dev/null @@ -1,415 +0,0 @@ -import { useState, useMemo, useEffect, useRef } from 'react'; -import { DocumentTextIcon, FunnelIcon } from '@heroicons/react/24/outline'; -import type { CollectionMessageTypes } from '@types'; -import semver from 'semver'; -import SchemaFilters from './SchemaFilters'; -import SchemaListItem from './SchemaListItem'; -import SchemaDetailsPanel from './SchemaDetailsPanel'; -import Pagination from './Pagination'; -import type { SchemaItem } from './types'; - -interface SchemaExplorerProps { - schemas: SchemaItem[]; - apiAccessEnabled?: boolean; -} - -export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: SchemaExplorerProps) { - const [searchQuery, setSearchQuery] = useState(() => { - // Load from localStorage - if (typeof window !== 'undefined') { - const stored = localStorage.getItem('schemaRegistrySearchQuery'); - return stored !== null ? stored : ''; - } - return ''; - }); - const [selectedType, setSelectedType] = useState<'all' | CollectionMessageTypes | 'services'>(() => { - // Load from localStorage - if (typeof window !== 'undefined') { - const stored = localStorage.getItem('schemaRegistrySelectedType'); - return stored !== null ? (stored as 'all' | CollectionMessageTypes | 'services') : 'all'; - } - return 'all'; - }); - const [selectedSchemaType, setSelectedSchemaType] = useState<'all' | string>(() => { - // Load from localStorage - if (typeof window !== 'undefined') { - const stored = localStorage.getItem('schemaRegistrySelectedSchemaType'); - return stored !== null ? stored : 'all'; - } - return 'all'; - }); - const [selectedMessage, setSelectedMessage] = useState(null); - const [selectedVersion, setSelectedVersion] = useState(null); - const [currentPage, setCurrentPage] = useState(1); - const [filtersExpanded, setFiltersExpanded] = useState(() => { - if (typeof window !== 'undefined') { - const stored = localStorage.getItem('schemaRegistryFiltersExpanded'); - return stored !== null ? stored === 'true' : true; - } - return true; - }); - const [isMounted, setIsMounted] = useState(false); - const searchInputRef = useRef(null); - const selectedItemRef = useRef(null); - const ITEMS_PER_PAGE = 50; - - // Set mounted state after hydration to prevent FOUC - useEffect(() => { - setIsMounted(true); - }, []); - - // Function to update URL with query params - const updateUrlParams = (message: SchemaItem) => { - if (typeof window === 'undefined') return; - - const params = new URLSearchParams(); - params.set('id', message.data.id); - params.set('version', message.data.version); - params.set('collection', message.collection); - - // For services, add spec type - if (message.collection === 'services') { - params.set('specType', message.specType || 'unknown'); - } - - const newUrl = `${window.location.pathname}?${params.toString()}`; - window.history.pushState({}, '', newUrl); - }; - - // Group messages by ID (and spec type for services) and get all versions - const messagesByIdAndVersions = useMemo(() => { - const grouped = new Map(); - schemas.forEach((message) => { - // For services, group by ID + spec type to keep different specs separate - const groupKey = - message.collection === 'services' ? `${message.data.id}__${message.specType || 'unknown'}` : message.data.id; - - const existingVersions = grouped.get(groupKey) || []; - grouped.set(groupKey, [...existingVersions, message]); - }); - - // Sort versions for each ID (descending - latest first) - grouped.forEach((versions, id) => { - versions.sort((a, b) => { - const aVersion = a.data.version; - const bVersion = b.data.version; - - // Try to use semver for comparison - const aValid = semver.valid(semver.coerce(aVersion)); - const bValid = semver.valid(semver.coerce(bVersion)); - - if (aValid && bValid) { - return semver.rcompare(aValid, bValid); // descending order - } - - // Fall back to numeric comparison - const aNum = parseFloat(aVersion); - const bNum = parseFloat(bVersion); - if (!isNaN(aNum) && !isNaN(bNum)) { - return bNum - aNum; - } - - // Final fallback to string comparison - return bVersion.localeCompare(aVersion); - }); - grouped.set(id, versions); - }); - - return grouped; - }, [schemas]); - - // Get latest version for each message (for sidebar display) - const latestMessages = useMemo(() => { - return Array.from(messagesByIdAndVersions.values()).map((versions) => versions[0]); - }, [messagesByIdAndVersions]); - - // Get unique schema types - const schemaTypes = useMemo(() => { - const types = new Set(); - latestMessages.forEach((msg) => { - if (msg.schemaExtension) { - types.add(msg.schemaExtension.toLowerCase()); - } - }); - return Array.from(types).sort(); - }, [latestMessages]); - - // Filter messages (using latest versions only) - const filteredMessages = useMemo(() => { - let result = [...latestMessages]; - - // Filter by message type - if (selectedType !== 'all') { - result = result.filter((msg) => msg.collection === selectedType); - } - - // Filter by schema type - if (selectedSchemaType !== 'all') { - result = result.filter((msg) => msg.schemaExtension?.toLowerCase() === selectedSchemaType); - } - - // Filter by search query - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter( - (msg) => - msg.data.name?.toLowerCase().includes(query) || - msg.data.summary?.toLowerCase().includes(query) || - msg.data.id?.toLowerCase().includes(query) - ); - } - - // Sort by name alphabetically - result.sort((a, b) => { - const nameA = a.data.name?.toLowerCase() || ''; - const nameB = b.data.name?.toLowerCase() || ''; - return nameA.localeCompare(nameB); - }); - - return result; - }, [latestMessages, searchQuery, selectedType, selectedSchemaType]); - - // Pagination - const totalPages = Math.ceil(filteredMessages.length / ITEMS_PER_PAGE); - const paginatedMessages = useMemo(() => { - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - return filteredMessages.slice(startIndex, startIndex + ITEMS_PER_PAGE); - }, [filteredMessages, currentPage]); - - useEffect(() => { - setCurrentPage(1); - }, [searchQuery, selectedType, selectedSchemaType]); - - // Load from query string on mount - useEffect(() => { - if (typeof window === 'undefined') return; - - const params = new URLSearchParams(window.location.search); - const id = params.get('id'); - const version = params.get('version'); - const collection = params.get('collection'); - const specType = params.get('specType'); - - if (id && version) { - // Find the matching message - const matchingMessage = schemas.find((msg) => { - const idMatch = msg.data.id === id; - const versionMatch = msg.data.version === version; - const collectionMatch = !collection || msg.collection === collection; - - // For services, also match spec type - if (msg.collection === 'services') { - const specTypeMatch = !specType || msg.specType === specType; - return idMatch && versionMatch && collectionMatch && specTypeMatch; - } - - return idMatch && versionMatch && collectionMatch; - }); - - if (matchingMessage) { - setSelectedMessage(matchingMessage); - setSelectedVersion(matchingMessage.data.version); - - // Scroll to the selected item after a brief delay to ensure DOM is ready - setTimeout(() => { - if (selectedItemRef.current) { - selectedItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - }, 100); - } - } - }, [schemas]); - - // Auto-select first message when filters change (only if no query params) - useEffect(() => { - if (typeof window === 'undefined') return; - - const params = new URLSearchParams(window.location.search); - const hasQueryParams = params.has('id'); - - if (filteredMessages.length > 0 && !selectedMessage && !hasQueryParams) { - const firstMessage = filteredMessages[0]; - setSelectedMessage(firstMessage); - setSelectedVersion(firstMessage.data.version); - } - }, [filteredMessages, selectedMessage]); - - // Get the message to display (based on selected version) - const displayMessage = useMemo(() => { - if (!selectedMessage) return null; - - // For services, use compound key (ID + spec type), otherwise just ID - const groupKey = - selectedMessage.collection === 'services' - ? `${selectedMessage.data.id}__${selectedMessage.specType || 'unknown'}` - : selectedMessage.data.id; - - const versions = messagesByIdAndVersions.get(groupKey); - if (!versions) return selectedMessage; - - // If no version selected, use the latest (which is the first in the sorted array) - if (!selectedVersion) return versions[0]; - - // Find the message with the selected version - const versionedMessage = versions.find((v) => v.data.version === selectedVersion); - return versionedMessage || versions[0]; - }, [selectedMessage, selectedVersion, messagesByIdAndVersions]); - - // Save filter expanded state to localStorage - useEffect(() => { - if (typeof window !== 'undefined') { - localStorage.setItem('schemaRegistryFiltersExpanded', filtersExpanded.toString()); - } - }, [filtersExpanded]); - - // Save filter states to localStorage - useEffect(() => { - if (typeof window !== 'undefined') { - localStorage.setItem('schemaRegistrySearchQuery', searchQuery); - } - }, [searchQuery]); - - useEffect(() => { - if (typeof window !== 'undefined') { - localStorage.setItem('schemaRegistrySelectedType', selectedType); - } - }, [selectedType]); - - useEffect(() => { - if (typeof window !== 'undefined') { - localStorage.setItem('schemaRegistrySelectedSchemaType', selectedSchemaType); - } - }, [selectedSchemaType]); - - // Keyboard shortcut for search (Cmd+K or Ctrl+K) - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault(); - searchInputRef.current?.focus(); - } - }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, []); - - // Get available versions for the selected message - const availableVersions = useMemo(() => { - if (!displayMessage) return []; - const groupKey = - displayMessage.collection === 'services' - ? `${displayMessage.data.id}__${displayMessage.specType || 'unknown'}` - : displayMessage.data.id; - return messagesByIdAndVersions.get(groupKey) || [displayMessage]; - }, [displayMessage, messagesByIdAndVersions]); - - const handleVersionChange = (newVersion: string) => { - setSelectedVersion(newVersion); - // Update URL with new version - const versionedMessage = availableVersions.find((v) => v.data.version === newVersion); - if (versionedMessage) { - updateUrlParams(versionedMessage); - } - }; - - return ( -
- {/* Compact Header */} -
-
-

Schema Explorer

-

- {filteredMessages.length} schema{filteredMessages.length !== 1 ? 's' : ''} available -

-
-
- - {/* Split View */} -
- {/* Left: Filters + Schema List */} -
- {/* Filters */} - setFiltersExpanded(!filtersExpanded)} - searchInputRef={searchInputRef} - isMounted={isMounted} - /> - - {/* Schema List - Independently Scrollable */} -
- {paginatedMessages.length > 0 ? ( -
- {paginatedMessages.map((message) => { - // For services, also check spec type to determine if selected - const isSelected = - message.collection === 'services' - ? selectedMessage?.data.id === message.data.id && selectedMessage?.specType === message.specType - : selectedMessage?.data.id === message.data.id; - - // Get versions using compound key for services - const groupKey = - message.collection === 'services' ? `${message.data.id}__${message.specType || 'unknown'}` : message.data.id; - - const versions = messagesByIdAndVersions.get(groupKey) || [message]; - - return ( - { - setSelectedMessage(message); - setSelectedVersion(message.data.version); - updateUrlParams(message); - }} - itemRef={isSelected ? selectedItemRef : undefined} - /> - ); - })} -
- ) : ( -
- -

No schemas found

-

Try adjusting your filters

-
- )} -
- - {/* Pagination */} - -
- - {/* Right: Schema Details */} -
- {displayMessage ? ( - - ) : ( -
-
- -

Select a schema to view details

-
-
- )} -
-
-
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx b/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx deleted file mode 100644 index 323674b1c..000000000 --- a/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { MagnifyingGlassIcon, XMarkIcon, FunnelIcon, ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/24/outline'; -import type { CollectionMessageTypes } from '@types'; -import { getSchemaTypeLabel } from './utils'; -import type { SchemaItem } from './types'; - -interface SchemaFiltersProps { - searchQuery: string; - onSearchChange: (query: string) => void; - selectedType: 'all' | CollectionMessageTypes | 'services'; - onTypeChange: (type: 'all' | CollectionMessageTypes | 'services') => void; - selectedSchemaType: string; - onSchemaTypeChange: (type: string) => void; - schemaTypes: string[]; - latestMessages: SchemaItem[]; - filtersExpanded: boolean; - onToggleExpanded: () => void; - searchInputRef: React.RefObject; - isMounted: boolean; -} - -export default function SchemaFilters({ - searchQuery, - onSearchChange, - selectedType, - onTypeChange, - selectedSchemaType, - onSchemaTypeChange, - schemaTypes, - latestMessages, - filtersExpanded, - onToggleExpanded, - searchInputRef, - isMounted, -}: SchemaFiltersProps) { - const activeFilterCount = [searchQuery, selectedType !== 'all', selectedSchemaType !== 'all'].filter(Boolean).length; - - return ( -
- {/* Filter Header */} - - - {/* Collapsible Filter Content - Only render after mount to prevent FOUC */} - {isMounted && filtersExpanded && ( -
- {/* Search */} -
- -
-
- -
- onSearchChange(e.target.value)} - className="w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring-primary text-xs pl-8 pr-8 py-1.5 border" - /> - {searchQuery && ( - - )} -
-
- - {/* Message Type Filter */} -
- - -
- - {/* Schema Type Filter */} -
- - -
- - {/* Active filters */} - {activeFilterCount > 0 && ( -
-
- {searchQuery && ( - - {searchQuery.substring(0, 15)} - {searchQuery.length > 15 ? '...' : ''} - - - )} - {selectedType !== 'all' && ( - - {selectedType} - - - )} - {selectedSchemaType !== 'all' && ( - - {getSchemaTypeLabel(selectedSchemaType)} - - - )} - -
-
- )} -
- )} -
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx b/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx deleted file mode 100644 index e2e7c1283..000000000 --- a/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { buildUrl } from '@utils/url-builder'; -import { getCollectionStyles } from '@components/Grids/utils'; -import { getSchemaTypeLabel } from './utils'; -import type { SchemaItem } from './types'; - -interface SchemaListItemProps { - message: SchemaItem; - isSelected: boolean; - versions: SchemaItem[]; - onClick: () => void; - itemRef?: React.RefObject; -} - -export default function SchemaListItem({ message, isSelected, versions, onClick, itemRef }: SchemaListItemProps) { - const { color, Icon } = getCollectionStyles(message.collection); - const hasMultipleVersions = versions.length > 1; - - return ( - - ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx b/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx deleted file mode 100644 index d696e3a57..000000000 --- a/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import * as Dialog from '@radix-ui/react-dialog'; -import { XMarkIcon, ArrowsPointingOutIcon } from '@heroicons/react/24/outline'; -import JSONSchemaViewer from './JSONSchemaViewer'; -import AvroSchemaViewer from './AvroSchemaViewer'; -import type { SchemaItem } from './types'; - -interface SchemaViewerModalProps { - isOpen: boolean; - onOpenChange: (open: boolean) => void; - message: SchemaItem; - parsedSchema: any; - parsedAvroSchema?: any; -} - -export default function SchemaViewerModal({ - isOpen, - onOpenChange, - message, - parsedSchema, - parsedAvroSchema, -}: SchemaViewerModalProps) { - if (!parsedSchema && !parsedAvroSchema) return null; - - const isAvro = !!parsedAvroSchema; - - return ( - - - - - {/* Header */} -
-
- -
- {message.data.name} - - v{message.data.version} · {isAvro ? 'Avro' : 'JSON'} Schema - -
-
- - - -
- - {/* Content */} -
- {isAvro ? ( - - ) : ( - - )} -
- - {/* Footer */} -
- - - -
-
-
-
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx b/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx deleted file mode 100644 index b09900512..000000000 --- a/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import * as Dialog from '@radix-ui/react-dialog'; -import { XMarkIcon, ArrowsPointingOutIcon } from '@heroicons/react/24/outline'; -import DiffViewer from './DiffViewer'; -import type { VersionDiff } from './types'; - -interface VersionHistoryModalProps { - isOpen: boolean; - onOpenChange: (open: boolean) => void; - diffs: VersionDiff[]; - messageName: string; - apiAccessEnabled?: boolean; -} - -export default function VersionHistoryModal({ - isOpen, - onOpenChange, - diffs, - messageName, - apiAccessEnabled = false, -}: VersionHistoryModalProps) { - return ( - - - - - {/* Header */} -
-
- -
- Version History - {messageName} -
-
- - - -
- - {/* Content */} -
- {diffs.length > 0 ? ( - - ) : ( -
-

No version history available

-
- )} -
- - {/* Footer */} -
- - - -
-
-
-
- ); -} diff --git a/eventcatalog/src/components/SchemaExplorer/types.ts b/eventcatalog/src/components/SchemaExplorer/types.ts deleted file mode 100644 index f05b13cbf..000000000 --- a/eventcatalog/src/components/SchemaExplorer/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { CollectionMessageTypes } from '@types'; - -export interface Producer { - id: string; - version: string; -} - -export interface Consumer { - id: string; - version: string; -} - -export interface Owner { - id: string; - name: string; - type: 'users' | 'teams'; - href: string; -} - -export interface SchemaItem { - collection: CollectionMessageTypes | 'services'; - data: { - id: string; - name: string; - version: string; - summary?: string; - schemaPath?: string; - producers?: Producer[]; - consumers?: Consumer[]; - owners?: Owner[]; - }; - schemaContent?: string; - schemaExtension?: string; - specType?: string; - specName?: string; - specFilenameWithoutExtension?: string; -} - -export interface VersionDiff { - newerVersion: string; - olderVersion: string; - diffHtml: string; - newerContent: string; - olderContent: string; -} diff --git a/eventcatalog/src/components/SchemaExplorer/utils.ts b/eventcatalog/src/components/SchemaExplorer/utils.ts deleted file mode 100644 index 4da542898..000000000 --- a/eventcatalog/src/components/SchemaExplorer/utils.ts +++ /dev/null @@ -1,81 +0,0 @@ -export const getLanguageForHighlight = (extension?: string): string => { - if (!extension) return 'json'; - const ext = extension.toLowerCase(); - switch (ext) { - case 'avro': - case 'avsc': - case 'json': - return 'json'; - case 'proto': - return 'protobuf'; - case 'xsd': - case 'xml': - return 'xml'; - case 'graphql': - case 'gql': - return 'graphql'; - case 'yaml': - case 'yml': - case 'openapi': - case 'asyncapi': - return 'yaml'; - case 'ts': - case 'typescript': - return 'typescript'; - case 'js': - case 'javascript': - return 'javascript'; - default: - return 'json'; - } -}; - -export const getSchemaTypeLabel = (extension?: string): string => { - if (!extension) return 'JSON'; - const ext = extension.toLowerCase(); - switch (ext) { - case 'avro': - case 'avsc': - return 'Avro'; - case 'proto': - return 'Protobuf'; - case 'xsd': - return 'XML Schema'; - case 'graphql': - case 'gql': - return 'GraphQL'; - case 'yaml': - case 'yml': - return 'YAML'; - case 'json': - return 'JSON Schema'; - case 'openapi': - return 'OpenAPI'; - case 'asyncapi': - return 'AsyncAPI'; - default: - return ext.toUpperCase(); - } -}; - -export const copyToClipboard = async (content: string): Promise => { - try { - await navigator.clipboard.writeText(content); - return true; - } catch (err) { - console.error('Failed to copy:', err); - return false; - } -}; - -export const downloadSchema = (content: string, filename: string, extension: string) => { - const blob = new Blob([content], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${filename}.${extension}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); -}; diff --git a/eventcatalog/src/components/Search/Search.astro b/eventcatalog/src/components/Search/Search.astro deleted file mode 100644 index b7f59bde1..000000000 --- a/eventcatalog/src/components/Search/Search.astro +++ /dev/null @@ -1,109 +0,0 @@ ---- -import MagnifyingGlassIcon from '@heroicons/react/24/outline/MagnifyingGlassIcon'; -import SearchModal from './SearchModal.tsx'; -import { buildUrl } from '@utils/url-builder.ts'; - -const pagefindJsUrl = buildUrl('/pagefind/pagefind.js', true); -const pagefindCssUrl = buildUrl('/pagefind/pagefind-ui.css', true); ---- - - - - -
-
- - -
- ⌘K -
-
-
- - - - - - diff --git a/eventcatalog/src/components/Search/SearchModal.tsx b/eventcatalog/src/components/Search/SearchModal.tsx deleted file mode 100644 index 87f165bc9..000000000 --- a/eventcatalog/src/components/Search/SearchModal.tsx +++ /dev/null @@ -1,766 +0,0 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { - MagnifyingGlassIcon, - QueueListIcon, - RectangleGroupIcon, - BoltIcon, - ChatBubbleLeftIcon, - ServerIcon, - UserGroupIcon, - UserIcon, - BookOpenIcon, - DocumentTextIcon, - CubeIcon, -} from '@heroicons/react/24/outline'; - -interface SearchResult { - id: string; - name: string; - type: string; - description: string; - url: string; - tags: string[]; -} - -// Memoized SearchResult component for better performance -const SearchResultItem = React.memo<{ - item: SearchResult; - typeConfig: any; - currentSearch: string; -}>(({ item, typeConfig, currentSearch }) => { - const config = typeConfig[item.type as keyof typeof typeConfig] || typeConfig.other; - const getDisplayType = (type: string) => { - if (type === 'other') return 'Page'; - if (type === 'asyncapi') return 'AsyncAPI'; - if (type === 'openapi') return 'OpenAPI'; - if (type === 'language') return 'Language'; - if (type === 'entities') return 'Entity'; - if (type === 'queries') return 'Query'; - // For plurals, remove 's' at the end, otherwise just capitalize - if (type.endsWith('s')) { - return type.charAt(0).toUpperCase() + type.slice(1, -1); - } - return type.charAt(0).toUpperCase() + type.slice(1); - }; - const displayType = getDisplayType(item.type); - const IconComponent = config.icon; - - return ( - -
-
-

{item.name}

- - - {displayType} - -
- {item.description && ( -
- {currentSearch.trim() ? ( - - ) : ( - {item.description.replace(/<[^>]*>/g, '')} - )} -
- )} - {item.tags.length > 0 && ( -
- {item.tags.map((tag, index) => ( - - {tag} - - ))} -
- )} -
-
- ); -}); - -// Memoized SearchResults component -const SearchResults = React.memo<{ - results: SearchResult[]; - typeConfig: any; - currentSearch: string; -}>(({ results, typeConfig, currentSearch }) => { - return ( - <> - {results.map((item) => ( - - ))} - - ); -}); - -const SearchModal: React.FC = () => { - const [isOpen, setIsOpen] = useState(false); - const [pagefind, setPagefind] = useState(null); - const [pagefindLoadError, setPagefindLoadError] = useState(false); - const [currentSearch, setCurrentSearch] = useState(''); - const [currentFilter, setCurrentFilter] = useState('all'); - const [allResults, setAllResults] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [exactMatch, setExactMatch] = useState(false); - - // Listen for modal state changes from Astro component - useEffect(() => { - const handleModalToggle = (event: CustomEvent) => { - setIsOpen(event.detail.isOpen); - }; - - window.addEventListener('searchModalToggle', handleModalToggle as EventListener); - return () => window.removeEventListener('searchModalToggle', handleModalToggle as EventListener); - }, []); - - const onClose = () => { - if ((window as any).searchModalState) { - (window as any).searchModalState.close(); - } - }; - - // Type colors and icons - memoized to prevent recreating on every render - const typeConfig = useMemo( - () => ({ - domains: { - bg: 'bg-orange-50/10', - badgeBg: 'bg-orange-500/20', - text: 'text-orange-800', - border: 'border-orange-200', - icon: RectangleGroupIcon, - }, - services: { - bg: 'bg-pink-50/10', - badgeBg: 'bg-pink-500/20', - text: 'text-pink-800', - border: 'border-pink-200', - icon: ServerIcon, - }, - events: { - bg: 'bg-orange-50/10', - badgeBg: 'bg-orange-500/20', - text: 'text-orange-800', - border: 'border-orange-200', - icon: BoltIcon, - }, - commands: { - bg: 'bg-blue-50/10', - badgeBg: 'bg-blue-500/20', - text: 'text-blue-800', - border: 'border-blue-200', - icon: ChatBubbleLeftIcon, - }, - queries: { - bg: 'bg-green-50/10', - badgeBg: 'bg-green-500/20', - text: 'text-green-800', - border: 'border-green-200', - icon: MagnifyingGlassIcon, - }, - entities: { - bg: 'bg-purple-50/10', - badgeBg: 'bg-purple-500/20', - text: 'text-purple-800', - border: 'border-purple-200', - icon: CubeIcon, - }, - channels: { - bg: 'bg-indigo-50/10', - badgeBg: 'bg-indigo-500/20', - text: 'text-indigo-800', - border: 'border-indigo-200', - icon: QueueListIcon, - }, - teams: { - bg: 'bg-teal-50/10', - badgeBg: 'bg-teal-500/20', - text: 'text-teal-800', - border: 'border-teal-200', - icon: UserGroupIcon, - }, - users: { - bg: 'bg-cyan-50/10', - badgeBg: 'bg-cyan-500/20', - text: 'text-cyan-800', - border: 'border-cyan-200', - icon: UserIcon, - }, - language: { - bg: 'bg-amber-50/10', - badgeBg: 'bg-amber-500/20', - text: 'text-amber-800', - border: 'border-amber-200', - icon: BookOpenIcon, - }, - openapi: { - bg: 'bg-emerald-50/10', - badgeBg: 'bg-emerald-500/20', - text: 'text-emerald-800', - border: 'border-emerald-200', - icon: DocumentTextIcon, - }, - asyncapi: { - bg: 'bg-violet-50/10', - badgeBg: 'bg-violet-500/20', - text: 'text-violet-800', - border: 'border-violet-200', - icon: DocumentTextIcon, - }, - other: { - bg: 'bg-gray-50/10', - badgeBg: 'bg-gray-500/20', - text: 'text-gray-800', - border: 'border-gray-200', - icon: DocumentTextIcon, - }, - }), - [] - ); - - // Initialize Pagefind - useEffect(() => { - if (typeof window !== 'undefined') { - const initPagefind = async () => { - try { - // Wait for Pagefind to be loaded by the Astro script - const waitForPagefind = () => - new Promise((resolve, reject) => { - if ((window as any).pagefind) { - resolve((window as any).pagefind); - } else { - const handler = () => { - window.removeEventListener('pagefindLoaded', handler); - resolve((window as any).pagefind); - }; - - // Set a timeout to detect if pagefind fails to load - const timeout = setTimeout(() => { - window.removeEventListener('pagefindLoaded', handler); - reject(new Error('Pagefind failed to load - catalog may not be indexed')); - }, 2000); // 5 second timeout - - window.addEventListener('pagefindLoaded', () => { - clearTimeout(timeout); - handler(); - }); - } - }); - - const pagefindModule = await waitForPagefind(); - await pagefindModule.init(); - setPagefind(pagefindModule); - } catch (error) { - console.error('Failed to initialize Pagefind:', error); - setPagefindLoadError(true); - } - }; - - initPagefind(); - } - }, []); - - // Type mapping based on URL patterns - memoized callback - const getTypeFromUrl = useCallback((url: string): string => { - // Check for language first since it can be nested under other paths - if (url.includes('/language/')) return 'language'; - // Check for spec types after language but before other types since they can be nested - if (url.includes('/spec/')) return 'openapi'; - if (url.includes('/asyncapi')) return 'asyncapi'; - if (url.includes('/domains/')) return 'domains'; - if (url.includes('/services/')) return 'services'; - if (url.includes('/events/')) return 'events'; - if (url.includes('/commands/')) return 'commands'; - if (url.includes('/queries/')) return 'queries'; - if (url.includes('/entities/')) return 'entities'; - if (url.includes('/channels/')) return 'channels'; - if (url.includes('/teams/')) return 'teams'; - if (url.includes('/users/')) return 'users'; - return 'other'; - }, []); - - // Perform search - const performSearch = useCallback( - async (searchTerm: string): Promise => { - if (!pagefind || !searchTerm.trim()) { - return []; - } - - setIsLoading(true); - - try { - const search = await pagefind.debouncedSearch(searchTerm); - if (!search || !search.results) { - return []; - } - const processedResults: SearchResult[] = []; - - for (const result of search.results) { - const data = await result.data(); - const type = getTypeFromUrl(data.url); - - // Clean the title by removing any "Type | " prefix if it exists - let cleanTitle = data.meta?.title || 'Untitled'; - - // Use regex for more efficient prefix removal - cleanTitle = cleanTitle.replace( - /^(Domains?|Services?|Events?|Commands?|Queries?|Entities?|Channels?|Teams?|Users?|Language) \| /, - '' - ); - - processedResults.push({ - id: result.id, - name: cleanTitle, - type: type, - description: data.excerpt || '', - url: data.url, - tags: data.meta?.tags ? data.meta.tags.split(',').map((tag: string) => tag.trim()) : [], - }); - } - - return processedResults; - } catch (error) { - console.error('Search error:', error); - return []; - } finally { - setIsLoading(false); - } - }, - [pagefind] - ); - - // Filter results - memoized callback - const filterResults = useCallback( - (results: SearchResult[], filterType: string): SearchResult[] => { - let filteredResults = results; - - // Apply type filter - if (filterType !== 'all') { - filteredResults = filteredResults.filter((item) => item.type === filterType); - } - - // Apply exact match filter if enabled - if (exactMatch && currentSearch.trim()) { - filteredResults = filteredResults.filter((item) => item.name.toLowerCase().includes(currentSearch.toLowerCase())); - } - - return filteredResults; - }, - [exactMatch, currentSearch] - ); - - // Update results with debouncing - const updateResults = useCallback(async () => { - if (currentSearch.trim()) { - const results = await performSearch(currentSearch); - setAllResults(results); - } else { - setAllResults([]); - } - }, [currentSearch, performSearch]); - - // Search on input change with debouncing - useEffect(() => { - if (!currentSearch.trim()) { - setAllResults([]); - return; - } - - const debounceTimer = setTimeout(() => { - updateResults(); - }, 300); // 300ms debounce - - return () => clearTimeout(debounceTimer); - }, [currentSearch, updateResults]); - - // Handle input change - const handleSearchChange = (e: React.ChangeEvent) => { - setCurrentSearch(e.target.value); - }; - - // Handle filter change - const handleFilterChange = (filter: string) => { - setCurrentFilter(filter); - }; - - // Get filtered results - memoized to prevent recalculation - const filteredResults = useMemo(() => { - return filterResults(allResults, currentFilter); - }, [allResults, currentFilter, exactMatch, currentSearch]); - - // Get filter counts - memoized to prevent recalculation on every render - const getFilterCounts = useMemo(() => { - return { - all: allResults.length, - domains: allResults.filter((r) => r.type === 'domains').length, - services: allResults.filter((r) => r.type === 'services').length, - events: allResults.filter((r) => r.type === 'events').length, - commands: allResults.filter((r) => r.type === 'commands').length, - queries: allResults.filter((r) => r.type === 'queries').length, - entities: allResults.filter((r) => r.type === 'entities').length, - channels: allResults.filter((r) => r.type === 'channels').length, - teams: allResults.filter((r) => r.type === 'teams').length, - users: allResults.filter((r) => r.type === 'users').length, - language: allResults.filter((r) => r.type === 'language').length, - openapi: allResults.filter((r) => r.type === 'openapi').length, - asyncapi: allResults.filter((r) => r.type === 'asyncapi').length, - }; - }, [allResults]); - - const counts = getFilterCounts; - - // Handle escape key - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - - if (isOpen) { - document.addEventListener('keydown', handleEscape); - return () => document.removeEventListener('keydown', handleEscape); - } - }, [isOpen, onClose]); - - if (!isOpen) return null; - - return ( -
- -
-
-
-
e.stopPropagation()} - > - {pagefindLoadError ? ( - // Show indexing required message when Pagefind fails to load - full modal content -
-
-
-

Search Index Not Found

-

- Your EventCatalog needs to be built to generate the search index. This enables fast searching across all - your domains, services, events, and documentation. -

-
- -
-

- - - - Build Your Catalog -

-
- npm run build -
-

This will generate your catalog and create the search index

-
- -
- - - -
-

Need to update search results?

-

- Run npm run build again after - making changes to your catalog content. -

-
-
-
-
- ) : ( - <> - {/* Search Input */} -
- - -
- - {/* Main Content Area */} -
- {/* Left Filters */} -
- {/* All Resources */} -
-
- -
-
- - {/* Resources Section */} -
-

Resources

-
- {Object.entries({ - domains: 'Domains', - services: 'Services', - entities: 'Entities', - language: 'Ubiquitous Language', - }).map(([key, label]) => { - const config = typeConfig[key as keyof typeof typeConfig]; - const IconComponent = config?.icon; - - return ( - - ); - })} -
-
- - {/* Messages Section */} -
-

Messages

-
- {Object.entries({ - events: 'Events', - commands: 'Commands', - queries: 'Queries', - channels: 'Channels', - }).map(([key, label]) => { - const config = typeConfig[key as keyof typeof typeConfig]; - const IconComponent = config?.icon; - - return ( - - ); - })} -
-
- - {/* Organization Section */} -
-

Organization

-
- {Object.entries({ - teams: 'Teams', - users: 'Users', - }).map(([key, label]) => { - const config = typeConfig[key as keyof typeof typeConfig]; - const IconComponent = config?.icon; - - return ( - - ); - })} -
-
- - {/* Specifications Section */} -
-

Specifications

-
- {Object.entries({ - openapi: 'OpenAPI Specification', - asyncapi: 'AsyncAPI Specification', - }).map(([key, label]) => { - const config = typeConfig[key as keyof typeof typeConfig]; - const IconComponent = config.icon; - - return ( - - ); - })} -
-
-
- - {/* Right Results */} -
- {/* Show stats and exact match toggle - only when there are results or search term */} - {currentSearch.trim() && (filteredResults.length > 0 || isLoading) && ( -
-
- {filteredResults.length} results for "{currentSearch}" - {isLoading && Loading...} -
- - {/* Exact Match Checkbox */} -
- setExactMatch(e.target.checked)} - className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded" - /> - -
-
- )} - - {/* Main content area */} -
- {!currentSearch.trim() ? ( - // Show when no search term is entered - centered -
-
- -

Discover your EventCatalog

-

- Start typing to search for domains, services, events, and more. -

-
-
- ) : filteredResults.length === 0 && !isLoading ? ( - // Show when search term exists but no results and not loading - centered -
-
- -

No results found

-

- No results found for "{currentSearch}". -

-
-
- ) : isLoading ? ( - // Show loading state - centered -
-
-
-

Searching...

-

- Finding results for "{currentSearch}" -

-
-
- ) : ( - // Show results in a grid with padding -
-
- -
-
- )} -
-
-
- - )} -
-
-
-
- ); -}; - -export default SearchModal; diff --git a/eventcatalog/src/components/SideBars/ChannelSideBar.astro b/eventcatalog/src/components/SideBars/ChannelSideBar.astro deleted file mode 100644 index e99685d3e..000000000 --- a/eventcatalog/src/components/SideBars/ChannelSideBar.astro +++ /dev/null @@ -1,204 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; -import PillListFlat from '@components/Lists/PillListFlat'; -import ProtocolList from '@components/Lists/ProtocolList'; -import OwnersList from '@components/Lists/OwnersList'; -import VersionList from '@components/Lists/VersionList.astro'; -import { buildUrl } from '@utils/url-builder'; -import { ScrollText } from 'lucide-react'; -import RepositoryList from '@components/Lists/RepositoryList.astro'; -import { getOwner } from '@utils/collections/owners'; -import { getProducersAndConsumersForChannel } from '@utils/collections/services'; -import { isChangelogEnabled } from '@utils/feature'; -interface Props { - channel: CollectionEntry<'channels'>; -} - -const { channel } = Astro.props; - -const ownersRaw = channel.data?.owners || []; -const owners = await Promise.all>(ownersRaw.map(getOwner)); -const filteredOwners = owners.filter((o) => o !== undefined); -const { producers, consumers } = await getProducersAndConsumersForChannel(channel); - -const channelParameters: Record = channel.data.parameters || {}; -const parameters = Object.keys(channelParameters).map((key) => ({ - key, - ...channelParameters[key], -})); - -const attachments = channel.data.attachments || []; - -const attachmentsList = attachments.map((a) => { - const attachmentIsURL = typeof a === 'string'; - return { - label: attachmentIsURL ? a : (a.title ?? a.url), - href: attachmentIsURL ? a : a.url, - icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'), - target: '_blank' as const, - subgroup: attachmentIsURL ? undefined : (a.type ?? ''), - }; -}); - -const paramsList = parameters.map((param) => ({ - label: param.key, - description: param?.description, - collection: 'channels-parameter', -})); - -const protocols = channel.data.protocols || []; -const protocolList = protocols.map((p) => ({ - label: p, - collection: 'channels-protocol', - // icon: p, - icon: p, -})); - -const ownersList = filteredOwners.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: o.collection === 'users' ? o.data.role : 'Team', - avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const messageList = (channel.data.messages ?? []).map((p) => ({ - label: p.name, - badge: p.collection, - color: p.collection === 'events' ? 'orange' : 'blue', - collection: p.collection, - tag: `v${p.version}`, - href: buildUrl(`/docs/${p.collection}/${p.id}/${p.version}`), -})); - -const producersList = producers.map((p) => ({ - label: p.data.name, - badge: p.collection, - color: 'pink', - collection: p.collection, - tag: `v${p.data.version}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const consumersList = consumers.map((c) => ({ - label: c.data.name, - badge: c.collection, - color: 'blue', - collection: c.collection, - tag: `v${c.data.version}`, - href: buildUrl(`/docs/${c.collection}/${c.data.id}/${c.data.version}`), -})); - -const shouldRenderSideBarSection = (section: string) => { - if (!channel.data.detailsPanel) { - return true; - } - // @ts-ignore - return channel.data.detailsPanel[section]?.visible ?? true; -}; ---- - - diff --git a/eventcatalog/src/components/SideBars/ContainerSideBar.astro b/eventcatalog/src/components/SideBars/ContainerSideBar.astro deleted file mode 100644 index 0ece9ed9e..000000000 --- a/eventcatalog/src/components/SideBars/ContainerSideBar.astro +++ /dev/null @@ -1,183 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; -import PillListFlat from '@components/Lists/PillListFlat'; -import OwnersList from '@components/Lists/OwnersList'; -import VersionList from '@components/Lists/VersionList.astro'; -import { buildUrl } from '@utils/url-builder'; -import { ScrollText, Workflow, RssIcon } from 'lucide-react'; -import RepositoryList from '@components/Lists/RepositoryList.astro'; -import { getOwner } from '@utils/collections/owners'; -import CustomSideBarSectionList from '@components/Lists/CustomSideBarSectionList.astro'; -import { isChangelogEnabled, isVisualiserEnabled } from '@utils/feature'; -import config from '@config'; - -interface Props { - container: CollectionEntry<'containers'>; -} - -const { container } = Astro.props; - -const servicesThatWriteToContainer = (container.data.servicesThatWriteToContainer as CollectionEntry<'services'>[]) || []; -const servicesThatReadFromContainer = (container.data.servicesThatReadFromContainer as CollectionEntry<'services'>[]) || []; -const ownersRaw = container.data?.owners || []; - -const owners = await Promise.all>(ownersRaw.map(getOwner)); -const filteredOwners = owners.filter((o) => o !== undefined); -const resourceGroups = container.data?.resourceGroups || []; -const attachments = container.data.attachments || []; - -const attachmentsList = attachments.map((a) => { - const attachmentIsURL = typeof a === 'string'; - return { - label: attachmentIsURL ? a : (a.title ?? a.url), - href: attachmentIsURL ? a : a.url, - icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'), - target: '_blank' as const, - subgroup: attachmentIsURL ? undefined : (a.type ?? ''), - }; -}); - -const writesToList = servicesThatWriteToContainer?.map((p) => ({ - label: p.data.name, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/services/${p.data.id}/${p.data.version}`), -})); - -const readsFromList = servicesThatReadFromContainer?.map((p) => ({ - label: p.data.name, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/services/${p.data.id}/${p.data.version}`), -})); - -const ownersList = filteredOwners.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: o.collection === 'users' ? o.data.role : 'Team', - avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const shouldRenderSideBarSection = (section: string) => { - if (!container.data.detailsPanel) { - return true; - } - // @ts-ignore - return container.data.detailsPanel[section]?.visible ?? true; -}; - -const isRSSEnabled = config.rss?.enabled; ---- - - diff --git a/eventcatalog/src/components/SideBars/DomainSideBar.astro b/eventcatalog/src/components/SideBars/DomainSideBar.astro deleted file mode 100644 index f43b16359..000000000 --- a/eventcatalog/src/components/SideBars/DomainSideBar.astro +++ /dev/null @@ -1,277 +0,0 @@ ---- -import OwnersList from '@components/Lists/OwnersList'; -import PillListFlat from '@components/Lists/PillListFlat'; -import RepositoryList from '@components/Lists/RepositoryList.astro'; -import VersionList from '@components/Lists/VersionList.astro'; -import { getUbiquitousLanguage, getMessagesForDomain, getParentDomains } from '@utils/collections/domains'; -import CustomSideBarSectionList from '@components/Lists/CustomSideBarSectionList.astro'; -import { getOwner } from '@utils/collections/owners'; -import { buildUrl } from '@utils/url-builder'; -import type { CollectionEntry } from 'astro:content'; -import { ScrollText, Workflow } from 'lucide-react'; -import { isChangelogEnabled, isVisualiserEnabled } from '@utils/feature'; - -interface Props { - domain: CollectionEntry<'domains'>; -} - -const { domain } = Astro.props; - -// @ts-ignore -const services = (domain.data.services as CollectionEntry<'services'>[]) || []; -// @ts-ignore -const subDomains = (domain.data.domains as CollectionEntry<'domains'>[]) || []; - -// @ts-ignore -const entities = (domain.data.entities as CollectionEntry<'entities'>[]) || []; - -const attachments = domain.data.attachments || []; - -const ubiquitousLanguage = await getUbiquitousLanguage(domain); -const hasUbiquitousLanguage = ubiquitousLanguage.length > 0; -const ubiquitousLanguageDictionary = hasUbiquitousLanguage ? ubiquitousLanguage[0].data.dictionary : []; - -const ownersRaw = domain.data?.owners || []; -const owners = await Promise.all>(ownersRaw.map(getOwner)); -const filteredOwners = owners.filter((o) => o !== undefined); - -const messagesForDomain = await getMessagesForDomain(domain); - -const resourceGroups = domain.data?.resourceGroups || []; - -const parentDomains = await getParentDomains(domain); - -const serviceList = services.map((p) => ({ - label: p.data.name, - badge: p.collection, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const subDomainList = subDomains.map((p) => ({ - label: p.data.name, - badge: p.collection, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const parentDomainList = parentDomains.map((p) => ({ - label: p.data.name, - badge: p.collection, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const sendsList = messagesForDomain.sends - .sort((a, b) => a.collection.localeCompare(b.collection)) - .map((p) => ({ - label: p.data.name, - badge: p.collection, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), - })); - -const receivesList = messagesForDomain.receives - .sort((a, b) => a.collection.localeCompare(b.collection)) - .map((p) => ({ - label: p.data.name, - badge: p.collection, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), - })); - -const ubiquitousLanguageList = ubiquitousLanguageDictionary?.map((l) => ({ - label: l.name, - badge: 'Ubiquitous Language', - collection: 'ubiquitousLanguages', - href: buildUrl(`/docs/${domain.collection}/${domain.data.id}/language?id=${l.id}`), -})); - -const entityList = entities.map((p) => ({ - label: p.data.name, - badge: p.collection, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const ownersList = filteredOwners.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: o.collection === 'users' ? o.data.role : 'Team', - avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const attachmentsList = attachments.map((a) => { - const attachmentIsURL = typeof a === 'string'; - - return { - label: attachmentIsURL ? a : (a.title ?? a.url), - href: attachmentIsURL ? a : a.url, - icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'), - target: '_blank' as const, - subgroup: attachmentIsURL ? undefined : (a.type ?? ''), - }; -}); - -const shouldRenderSideBarSection = (section: string) => { - if (!domain.data.detailsPanel) { - return true; - } - // @ts-ignore - return domain.data.detailsPanel[section]?.visible ?? true; -}; ---- - - diff --git a/eventcatalog/src/components/SideBars/EntitySideBar.astro b/eventcatalog/src/components/SideBars/EntitySideBar.astro deleted file mode 100644 index ceba00f68..000000000 --- a/eventcatalog/src/components/SideBars/EntitySideBar.astro +++ /dev/null @@ -1,139 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; -import PillListFlat from '@components/Lists/PillListFlat'; -import OwnersList from '@components/Lists/OwnersList'; -import VersionList from '@components/Lists/VersionList.astro'; -import { buildUrl } from '@utils/url-builder'; -import { ScrollText } from 'lucide-react'; -import { getOwner } from '@utils/collections/owners'; -import { isChangelogEnabled } from '@utils/feature'; - -interface Props { - entity: CollectionEntry<'entities'>; -} - -const { entity } = Astro.props; - -const ownersRaw = entity.data?.owners || []; -const owners = await Promise.all>(ownersRaw.map(getOwner)); -const filteredOwners = owners.filter((o) => o !== undefined); - -// @ts-ignore -const services = (entity.data.services as CollectionEntry<'services'>[]) || []; -// @ts-ignore -const domains = (entity.data.domains as CollectionEntry<'domains'>[]) || []; - -const attachments = entity.data.attachments || []; - -const attachmentsList = attachments.map((a) => { - const attachmentIsURL = typeof a === 'string'; - return { - label: attachmentIsURL ? a : (a.title ?? a.url), - href: attachmentIsURL ? a : a.url, - icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'), - target: '_blank' as const, - subgroup: attachmentIsURL ? undefined : (a.type ?? ''), - }; -}); - -const ownersList = filteredOwners.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: o.collection === 'users' ? o.data.role : 'Team', - avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const servicesList = services.map((p) => ({ - label: p.data.name, - badge: p.collection, - color: 'pink', - collection: p.collection, - tag: `v${p.data.version}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const domainsList = domains.map((p) => ({ - label: p.data.name, - badge: p.collection, - color: 'blue', - collection: p.collection, - tag: `v${p.data.version}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const shouldRenderSideBarSection = (section: string) => { - if (!entity.data.detailsPanel) { - return true; - } - // @ts-ignore - return entity.data.detailsPanel[section]?.visible ?? true; -}; ---- - - diff --git a/eventcatalog/src/components/SideBars/FlowSideBar.astro b/eventcatalog/src/components/SideBars/FlowSideBar.astro deleted file mode 100644 index 0bcaf45d4..000000000 --- a/eventcatalog/src/components/SideBars/FlowSideBar.astro +++ /dev/null @@ -1,132 +0,0 @@ ---- -import OwnersList from '@components/Lists/OwnersList'; -import VersionList from '@components/Lists/VersionList.astro'; -import PillListFlat from '@components/Lists/PillListFlat'; -import { buildUrl } from '@utils/url-builder'; -import { getOwner } from '@utils/collections/owners'; -import type { CollectionEntry } from 'astro:content'; -import { ScrollText, Workflow, RssIcon } from 'lucide-react'; -import config from '@config'; -import { isChangelogEnabled, isVisualiserEnabled } from '@utils/feature'; -import CustomSideBarSectionList from '@components/Lists/CustomSideBarSectionList.astro'; -interface Props { - flow: CollectionEntry<'flows'>; -} - -const { flow } = Astro.props; - -const ownersRaw = flow.data?.owners || []; -const owners = await Promise.all>(ownersRaw.map(getOwner)); -const filteredOwners = owners.filter((o) => o !== undefined); - -const resourceGroups = flow.data?.resourceGroups || []; - -const ownersList = filteredOwners.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: o.collection === 'users' ? o.data.role : 'Team', - avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const isRSSEnabled = config.rss?.enabled; - -const attachments = flow.data.attachments || []; - -const attachmentsList = attachments.map((a) => { - const attachmentIsURL = typeof a === 'string'; - return { - label: attachmentIsURL ? a : (a.title ?? a.url), - href: attachmentIsURL ? a : a.url, - icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'), - target: '_blank' as const, - subgroup: attachmentIsURL ? undefined : (a.type ?? ''), - }; -}); - -const shouldRenderSideBarSection = (section: string) => { - if (!flow.data.detailsPanel) { - return true; - } - // @ts-ignore - return flow.data.detailsPanel[section]?.visible ?? true; -}; ---- - - diff --git a/eventcatalog/src/components/SideBars/MessageSideBar.astro b/eventcatalog/src/components/SideBars/MessageSideBar.astro deleted file mode 100644 index a75ac0297..000000000 --- a/eventcatalog/src/components/SideBars/MessageSideBar.astro +++ /dev/null @@ -1,251 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; -import PillListFlat from '@components/Lists/PillListFlat'; -import OwnersList from '@components/Lists/OwnersList'; -import type { CollectionMessageTypes } from '@types'; -import * as path from 'path'; -import VersionList from '@components/Lists/VersionList.astro'; -import { buildUrl } from '@utils/url-builder'; -import { FileDownIcon, ScrollText, Workflow, RssIcon } from 'lucide-react'; -import RepositoryList from '@components/Lists/RepositoryList.astro'; -import { getOwner } from '@utils/collections/owners'; -import CustomSideBarSectionList from '@components/Lists/CustomSideBarSectionList.astro'; -import config from '@config'; -import { getSchemaFormatFromURL } from '@utils/collections/schemas'; -import { isChangelogEnabled, isVisualiserEnabled } from '@utils/feature'; -interface Props { - message: CollectionEntry; -} - -const { message } = Astro.props; - -const producers = (message.data.producers as CollectionEntry<'services'>[]) || []; -const consumers = (message.data.consumers as CollectionEntry<'services'>[]) || []; -const channels = (message.data.messageChannels as CollectionEntry<'channels'>[]) || []; - -const ownersRaw = message.data?.owners || []; -const owners = await Promise.all>(ownersRaw.map(getOwner)); -const filteredOwners = owners.filter((o) => o !== undefined); - -const resourceGroups = message.data?.resourceGroups || []; - -const attachments = message.data.attachments || []; - -const attachmentsList = attachments.map((a) => { - const attachmentIsURL = typeof a === 'string'; - return { - label: attachmentIsURL ? a : (a.title ?? a.url), - href: attachmentIsURL ? a : a.url, - icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'), - target: '_blank' as const, - subgroup: attachmentIsURL ? undefined : (a.type ?? ''), - }; -}); - -const producerList = producers.map((p) => ({ - label: `${p.data.name}`, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/services/${p.data.id}/${p.data.version}`), -})); - -const consumerList = consumers.map((p) => ({ - label: `${p.data.name}`, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/services/${p.data.id}/${p.data.version}`), -})); - -const ownersList = filteredOwners.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: o.collection === 'users' ? o.data.role : 'Team', - avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const channelList = channels.map((c) => ({ - label: `${c.data.name}`, - tag: `v${c.data.version}`, - collection: c.collection, - href: buildUrl(`/docs/channels/${c.data.id}/${c.data.version}`), -})); - -const getType = (type: string) => { - switch (type) { - case 'queries': - return 'Query'; - default: - return message.collection.slice(0, -1); - } -}; - -const getProducerEmptyMessage = (type: string) => { - const value = type.toLowerCase(); - switch (value) { - case 'query': - case 'command': - return `This ${value} does not get invoked by any services.`; - default: - return `This ${value} does not get produced by any services.`; - } -}; - -const getConsumerEmptyMessage = (type: string) => { - const value = type.toLowerCase(); - switch (value) { - case 'query': - case 'command': - return `This ${value} does not invoke any service.`; - default: - return `This ${value} does not get consumed by any services.`; - } -}; - -const type = getType(message.collection); - -const isRSSEnabled = config.rss?.enabled; - -// @ts-ignore -const publicPath = message?.catalog?.publicPath; - -const schemaFilePath = message?.data?.schemaPath; -const schemaURL = path.join(publicPath, schemaFilePath || ''); - -const shouldRenderSideBarSection = (section: string) => { - if (!message.data.detailsPanel) { - return true; - } - // @ts-ignore - return message.data.detailsPanel[section]?.visible ?? true; -}; ---- - - diff --git a/eventcatalog/src/components/SideBars/ServiceSideBar.astro b/eventcatalog/src/components/SideBars/ServiceSideBar.astro deleted file mode 100644 index 9f9231522..000000000 --- a/eventcatalog/src/components/SideBars/ServiceSideBar.astro +++ /dev/null @@ -1,298 +0,0 @@ ---- -import OwnersList from '@components/Lists/OwnersList'; -import PillListFlat from '@components/Lists/PillListFlat'; -import RepositoryList from '@components/Lists/RepositoryList.astro'; -import SpecificationsList from '@components/Lists/SpecificationsList.astro'; -import CustomSideBarSectionList from '@components/Lists/CustomSideBarSectionList.astro'; -import VersionList from '@components/Lists/VersionList.astro'; -import { buildUrl } from '@utils/url-builder'; -import { getOwner } from '@utils/collections/owners'; -import type { CollectionEntry } from 'astro:content'; -import { ScrollText, Workflow, FileDownIcon, Code, Link, RssIcon } from 'lucide-react'; -import { join } from 'node:path'; -import config from '@config'; -import { getDomainsForService } from '@utils/collections/domains'; -import { isChangelogEnabled, isVisualiserEnabled } from '@utils/feature'; -interface Props { - service: CollectionEntry<'services'>; -} - -const { service } = Astro.props; - -// @ts-ignore -const sends = (service.data.sends as CollectionEntry<'events'>[]) || []; -// @ts-ignore -const receives = (service.data.receives as CollectionEntry<'events'>[]) || []; - -// @ts-ignore -const writesTo = (service.data.writesTo as CollectionEntry<'containers'>[]) || []; -// @ts-ignore -const readsFrom = (service.data.readsFrom as CollectionEntry<'containers'>[]) || []; - -// @ts-ignore -const entities = (service.data.entities as CollectionEntry<'entities'>[]) || []; - -const ownersRaw = service.data?.owners || []; -const owners = await Promise.all>(ownersRaw.map(getOwner)); -const filteredOwners = owners.filter((o) => o !== undefined); - -const resourceGroups = service.data?.resourceGroups || []; - -const domainsServiceBelongsTo = await getDomainsForService(service); - -const attachments = service.data.attachments || []; - -const attachmentsList = attachments.map((a) => { - const attachmentIsURL = typeof a === 'string'; - return { - label: attachmentIsURL ? a : (a.title ?? a.url), - href: attachmentIsURL ? a : a.url, - icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'), - target: '_blank' as const, - subgroup: attachmentIsURL ? undefined : (a.type ?? ''), - }; -}); - -const sendsList = sends - .sort((a, b) => a.collection.localeCompare(b.collection)) - .map((p) => ({ - label: p.data.name, - badge: p.collection, - color: p.collection === 'events' ? 'orange' : 'blue', - collection: p.collection, - tag: `v${p.data.version}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), - })); - -const receivesList = receives - .sort((a, b) => a.collection.localeCompare(b.collection)) - .map((p) => ({ - label: p.data.name, - badge: p.collection, - color: p.collection === 'events' ? 'orange' : 'blue', - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), - })); - -const writesToList = writesTo.map((p) => ({ - label: p.data.name, - badge: p.collection, - color: p.collection === 'containers' ? 'green' : 'blue', - collection: p.collection, - tag: `v${p.data.version}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const readsFromList = readsFrom.map((p) => ({ - label: p.data.name, - badge: p.collection, - color: p.collection === 'containers' ? 'green' : 'blue', - collection: p.collection, - tag: `v${p.data.version}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const ownersList = filteredOwners.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: o.collection === 'users' ? o.data.role : 'Team', - avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const domainList = domainsServiceBelongsTo.map((d) => ({ - label: d.data.name, - badge: d.collection, - tag: `v${d.data.version}`, - collection: d.collection, - href: buildUrl(`/docs/${d.collection}/${d.data.id}/${d.data.version}`), -})); - -const entityList = entities.map((p) => ({ - label: p.data.name, - badge: p.collection, - tag: `v${p.data.version}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const isRSSEnabled = config.rss?.enabled; - -// @ts-ignore -const publicPath = service?.catalog?.publicPath; -const schemaFilePath = service?.data?.schemaPath; -const schemaURL = join(publicPath, schemaFilePath || ''); - -const shouldRenderSideBarSection = (section: string) => { - if (!service.data.detailsPanel) { - return true; - } - // @ts-ignore - return service.data.detailsPanel[section]?.visible ?? true; -}; ---- - - diff --git a/eventcatalog/src/components/SideNav/ListViewSideBar/components/CollapsibleGroup.tsx b/eventcatalog/src/components/SideNav/ListViewSideBar/components/CollapsibleGroup.tsx deleted file mode 100644 index a661bb9a2..000000000 --- a/eventcatalog/src/components/SideNav/ListViewSideBar/components/CollapsibleGroup.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import { ChevronDownIcon } from '@heroicons/react/24/outline'; - -interface CollapsibleGroupProps { - isCollapsed: boolean; - onToggle: () => void; - title: React.ReactNode; - children: React.ReactNode; - className?: string; -} - -const CollapsibleGroup: React.FC = ({ isCollapsed, onToggle, title, children, className = '' }) => ( -
-
- - {typeof title === 'string' ? ( - - ) : ( - title - )} -
-
- {children} -
-
-); - -export default CollapsibleGroup; diff --git a/eventcatalog/src/components/SideNav/ListViewSideBar/components/MessageList.tsx b/eventcatalog/src/components/SideNav/ListViewSideBar/components/MessageList.tsx deleted file mode 100644 index 3c955b29e..000000000 --- a/eventcatalog/src/components/SideNav/ListViewSideBar/components/MessageList.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import { getMessageColorByCollection, getMessageCollectionName } from '../index'; - -interface MessageListProps { - messages: any[]; - decodedCurrentPath: string; - searchTerm?: string; -} - -const HighlightedText = React.memo(({ text, searchTerm }: { text: string; searchTerm?: string }) => { - if (!searchTerm) return <>{text}; - - const regex = new RegExp(`(${searchTerm})`, 'gi'); - const parts = text.split(regex); - - return ( - <> - {parts.map((part, index) => - regex.test(part) ? ( - - {part} - - ) : ( - {part} - ) - )} - - ); -}); - -const getMessageColorByLabelOrCollection = (collection: string, badge?: string) => { - if (!badge) { - return getMessageColorByCollection(collection); - } - - // Will try and match the label against HTTP verbs - const httpVerbs = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']; - if (badge && httpVerbs.includes(badge.toUpperCase())) { - if (badge.toUpperCase() === 'GET') return 'bg-blue-50 text-blue-600'; - if (badge.toUpperCase() === 'POST') return 'bg-green-50 text-green-600'; - if (badge.toUpperCase() === 'PUT') return 'bg-yellow-50 text-yellow-600'; - if (badge.toUpperCase() === 'DELETE') return 'bg-red-50 text-red-600'; - if (badge.toUpperCase() === 'PATCH') return 'bg-purple-50 text-purple-600'; - if (badge.toUpperCase() === 'HEAD') return 'bg-gray-50 text-gray-600'; - if (badge.toUpperCase() === 'OPTIONS') return 'bg-orange-50 text-orange-600'; - } - - return getMessageColorByCollection(collection); -}; - -const MessageList: React.FC = ({ messages, decodedCurrentPath, searchTerm }) => ( - -); - -export default MessageList; diff --git a/eventcatalog/src/components/SideNav/ListViewSideBar/components/SpecificationList.tsx b/eventcatalog/src/components/SideNav/ListViewSideBar/components/SpecificationList.tsx deleted file mode 100644 index fd51c0908..000000000 --- a/eventcatalog/src/components/SideNav/ListViewSideBar/components/SpecificationList.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import { buildUrl } from '@utils/url-builder'; -import type { ServiceItem } from '../types'; - -interface SpecificationListProps { - specifications: { - type: string; - path: string; - name?: string; - filename?: string; - filenameWithoutExtension?: string; - }[]; - id: string; - version: string; -} - -const SpecificationList: React.FC = ({ specifications, id, version }) => { - const asyncAPISpecifications = specifications.filter((spec) => spec.type === 'asyncapi'); - const openAPISpecifications = specifications.filter((spec) => spec.type === 'openapi'); - const graphQLSpecifications = specifications.filter((spec) => spec.type === 'graphql'); - - return ( -
    - {asyncAPISpecifications && - asyncAPISpecifications.length > 0 && - asyncAPISpecifications.map((spec) => ( - - {spec.name} - - AsyncAPI - - - ))} - {openAPISpecifications && - openAPISpecifications.length > 0 && - openAPISpecifications.map((spec) => ( - - {spec.name} - - OpenAPI - - - ))} - {graphQLSpecifications && - graphQLSpecifications.length > 0 && - graphQLSpecifications.map((spec) => ( - - {spec.name} - - GraphQL - - - ))} -
- ); -}; - -export default SpecificationList; diff --git a/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx b/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx deleted file mode 100644 index 797860f42..000000000 --- a/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +++ /dev/null @@ -1,1250 +0,0 @@ -import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; -import { ChevronDownIcon, ChevronDoubleDownIcon, ChevronDoubleUpIcon, XMarkIcon } from '@heroicons/react/24/outline'; -import { buildUrl, buildUrlWithParams } from '@utils/url-builder'; -import CollapsibleGroup from './components/CollapsibleGroup'; -import MessageList from './components/MessageList'; -import SpecificationsList from './components/SpecificationList'; -import type { MessageItem, ServiceItem, ListViewSideBarProps, DomainItem, FlowItem, Resources } from './types'; -import { PanelLeft } from 'lucide-react'; -const STORAGE_KEY = 'EventCatalog:catalogSidebarCollapsedGroups'; -const DEBOUNCE_DELAY = 300; // 300ms debounce delay - -const HighlightedText = React.memo(({ text, searchTerm }: { text: string; searchTerm: string }) => { - if (!searchTerm) return <>{text}; - - const regex = new RegExp(`(${searchTerm})`, 'gi'); - const parts = text.split(regex); - - return ( - <> - {parts.map((part, index) => - regex.test(part) ? ( - - {part} - - ) : ( - {part} - ) - )} - - ); -}); - -export const getMessageColorByCollection = (collection: string) => { - if (collection === 'commands') return 'bg-blue-50 text-blue-600'; - if (collection === 'queries') return 'bg-green-50 text-green-600'; - if (collection === 'events') return 'bg-orange-50 text-orange-600'; - if (collection === 'entities') return 'bg-purple-50 text-purple-600'; - return 'text-gray-600'; -}; - -export const getMessageCollectionName = (collection: string, item: any) => { - if (collection === 'commands') return 'Command'; - if (collection === 'queries') return 'Query'; - if (collection === 'events') return 'Event'; - if (collection === 'entities' && item.data.aggregateRoot) return 'Entity (Root)'; - if (collection === 'entities') return 'Entity'; - return collection.slice(0, collection.length - 1).toUpperCase(); -}; - -const NoResultsFound = React.memo(({ searchTerm }: { searchTerm: string }) => ( -
-
No results found for "{searchTerm}"
-
- Try: -
    -
  • Checking for typos
  • -
  • Using fewer keywords
  • -
  • Using more general terms
  • -
-
-
-)); - -const ServiceItem = React.memo( - ({ - item, - decodedCurrentPath, - collapsedGroups, - toggleGroupCollapse, - isVisualizer, - searchTerm, - }: { - item: ServiceItem; - decodedCurrentPath: string; - collapsedGroups: { [key: string]: boolean }; - toggleGroupCollapse: (group: string) => void; - isVisualizer: boolean; - searchTerm: string; - }) => { - const readsAndWritesTo = item.writesTo.filter((writeTo) => item.readsFrom.some((readFrom) => readFrom.id === writeTo.id)); - const resourceReads = item.readsFrom.filter((readFrom) => !readsAndWritesTo.some((writeTo) => writeTo.id === readFrom.id)); - const resourceWrites = item.writesTo.filter((writeTo) => !readsAndWritesTo.some((readFrom) => readFrom.id === writeTo.id)); - const hasData = item.writesTo.length > 0 || item.readsFrom.length > 0; - - const sendsMessages = item.sends && item.sends.length > 0; - const receivesMessages = item.receives && item.receives.length > 0; - - return ( - toggleGroupCollapse(item.href)} - title={ - - } - > -
- - Overview - - {isVisualizer && hasData && ( - - Data Diagram - - )} - {!isVisualizer && ( - - Architecture - - )} - - {!isVisualizer && item.specifications && item.specifications.length > 0 && ( - toggleGroupCollapse(`${item.href}-specifications`)} - title={ - - } - > - - - )} - - {receivesMessages && ( - toggleGroupCollapse(`${item.href}-receives`)} - title={ - - } - > - - - )} - {sendsMessages && ( - toggleGroupCollapse(`${item.href}-sends`)} - title={ - - } - > - - - )} - {!isVisualizer && hasData && ( - toggleGroupCollapse(`${item.href}-data`)} - title={ - - } - > - {readsAndWritesTo.length > 0 && ( - toggleGroupCollapse(`${item.href}-writesTo-data`)} - title={ - - } - > - - - )} - {resourceWrites.length > 0 && ( - toggleGroupCollapse(`${item.href}-writesTo-data`)} - title={ - - } - > - - - )} - {resourceReads.length > 0 && ( - toggleGroupCollapse(`${item.href}-readsFrom-data`)} - title={ - - } - > - - - )} - - )} - {!isVisualizer && item.entities.length > 0 && ( - toggleGroupCollapse(`${item.href}-entities`)} - title={ - - } - > - - - )} -
-
- ); - } -); - -const ListViewSideBar: React.FC = ({ resources, currentPath, showOrphanedMessages }) => { - const navRef = useRef(null); - const [data] = useState(resources); - const [searchTerm, setSearchTerm] = useState(''); - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); - const [isInitialized, setIsInitialized] = useState(false); - const [isExpanded, setIsExpanded] = useState(true); - const [isSearchPinned, setIsSearchPinned] = useState(false); - const [lastScrollTop, setLastScrollTop] = useState(0); - const [collapsedGroups, setCollapsedGroups] = useState<{ [key: string]: boolean }>(() => { - if (typeof window !== 'undefined') { - const saved = window.localStorage.getItem(STORAGE_KEY); - const savedState = saved ? JSON.parse(saved) : {}; - const currentPath = window.location.pathname; - - // Default all sections to collapsed - const defaultCollapsedState: { [key: string]: boolean } = { - 'all-services-group': true, - 'flows-group': true, - 'data-group': true, - 'designs-group': true, - 'messagesNotInService-group': true, - }; - - // Default all domains, services, and their subsections to collapsed - resources.domains?.forEach((domain: any) => { - const isDomainActive = currentPath.includes(domain.href); - defaultCollapsedState[domain.href] = !isDomainActive; - defaultCollapsedState[`${domain.href}-entities`] = true; - defaultCollapsedState[`${domain.href}-subdomains`] = true; - defaultCollapsedState[`${domain.href}-services`] = true; - }); - - resources.services?.forEach((service: any) => { - const isServiceActive = currentPath.includes(service.href); - defaultCollapsedState[service.href] = !isServiceActive; - defaultCollapsedState[`${service.href}-specifications`] = true; - defaultCollapsedState[`${service.href}-receives`] = true; - defaultCollapsedState[`${service.href}-sends`] = true; - defaultCollapsedState[`${service.href}-entities`] = true; - defaultCollapsedState[`${service.href}-data`] = true; - defaultCollapsedState[`${service.href}-writesTo-data`] = true; - defaultCollapsedState[`${service.href}-readsFrom-data`] = true; - }); - - setIsInitialized(true); - return { ...defaultCollapsedState, ...savedState }; - } - return {}; - }); - - const decodedCurrentPath = window.location.pathname; - const isVisualizer = window.location.pathname.includes('/visualiser/'); - - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearchTerm(searchTerm.toLowerCase()); - }, DEBOUNCE_DELAY); - - return () => clearTimeout(timer); - }, [searchTerm]); - - // Filter data based on search term - const filteredData: Resources = useMemo(() => { - if (!debouncedSearchTerm) return data; - - const filterItem = (item: { label: string; id?: string }) => { - return ( - item.label.toLowerCase().includes(debouncedSearchTerm) || (item.id && item.id.toLowerCase().includes(debouncedSearchTerm)) - ); - }; - - const filterMessages = (messages: MessageItem[]) => { - return messages.filter( - (message) => - message.data.name.toLowerCase().includes(debouncedSearchTerm) || message.id.toLowerCase().includes(debouncedSearchTerm) - ); - }; - - // Enhanced domain filtering that considers parent-subdomain relationships - const filterDomains = (domains: any[]) => { - const filteredDomains: any[] = []; - - domains.forEach((domain: any) => { - const domainMatches = filterItem(domain); - - // Check if this domain is a subdomain of another domain - const isSubdomain = domains.some((parentDomain: any) => { - const subdomains = parentDomain.domains || []; - return subdomains.some((subdomain: any) => subdomain.data.id === domain.id); - }); - - // If this is a parent domain, check if any of its subdomains match - let hasMatchingSubdomains = false; - if (!isSubdomain) { - const subdomains = domain.domains || []; - hasMatchingSubdomains = domains.some((potentialSubdomain: any) => - subdomains.some((subdomain: any) => subdomain.data.id === potentialSubdomain.id && filterItem(potentialSubdomain)) - ); - } - - // Include domain if: - // 1. The domain itself matches the search - // 2. It's a parent domain and has matching subdomains - // 3. It's a subdomain and matches the search - if (domainMatches || hasMatchingSubdomains || (isSubdomain && domainMatches)) { - filteredDomains.push(domain); - } - - // If this is a subdomain that matches, also include its parent domain - if (isSubdomain && domainMatches) { - const parentDomain = domains.find((parentDomain: any) => { - const subdomains = parentDomain.domains || []; - return subdomains.some((subdomain: any) => subdomain.data.id === domain.id); - }); - - if (parentDomain && !filteredDomains.some((d: any) => d.id === parentDomain.id)) { - filteredDomains.push(parentDomain); - } - } - }); - - return filteredDomains; - }; - - return { - 'context-map': data['context-map']?.filter(filterItem) || [], - domains: data.domains ? filterDomains(data.domains) : [], - services: - data.services - ?.map((service: ServiceItem) => ({ - ...service, - sends: filterMessages(service.sends), - receives: filterMessages(service.receives), - isVisible: - filterItem(service) || - service.sends.some( - (msg: MessageItem) => - msg.data.name.toLowerCase().includes(debouncedSearchTerm) || msg.id.toLowerCase().includes(debouncedSearchTerm) - ) || - service.receives.some( - (msg: MessageItem) => - msg.data.name.toLowerCase().includes(debouncedSearchTerm) || msg.id.toLowerCase().includes(debouncedSearchTerm) - ), - })) - .filter((service: ServiceItem & { isVisible: boolean }) => service.isVisible) || [], - flows: data.flows?.filter(filterItem) || [], - designs: data.designs?.filter(filterItem) || [], - messagesNotInService: - data.messagesNotInService?.filter( - (msg: MessageItem) => - msg.label.toLowerCase().includes(debouncedSearchTerm) || msg.id.toLowerCase().includes(debouncedSearchTerm) - ) || [], - }; - }, [data, debouncedSearchTerm]); - - // Auto-expand groups when searching - useEffect(() => { - if (debouncedSearchTerm) { - // Expand all groups when searching - const newCollapsedState = { ...collapsedGroups }; - Object.keys(newCollapsedState).forEach((key) => { - newCollapsedState[key] = false; - }); - setCollapsedGroups(newCollapsedState); - } - }, [debouncedSearchTerm]); - - // Handle scroll for sticky search bar - useEffect(() => { - const nav = navRef.current; - if (!nav) return; - - const handleScroll = () => { - const scrollTop = nav.scrollTop; - const scrollThreshold = 50; // Pin after scrolling 50px - - // Scrolling down past threshold - if (scrollTop > scrollThreshold && scrollTop > lastScrollTop) { - setIsSearchPinned(true); - } - // Scrolling up near the top - else if (scrollTop <= scrollThreshold) { - setIsSearchPinned(false); - } - - setLastScrollTop(scrollTop); - }; - - nav.addEventListener('scroll', handleScroll); - return () => nav.removeEventListener('scroll', handleScroll); - }, [lastScrollTop]); - - // Store collapsed groups in local storage - useEffect(() => { - if (typeof window !== 'undefined') { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(collapsedGroups)); - } - }, [collapsedGroups]); - - // If we find a data-active element, scroll to it on mount and open its section - useEffect(() => { - const activeElement = document.querySelector('[data-active="true"]'); - if (activeElement) { - // Add y offset to the scroll position - activeElement.scrollIntoView({ behavior: 'instant', block: 'center' }); - - // Check which section the active element belongs to and open it - const newCollapsedState = { ...collapsedGroups }; - - // Check if active page is in a domain - data.domains?.forEach((domain: any) => { - if (decodedCurrentPath.includes(domain.href)) { - newCollapsedState[domain.href] = false; - - // Check if it's in domain entities - const isInDomainEntities = domain.entities?.some((entity: any) => decodedCurrentPath.includes(entity.href)); - if (isInDomainEntities) { - newCollapsedState[`${domain.href}-entities`] = false; - } - - // Check if it's in domain services - const isInDomainServices = domain.services?.some((service: any) => decodedCurrentPath.includes(service.data.id)); - if (isInDomainServices) { - newCollapsedState[`${domain.href}-services`] = false; - } - - // Check if it's in a subdomain - const subdomains = domain.domains || []; - subdomains.forEach((subdomain: any) => { - const actualSubdomain = data.domains?.find((d: any) => d.id === subdomain.data.id); - if (actualSubdomain && decodedCurrentPath.includes(actualSubdomain.href)) { - newCollapsedState[`${domain.href}-subdomains`] = false; - newCollapsedState[actualSubdomain.href] = false; - - // Check subdomain entities - const isInSubdomainEntities = actualSubdomain.entities?.some((entity: any) => - decodedCurrentPath.includes(entity.href) - ); - if (isInSubdomainEntities) { - newCollapsedState[`${actualSubdomain.href}-entities`] = false; - } - - // Check subdomain services - const isInSubdomainServices = actualSubdomain.services?.some((service: any) => - decodedCurrentPath.includes(service.data.id) - ); - if (isInSubdomainServices) { - newCollapsedState[`${actualSubdomain.href}-services`] = false; - } - } - }); - } - }); - - // Check if active page is a service - data.services?.forEach((service: ServiceItem) => { - if (decodedCurrentPath.includes(service.href)) { - newCollapsedState['all-services-group'] = false; - newCollapsedState[service.href] = false; - - // Open specific service sections if active - if (decodedCurrentPath.includes('/data')) { - newCollapsedState[`${service.href}-data`] = false; - } - } - }); - - // Check if active page is a flow - const isFlow = data.flows?.some((flow: FlowItem) => decodedCurrentPath === flow.href); - if (isFlow) { - newCollapsedState['flows-group'] = false; - } - - // Check if active page is a container/data store - const isContainer = data.containers?.some((container: any) => decodedCurrentPath === container.href); - if (isContainer) { - newCollapsedState['data-group'] = false; - } - - // Check if active page is a design - const isDesign = data.designs?.some((design: any) => decodedCurrentPath === design.href); - if (isDesign) { - newCollapsedState['designs-group'] = false; - } - - // Check if active page is an orphaned message - const isOrphanedMessage = data.messagesNotInService?.some((msg: MessageItem) => decodedCurrentPath === msg.href); - if (isOrphanedMessage) { - newCollapsedState['messagesNotInService-group'] = false; - } - - setCollapsedGroups(newCollapsedState); - } - }, []); - - const toggleGroupCollapse = useCallback((group: string) => { - setCollapsedGroups((prev) => ({ - ...prev, - [group]: !prev[group], - })); - }, []); - - const handleSearchChange = useCallback((e: React.ChangeEvent) => { - setSearchTerm(e.target.value); - }, []); - - const collapseAll = useCallback(() => { - const newCollapsedState: { [key: string]: boolean } = {}; - - // Collapse all domains - filteredData.domains?.forEach((domain: any) => { - newCollapsedState[domain.href] = true; - newCollapsedState[`${domain.href}-entities`] = true; - newCollapsedState[`${domain.href}-subdomains`] = true; - newCollapsedState[`${domain.href}-services`] = true; - }); - - // Collapse all services - filteredData.services?.forEach((service: any) => { - newCollapsedState[service.href] = true; - newCollapsedState[`${service.href}-specifications`] = true; - newCollapsedState[`${service.href}-receives`] = true; - newCollapsedState[`${service.href}-sends`] = true; - newCollapsedState[`${service.href}-entities`] = true; - }); - - setCollapsedGroups(newCollapsedState); - setIsExpanded(false); - }, [filteredData]); - - const expandAll = useCallback(() => { - const newCollapsedState: { [key: string]: boolean } = {}; - - // Expand all domains - filteredData.domains?.forEach((domain: any) => { - newCollapsedState[domain.href] = false; - newCollapsedState[`${domain.href}-entities`] = false; - newCollapsedState[`${domain.href}-subdomains`] = false; - newCollapsedState[`${domain.href}-services`] = false; - }); - - // Expand all services - filteredData.services?.forEach((service: any) => { - newCollapsedState[service.href] = false; - newCollapsedState[`${service.href}-specifications`] = false; - newCollapsedState[`${service.href}-receives`] = false; - newCollapsedState[`${service.href}-sends`] = false; - newCollapsedState[`${service.href}-entities`] = false; - }); - - setCollapsedGroups(newCollapsedState); - setIsExpanded(true); - }, [filteredData]); - - const toggleExpandCollapse = useCallback(() => { - if (isExpanded) { - collapseAll(); - } else { - expandAll(); - } - }, [isExpanded, collapseAll, expandAll]); - - const hideSidebar = useCallback(() => { - // Dispatch custom event that the Astro layout will listen for - window.dispatchEvent(new CustomEvent('sidebarToggle', { detail: { action: 'hide' } })); - }, []); - - const isDomainSubDomain = useMemo(() => { - return (domain: any) => { - const domains = data.domains || []; - return domains.some((d: any) => { - const subdomains = d.domains || []; - return subdomains.some((subdomain: any) => subdomain.data.id === domain.id); - }); - }; - }, [data.domains]); - - // Helper function to get parent domains (domains that are not subdomains) - const getParentDomains = useMemo(() => { - return (domains: any[]) => { - return domains.filter((domain: any) => !isDomainSubDomain(domain)); - }; - }, [isDomainSubDomain]); - - // Helper function to get subdomains for a specific parent domain - const getSubdomainsForParent = useMemo(() => { - return (parentDomain: any, allDomains: any[]) => { - const subdomains = parentDomain.domains || []; - return allDomains.filter((domain: any) => subdomains.some((subdomain: any) => subdomain.data.id === domain.id)); - }; - }, []); - - // Helper function to get services for a specific domain (only direct services, not from subdomains) - const getServicesForDomain = useMemo(() => { - return (domain: any, allServices: ServiceItem[], allDomains: any[]) => { - const domainServices = domain.services || []; - const subdomains = getSubdomainsForParent(domain, allDomains); - - // Get all service IDs from subdomains - const subdomainServiceIds = subdomains.flatMap((subdomain: any) => (subdomain.services || []).map((s: any) => s.data.id)); - - // Filter services that belong to this domain but NOT to any of its subdomains - return allServices.filter( - (service: ServiceItem) => - domainServices.some((domainService: any) => domainService.data.id === service.id) && - !subdomainServiceIds.includes(service.id) - ); - }; - }, [getSubdomainsForParent]); - - // Component to render a single domain item - const DomainItem = React.memo( - ({ item, isSubdomain = false, nestingLevel = 0 }: { item: any; isSubdomain?: boolean; nestingLevel?: number }) => { - const marginLeft = nestingLevel > 0 ? `ml-${nestingLevel * 4}` : ''; - - return ( -
- - -
- ); - } - ); - - // Component to render domain content (Overview, Architecture, etc.) - const DomainContent = React.memo( - ({ - item, - nestingLevel = 0, - className = '', - isSubdomain = false, - }: { - item: any; - nestingLevel?: number; - className?: string; - isSubdomain?: boolean; - }) => { - const marginLeft = nestingLevel > 0 ? `ml-${nestingLevel * 4}` : ''; - const hasEntities = item.entities && item.entities.length > 0; - - // Get services for this domain - const domainServices = getServicesForDomain(item, filteredData['services'] || [], filteredData['domains'] || []); - - return ( -
-
- - Overview - - - {isVisualizer && hasEntities && ( - - Entity Map - - )} - {!isVisualizer && ( - service.data.id).join(','), - domainId: item.id, - domainName: item.name, - })} - data-active={window.location.href.includes(`domainId=${item.id}`)} - className={`flex items-center px-2 py-1.5 text-xs text-gray-600 hover:bg-purple-100 rounded-md ${ - window.location.href.includes(`domainId=${item.id}`) ? 'bg-purple-100 ' : 'hover:bg-purple-100' - }`} - > - Architecture - - )} - {!isVisualizer && ( - - Ubiquitous Language - - )} - - {/* Render services before entities */} - {domainServices.length > 0 && ( - toggleGroupCollapse(`${item.href}-services`)} - title={ - - } - > -
- {domainServices.map((service: ServiceItem) => ( - - ))} -
-
- )} - - {item.entities.length > 0 && !isVisualizer && ( - toggleGroupCollapse(`${item.href}-entities`)} - title={ - - } - > - - - )} -
-
- ); - } - ); - - if (!isInitialized) return null; - - const hasNoResults = - debouncedSearchTerm && - !filteredData['context-map']?.length && - !filteredData.domains?.length && - !filteredData.services?.length && - !filteredData.flows?.length && - !filteredData.messagesNotInService?.length; - - return ( - - ); -}; - -export default React.memo(ListViewSideBar); diff --git a/eventcatalog/src/components/SideNav/ListViewSideBar/types.ts b/eventcatalog/src/components/SideNav/ListViewSideBar/types.ts deleted file mode 100644 index f38199905..000000000 --- a/eventcatalog/src/components/SideNav/ListViewSideBar/types.ts +++ /dev/null @@ -1,91 +0,0 @@ -export interface MessageItem { - href: string; - label: string; - service: string; - id: string; - direction: 'sends' | 'receives'; - type: 'command' | 'query' | 'event'; - collection: string; - data: { - name: string; - }; -} - -export interface EntityItem { - href: string; - label: string; - id: string; - name: string; -} - -export interface ServiceItem { - href: string; - label: string; - name: string; - id: string; - version: string; - sidebar?: { - badge?: string; - color?: string; - backgroundColor?: string; - }; - draft: boolean | { title?: string; message: string }; - sends: MessageItem[]; - receives: MessageItem[]; - entities: EntityItem[]; - writesTo: MessageItem[]; - readsFrom: MessageItem[]; - specifications?: { - type: string; - path: string; - name?: string; - filename?: string; - filenameWithoutExtension?: string; - }[]; -} - -export interface DomainItem { - href: string; - label: string; - id: string; - name: string; - services: any[]; - domains: any[]; - entities: EntityItem[]; -} - -export interface FlowItem { - href: string; - label: string; -} - -export interface DesignItem { - href: string; - label: string; - id: string; - name: string; -} - -export interface Resources { - 'context-map'?: Array<{ - href: string; - label: string; - id: string; - name: string; - }>; - domains?: DomainItem[]; - services?: ServiceItem[]; - flows?: FlowItem[]; - designs?: DesignItem[]; - messagesNotInService?: MessageItem[]; - commands?: MessageItem[]; - queries?: MessageItem[]; - events?: MessageItem[]; - containers?: MessageItem[]; -} - -export interface ListViewSideBarProps { - resources: Resources; - currentPath: string; - showOrphanedMessages: boolean; -} diff --git a/eventcatalog/src/components/SideNav/ListViewSideBar/utils.ts b/eventcatalog/src/components/SideNav/ListViewSideBar/utils.ts deleted file mode 100644 index 693e57aaa..000000000 --- a/eventcatalog/src/components/SideNav/ListViewSideBar/utils.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { isCollectionVisibleInCatalog } from '@eventcatalog'; -import { buildUrl } from '@utils/url-builder'; -import { getChannels } from '@utils/channels'; -import { getDomains } from '@utils/collections/domains'; -import { getFlows } from '@utils/collections/flows'; -import { getServices, getSpecificationsForService } from '@utils/collections/services'; -import { getCommands } from '@utils/commands'; -import { getEvents } from '@utils/events'; -import { getQueries } from '@utils/queries'; -import { getDesigns } from '@utils/collections/designs'; -import { getContainers } from '@utils/collections/containers'; - -const stripCollection = (collection: any) => { - return collection.map((item: any) => ({ - data: { - id: item.data.id, - version: item.data.version, - }, - })); -}; - -export async function getResourcesForNavigation({ currentPath }: { currentPath: string }) { - const events = await getEvents({ getAllVersions: false }); - const commands = await getCommands({ getAllVersions: false }); - const queries = await getQueries({ getAllVersions: false }); - const services = await getServices({ getAllVersions: false }); - const domains = await getDomains({ getAllVersions: false }); - const channels = await getChannels({ getAllVersions: false }); - const flows = await getFlows({ getAllVersions: false }); - const containers = await getContainers({ getAllVersions: false }); - const designs = await getDesigns({ getAllVersions: false }); - - const messages = [...events, ...commands, ...queries]; - - // messages that are not in a service (sends or receives) - const messagesNotInService = messages.filter( - (message) => - !services.some( - (service) => - service.data?.sends?.some((send: any) => send.data.id === message.data.id) || - service.data?.receives?.some((receive: any) => receive.data.id === message.data.id) - ) - ); - - const route = currentPath.includes('visualiser') ? 'visualiser' : 'docs'; - - // Just the domains for now. - const allDataAsSideNav = [...domains, ...services, ...flows, ...channels, ...containers].reduce((acc, item) => { - const title = item.collection; - const group = acc[title] || []; - - const isCollectionDomain = item.collection === 'domains'; - const isCollectionService = item.collection === 'services'; - - const servicesCount = isCollectionDomain ? item.data.services?.length || 0 : 0; - const sends = isCollectionService ? item.data.sends || null : null; - const receives = isCollectionService ? item.data.receives || null : null; - const entities = isCollectionDomain || isCollectionService ? item.data.entities || null : null; - - const writesTo = isCollectionService ? item.data.writesTo || null : null; - const readsFrom = isCollectionService ? item.data.readsFrom || null : null; - - // Add href to the sends and receives - const sendsWithHref = sends?.map((send: any) => ({ - id: send.data.id, - data: { - name: send.data.name, - sidebar: send.data.sidebar, - aggregateRoot: send?.data?.aggregateRoot, - draft: send.data.draft, - }, - collection: send.collection, - href: buildUrl(`/${route}/${send.collection}/${send.data.id}/${send.data.version}`), - })); - const receivesWithHref = receives?.map((receive: any) => ({ - id: receive.data.id, - data: { - name: receive.data.name, - sidebar: receive.data.sidebar, - aggregateRoot: receive?.data?.aggregateRoot, - draft: receive.data.draft, - }, - collection: receive.collection, - href: buildUrl(`/${route}/${receive.collection}/${receive.data.id}/${receive.data.version}`), - })); - const entitiesWithHref = entities?.map((entity: any) => ({ - id: entity.data.id, - data: { - name: entity.data.name, - sidebar: entity.data.sidebar, - aggregateRoot: entity?.data?.aggregateRoot, - draft: entity.data.draft, - }, - collection: entity.collection, - href: buildUrl(`/${route}/${entity.collection}/${entity.data.id}/${entity.data.version}`), - })); - - const writesToWithHref = writesTo?.map((writeTo: any) => ({ - id: writeTo.data.id, - data: { - name: writeTo.data.name, - sidebar: { - badge: writeTo.data.container_type || writeTo.collection, - backgroundColor: 'bg-blue-100 text-blue-600', - }, - }, - collection: writeTo.collection, - href: buildUrl(`/${route}/${writeTo.collection}/${writeTo.data.id}/${writeTo.data.version}`), - })); - - const readsFromWithHref = readsFrom?.map((readFrom: any) => ({ - id: readFrom.data.id, - data: { - name: readFrom.data.name, - sidebar: { - badge: readFrom.data.container_type || readFrom.collection, - backgroundColor: 'bg-blue-100 text-indigo-600', - }, - }, - collection: readFrom.collection, - href: buildUrl(`/${route}/${readFrom.collection}/${readFrom.data.id}/${readFrom.data.version}`), - })); - - // don't render items if we are in the visualiser and the item has visualiser set to false - if (currentPath.includes('visualiser') && item.data.visualiser === false) { - return acc; - } - - const navigationItem = { - label: item.data.name, - version: item.data.version, - // items: item.collection === 'users' ? [] : item.headings, - visible: isCollectionVisibleInCatalog(item.collection), - // @ts-ignore - href: item.data.version - ? // @ts-ignore - buildUrl(`/${route}/${item.collection}/${item.data.id}/${item.data.version}`) - : buildUrl(`/${route}/${item.collection}/${item.data.id}`), - collection: item.collection, - servicesCount, - id: item.data.id, - name: item.data.name, - draft: item.data.draft, - services: isCollectionDomain ? stripCollection(item.data.services) : null, - domains: isCollectionDomain ? stripCollection(item.data.domains) : null, - sends: sendsWithHref, - receives: receivesWithHref, - entities: entitiesWithHref, - specifications: isCollectionService ? getSpecificationsForService(item) : null, - writesTo: writesToWithHref, - readsFrom: readsFromWithHref, - sidebar: item.data?.sidebar, - renderInVisualiser: item.data?.visualiser ?? true, - }; - - group.push(navigationItem); - - return { - ...acc, - [title]: group, - }; - }, {} as any); - - // Add messagesNotInService - const messagesNotInServiceAsSideNav = messagesNotInService.map((item) => ({ - label: item.data.name, - version: item.data.version, - id: item.data.id, - name: item.data.name, - draft: item.data.draft, - href: buildUrl(`/${route}/${item.collection}/${item.data.id}/${item.data.version}`), - collection: item.collection, - })); - - const sideNav = { - ...(currentPath.includes('visualiser') - ? { - 'context-map': [ - { - label: 'Domain Integration Map', - href: buildUrl('/visualiser/domain-integrations'), - collection: 'domain-integrations', - }, - ], - } - : {}), - ...allDataAsSideNav, - messagesNotInService: messagesNotInServiceAsSideNav, - }; - - // Add designs? - if (designs.length > 0) { - sideNav['designs'] = designs.map((design) => ({ - label: design.data.name, - href: buildUrl(`/visualiser/designs/${design.data.id}`), - collection: 'designs', - })); - } - - return sideNav; -} diff --git a/eventcatalog/src/components/SideNav/SideNav.astro b/eventcatalog/src/components/SideNav/SideNav.astro deleted file mode 100644 index 427f8c361..000000000 --- a/eventcatalog/src/components/SideNav/SideNav.astro +++ /dev/null @@ -1,37 +0,0 @@ ---- -import type { HTMLAttributes } from 'astro/types'; -import config from '@config'; - -// FlatView -import { getResourcesForNavigation as getListViewResources } from './ListViewSideBar/utils'; - -// TreeView -import { SideNavTreeView } from './TreeView'; -import { getTreeView } from './TreeView/getTreeView'; - -import ListViewSideBar from './ListViewSideBar'; - -interface Props extends Omit, 'children'> {} - -const currentPath = Astro.url.pathname; - -let props; - -const SIDENAV_TYPE = config?.docs?.sidebar?.type ?? 'LIST_VIEW'; -const SHOW_ORPHANED_MESSAGES = config?.docs?.sidebar?.showOrphanedMessages ?? true; - -if (SIDENAV_TYPE === 'LIST_VIEW') { - props = await getListViewResources({ currentPath }); -} else if (SIDENAV_TYPE === 'TREE_VIEW') { - props = getTreeView({ projectDir: process.env.PROJECT_DIR!, currentPath }); -} ---- - -
- { - SIDENAV_TYPE === 'LIST_VIEW' && ( - - ) - } - {SIDENAV_TYPE === 'TREE_VIEW' && } -
diff --git a/eventcatalog/src/components/SideNav/TreeView/getTreeView.ts b/eventcatalog/src/components/SideNav/TreeView/getTreeView.ts deleted file mode 100644 index b82868382..000000000 --- a/eventcatalog/src/components/SideNav/TreeView/getTreeView.ts +++ /dev/null @@ -1,190 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import os from 'node:os'; -import gm from 'gray-matter'; -import { globSync } from 'glob'; -import type { CollectionKey } from 'astro:content'; -import { buildUrl } from '@utils/url-builder'; - -export type TreeNode = { - id: string; - name: string; - version: string; - href?: string; - type: CollectionKey | null; - children: TreeNode[]; -}; - -/** - * Resource types that should be in the sidenav - */ -const RESOURCE_TYPES = ['domains', 'entities', 'services', 'events', 'commands', 'queries', 'flows', 'channels', 'containers']; -// const RESOURCE_TYPES = ['domains', 'services', 'events', 'commands', 'queries', 'flows', 'channels']; - -/** - * Check if the path has a RESOURCE_TYPE on path - */ -function canBeResource(dirPath: string) { - const parts = dirPath.split(path.sep); - for (let i = parts.length - 1; i >= 0; i--) { - if (RESOURCE_TYPES.includes(parts[i])) return true; - } - return false; -} - -function isNotVersioned(dirPath: string) { - const parts = dirPath.split(path.sep); - return parts.every((p) => p !== 'versioned'); -} - -function getResourceType(filePath: string): CollectionKey | null { - const parts = filePath.split(path.sep); - for (let i = parts.length - 1; i >= 0; i--) { - if (RESOURCE_TYPES.includes(parts[i])) return parts[i] as CollectionKey; - } - return null; -} - -function buildTreeOfDir(directory: string, parentNode: TreeNode, options: { ignore?: CollectionKey[] }) { - let node: TreeNode | null = null; - - const resourceType = getResourceType(directory); - - const markdownFiles = globSync(path.join(directory, '/*.mdx'), { windowsPathsNoEscape: os.platform() === 'win32' }); - const isResourceIgnored = options?.ignore && resourceType && options.ignore.includes(resourceType); - - if (markdownFiles.length > 0 && !isResourceIgnored) { - const resourceFilePath = markdownFiles.find((md) => md.endsWith('index.mdx')); - if (resourceFilePath) { - const resourceDef = gm.read(resourceFilePath); - node = { - id: resourceDef.data.id, - name: resourceDef.data.name, - type: resourceType, - version: resourceDef.data.version, - children: [], - }; - parentNode.children.push(node); - } - } - - const directories = fs.readdirSync(directory).filter((name) => { - const dirPath = path.join(directory, name); - return fs.statSync(dirPath).isDirectory() && isNotVersioned(dirPath) && canBeResource(dirPath); - }); - for (const dir of directories) { - buildTreeOfDir(path.join(directory, dir), node || parentNode, options); - } -} - -function forEachTreeNodeOf(node: TreeNode, ...callbacks: Array<(node: TreeNode) => void>) { - const next = node.children; - - callbacks.forEach((cb) => cb(node)); - - // Go to next level - next.forEach((n) => { - forEachTreeNodeOf(n, ...callbacks); - }); -} - -function addHrefToNode(basePathname: 'docs' | 'visualiser') { - return (node: TreeNode) => { - node.href = encodeURI( - buildUrl( - `/${basePathname}/${node.type}/${node.id}${node.type === 'teams' || node.type === 'users' ? '' : `/${node.version}`}` - ) - ); - }; -} - -function orderChildrenByName(parentNode: TreeNode) { - parentNode.children.sort((a, b) => a.name.localeCompare(b.name)); -} - -function groupChildrenByType(parentNode: TreeNode) { - if (parentNode.children.length === 0) return; // Only group if there are children - - const acc: Record = {}; - - // Flows and messages are collapsed by default - - parentNode.children.forEach((n) => { - if (n.type === null) return; // TODO: Just ignore or remove the type null??? - if (!(n.type in acc)) acc[n.type] = []; - acc[n.type].push(n); - }); - - // Collapse everything except domains - const AUTO_EXPANDED_TYPES = ['domains']; - - parentNode.children = Object.entries(acc) - // Order label nodes by RESOURCE_TYPES - .sort(([aType], [bType]) => RESOURCE_TYPES.indexOf(aType) - RESOURCE_TYPES.indexOf(bType)) - // Construct the label nodes - .map(([type, nodes]) => { - return { - id: `${parentNode.id}/${type}`, - name: type === 'containers' ? 'Data' : type, - type: type as CollectionKey, - version: '0', - children: nodes, - isExpanded: AUTO_EXPANDED_TYPES.includes(type), - isLabel: true, - }; - }); -} - -const treeViewCache = new Map(); - -export function getTreeView({ projectDir, currentPath }: { projectDir: string; currentPath: string }): TreeNode { - const basePathname = currentPath.split('/').find((p) => p === 'docs' || p === 'visualiser') || 'docs'; - - const cacheKey = `${projectDir}:${basePathname}`; - if (treeViewCache.has(cacheKey)) return treeViewCache.get(cacheKey)!; - - const rootNode: TreeNode = { - id: '/', - name: 'root', - type: null, - version: '0', - children: [], - }; - - buildTreeOfDir(projectDir, rootNode, { - ignore: basePathname === 'visualiser' ? ['teams', 'users', 'channels'] : undefined, - }); - - // prettier-ignore - forEachTreeNodeOf( - rootNode, - addHrefToNode(basePathname), - orderChildrenByName, - groupChildrenByType, - ); - - if (basePathname === 'visualiser') { - rootNode.children.unshift({ - id: '/bounded-context-map', - name: 'bounded context map', - type: 'bounded-context-map' as any, - version: '0', - isLabel: true, - children: [ - { - id: '/domain-map', - name: 'Domain map', - href: buildUrl('/visualiser/context-map'), - type: 'bounded-context-map' as any, - version: '', - children: [], - }, - ], - } as TreeNode); - } - - // Store in cache before returning - treeViewCache.set(cacheKey, rootNode); - - return rootNode; -} diff --git a/eventcatalog/src/components/SideNav/TreeView/index.tsx b/eventcatalog/src/components/SideNav/TreeView/index.tsx deleted file mode 100644 index fbe6a7f08..000000000 --- a/eventcatalog/src/components/SideNav/TreeView/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { gray } from 'tailwindcss/colors'; -import { TreeView } from '@components/TreeView'; -import { navigate } from 'astro:transitions/client'; -import type { TreeNode as RawTreeNode } from './getTreeView'; -import { getIconForCollection } from '@utils/collections/icons'; -import { useEffect, useState } from 'react'; - -type TreeNode = RawTreeNode & { isLabel?: true; isDefaultExpanded?: boolean; isExpanded?: boolean }; - -function isCurrentNode(node: TreeNode, currentPathname: string) { - return currentPathname === node.href; -} - -function TreeNode({ node }: { node: TreeNode }) { - const Icon = getIconForCollection(node.type ?? ''); - const [isCurrent, setIsCurrent] = useState(document.location.pathname === node.href); - - useEffect(() => { - const abortCtrl = new AbortController(); - // prettier-ignore - document.addEventListener( - 'astro:page-load', - () => setIsCurrent(document.location.pathname === node.href), - { signal: abortCtrl.signal }, - ); - return () => abortCtrl.abort(); - }, [document, node]); - - return ( - navigate(node.href!)} - > - {!node?.isLabel && ( - - - - )} - - {node.name} {node.isLabel ? `(${node.children.length})` : ''} - - {(node.children || []).length > 0 && ( - - {node.children!.map((childNode) => ( - - ))} - - )} - - ); -} - -export function SideNavTreeView({ tree }: { tree: TreeNode }) { - function bubbleUpExpanded(parentNode: TreeNode) { - if (isCurrentNode(parentNode, document.location.pathname)) return true; - return (parentNode.isDefaultExpanded = parentNode.children.some(bubbleUpExpanded)); - } - bubbleUpExpanded(tree); - - return ( - - ); -} diff --git a/eventcatalog/src/components/Tables/Table.tsx b/eventcatalog/src/components/Tables/Table.tsx deleted file mode 100644 index e1a905ace..000000000 --- a/eventcatalog/src/components/Tables/Table.tsx +++ /dev/null @@ -1,416 +0,0 @@ -import { - flexRender, - getCoreRowModel, - getFacetedMinMaxValues, - getFacetedRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - useReactTable, - type Column, - type ColumnFiltersState, -} from '@tanstack/react-table'; -import DebouncedInput from './DebouncedInput'; - -import { getColumnsByCollection } from './columns'; -import { useEffect, useMemo, useState } from 'react'; -import type { CollectionMessageTypes, TableConfiguration } from '@types'; -import { isSameVersion } from '@utils/collections/util'; - -declare module '@tanstack/react-table' { - // @ts-ignore - interface ColumnMeta { - filterVariant?: 'collection' | 'name' | 'badges' | 'text'; - collectionFilterKey?: - | 'producers' - | 'consumers' - | 'sends' - | 'receives' - | 'services' - | 'ownedCommands' - | 'ownedQueries' - | 'ownedEvents' - | 'ownedServices' - | 'associatedTeams' - | 'servicesThatWriteToContainer' - | 'servicesThatReadFromContainer'; - filteredItemHasVersion?: boolean; - showFilter?: boolean; - className?: string; - } -} - -export type TCollectionTypes = 'domains' | 'services' | CollectionMessageTypes | 'flows' | 'users' | 'teams' | 'containers'; - -export type TData = { - collection: T; - data: { - id: string; - name: string; - summary: string; - version: string; - latestVersion?: string; // Defined on getter collection utility - draft?: boolean | { title?: string; message: string }; // Draft property from base schema - badges?: Array<{ - id: string; // Where is it defined? - content: string; - backgroundColor: string; - textColor: string; - icon: any; // Where is it defined? - }>; - // --------------------------------------------------------------------------- - // Domains - services?: Array<{ - collection: string; // Be more specific; - data: { - id: string; - name: string; - version: string; - }; - }>; - // --------------------------------------------------------------------------- - // Services - receives?: Array<{ - collection: string; // Be more specific; - data: { - id: string; - name: string; - version: string; - }; - }>; - sends?: Array<{ - collection: string; // Be more specific; - data: { - id: string; - name: string; - version: string; - }; - }>; - // --------------------------------------------------------------------------- - // Messages - producers?: Array<{ - collection: string; // Specify only 'services'? - data: { - id: string; - name: string; - version: string; - }; - }>; - // Only for messages - consumers?: Array<{ - collection: string; // Specify only 'services'? - data: { - id: string; - name: string; - version: string; - }; - }>; - // Only for containers - servicesThatWriteToContainer?: Array<{ - collection: string; // Specify only 'services'? - data: { - id: string; - name: string; - version: string; - }; - }>; - // Only for containers - servicesThatReadFromContainer?: Array<{ - collection: string; // Specify only 'services'? - data: { - id: string; - name: string; - version: string; - }; - }>; - // --------------------------------------------------------------------------- - // Users - avatarUrl?: string; - email?: string; - slackDirectMessageUrl?: string; - msTeamsDirectMessageUrl?: string; - role?: string; - ownedCommands: any; - ownedEvents: any; - ownedServices: any; - associatedTeams: any; - ownedQueries: any; - // Teams - members: any; - }; -}; - -export const Table = ({ - data: initialData, - collection, - mode = 'simple', - checkboxLatestId, - checkboxDraftsId, - tableConfiguration, -}: { - data: TData[]; - collection: T; - checkboxLatestId: string; - checkboxDraftsId: string; - mode?: 'simple' | 'full'; - tableConfiguration?: TableConfiguration; -}) => { - const [columnFilters, setColumnFilters] = useState([]); - - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const id = urlParams.get('id'); - if (id) { - setColumnFilters([{ id: 'name', value: id }]); - } - }, []); - - const [showOnlyLatest, setShowOnlyLatest] = useState(true); - const [onlyShowDrafts, setOnlyShowDrafts] = useState(false); - - useEffect(() => { - const checkbox = document.getElementById(checkboxLatestId); - function handleChange(evt: Event) { - const checked = (evt.target as HTMLInputElement).checked; - setShowOnlyLatest(checked); - } - - checkbox?.addEventListener('change', handleChange); - - return () => checkbox?.removeEventListener('change', handleChange); - }, [checkboxLatestId]); - - useEffect(() => { - const checkbox = document.getElementById(checkboxDraftsId); - function handleChange(evt: Event) { - const checked = (evt.target as HTMLInputElement).checked; - setOnlyShowDrafts(checked); - } - - checkbox?.addEventListener('change', handleChange); - - return () => checkbox?.removeEventListener('change', handleChange); - }, [checkboxDraftsId]); - - // Filter data based on checkbox states - const filteredData = useMemo(() => { - return initialData.filter((row) => { - // Check if item is a draft - const isDraft = row.data.draft === true || (typeof row.data.draft === 'object' && row.data.draft !== null); - - // If "Only show drafts" is enabled, show only drafts - if (onlyShowDrafts && !isDraft) { - return false; - } - - // If "Only show drafts" is enabled, don't apply other filters - if (onlyShowDrafts) { - return true; - } - - // Check latest version filter (only when not showing only drafts) - if (showOnlyLatest) { - return isSameVersion(row.data.version, row.data.latestVersion); - } - - return true; - }); - }, [initialData, showOnlyLatest, onlyShowDrafts]); - - const columns = useMemo( - () => getColumnsByCollection(collection, tableConfiguration ?? ({ columns: {} } as TableConfiguration)), - [collection, tableConfiguration] - ); - - const table = useReactTable({ - data: filteredData, - columns, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getFacetedRowModel: getFacetedRowModel(), // client-side faceting - getFacetedUniqueValues: getFacetedUniqueValues(), // generate unique values for select filter/autocomplete - getFacetedMinMaxValues: getFacetedMinMaxValues(), // generate min/max values for range filter - getPaginationRowModel: getPaginationRowModel(), - state: { - columnFilters, - columnVisibility: Object.fromEntries( - Object.entries(tableConfiguration?.columns ?? {}).map(([key, value]) => [key, value.visible ?? true]) - ), - }, - }); - - return ( -
- {/*
{table.getPrePaginationRowModel().rows.length} results
*/} -
- - - {table.getHeaderGroups().map((headerGroup, index) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - - {table.getRowModel().rows.map((row, index) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} - -
-
-
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} -
-
- {header.column.columnDef.meta?.showFilter !== false && } - {header.column.columnDef.meta?.showFilter == false &&
} -
-
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
-
-
-
- - - - - -
Page
- - {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} - -
- - | Go to page: - { - const page = e.target.value ? Number(e.target.value) - 1 : 0; - table.setPageIndex(page); - }} - className="border border-gray-300 p-1 rounded w-16" - /> - - -
-
-
- ); -}; - -function Filter({ column }: { column: Column, unknown> }) { - const { filterVariant, collectionFilterKey, filteredItemHasVersion = true } = column.columnDef.meta ?? {}; - - const columnFilterValue = column.getFilterValue(); - - const sortedUniqueValues = useMemo(() => { - if (filterVariant === 'collection' && collectionFilterKey) { - const rows = column.getFacetedRowModel().rows; - const data = rows.map((row) => row.original.data?.[collectionFilterKey] ?? []).flat(); - - const allItems = data.map((item) => - filteredItemHasVersion ? `${item?.data.name} (v${item?.data.version})` : `${item?.data.name}` - ); - const uniqueItemsInList = Array.from(new Set(allItems)); - - return uniqueItemsInList.sort().slice(0, 2000); - } - if (filterVariant === 'name') { - const rows = column.getFacetedRowModel().rows; - const data = rows - .map((row) => { - const data = row.original; - return data; - }) - .flat(); - - const allItems = data.map((item) => - filteredItemHasVersion ? `${item.data.name} (v${item.data.version})` : `${item.data.name}` - ); - const uniqueItemsInList = Array.from(new Set(allItems)); - - return uniqueItemsInList.sort().slice(0, 2000); - } - if (filterVariant === 'badges') { - const allBadges = column.getFacetedUniqueValues().keys(); - // join all badges into a single array - const allBadgesArray = Array.from(allBadges) - .flat() - .filter((b) => !!b); - const allBadgesString = allBadgesArray.map((badge) => badge.content); - const uniqueBadges = Array.from(new Set(allBadgesString)); - return uniqueBadges.sort().slice(0, 2000); - } - return Array.from(column.getFacetedUniqueValues().keys()).sort().slice(0, 2000); - }, [column.getFacetedUniqueValues(), filterVariant]); - - return ( -
- {/* Autocomplete suggestions from faceted values feature */} - - {sortedUniqueValues.map((value: any, index) => ( - - column.setFilterValue(value)} - placeholder={`Search... ${!column?.columnDef?.meta?.filterVariant ? `(${column.getFacetedUniqueValues().size})` : ''}`} - className="w-full p-2 border shadow rounded" - list={column.id + 'list'} - /> -
-
- ); -} diff --git a/eventcatalog/src/components/Tables/columns/ContainersTableColumns.tsx b/eventcatalog/src/components/Tables/columns/ContainersTableColumns.tsx deleted file mode 100644 index b3a810866..000000000 --- a/eventcatalog/src/components/Tables/columns/ContainersTableColumns.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { createColumnHelper } from '@tanstack/react-table'; -import { filterByName } from '../filters/custom-filters'; -import { buildUrl } from '@utils/url-builder'; -import { ServerIcon } from '@heroicons/react/24/solid'; -import { DatabaseIcon } from 'lucide-react'; -import { createBadgesColumn } from './SharedColumns'; -import type { TData } from '../Table'; -import { filterCollectionByName } from '../filters/custom-filters'; -import type { TableConfiguration } from '@types'; -const columnHelper = createColumnHelper>(); - -export const columns = (tableConfiguration: TableConfiguration) => [ - columnHelper.accessor('data.name', { - id: 'name', - header: () => {tableConfiguration.columns?.name?.label || 'Storage'}, - cell: (info) => { - const containerRaw = info.row.original; - const color = 'blue'; - return ( - - ); - }, - footer: (info) => info.column.id, - meta: { - filterVariant: 'name', - }, - filterFn: filterByName, - }), - columnHelper.accessor('data.summary', { - id: 'summary', - header: () => {tableConfiguration.columns?.summary?.label || 'Summary'}, - cell: (info) => {info.renderValue()}, - footer: (info) => info.column.id, - meta: { - showFilter: false, - className: 'max-w-md', - }, - }), - columnHelper.accessor('data.servicesThatWriteToContainer', { - id: 'writes', - header: () => {tableConfiguration.columns?.writes?.label || 'Writes'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'servicesThatWriteToContainer', - }, - cell: (info) => { - const services = info.getValue(); - if (services?.length === 0 || !services) - return
No services documented
; - return ( - - ); - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('servicesThatWriteToContainer'), - }), - columnHelper.accessor('data.servicesThatReadFromContainer', { - id: 'reads', - header: () => {tableConfiguration.columns?.reads?.label || 'Reads'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'servicesThatReadFromContainer', - }, - cell: (info) => { - const services = info.getValue(); - if (services?.length === 0 || !services) - return
No services documented
; - return ( - - ); - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('servicesThatReadFromContainer'), - }), - createBadgesColumn(columnHelper, tableConfiguration), - columnHelper.accessor('data.name', { - header: () => {tableConfiguration.columns?.actions?.label || 'Actions'}, - cell: (info) => { - const container = info.row.original; - return ( - - Visualiser → - - ); - }, - id: 'actions', - meta: { - showFilter: false, - }, - }), -]; diff --git a/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx b/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx deleted file mode 100644 index 790e7af1e..000000000 --- a/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { ServerIcon, RectangleGroupIcon } from '@heroicons/react/20/solid'; -import { createColumnHelper } from '@tanstack/react-table'; -import { filterByName, filterCollectionByName } from '../filters/custom-filters'; -import { buildUrl } from '@utils/url-builder'; -import { createBadgesColumn } from './SharedColumns'; -import type { TData } from '../Table'; -import type { TableConfiguration } from '@types'; - -const columnHelper = createColumnHelper>(); - -export const columns = (tableConfiguration: TableConfiguration) => [ - columnHelper.accessor('data.name', { - id: 'name', - header: () => {tableConfiguration.columns?.name?.label || 'Domain'}, - cell: (info) => { - const messageRaw = info.row.original; - const color = 'yellow'; - return ( - - ); - }, - footer: (info) => info.column.id, - meta: { - filterVariant: 'name', - }, - filterFn: filterByName, - }), - columnHelper.accessor('data.summary', { - id: 'summary', - header: () => {tableConfiguration.columns?.summary?.label || 'Summary'}, - cell: (info) => ( - - {info.renderValue()} {info.row.original.data.draft ? ' (Draft)' : ''} - - ), - footer: (info) => info.column.id, - meta: { - showFilter: false, - className: 'max-w-md', - }, - }), - columnHelper.accessor('data.services', { - id: 'services', - header: () => Services, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'services', - }, - cell: (info) => { - const services = info.getValue(); - if (services?.length === 0 || !services) - return
Domain has no services.
; - - return ( - - ); - }, - filterFn: filterCollectionByName('services'), - }), - createBadgesColumn(columnHelper, tableConfiguration), - columnHelper.accessor('data.name', { - id: 'actions', - header: () => {tableConfiguration.columns?.actions?.label || 'Actions'}, - cell: (info) => { - const domain = info.row.original; - return ( - - Visualiser → - - ); - }, - meta: { - showFilter: false, - }, - }), -]; diff --git a/eventcatalog/src/components/Tables/columns/FlowTableColumns.tsx b/eventcatalog/src/components/Tables/columns/FlowTableColumns.tsx deleted file mode 100644 index d46fab1a2..000000000 --- a/eventcatalog/src/components/Tables/columns/FlowTableColumns.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { createColumnHelper } from '@tanstack/react-table'; -import { filterByName } from '../filters/custom-filters'; -import { buildUrl } from '@utils/url-builder'; -import { QueueListIcon } from '@heroicons/react/24/solid'; -import { createBadgesColumn } from './SharedColumns'; -import type { TData } from '../Table'; -import type { TableConfiguration } from '@types'; - -const columnHelper = createColumnHelper>(); - -export const columns = (tableConfiguration: TableConfiguration) => [ - columnHelper.accessor('data.name', { - id: 'name', - header: () => {tableConfiguration.columns?.name?.label || 'Flow'}, - cell: (info) => { - const flowRaw = info.row.original; - const color = 'teal'; - return ( - - ); - }, - footer: (info) => info.column.id, - meta: { - filterVariant: 'name', - }, - filterFn: filterByName, - }), - columnHelper.accessor('data.version', { - id: 'version', - header: () => {tableConfiguration.columns?.version?.label || 'Version'}, - cell: (info) => { - const service = info.row.original; - return ( -
{`v${info.getValue()} ${service.data.latestVersion === service.data.version ? '(latest)' : ''}`}
- ); - }, - footer: (info) => info.column.id, - }), - columnHelper.accessor('data.summary', { - id: 'summary', - header: () => {tableConfiguration.columns?.summary?.label || 'Summary'}, - cell: (info) => {info.renderValue()}, - footer: (info) => info.column.id, - meta: { - showFilter: false, - className: 'max-w-md', - }, - }), - createBadgesColumn(columnHelper, tableConfiguration), - columnHelper.accessor('data.name', { - id: 'actions', - header: () => {tableConfiguration.columns?.actions?.label || 'Actions'}, - cell: (info) => { - const domain = info.row.original; - return ( - - Visualiser → - - ); - }, - meta: { - showFilter: false, - }, - }), -]; diff --git a/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx b/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx deleted file mode 100644 index ad8ed307b..000000000 --- a/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { ServerIcon, BoltIcon, ChatBubbleLeftIcon, MagnifyingGlassIcon } from '@heroicons/react/24/solid'; -import { createColumnHelper } from '@tanstack/react-table'; -import { useMemo } from 'react'; -import { filterByName, filterCollectionByName } from '../filters/custom-filters'; -import { buildUrl } from '@utils/url-builder'; -import { createBadgesColumn } from './SharedColumns'; -import type { TData } from '../Table'; -import type { CollectionMessageTypes, TableConfiguration } from '@types'; - -const columnHelper = createColumnHelper>(); - -export const getColorAndIconForMessageType = (type: string) => { - switch (type) { - case 'event': - return { color: 'orange', Icon: BoltIcon }; - case 'command': - return { color: 'blue', Icon: ChatBubbleLeftIcon }; - case 'querie': - case 'query': - return { color: 'green', Icon: MagnifyingGlassIcon }; - default: - return { color: 'gray', Icon: ChatBubbleLeftIcon }; - } -}; - -export const columns = (tableConfiguration: TableConfiguration) => [ - columnHelper.accessor('data.name', { - id: 'name', - header: () => {tableConfiguration.columns?.name?.label || 'Name'}, - cell: (info) => { - const messageRaw = info.row.original; - const type = useMemo(() => messageRaw.collection.slice(0, -1), [messageRaw.collection]); - const { color, Icon } = getColorAndIconForMessageType(type); - return ( - - ); - }, - meta: { - filterVariant: 'name', - }, - filterFn: filterByName, - }), - - columnHelper.accessor('data.summary', { - id: 'summary', - header: () => {tableConfiguration.columns?.summary?.label || 'Summary'}, - cell: (info) => ( - - {info.renderValue()} {info.row.original.data.draft ? ' (Draft)' : ''} - - ), - footer: (info) => info.column.id, - meta: { - showFilter: false, - className: 'max-w-[200px]', - }, - }), - - columnHelper.accessor('data.producers', { - id: 'producers', - header: () => {tableConfiguration.columns?.producers?.label || 'Producers'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'producers', - }, - cell: (info) => { - const producers = info.getValue(); - if (producers?.length === 0 || !producers) - return
No producers documented
; - return ( - - ); - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('producers'), - }), - columnHelper.accessor('data.consumers', { - id: 'consumers', - header: () => {tableConfiguration.columns?.consumers?.label || 'Consumers'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'consumers', - }, - cell: (info) => { - const consumers = info.getValue(); - if (consumers?.length === 0 || !consumers) - return
No consumers documented
; - - return ( - - ); - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('consumers'), - }), - createBadgesColumn(columnHelper, tableConfiguration), - columnHelper.accessor('data.name', { - id: 'actions', - header: () => {tableConfiguration.columns?.actions?.label || 'Actions'}, - cell: (info) => { - const domain = info.row.original; - return ( - - Visualiser → - - ); - }, - meta: { - showFilter: false, - }, - }), -]; diff --git a/eventcatalog/src/components/Tables/columns/ServiceTableColumns.tsx b/eventcatalog/src/components/Tables/columns/ServiceTableColumns.tsx deleted file mode 100644 index ffe2f32a6..000000000 --- a/eventcatalog/src/components/Tables/columns/ServiceTableColumns.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { ServerIcon } from '@heroicons/react/24/solid'; -import { createColumnHelper } from '@tanstack/react-table'; -import { useMemo, useState } from 'react'; -import { filterByName, filterCollectionByName } from '../filters/custom-filters'; -import { buildUrl } from '@utils/url-builder'; -import { getColorAndIconForCollection } from '@utils/collections/icons'; -import { createBadgesColumn } from './SharedColumns'; -import type { TData } from '../Table'; -import type { TableConfiguration } from '@types'; -const columnHelper = createColumnHelper>(); - -export const columns = (tableConfiguration: TableConfiguration) => [ - columnHelper.accessor('data.name', { - id: 'name', - header: () => {tableConfiguration.columns?.name?.label || 'Service'}, - cell: (info) => { - const messageRaw = info.row.original; - const color = 'pink'; - return ( - - ); - }, - meta: { - filterVariant: 'name', - }, - filterFn: filterByName, - }), - columnHelper.accessor('data.summary', { - id: 'summary', - header: () => {tableConfiguration.columns?.summary?.label || 'Summary'}, - cell: (info) => ( - - {info.renderValue()} {info.row.original.data.draft ? ' (Draft)' : ''} - - ), - footer: (info) => info.column.id, - meta: { - showFilter: false, - className: 'max-w-md', - }, - }), - columnHelper.accessor('data.receives', { - id: 'receives', - header: () => Receives, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'receives', - }, - cell: (info) => { - const receives = info.getValue() || []; - const isExpandable = receives?.length > 10; - const isOpen = isExpandable ? receives?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - const receiversWithIcons = useMemo( - () => - receives?.map((consumer) => { - return { - ...consumer, - ...getColorAndIconForCollection(consumer.collection), - }; - }) || [], - [receives] - ); - - if (receives?.length === 0 || !receives) - return
Service receives no messages.
; - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - }, - filterFn: filterCollectionByName('receives'), - }), - columnHelper.accessor('data.sends', { - id: 'sends', - header: () => Sends, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'sends', - }, - cell: (info) => { - const sends = info.getValue() || []; - const isExpandable = sends?.length > 10; - const isOpen = isExpandable ? sends?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - if (sends?.length === 0 || !sends) - return
Service sends no messages.
; - - const sendersWithIcons = useMemo( - () => - sends?.map((sender) => { - return { - ...sender, - ...getColorAndIconForCollection(sender.collection), - }; - }) || [], - [sends] - ); - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - }, - filterFn: filterCollectionByName('sends'), - }), - createBadgesColumn(columnHelper, tableConfiguration), - columnHelper.accessor('data.name', { - id: 'actions', - header: () => {tableConfiguration.columns?.actions?.label || 'Actions'}, - cell: (info) => { - const domain = info.row.original; - return ( - - Visualiser → - - ); - }, - meta: { - showFilter: false, - }, - }), -]; diff --git a/eventcatalog/src/components/Tables/columns/SharedColumns.tsx b/eventcatalog/src/components/Tables/columns/SharedColumns.tsx deleted file mode 100644 index deea4bcb2..000000000 --- a/eventcatalog/src/components/Tables/columns/SharedColumns.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { createColumnHelper } from '@tanstack/react-table'; -import { Tag } from 'lucide-react'; -import { filterByBadge } from '../filters/custom-filters'; -import type { TCollectionTypes, TData } from '../Table'; -import { getIcon } from '@utils/badges'; -import type { TableConfiguration } from '@types'; - -export const createBadgesColumn = ['data'], 'badges'> }, U extends TCollectionTypes>( - columnHelper: ReturnType>, - tableConfiguration: TableConfiguration -) => { - return columnHelper.accessor((row) => row.data.badges, { - id: 'badges', - header: () => {tableConfiguration.columns?.badges?.label || 'Badges'}, - cell: (info) => { - const item = info.row.original; - const badges = item.data.badges || []; - - if (badges?.length === 0 || !badges) - return
No badges documented
; - - return ( -
    - {badges.map((badge, index) => { - return ( -
  • -
    -
    - - - {(() => { - if (badge.icon) { - const IconComponent = getIcon(badge.icon); - return IconComponent ? ( - - ) : ( - - ); - } - return ; - })()} - - {badge.content} - -
    -
    -
  • - ); - })} -
- ); - }, - meta: { - filterVariant: 'badges', - }, - filterFn: filterByBadge, - }); -}; diff --git a/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx b/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx deleted file mode 100644 index d025046de..000000000 --- a/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import { MagnifyingGlassIcon } from '@heroicons/react/24/solid'; -import { createColumnHelper } from '@tanstack/react-table'; -import { useMemo, useState } from 'react'; -import { filterByName, filterCollectionByName } from '../filters/custom-filters'; -import { buildUrl } from '@utils/url-builder'; -import type { TData } from '../Table'; -import type { CollectionUserTypes } from '@types'; -import type { CollectionEntry } from 'astro:content'; -import { ServerIcon, BoltIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/solid'; -import type { TableConfiguration } from '@types'; -const columnHelper = createColumnHelper>(); - -export const columns = (tableConfiguration: TableConfiguration) => [ - columnHelper.accessor('data.name', { - id: 'name', - header: () => {tableConfiguration.columns?.name?.label || 'Name'}, - cell: (info) => { - const messageRaw = info.row.original; - const type = useMemo(() => messageRaw.collection.slice(0, -1), [messageRaw.collection]); - return ( - - ); - }, - meta: { - filterVariant: 'name', - filteredItemHasVersion: false, - }, - filterFn: filterByName, - }), - - columnHelper.accessor('data.ownedEvents', { - id: 'ownedEvents', - header: () => {tableConfiguration.columns?.ownedEvents?.label || 'Owned events'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'ownedEvents', - }, - cell: (info) => { - const events = info.getValue(); - if (events?.length === 0 || !events) - return
Team owns no events
; - - const isExpandable = events?.length > 10; - const isOpen = isExpandable ? events?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('ownedEvents'), - }), - - columnHelper.accessor('data.ownedCommands', { - id: 'ownedCommands', - header: () => {tableConfiguration.columns?.ownedCommands?.label || 'Owned commands'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'ownedCommands', - }, - cell: (info) => { - const commands = info.getValue(); - if (commands?.length === 0 || !commands) - return
Team owns no commands
; - - const isExpandable = commands?.length > 10; - const isOpen = isExpandable ? commands?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - - // return commands.length; - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('ownedCommands'), - }), - - columnHelper.accessor('data.ownedQueries', { - id: 'ownedQueries', - header: () => {tableConfiguration.columns?.ownedQueries?.label || 'Owned queries'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'ownedQueries', - }, - cell: (info) => { - const queries = info.getValue(); - if (queries?.length === 0 || !queries) - return
Team owns no queries
; - - const isExpandable = queries?.length > 10; - const isOpen = isExpandable ? queries?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - - // return commands.length; - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('ownedCommands'), - }), - - columnHelper.accessor('data.ownedServices', { - id: 'ownedServices', - header: () => {tableConfiguration.columns?.ownedServices?.label || 'Owned Services'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'ownedServices', - }, - cell: (info) => { - const services = info.getValue(); - if (services?.length === 0 || !services) - return
Team owns no services
; - - const isExpandable = services?.length > 10; - const isOpen = isExpandable ? services?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('ownedServices'), - }), - - columnHelper.accessor('data.name', { - header: () => {tableConfiguration.columns?.actions?.label || 'Actions'}, - cell: (info) => { - const domain = info.row.original; - return ( - - View → - - ); - }, - id: 'actions', - meta: { - showFilter: false, - }, - }), -]; diff --git a/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx b/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx deleted file mode 100644 index fa7243331..000000000 --- a/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import { createColumnHelper } from '@tanstack/react-table'; -import { useMemo, useState } from 'react'; -import { filterByName, filterCollectionByName } from '../filters/custom-filters'; -import { buildUrl } from '@utils/url-builder'; -import type { TData } from '../Table'; -import type { CollectionUserTypes } from '@types'; -import { User, Users } from 'lucide-react'; -import type { CollectionEntry } from 'astro:content'; -import { ServerIcon, BoltIcon, ChatBubbleLeftIcon, MagnifyingGlassIcon } from '@heroicons/react/24/solid'; -import type { TableConfiguration } from '@types'; -const columnHelper = createColumnHelper>(); - -export const columns = (tableConfiguration: TableConfiguration) => [ - columnHelper.accessor('data.name', { - id: 'name', - header: () => {tableConfiguration.columns?.name?.label || 'Name'}, - cell: (info) => { - const messageRaw = info.row.original; - const type = useMemo(() => messageRaw.collection.slice(0, -1), [messageRaw.collection]); - return ( - - ); - }, - meta: { - filterVariant: 'name', - filteredItemHasVersion: false, - }, - filterFn: filterByName, - }), - - columnHelper.accessor('data.ownedEvents', { - id: 'ownedEvents', - header: () => {tableConfiguration.columns?.ownedEvents?.label || 'Owned events'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'ownedEvents', - }, - cell: (info) => { - const events = info.getValue(); - if (events?.length === 0 || !events) - return
User owns no events
; - - const isExpandable = events?.length > 10; - const isOpen = isExpandable ? events?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('ownedEvents'), - }), - - columnHelper.accessor('data.ownedCommands', { - id: 'ownedCommands', - header: () => {tableConfiguration.columns?.ownedCommands?.label || 'Owned commands'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'ownedCommands', - }, - cell: (info) => { - const commands = info.getValue(); - if (commands?.length === 0 || !commands) - return
User owns no commands
; - - const isExpandable = commands?.length > 10; - const isOpen = isExpandable ? commands?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - - // return commands.length; - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('ownedCommands'), - }), - - columnHelper.accessor('data.ownedQueries', { - id: 'ownedQueries', - header: () => {tableConfiguration.columns?.ownedQueries?.label || 'Owned queries'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'ownedQueries', - }, - cell: (info) => { - const queries = info.getValue(); - if (queries?.length === 0 || !queries) - return
User owns no queries
; - - const isExpandable = queries?.length > 10; - const isOpen = isExpandable ? queries?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - - // return commands.length; - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('ownedCommands'), - }), - - columnHelper.accessor('data.ownedServices', { - id: 'ownedServices', - header: () => {tableConfiguration.columns?.ownedServices?.label || 'Owned Services'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'ownedServices', - }, - cell: (info) => { - const services = info.getValue(); - if (services?.length === 0 || !services) - return
User owns no services
; - - const isExpandable = services?.length > 10; - const isOpen = isExpandable ? services?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('ownedServices'), - }), - columnHelper.accessor('data.associatedTeams', { - id: 'associatedTeams', - header: () => {tableConfiguration.columns?.associatedTeams?.label || 'Teams'}, - meta: { - filterVariant: 'collection', - collectionFilterKey: 'associatedTeams', - filteredItemHasVersion: false, - }, - cell: (info) => { - const teams = info.getValue(); - - const isExpandable = teams?.length > 10; - const isOpen = isExpandable ? teams?.length < 10 : true; - const [isExpanded, setIsExpanded] = useState(isOpen); - - if (teams?.length === 0 || !teams) - return
User is not associated with any teams
; - - return ( -
- {isExpandable && ( - - )} - {isExpanded && ( - - )} -
- ); - }, - footer: (info) => info.column.id, - filterFn: filterCollectionByName('associatedTeams'), - }), - columnHelper.accessor('data.name', { - header: () => {tableConfiguration.columns?.actions?.label || 'Actions'}, - cell: (info) => { - const domain = info.row.original; - return ( - - View → - - ); - }, - id: 'actions', - meta: { - showFilter: false, - }, - }), -]; diff --git a/eventcatalog/src/components/TreeView/index.tsx b/eventcatalog/src/components/TreeView/index.tsx deleted file mode 100644 index 570b724b1..000000000 --- a/eventcatalog/src/components/TreeView/index.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import React, { useCallback, useEffect } from 'react'; -import classes from './styles.module.css'; -import { useSlots } from './useSlots'; -import { ChevronDownIcon, ChevronRightIcon } from 'lucide-react'; - -// ---------------------------------------------------------------------------- -// Context - -const RootContext = React.createContext<{ - // We cache the expanded state of tree items so we can preserve the state - // across remounts. This is necessary because we unmount tree items - // when their parent is collapsed. - expandedStateCache: React.RefObject | null>; -}>({ - expandedStateCache: { current: new Map() }, -}); - -const ItemContext = React.createContext<{ - level: number; - isExpanded: boolean; -}>({ - level: 1, - isExpanded: false, -}); - -// ---------------------------------------------------------------------------- -// TreeView - -export type TreeViewProps = { - 'aria-label'?: React.AriaAttributes['aria-label']; - 'aria-labelledby'?: React.AriaAttributes['aria-labelledby']; - children: React.ReactNode; - flat?: boolean; - truncate?: boolean; - style?: React.CSSProperties; -}; - -/* Size of toggle icon in pixels. */ -const TOGGLE_ICON_SIZE = 12; - -const Root: React.FC = ({ - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledby, - children, - flat, - truncate = true, - style, -}) => { - const containerRef = React.useRef(null); - const mouseDownRef = React.useRef(false); - - const onMouseDown = useCallback(() => { - mouseDownRef.current = true; - }, []); - - useEffect(() => { - function onMouseUp() { - mouseDownRef.current = false; - } - document.addEventListener('mouseup', onMouseUp); - return () => { - document.removeEventListener('mouseup', onMouseUp); - }; - }, []); - - const expandedStateCache = React.useRef | null>(null); - - if (expandedStateCache.current === null) { - expandedStateCache.current = new Map(); - } - - return ( - -
    - {children} -
-
- ); -}; - -Root.displayName = 'TreeView'; - -// ---------------------------------------------------------------------------- -// TreeView.Item - -export type TreeViewItemProps = { - id: string; - children: React.ReactNode; - current?: boolean; - defaultExpanded?: boolean; - onSelect?: (event: React.MouseEvent | React.KeyboardEvent) => void; -}; - -const Item = React.forwardRef( - ({ id: itemId, current: isCurrentItem = false, defaultExpanded, onSelect, children }, ref) => { - const [slots, rest] = useSlots(children, { - leadingVisual: LeadingVisual, - }); - const { expandedStateCache } = React.useContext(RootContext); - - const [isExpanded, setIsExpanded] = React.useState( - expandedStateCache.current?.get(itemId) ?? defaultExpanded ?? isCurrentItem - ); - const { level } = React.useContext(ItemContext); - const { hasSubTree, subTree, childrenWithoutSubTree } = useSubTree(rest); - const [isFocused, setIsFocused] = React.useState(false); - - // Set the expanded state and cache it - const setIsExpandedWithCache = React.useCallback( - (newIsExpanded: boolean) => { - setIsExpanded(newIsExpanded); - expandedStateCache.current?.set(itemId, newIsExpanded); - }, - [itemId, setIsExpanded, expandedStateCache] - ); - - // Expand or collapse the subtree - const toggle = React.useCallback( - (event?: React.MouseEvent | React.KeyboardEvent) => { - setIsExpandedWithCache(!isExpanded); - event?.stopPropagation(); - }, - [isExpanded, setIsExpandedWithCache] - ); - - const handleKeyDown = React.useCallback( - (event: React.KeyboardEvent) => { - switch (event.key) { - case 'Enter': - case ' ': - if (onSelect) { - onSelect(event); - } else { - toggle(event); - } - event.stopPropagation(); - break; - case 'ArrowRight': - // Ignore if modifier keys are pressed - if (event.altKey || event.metaKey) return; - event.preventDefault(); - event.stopPropagation(); - setIsExpandedWithCache(true); - break; - case 'ArrowLeft': - // Ignore if modifier keys are pressed - if (event.altKey || event.metaKey) return; - event.preventDefault(); - event.stopPropagation(); - setIsExpandedWithCache(false); - break; - } - }, - [onSelect, setIsExpandedWithCache, toggle] - ); - - return ( - -
  • } - tabIndex={0} - id={itemId} - role="treeitem" - aria-level={level} - aria-expanded={isExpanded} - aria-current={isCurrentItem ? 'true' : undefined} - aria-selected={isFocused ? 'true' : 'false'} - onKeyDown={handleKeyDown} - onFocus={(event) => { - // Scroll the first child into view when the item receives focus - event.currentTarget.firstElementChild?.scrollIntoView({ block: 'nearest', inline: 'nearest' }); - - // Set the focused state - setIsFocused(true); - - // Prevent focus event from bubbling up to parent items - event.stopPropagation(); - }} - onBlur={() => setIsFocused(false)} - onClick={(event) => { - if (onSelect) { - onSelect(event); - // if has children open them too - if (hasSubTree) { - toggle(event); - } - } else { - toggle(event); - } - event.stopPropagation(); - }} - onAuxClick={(event) => { - if (onSelect && event.button === 1) { - onSelect(event); - } - event.stopPropagation(); - }} - > -
    -
    {/* */}
    - -
    - {slots.leadingVisual} - {childrenWithoutSubTree} -
    - {hasSubTree ? ( -
    { - if (onSelect) { - toggle(event); - } - }} - > - {isExpanded ? : } -
    - ) : null} -
    - {subTree} -
  • -
    - ); - } -); - -Item.displayName = 'TreeView.Item'; - -// ---------------------------------------------------------------------------- -// TreeView.SubTree - -export type TreeViewSubTreeProps = { - children?: React.ReactNode; -}; - -const SubTree: React.FC = ({ children }) => { - const { isExpanded } = React.useContext(ItemContext); - const ref = React.useRef(null); - - if (!isExpanded) { - return null; - } - - return ( -
      - {children} -
    - ); -}; - -SubTree.displayName = 'TreeView.SubTree'; - -function useSubTree(children: React.ReactNode) { - return React.useMemo(() => { - const subTree = React.Children.toArray(children).find((child) => React.isValidElement(child) && child.type === SubTree); - - const childrenWithoutSubTree = React.Children.toArray(children).filter( - (child) => !(React.isValidElement(child) && child.type === SubTree) - ); - - return { - subTree, - childrenWithoutSubTree, - hasSubTree: Boolean(subTree), - }; - }, [children]); -} - -// ---------------------------------------------------------------------------- -// TreeView.LeadingVisual - -export type TreeViewLeadingVisualProps = { - children: React.ReactNode | ((props: { isExpanded: boolean }) => React.ReactNode); -}; - -const LeadingVisual: React.FC = (props) => { - const { isExpanded } = React.useContext(ItemContext); - const children = typeof props.children === 'function' ? props.children({ isExpanded }) : props.children; - return ( -
    - {children} -
    - ); -}; - -LeadingVisual.displayName = 'TreeView.LeadingVisual'; - -// ---------------------------------------------------------------------------- -// Export - -export const TreeView = Object.assign(Root, { - Item, - SubTree, - LeadingVisual, -}); diff --git a/eventcatalog/src/components/TreeView/styles.module.css b/eventcatalog/src/components/TreeView/styles.module.css deleted file mode 100644 index e4b19dd34..000000000 --- a/eventcatalog/src/components/TreeView/styles.module.css +++ /dev/null @@ -1,264 +0,0 @@ -.TreeViewRootUlStyles { - padding: 0; - margin: 0; - list-style: none; - - /* - * WARNING: This is a performance optimization. - * - * We define styles for the tree items at the root level of the tree - * to avoid recomputing the styles for each item when the tree updates. - * We're sacrificing maintainability for performance because TreeView - * needs to be performant enough to handle large trees (thousands of items). - * - * This is intended to be a temporary solution until we can improve the - * performance of our styling patterns. - * - * Do NOT copy this pattern without understanding the tradeoffs. - */ - .TreeViewItem { - outline: none; - - &:focus-visible > div, - &.focus-visible > div { - box-shadow: var(--boxShadow-thick) /* var(--fgColor-accent) */ slategray; - - @media (forced-colors: active) { - outline: 2px solid HighlightText; - outline-offset: -2; - } - } - - &[data-has-leading-action] { - --has-leading-action: 1; - } - } - - .TreeViewItemContainer { - --level: 1; - --toggle-width: 1rem; - --min-item-height: 2rem; - - position: relative; - display: grid; - width: 100%; - font-size: var(--text-body-size-medium); - color: var(--fgColor-default); - cursor: pointer; - border-radius: var(--borderRadius-medium); - grid-template-columns: var(--spacer-width) var(--leading-action-width) 1fr var(--toggle-width); - grid-template-areas: 'spacer leadingAction content toggle'; - - --leading-action-width: calc(var(--has-leading-action, 0) * 1.5rem); - --spacer-width: calc(calc(var(--level) - 1) * (var(--toggle-width) / 2)); - - &:hover { - background-color: var(--control-transparent-bgColor-hover); - - @media (forced-colors: active) { - outline: 2px solid transparent; - outline-offset: -2px; - } - } - - @media (pointer: coarse) { - --toggle-width: 1.5rem; - --min-item-height: 2.75rem; - } - - &:has(.TreeViewItemSkeleton):hover { - cursor: default; - background-color: transparent; - - @media (forced-colors: active) { - outline: none; - } - } - } - - &:where([data-omit-spacer='true']) .TreeViewItemContainer { - grid-template-columns: 0 0 1fr 0; - } - - .TreeViewItem[aria-current='true'] > .TreeViewItemContainer { - background-color: var(--control-transparent-bgColor-selected); - - /* Current item indicator */ - /* stylelint-disable-next-line selector-max-specificity */ - &::after { - position: absolute; - top: calc(50% - var(--base-size-12)); - left: calc(-1 * var(--base-size-8)); - width: 0.25rem; - height: 1.5rem; - content: ''; - - /* - * Use fgColor accent for consistency across all themes. Using the "correct" variable, - * --bgColor-accent-emphasis, causes vrt failures for dark high contrast mode - */ - /* stylelint-disable-next-line primer/colors */ - background-color: var(--fgColor-accent); - border-radius: var(--borderRadius-medium); - - @media (forced-colors: active) { - background-color: HighlightText; - } - } - } - - .TreeViewItemToggle { - display: flex; - height: 100%; - - /* The toggle should appear vertically centered for single-line items, but remain at the top for items that wrap - across more lines. */ - /* stylelint-disable-next-line primer/spacing */ - padding-top: calc(var(--min-item-height) / 2 - var(--base-size-12) / 2); - color: var(--fgColor-muted); - grid-area: toggle; - justify-content: center; - align-items: flex-start; - } - - .TreeViewItemToggleHover:hover { - background-color: var(--control-transparent-bgColor-hover); - } - - .TreeViewItemToggleEnd { - border-top-left-radius: var(--borderRadius-medium); - border-bottom-left-radius: var(--borderRadius-medium); - } - - .TreeViewItemContent { - display: flex; - height: 100%; - padding: 0 var(--base-size-8); - - /* The dynamic top and bottom padding to maintain the minimum item height for single line items */ - /* stylelint-disable-next-line primer/spacing */ - padding-top: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2); - /* stylelint-disable-next-line primer/spacing */ - padding-bottom: calc((var(--min-item-height) - var(--custom-line-height, 1.3rem)) / 2); - line-height: var(--custom-line-height, var(--text-body-lineHeight-medium, 1.4285)); - grid-area: content; - gap: var(--stack-gap-condensed); - } - - .TreeViewItemContentText { - flex: 1 1 auto; - width: 0; - } - - &:where([data-truncate-text='true']) .TreeViewItemContentText { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &:where([data-truncate-text='false']) .TreeViewItemContentText { - word-break: break-word; - } - - .TreeViewItemVisual { - display: flex; - - /* The visual icons should appear vertically centered for single-line items, but remain at the top for items that wrap - across more lines. */ - height: var(--custom-line-height, 1.3rem); - color: var(--fgColor-muted); - align-items: center; - } - - .TreeViewItemLeadingAction { - display: flex; - color: var(--fgColor-muted); - grid-area: leadingAction; - - & > button { - flex-shrink: 1; - } - } - - .TreeViewItemLevelLine { - width: 100%; - height: 100%; - - /* - * On devices without hover, the nesting indicator lines - * appear at all times. - */ - border-color: var(--borderColor-muted); - border-right: var(--borderWidth-thin) solid; - } - - /* - * On devices with :hover support, the nesting indicator lines - * fade in when the user mouses over the entire component, - * or when there's focus inside the component. This makes - * sure the component remains simple when not in use. - */ - @media (hover: hover) { - .TreeViewItemLevelLine { - border-color: transparent; - } - - &:hover .TreeViewItemLevelLine, - &:focus-within .TreeViewItemLevelLine { - border-color: var(--borderColor-muted); - } - } - - .TreeViewDirectoryIcon { - display: grid; - color: var(--treeViewItem-leadingVisual-iconColor-rest); - } - - .TreeViewVisuallyHidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - /* stylelint-disable-next-line primer/spacing */ - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; - } -} - -.TreeViewSkeletonItemContainerStyle { - display: flex; - align-items: center; - column-gap: 0.5rem; - height: 2rem; - - @media (pointer: coarse) { - height: 2.75rem; - } - - &:nth-of-type(5n + 1) { - --tree-item-loading-width: 67%; - } - - &:nth-of-type(5n + 2) { - --tree-item-loading-width: 47%; - } - - &:nth-of-type(5n + 3) { - --tree-item-loading-width: 73%; - } - - &:nth-of-type(5n + 4) { - --tree-item-loading-width: 64%; - } - - &:nth-of-type(5n + 5) { - --tree-item-loading-width: 50%; - } -} - -.TreeItemSkeletonTextStyles { - width: var(--tree-item-loading-width, 67%); -} diff --git a/eventcatalog/src/components/TreeView/useSlots.ts b/eventcatalog/src/components/TreeView/useSlots.ts deleted file mode 100644 index 7f00b0b94..000000000 --- a/eventcatalog/src/components/TreeView/useSlots.ts +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -// import {warning} from '../utils/warning' - -// slot config allows 2 options: -// 1. Component to match, example: { leadingVisual: LeadingVisual } -type ComponentMatcher = React.ElementType; -// 2. Component to match + a test function, example: { blockDescription: [Description, props => props.variant === 'block'] } -type ComponentAndPropsMatcher = [ComponentMatcher, (props: Props) => boolean]; - -export type SlotConfig = Record; - -// We don't know what the props are yet, we set them later based on slot config -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Props = any; - -type SlotElements = { - [Property in keyof Config]: SlotValue; -}; - -type SlotValue = Config[Property] extends React.ElementType // config option 1 - ? React.ReactElement, Config[Property]> - : Config[Property] extends readonly [ - infer ElementType extends React.ElementType, // config option 2, infer array[0] as component - // eslint-disable-next-line @typescript-eslint/no-unused-vars - infer _testFn, // even though we don't use testFn, we need to infer it to support types for slots.*.props - ] - ? React.ReactElement, ElementType> - : never; // useful for narrowing types, third option is not possible - -/** - * Extract components from `children` so we can render them in different places, - * allowing us to implement components with SSR-compatible slot APIs. - * Note: We can only extract direct children, not nested ones. - */ -export function useSlots( - children: React.ReactNode, - config: Config -): [Partial>, React.ReactNode[]] { - // Object mapping slot names to their elements - const slots: Partial> = mapValues(config, () => undefined); - - // Array of elements that are not slots - const rest: React.ReactNode[] = []; - - const keys = Object.keys(config) as Array; - const values = Object.values(config); - - // eslint-disable-next-line github/array-foreach - React.Children.forEach(children, (child) => { - if (!React.isValidElement(child)) { - rest.push(child); - return; - } - - const index = values.findIndex((value) => { - if (Array.isArray(value)) { - const [component, testFn] = value; - return child.type === component && testFn(child.props); - } else { - return child.type === value; - } - }); - - // If the child is not a slot, add it to the `rest` array - if (index === -1) { - rest.push(child); - return; - } - - const slotKey = keys[index]; - - // If slot is already filled, ignore duplicates - if (slots[slotKey]) { - // warning(true, `Found duplicate "${String(slotKey)}" slot. Only the first will be rendered.`) - return; - } - - // If the child is a slot, add it to the `slots` object - - slots[slotKey] = child as SlotValue; - }); - - return [slots, rest]; -} - -/** Map the values of an object */ -function mapValues, V>(obj: T, fn: (value: T[keyof T]) => V) { - return Object.keys(obj).reduce( - (result, key: keyof T) => { - result[key] = fn(obj[key]); - return result; - }, - {} as Record - ); -} diff --git a/eventcatalog/src/content.config.ts b/eventcatalog/src/content.config.ts deleted file mode 100644 index b7d6c55db..000000000 --- a/eventcatalog/src/content.config.ts +++ /dev/null @@ -1,727 +0,0 @@ -import { z, defineCollection, reference } from 'astro:content'; -import { glob } from 'astro/loaders'; -import { glob as globPackage } from 'glob'; -import { v4 as uuidv4 } from 'uuid'; -import { badge, ownerReference } from './content.config-shared-collections'; -import fs from 'fs'; -import path from 'path'; - -// Enterprise Collections -import { chatPromptsSchema, customPagesSchema } from './enterprise/collections'; - -export const projectDirBase = (() => { - if (process.platform === 'win32') { - const projectDirPath = process.env.PROJECT_DIR!.replace(/\\/g, '/'); - return projectDirPath.startsWith('/') ? projectDirPath : `/${projectDirPath}`; - } - return process.env.PROJECT_DIR; -})(); - -const pages = defineCollection({ - loader: glob({ - pattern: ['**/pages/*.(md|mdx)'], - base: projectDirBase, - }), - schema: z - .object({ - id: z.string(), - }) - .optional(), -}); - -const pointer = z.object({ - id: z.string(), - version: z.string().optional().default('latest'), -}); - -const detailPanelPropertySchema = z.object({ - visible: z.boolean().optional(), -}); - -const channelPointer = z - .object({ - parameters: z.record(z.string()).optional(), - }) - .merge(pointer); - -const sendsPointer = z.object({ - id: z.string(), - version: z.string().optional().default('latest'), - to: z - .array( - z.object({ - ...channelPointer.shape, - delivery_mode: z.enum(['push', 'pull', 'push-pull']).optional().default('push'), - }) - ) - .optional(), -}); - -const receivesPointer = z.object({ - id: z.string(), - version: z.string().optional().default('latest'), - from: z - .array( - z.object({ - ...channelPointer.shape, - delivery_mode: z.enum(['push', 'pull', 'push-pull']).optional().default('push'), - }) - ) - .optional(), -}); - -const resourcePointer = z.object({ - id: z.string(), - version: z.string().optional().default('latest'), - type: z.enum(['service', 'event', 'command', 'query', 'flow', 'channel', 'domain', 'user', 'team', 'container']), -}); - -const changelogs = defineCollection({ - loader: glob({ - pattern: ['**/changelog.(md|mdx)'], - base: projectDirBase, - }), - schema: z.object({ - createdAt: z.date().optional(), - badges: z.array(badge).optional(), - // Used by eventcatalog - version: z.string().optional(), - versions: z.array(z.string()).optional(), - latestVersion: z.string().optional(), - catalog: z - .object({ - path: z.string(), - absoluteFilePath: z.string(), - astroContentFilePath: z.string(), - filePath: z.string(), - publicPath: z.string(), - type: z.string(), - }) - .optional(), - }), -}); - -const baseSchema = z.object({ - id: z.string(), - name: z.string(), - summary: z.string().optional(), - // version: z.union([z.string(), z.number()]), - version: z.string(), - draft: z.union([z.boolean(), z.object({ title: z.string().optional(), message: z.string() })]).optional(), - badges: z.array(badge).optional(), - owners: z.array(ownerReference).optional(), - schemaPath: z.string().optional(), - sidebar: z - .object({ - label: z.string().optional(), - badge: z.string().optional(), - color: z.string().optional(), - backgroundColor: z.string().optional(), - }) - .optional(), - repository: z - .object({ - language: z.string().optional(), - url: z.string().optional(), - }) - .optional(), - specifications: z - .union([ - z.object({ - openapiPath: z.string().optional(), - asyncapiPath: z.string().optional(), - graphqlPath: z.string().optional(), - }), - z.array( - z.object({ - type: z.enum(['openapi', 'asyncapi', 'graphql']), - path: z.string(), - name: z.string().optional(), - }) - ), - ]) - .optional(), - hidden: z.boolean().optional(), - editUrl: z.string().optional(), - resourceGroups: z - .array( - z.object({ - id: z.string().optional(), - title: z.string().optional(), - items: z.array(resourcePointer), - limit: z.number().optional().default(10), - sidebar: z.boolean().optional().default(true), - }) - ) - .optional(), - styles: z - .object({ - icon: z.string().optional(), - node: z - .object({ - color: z.string().optional(), - label: z.string().optional(), - }) - .optional(), - }) - .optional(), - deprecated: z - .union([ - z.object({ - message: z.string().optional(), - date: z.union([z.string(), z.date()]).optional(), - }), - z.boolean().optional(), - ]) - .optional(), - visualiser: z.boolean().optional(), - attachments: z - .array( - z.union([ - z.string().url(), // simple case - z.object({ - url: z.string().url(), - title: z.string().optional(), - type: z.string().optional(), // e.g. "architecture-record", "diagram" - description: z.string().optional(), - icon: z.string().optional(), - }), - ]) - ) - .optional(), - // Used by eventcatalog - versions: z.array(z.string()).optional(), - latestVersion: z.string().optional(), - catalog: z - .object({ - path: z.string(), - filePath: z.string(), - astroContentFilePath: z.string(), - publicPath: z.string(), - type: z.string(), - }) - .optional(), -}); - -const flowStep = z - .union([ - // Can be a string or a number just to reference a step - z.union([z.string(), z.number()]), - z - .object({ - id: z.union([z.string(), z.number()]), - label: z.string().optional(), - }) - .optional(), - ]) - .optional(); - -const flows = defineCollection({ - loader: glob({ - pattern: ['**/flows/*/index.(md|mdx)', '**/flows/*/versioned/*/index.(md|mdx)'], - base: projectDirBase, - generateId: ({ data }) => { - return `${data.id}-${data.version}`; - }, - }), - schema: z - .object({ - detailsPanel: z - .object({ - owners: detailPanelPropertySchema.optional(), - versions: detailPanelPropertySchema.optional(), - changelog: detailPanelPropertySchema.optional(), - }) - .optional(), - steps: z.array( - z - .object({ - id: z.union([z.string(), z.number()]), - type: z.enum(['node', 'message', 'user', 'actor']).optional(), - title: z.string(), - summary: z.string().optional(), - message: pointer.optional(), - service: pointer.optional(), - flow: pointer.optional(), - - actor: z - .object({ - name: z.string(), - summary: z.string().optional(), - }) - .optional(), - custom: z - .object({ - title: z.string(), - icon: z.string().optional(), - type: z.string().optional(), - summary: z.string().optional(), - url: z.string().url().optional(), - color: z.string().optional(), - properties: z.record(z.union([z.string(), z.number()])).optional(), - height: z.number().optional(), - menu: z - .array( - z.object({ - label: z.string(), - url: z.string().url().optional(), - }) - ) - .optional(), - }) - .optional(), - externalSystem: z - .object({ - name: z.string(), - summary: z.string().optional(), - url: z.string().url().optional(), - }) - .optional(), - next_step: flowStep, - next_steps: z.array(flowStep).optional(), - }) - .refine((data) => { - // Cant have both next_steps and next_steps - if (data.next_step && data.next_steps) return false; - - // Either one or non types can be present - const typesUsed = [data.message, data.service, data.flow, data.actor, data.custom].filter((v) => v).length; - return typesUsed === 0 || typesUsed === 1; - }) - ), - }) - .merge(baseSchema), -}); - -const messageDetailsPanelPropertySchema = z.object({ - producers: detailPanelPropertySchema.optional(), - consumers: detailPanelPropertySchema.optional(), - channels: detailPanelPropertySchema.optional(), - versions: detailPanelPropertySchema.optional(), - repository: detailPanelPropertySchema.optional(), - owners: detailPanelPropertySchema.optional(), - changelog: detailPanelPropertySchema.optional(), - attachments: detailPanelPropertySchema.optional(), -}); - -const events = defineCollection({ - loader: glob({ - pattern: ['**/events/*/index.(md|mdx)', '**/events/*/versioned/*/index.(md|mdx)'], - base: projectDirBase, - generateId: ({ data, ...rest }) => { - return `${data.id}-${data.version}`; - }, - }), - schema: z - .object({ - producers: z.array(reference('services')).optional(), - consumers: z.array(reference('services')).optional(), - channels: z.array(channelPointer).optional(), - // Used by eventcatalog - messageChannels: z.array(reference('channels')).optional(), - detailsPanel: messageDetailsPanelPropertySchema.optional(), - }) - .merge(baseSchema), -}); - -const commands = defineCollection({ - loader: glob({ - pattern: ['**/commands/*/index.(md|mdx)', '**/commands/*/versioned/*/index.(md|mdx)'], - base: projectDirBase, - generateId: ({ data }) => { - return `${data.id}-${data.version}`; - }, - }), - schema: z - .object({ - producers: z.array(reference('services')).optional(), - consumers: z.array(reference('services')).optional(), - channels: z.array(channelPointer).optional(), - detailsPanel: messageDetailsPanelPropertySchema.optional(), - // Used by eventcatalog - messageChannels: z.array(reference('channels')).optional(), - }) - .merge(baseSchema), -}); - -const queries = defineCollection({ - loader: glob({ - pattern: ['**/queries/*/index.(md|mdx)', '**/queries/*/versioned/*/index.(md|mdx)'], - base: projectDirBase, - generateId: ({ data }) => { - return `${data.id}-${data.version}`; - }, - }), - schema: z - .object({ - producers: z.array(reference('services')).optional(), - consumers: z.array(reference('services')).optional(), - channels: z.array(channelPointer).optional(), - detailsPanel: messageDetailsPanelPropertySchema.optional(), - // Used by eventcatalog - messageChannels: z.array(reference('channels')).optional(), - }) - .merge(baseSchema), -}); - -const services = defineCollection({ - loader: glob({ - pattern: [ - 'domains/*/services/*/index.(md|mdx)', - 'domains/*/services/*/versioned/*/index.(md|mdx)', - - // Capture subdomain folders - 'domains/*/subdomains/*/services/*/index.(md|mdx)', - 'domains/*/subdomains/*/services/*/versioned/*/index.(md|mdx)', - - // Capture services in the root - 'services/*/index.(md|mdx)', // ✅ Capture only services markdown files - 'services/*/versioned/*/index.(md|mdx)', // ✅ Capture versioned files inside services - ], - base: projectDirBase, - generateId: ({ data, ...rest }) => { - return `${data.id}-${data.version}`; - }, - }), - schema: z - .object({ - sends: z.array(sendsPointer).optional(), - receives: z.array(receivesPointer).optional(), - entities: z.array(pointer).optional(), - writesTo: z.array(pointer).optional(), - readsFrom: z.array(pointer).optional(), - detailsPanel: z - .object({ - domains: detailPanelPropertySchema.optional(), - messages: detailPanelPropertySchema.optional(), - versions: detailPanelPropertySchema.optional(), - specifications: detailPanelPropertySchema.optional(), - entities: detailPanelPropertySchema.optional(), - repository: detailPanelPropertySchema.optional(), - owners: detailPanelPropertySchema.optional(), - changelog: detailPanelPropertySchema.optional(), - containers: detailPanelPropertySchema.optional(), - }) - .optional(), - }) - .merge(baseSchema), -}); - -// 1) Put this near your other enums/utilities -const containerTypeEnum = z.enum([ - // Core - 'database', - 'cache', - 'objectStore', - 'searchIndex', - 'dataWarehouse', - 'dataLake', - 'externalSaaS', - // Fallback - 'other', -]); - -const accessModeEnum = z.enum(['read', 'write', 'readWrite', 'appendOnly']); -const dataClassificationEnum = z.enum(['public', 'internal', 'confidential', 'regulated']); - -const containers = defineCollection({ - loader: glob({ - pattern: ['**/containers/*/index.(md|mdx)', '**/containers/*/versioned/*/index.(md|mdx)'], - base: projectDirBase, - generateId: ({ data }) => { - return `${data.id}-${data.version}`; - }, - }), - schema: z - .object({ - container_type: containerTypeEnum, // <— the important discriminator inside DataContainer - technology: z.string().optional(), // e.g. "postgres@14", "kafka", "s3" - authoritative: z.boolean().optional().default(false), - access_mode: accessModeEnum.optional(), // read/write/readWrite/appendOnly - classification: dataClassificationEnum.optional(), - residency: z.string().optional(), - retention: z.string().optional(), - // details panel toggles (aligns with your pattern) - detailsPanel: z - .object({ - versions: detailPanelPropertySchema.optional(), - repository: detailPanelPropertySchema.optional(), - owners: detailPanelPropertySchema.optional(), - changelog: detailPanelPropertySchema.optional(), - attachments: detailPanelPropertySchema.optional(), - }) - .optional(), - services: z.array(reference('services')).optional(), - - servicesThatWriteToContainer: z.array(reference('services')).optional(), - servicesThatReadFromContainer: z.array(reference('services')).optional(), - }) - .merge(baseSchema), -}); - -const customPages = defineCollection({ - loader: glob({ - // any number of child folders - pattern: ['docs/*.(md|mdx)', 'docs/**/*.@(md|mdx)'], - base: projectDirBase, - }), - schema: customPagesSchema, -}); - -const chatPrompts = defineCollection({ - loader: glob({ - pattern: ['chat-prompts/*.(md|mdx)', 'chat-prompts/**/*.@(md|mdx)'], - base: projectDirBase, - }), - schema: chatPromptsSchema, -}); - -const domains = defineCollection({ - loader: glob({ - pattern: [ - // ✅ Strictly include only index.md at the expected levels - 'domains/*/index.(md|mdx)', - 'domains/*/versioned/*/index.(md|mdx)', - - // Capture subdomain folders - 'domains/*/subdomains/*/index.(md|mdx)', - 'domains/*/subdomains/*/versioned/*/index.(md|mdx)', - ], - base: projectDirBase, - generateId: ({ data, ...rest }) => { - return `${data.id}-${data.version}`; - }, - }), - schema: z - .object({ - services: z.array(pointer).optional(), - domains: z.array(pointer).optional(), - entities: z.array(pointer).optional(), - detailsPanel: z - .object({ - parentDomains: detailPanelPropertySchema.optional(), - subdomains: detailPanelPropertySchema.optional(), - services: detailPanelPropertySchema.optional(), - entities: detailPanelPropertySchema.optional(), - messages: detailPanelPropertySchema.optional(), - ubiquitousLanguage: detailPanelPropertySchema.optional(), - repository: detailPanelPropertySchema.optional(), - versions: detailPanelPropertySchema.optional(), - owners: detailPanelPropertySchema.optional(), - changelog: detailPanelPropertySchema.optional(), - attachments: detailPanelPropertySchema.optional(), - }) - .optional(), - }) - .merge(baseSchema), -}); - -const channels = defineCollection({ - loader: glob({ - pattern: ['**/channels/**/index.(md|mdx)', '**/channels/**/versioned/*/index.(md|mdx)'], - base: projectDirBase, - generateId: ({ data }) => { - return `${data.id}-${data.version}`; - }, - }), - schema: z - .object({ - channels: z.array(channelPointer).optional(), - address: z.string().optional(), - protocols: z.array(z.string()).optional(), - routes: z.array(channelPointer).optional(), - parameters: z - .record( - z.object({ - enum: z.array(z.string()).optional(), - default: z.string().optional(), - examples: z.array(z.string()).optional(), - description: z.string().optional(), - }) - ) - .optional(), - messages: z.array(z.object({ collection: z.string(), name: z.string(), ...pointer.shape })).optional(), - detailsPanel: z - .object({ - producers: detailPanelPropertySchema.optional(), - consumers: detailPanelPropertySchema.optional(), - messages: detailPanelPropertySchema.optional(), - protocols: detailPanelPropertySchema.optional(), - parameters: detailPanelPropertySchema.optional(), - versions: detailPanelPropertySchema.optional(), - repository: detailPanelPropertySchema.optional(), - owners: detailPanelPropertySchema.optional(), - changelog: detailPanelPropertySchema.optional(), - attachments: detailPanelPropertySchema.optional(), - }) - .optional(), - }) - .merge(baseSchema), -}); - -const ubiquitousLanguages = defineCollection({ - loader: glob({ - pattern: ['domains/*/ubiquitous-language.(md|mdx)', 'domains/*/subdomains/*/ubiquitous-language.(md|mdx)'], - base: projectDirBase, - generateId: ({ data }) => { - // File has no id, so we need to generate one - return uuidv4(); - }, - }), - schema: z.object({ - dictionary: z - .array( - z.object({ - id: z.string(), - name: z.string(), - summary: z.string().optional(), - description: z.string().optional(), - icon: z.string().optional(), - }) - ) - .optional(), - }), -}); - -const entities = defineCollection({ - loader: glob({ - pattern: ['**/entities/*/index.(md|mdx)', '**/entities/*/versioned/*/index.(md|mdx)'], - base: projectDirBase, - generateId: ({ data, ...rest }) => { - return `${data.id}-${data.version}`; - }, - }), - schema: z - .object({ - aggregateRoot: z.boolean().optional(), - identifier: z.string().optional(), - properties: z - .array( - z.object({ - name: z.string(), - type: z.string(), - required: z.boolean().optional(), - description: z.string().optional(), - references: z.string().optional(), - referencesIdentifier: z.string().optional(), - relationType: z.string().optional(), - enum: z.array(z.string()).optional(), - items: z - .object({ - type: z.string(), - }) - .optional(), - }) - ) - .optional(), - services: z.array(reference('services')).optional(), - domains: z.array(reference('domains')).optional(), - detailsPanel: z - .object({ - domains: detailPanelPropertySchema.optional(), - services: detailPanelPropertySchema.optional(), - messages: detailPanelPropertySchema.optional(), - versions: detailPanelPropertySchema.optional(), - owners: detailPanelPropertySchema.optional(), - changelog: detailPanelPropertySchema.optional(), - attachments: detailPanelPropertySchema.optional(), - }) - .optional(), - }) - - .merge(baseSchema), -}); - -const users = defineCollection({ - loader: glob({ pattern: 'users/*.(md|mdx)', base: projectDirBase, generateId: ({ data }) => data.id as string }), - schema: z.object({ - id: z.string(), - name: z.string(), - avatarUrl: z.string(), - role: z.string().optional(), - hidden: z.boolean().optional(), - email: z.string().optional(), - slackDirectMessageUrl: z.string().optional(), - msTeamsDirectMessageUrl: z.string().optional(), - ownedDomains: z.array(reference('domains')).optional(), - ownedServices: z.array(reference('services')).optional(), - ownedEvents: z.array(reference('events')).optional(), - ownedCommands: z.array(reference('commands')).optional(), - ownedQueries: z.array(reference('queries')).optional(), - associatedTeams: z.array(reference('teams')).optional(), - }), -}); - -const teams = defineCollection({ - loader: glob({ pattern: 'teams/*.(md|mdx)', base: projectDirBase, generateId: ({ data }) => data.id as string }), - schema: z.object({ - id: z.string(), - name: z.string(), - summary: z.string().optional(), - email: z.string().optional(), - hidden: z.boolean().optional(), - slackDirectMessageUrl: z.string().optional(), - msTeamsDirectMessageUrl: z.string().optional(), - members: z.array(reference('users')).optional(), - ownedCommands: z.array(reference('commands')).optional(), - ownedQueries: z.array(reference('queries')).optional(), - ownedDomains: z.array(reference('domains')).optional(), - ownedServices: z.array(reference('services')).optional(), - ownedEvents: z.array(reference('events')).optional(), - }), -}); - -const designs = defineCollection({ - loader: async () => { - const data = await globPackage('**/**/*.ecstudio', { cwd: projectDirBase }); - // File all the files in the designs folder - // Limit 3 designs community edition? - const files = data.reduce<{ id: string; name: string }[]>((acc, filePath) => { - try { - const data = fs.readFileSync(path.join(projectDirBase!, filePath), 'utf-8'); - const json = JSON.parse(data); - return [...acc, { ...json }]; - } catch (error) { - console.error('Error loading design', error); - return acc; - } - }, []); - return files; - }, - schema: z.object({ - id: z.string(), - name: z.string(), - creationDate: z.string(), - source: z.string(), - nodes: z.any(), - edges: z.any(), - viewport: z.any(), - version: z.string(), - }), -}); - -export const collections = { - events, - commands, - queries, - services, - channels, - users, - teams, - domains, - flows, - pages, - changelogs, - containers, - - // DDD Collections - ubiquitousLanguages, - entities, - - // EventCatalog Pro Collections - customPages, - chatPrompts, - - // EventCatalog Studio Collections - designs, -}; diff --git a/eventcatalog/src/enterprise/LICENSE.txt b/eventcatalog/src/enterprise/LICENSE.txt deleted file mode 100644 index c869e966c..000000000 --- a/eventcatalog/src/enterprise/LICENSE.txt +++ /dev/null @@ -1,4 +0,0 @@ -Usage of files in this directory and its subdirectories, and of EventCatalog Plugin / Enterprise features, is subject to -the EventCatalog Plugin Licenses (https://www.eventcatalog.dev), and conditional on having a -valid plugin license from EventCatalog. Access to files in this directory and its subdirectories does not constitute -permission to use this code or EventCatalog Plugin / Enterprise features. \ No newline at end of file diff --git a/eventcatalog/src/enterprise/collections/chat-prompts.ts b/eventcatalog/src/enterprise/collections/chat-prompts.ts deleted file mode 100644 index e170a62f4..000000000 --- a/eventcatalog/src/enterprise/collections/chat-prompts.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { z } from 'astro:content'; - -export const chatPromptsSchema = z.object({ - title: z.string(), - type: z.enum(['text', 'code']).default('text'), - inputs: z - .array( - z.object({ - id: z.string(), - label: z.string(), - type: z - .enum([ - 'text', - 'resource-list-events', - 'resource-list-services', - 'resource-list-commands', - 'resource-list-queries', - 'code', - 'text-area', - 'select', - ]) - .default('text'), - options: z.array(z.string()).optional(), - }) - ) - .optional(), - category: z.object({ - id: z.string(), - label: z.string(), - icon: z.string().optional(), - }), -}); diff --git a/eventcatalog/src/enterprise/collections/custom-pages.ts b/eventcatalog/src/enterprise/collections/custom-pages.ts deleted file mode 100644 index e51294d03..000000000 --- a/eventcatalog/src/enterprise/collections/custom-pages.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from 'astro:content'; -import { badge, ownerReference } from '../../content.config-shared-collections'; - -export const customPagesSchema = z.object({ - title: z.string(), - summary: z.string(), - slug: z.string().optional(), - sidebar: z - .object({ - label: z.string(), - order: z.number(), - }) - .optional(), - owners: z.array(ownerReference).optional(), - badges: z.array(badge).optional(), -}); diff --git a/eventcatalog/src/enterprise/collections/index.ts b/eventcatalog/src/enterprise/collections/index.ts deleted file mode 100644 index 076aabf01..000000000 --- a/eventcatalog/src/enterprise/collections/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { customPagesSchema } from './custom-pages'; -export { chatPromptsSchema } from './chat-prompts'; diff --git a/eventcatalog/src/enterprise/custom-documentation/__tests__/custom-docs/mocks.ts b/eventcatalog/src/enterprise/custom-documentation/__tests__/custom-docs/mocks.ts deleted file mode 100644 index 174c77ad8..000000000 --- a/eventcatalog/src/enterprise/custom-documentation/__tests__/custom-docs/mocks.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const mockDocs = [ - { - id: 'docs/auto-generated/01-introduction', - collection: 'docs', - data: { - title: 'Introduction', - description: 'Introduction example', - }, - }, - { - id: 'docs/auto-generated/02-hello-world', - collection: 'docs', - data: { - title: 'Hello World', - description: 'Hello World example', - }, - }, -]; diff --git a/eventcatalog/src/enterprise/custom-documentation/collection.ts b/eventcatalog/src/enterprise/custom-documentation/collection.ts deleted file mode 100644 index e51294d03..000000000 --- a/eventcatalog/src/enterprise/custom-documentation/collection.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from 'astro:content'; -import { badge, ownerReference } from '../../content.config-shared-collections'; - -export const customPagesSchema = z.object({ - title: z.string(), - summary: z.string(), - slug: z.string().optional(), - sidebar: z - .object({ - label: z.string(), - order: z.number(), - }) - .optional(), - owners: z.array(ownerReference).optional(), - badges: z.array(badge).optional(), -}); diff --git a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/CustomDocsNavWrapper.tsx b/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/CustomDocsNavWrapper.tsx deleted file mode 100644 index d8abb388d..000000000 --- a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/CustomDocsNavWrapper.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import CustomDocsNav from './'; -import type { SidebarSection } from './types'; - -interface CustomDocsNavWrapperProps { - sidebarItems: SidebarSection[]; - currentPath: string; -} - -export default function CustomDocsNavWrapper(props: CustomDocsNavWrapperProps) { - return ; -} diff --git a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NestedItem.tsx b/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NestedItem.tsx deleted file mode 100644 index 2d7f28e53..000000000 --- a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NestedItem.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React from 'react'; -import { buildUrl } from '@utils/url-builder'; -import type { SidebarItem } from '../types'; -import { ExternalLinkIcon } from 'lucide-react'; - -interface NestedItemProps { - item: SidebarItem; - currentPath: string; - parentId: string; - itemIndex: number; - collapsedGroups: { [key: string]: boolean }; - toggleGroupCollapse: (group: string) => void; -} - -const NestedItem: React.FC = ({ - item, - currentPath, - parentId, - itemIndex, - collapsedGroups, - toggleGroupCollapse, -}) => { - const hasNestedItems = item.items && item.items.length > 0; - const itemId = `${parentId}-${itemIndex}`; - - if (hasNestedItems && item.items) { - return ( -
    -
    - - -
    - -
    -
    - {item.items.map((nestedItem: SidebarItem, nestedIndex: number) => ( - - ))} -
    -
    -
    - ); - } - - let itemPath = item.slug ? buildUrl(`/docs/custom/${item.slug}`) : '#'; - const isActive = currentPath === itemPath || currentPath.endsWith(`/${item.slug}`); - - // Convert string style to React CSSProperties if needed - const attrs = item.attrs - ? { - ...item.attrs, - style: - typeof item.attrs.style === 'string' - ? item.attrs.style - .split(';') - .filter((style) => style.trim()) - .reduce((acc, style) => { - const [key, value] = style.split(':').map((s) => s.trim()); - const camelKey = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); - return { ...acc, [camelKey]: value }; - }, {}) - : item.attrs.style, - } - : null; - - const isExternalLink = item.slug?.startsWith('http'); - - if (isExternalLink && item.slug) { - itemPath = item.slug; - } - - return ( - - - {item.label} - {isExternalLink && } - - {item.badge && item?.badge?.text && ( - - {item.badge.text} - - )} - - ); -}; - -export default React.memo(NestedItem); diff --git a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NoResultsFound.tsx b/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NoResultsFound.tsx deleted file mode 100644 index 5e5104bc5..000000000 --- a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NoResultsFound.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -interface NoResultsFoundProps { - searchTerm: string; -} - -const NoResultsFound: React.FC = ({ searchTerm }) => ( -
    -
    No results found for "{searchTerm}"
    -
    - Try: -
      -
    • Checking for typos
    • -
    • Using fewer keywords
    • -
    • Using more general terms
    • -
    -
    -
    -); - -export default React.memo(NoResultsFound); diff --git a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx b/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx deleted file mode 100644 index 41da5407e..000000000 --- a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +++ /dev/null @@ -1,329 +0,0 @@ -import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; -import { ChevronDoubleDownIcon, ChevronDoubleUpIcon } from '@heroicons/react/24/outline'; -import { buildUrl } from '@utils/url-builder'; -import type { CustomDocsNavProps, SidebarSection, SidebarItem } from './types'; -import NestedItem from './components/NestedItem'; -import NoResultsFound from './components/NoResultsFound'; - -const STORAGE_KEY = 'EventCatalog:customDocsSidebarCollapsedGroups'; -const DEBOUNCE_DELAY = 300; // 300ms debounce delay - -const CustomDocsNav: React.FC = ({ sidebarItems }) => { - const navRef = useRef(null); - const [searchTerm, setSearchTerm] = useState(''); - const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); - const [isInitialized, setIsInitialized] = useState(false); - const [isExpanded, setIsExpanded] = useState(true); - const [collapsedGroups, setCollapsedGroups] = useState<{ [key: string]: boolean }>(() => { - if (typeof window !== 'undefined') { - const saved = window.localStorage.getItem(STORAGE_KEY); - setIsInitialized(true); - return saved ? JSON.parse(saved) : {}; - } - return {}; - }); - - const currentPath = window.location.pathname; - - // Set up debounced search - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearchTerm(searchTerm.toLowerCase()); - }, DEBOUNCE_DELAY); - - return () => clearTimeout(timer); - }, [searchTerm]); - - // Filter sidebar items based on search term - const filteredSidebarItems = useMemo(() => { - if (!debouncedSearchTerm) return sidebarItems; - - const matchesSearchTerm = (text: string) => text.toLowerCase().includes(debouncedSearchTerm); - - // Helper function to check if an item or any of its nested items match the search term - const itemContainsSearchTerm = (item: SidebarItem): boolean => { - if (matchesSearchTerm(item.label)) return true; - - if (item.items && item.items.length > 0) { - return item.items.some(itemContainsSearchTerm); - } - - return false; - }; - - return sidebarItems - .map((section) => { - if (!section.items) { - return matchesSearchTerm(section.label) ? section : null; - } - - const filteredItems = section.items.filter(itemContainsSearchTerm); - - if (filteredItems.length > 0 || matchesSearchTerm(section.label)) { - return { - ...section, - items: filteredItems, - }; - } - - return null; - }) - .filter(Boolean) as SidebarSection[]; - }, [sidebarItems, debouncedSearchTerm]); - - // Auto-expand groups when searching - useEffect(() => { - if (debouncedSearchTerm) { - // Expand all groups when searching - const newCollapsedState = { ...collapsedGroups }; - Object.keys(newCollapsedState).forEach((key) => { - newCollapsedState[key] = false; - }); - setCollapsedGroups(newCollapsedState); - } - }, [debouncedSearchTerm]); - - // Store collapsed groups in local storage - useEffect(() => { - if (typeof window !== 'undefined' && isInitialized) { - window.localStorage.setItem(STORAGE_KEY, JSON.stringify(collapsedGroups)); - } - }, [collapsedGroups, isInitialized]); - - // Initialize collapsed state from section config - useEffect(() => { - if (isInitialized && sidebarItems && sidebarItems.length > 0) { - const initialState = { ...collapsedGroups }; - - sidebarItems.forEach((section, index) => { - const sectionKey = `section-${index}`; - if (section.collapsed !== undefined && initialState[sectionKey] === undefined) { - initialState[sectionKey] = section.collapsed; - } - }); - - setCollapsedGroups(initialState); - } - }, [sidebarItems, isInitialized]); - - // If we find a data-active element, scroll to it on mount - useEffect(() => { - const activeElement = document.querySelector('[data-active="true"]'); - if (activeElement) { - // Add y offset to the scroll position - setTimeout(() => { - activeElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); - }, 300); - } - }, []); - - const toggleGroupCollapse = useCallback((group: string) => { - setCollapsedGroups((prev) => ({ - ...prev, - [group]: !prev[group], - })); - }, []); - - const handleSearchChange = useCallback((e: React.ChangeEvent) => { - setSearchTerm(e.target.value); - }, []); - - const collapseAll = useCallback(() => { - const newCollapsedState: { [key: string]: boolean } = {}; - - // Collapse all sections - filteredSidebarItems.forEach((section, index) => { - const sectionKey = `section-${index}`; - newCollapsedState[sectionKey] = true; - - // Collapse all nested items in the section - const collapseNestedItems = (items: SidebarItem[], parentId: string) => { - items.forEach((item, itemIndex) => { - if (item.items && item.items.length > 0) { - const itemKey = `${parentId}-${itemIndex}`; - newCollapsedState[itemKey] = true; - collapseNestedItems(item.items, itemKey); - } - }); - }; - - if (section.items) { - collapseNestedItems(section.items, sectionKey); - } - }); - - setCollapsedGroups(newCollapsedState); - setIsExpanded(false); - }, [filteredSidebarItems]); - - const expandAll = useCallback(() => { - const newCollapsedState: { [key: string]: boolean } = {}; - - // Expand all sections - filteredSidebarItems.forEach((section, index) => { - const sectionKey = `section-${index}`; - newCollapsedState[sectionKey] = false; - - // Expand all nested items in the section - const expandNestedItems = (items: SidebarItem[], parentId: string) => { - items.forEach((item, itemIndex) => { - if (item.items && item.items.length > 0) { - const itemKey = `${parentId}-${itemIndex}`; - newCollapsedState[itemKey] = false; - expandNestedItems(item.items, itemKey); - } - }); - }; - - if (section.items) { - expandNestedItems(section.items, sectionKey); - } - }); - - setCollapsedGroups(newCollapsedState); - setIsExpanded(true); - }, [filteredSidebarItems]); - - const toggleExpandCollapse = useCallback(() => { - if (isExpanded) { - collapseAll(); - } else { - expandAll(); - } - }, [isExpanded, collapseAll, expandAll]); - - if (!isInitialized) return null; - - const hasNoResults = debouncedSearchTerm && filteredSidebarItems.length === 0; - - return ( - - ); -}; - -export default React.memo(CustomDocsNav); diff --git a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/types.ts b/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/types.ts deleted file mode 100644 index f94c502d7..000000000 --- a/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface SidebarItem { - label: string; - slug?: string; - link?: string; - attrs?: Record; - items?: SidebarItem[]; - badge?: { - text: string; - color: string; - }; - collapsed?: boolean; -} - -export interface SidebarSection { - label: string; - items?: SidebarItem[]; - slug?: string; - autogenerated?: { - directory: string; - collapsed?: boolean; - }; - badge?: { - text: string; - color: string; - }; - collapsed?: boolean; -} - -export interface CustomDocsNavProps { - sidebarItems: SidebarSection[]; - currentPath: string; -} diff --git a/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro b/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro deleted file mode 100644 index 863d5c859..000000000 --- a/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +++ /dev/null @@ -1,490 +0,0 @@ ---- -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { render } from 'astro:content'; -import config from '@config'; -import { AlignLeftIcon } from 'lucide-react'; - -import mdxComponents from '@components/MDX/components'; -import OwnersList from '@components/Lists/OwnersList'; - -import { getOwner } from '@utils/collections/owners'; -import { buildUrl } from '@utils/url-builder'; -import { resourceToCollectionMap } from '@utils/collections/util'; -import { getMDXComponentsByName } from '@utils/markdown'; -import { getAdjacentPages } from '@enterprise/custom-documentation/utils/custom-docs'; - -import CustomDocsNav from '@enterprise/custom-documentation/components/CustomDocsNav/CustomDocsNav.astro'; -import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro'; - -import { isVisualiserEnabled } from '@utils/feature'; - -const props = Astro.props; -const doc = props.data; -const { Content, headings } = await render(props as any); -const currentSlug = props.id; - -const nodeGraphs = getMDXComponentsByName(props.body, 'NodeGraph') || []; -// Get sidebar data -const sidebar = config?.customDocs?.sidebar || []; - -// Flatten the sidebar to find previous and next pages -type FlatItem = { - label: string; - slug: string; -}; - -// Define sidebar section type -type SidebarSection = { - label: string; - slug?: string; - items?: Array<{ - label: string; - slug: string; - }>; -}; - -const flattenedItems: FlatItem[] = []; - -// Process all sidebar sections to create a flattened array -sidebar.forEach((section: SidebarSection) => { - if (section.slug && !section.items) { - flattenedItems.push({ - label: section.label, - slug: section.slug, - }); - } - if (section.items) { - section.items.forEach((item: { label: string; slug: string }) => { - flattenedItems.push({ - label: item.label, - slug: item.slug, - }); - }); - } -}); - -const { prev, next } = await getAdjacentPages(currentSlug.replace()); - -const ownersRaw = doc?.owners || []; -const owners = await Promise.all>(ownersRaw.map(getOwner)); -const filteredOwners = owners.filter((o) => o !== undefined); - -const ownersList = filteredOwners.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: o.collection === 'users' ? o.data.role : 'Team', - avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const badges = doc?.badges || []; ---- - - -
    - - - - -
    -
    -
    -
    -

    {doc.title}

    -

    {doc.summary}

    - { - badges && ( -
    - {badges.map((badge: any) => { - return ( - - - {badge.icon && } - {badge.iconURL && } - {badge.content} - - - ); - })} -
    - ) - } -
    -
    -
    - -
    - - { - nodeGraphs.length > 0 && - nodeGraphs.map((nodeGraph: any) => { - const collection = resourceToCollectionMap[nodeGraph.type as keyof typeof resourceToCollectionMap]; - return ( - - ); - }) - } - - -
    -
    - { - prev && ( - - Previous - {prev.label} - - ) - } - - {!prev &&
    } - - { - next && ( - - Next - {next.label} - - ) - } -
    -
    -
    -
    - - - -
    -
    - - - - - - - - diff --git a/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts b/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts deleted file mode 100644 index a2ef7ff56..000000000 --- a/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts +++ /dev/null @@ -1,215 +0,0 @@ -import config from '@config'; -import fs from 'node:fs'; -import path from 'node:path'; -import { getEntry } from 'astro:content'; -import matter from 'gray-matter'; - -type Badge = { - text: string; - color: string; -}; - -type SidebarItem = { - label: string; - slug?: string; - items?: SidebarItem[]; - badge?: Badge; - collapsed?: boolean; - link?: string; - attrs?: Record; -}; - -type SideBarConfigurationItem = { - label: string; - items?: SideBarConfigurationItem[]; - slug?: string; - autogenerated?: { - directory: string; - collapsed?: boolean; - }; - badge?: Badge; - collapsed?: boolean; - link?: string; - attrs?: Record; -}; - -type AdjacentPage = { - label: string; - slug: string; -}; - -type AdjacentPages = { - prev: AdjacentPage | null; - next: AdjacentPage | null; -}; - -const DOCS_DIR = 'docs'; - -/** - * Processes auto-generated directory and returns navigation items - */ -const processAutoGeneratedDirectory = async ( - directory: string, - label: string, - badge?: Badge, - collapsed?: boolean -): Promise => { - // @ts-ignore - const items = fs.readdirSync(path.join(process.env.PROJECT_DIR || '', DOCS_DIR, directory)); - - const allItems: SidebarItem[] = []; - - for (const item of items) { - const fullPath = path.join(process.env.PROJECT_DIR || '', DOCS_DIR, directory, item); - const isDirectory = fs.statSync(fullPath).isDirectory(); - - if (isDirectory) { - // Recursively process subdirectory - const subdirResult = await processAutoGeneratedDirectory( - path.join(directory, item), - item, // Use directory name as label - undefined, // No badge for subdirectories - collapsed // Inherit collapsed state - ); - allItems.push(subdirResult); - } else { - // Process file - const content = fs.readFileSync(fullPath, 'utf8'); - const { data } = matter(content); - - const astroId = data.slug || path.join(DOCS_DIR, directory, item).replace('.mdx', ''); - const entry = await getEntry('customPages', astroId.toLowerCase()); - - if (entry) { - allItems.push({ - label: entry.data.title, - slug: entry.data.slug || entry.id.replace(DOCS_DIR, ''), - }); - } - } - } - - return { - label, - badge, - collapsed, - items: allItems, - }; -}; - -/** - * Recursively process sidebar items to handle auto-generated content at any nesting level - */ -const processSidebarItems = async (items: SideBarConfigurationItem[]): Promise => { - const processedItems: SidebarItem[] = []; - - for (const item of items) { - // If item has autogenerated property, process it - if (item.autogenerated) { - const processedItem = await processAutoGeneratedDirectory( - item.autogenerated.directory, - item.label, - item.badge, - item.autogenerated.collapsed !== undefined ? item.autogenerated.collapsed : item.collapsed - ); - processedItems.push(processedItem); - } - // If item has nested items, process them recursively - else if (item.items && item.items.length > 0) { - const processedNestedItems = await processSidebarItems(item.items); - processedItems.push({ - label: item.label, - slug: item.slug, - items: processedNestedItems, - badge: item.badge, - collapsed: item.collapsed, - }); - } - // Otherwise, it's a regular item - else { - // if its a link, add it to the processedItems - if (item.link) { - processedItems.push({ - label: item.label, - slug: item.link, - attrs: item.attrs, - }); - } else { - processedItems.push(item as SidebarItem); - } - } - } - - return processedItems; -}; - -/** - * Flatten all navigation items into a single array of pages with slugs - * This is used to find previous and next pages for navigation - */ -const flattenNavigationItems = (items: SidebarItem[]): AdjacentPage[] => { - const flatPages: AdjacentPage[] = []; - - const processItem = (item: SidebarItem) => { - // Add the current item if it has a slug - if (item.slug) { - flatPages.push({ - label: item.label, - slug: item.slug, - }); - } - - // Process nested items if they exist - if (item.items && item.items.length > 0) { - item.items.forEach(processItem); - } - }; - - items.forEach(processItem); - return flatPages; -}; - -/** - * Get the previous and next pages for a given slug - * Returns null for prev if it's the first page, and null for next if it's the last page - */ -export const getAdjacentPages = async (slug: string): Promise => { - const navigationItems = await getNavigationItems(); - const flatPages = flattenNavigationItems(navigationItems); - - // Normalize the slug by removing 'docs/' prefix if it exists - // and ensure consistent formatting with or without leading slash - let normalizedSlug = slug; - if (normalizedSlug.startsWith('docs/')) { - normalizedSlug = normalizedSlug.substring(5); // Remove 'docs/' prefix - } - - // Find the current page by comparing normalized slugs - const currentIndex = flatPages.findIndex((page) => { - // Normalize page slug for comparison - let pageSlug = page.slug; - if (pageSlug.startsWith('/')) { - pageSlug = pageSlug.substring(1); - } - - return pageSlug === normalizedSlug; - }); - - // If page not found, return null for both prev and next - if (currentIndex === -1) { - return { prev: null, next: null }; - } - - // Get previous page if it exists - const prev = currentIndex > 0 ? flatPages[currentIndex - 1] : null; - - // Get next page if it exists - const next = currentIndex < flatPages.length - 1 ? flatPages[currentIndex + 1] : null; - - return { prev, next }; -}; - -export const getNavigationItems = async (): Promise => { - const configuredSidebar = config.customDocs.sidebar; - return processSidebarItems(configuredSidebar as SideBarConfigurationItem[]); -}; diff --git a/eventcatalog/src/enterprise/eventcatalog-chat/__tests__/utils/chat-prompts.spec.ts b/eventcatalog/src/enterprise/eventcatalog-chat/__tests__/utils/chat-prompts.spec.ts deleted file mode 100644 index 303d6c760..000000000 --- a/eventcatalog/src/enterprise/eventcatalog-chat/__tests__/utils/chat-prompts.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { ContentCollectionKey } from 'astro:content'; -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import type { ChatPrompt } from '../../utils/chat-prompts'; - -// Mock data for chat prompts -const mockChatPrompts: ChatPrompt[] = [ - { - id: 'prompt1.md', - body: 'Prompt 1 body', - collection: 'chatPrompts', - data: { - title: 'Prompt 1', - type: 'text', - category: { id: 'cat1', label: 'Category 1', icon: '💡' }, - }, - }, - { - id: 'prompt2.md', - body: 'Prompt 2 body', - collection: 'chatPrompts', - data: { - title: 'Prompt 2', - type: 'text', - category: { id: 'cat1', label: 'Category 1', icon: '💡' }, - }, - }, - { - id: 'prompt3.md', - body: 'Prompt 3 body', - collection: 'chatPrompts', - data: { - title: 'Prompt 3', - type: 'text', - category: { id: 'cat2', label: 'Category 2' }, - }, - }, -]; - -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - getCollection: (key: ContentCollectionKey) => { - switch (key) { - case 'chatPrompts': - return Promise.resolve(mockChatPrompts); - default: - return Promise.resolve([]); - } - }, - }; -}); - -// Clear cache before each test -beforeEach(() => { - vi.resetModules(); -}); - -describe('Chat Prompts Utilities', () => { - describe('getChatPrompts', () => { - it('should return an array of chat prompts', async () => { - // Need to re-import after vi.resetModules() - const { getChatPrompts } = await import('../../utils/chat-prompts'); - const prompts = await getChatPrompts(); - expect(prompts).toEqual(mockChatPrompts); - }); - }); - - describe('getChatPromptsGroupedByCategory', () => { - it('should group chat prompts by category', async () => { - // Re-import after reset - const { getChatPromptsGroupedByCategory } = await import('../../utils/chat-prompts'); - const groupedPrompts = await getChatPromptsGroupedByCategory(); - - const expectedGrouping = [ - { - label: 'Category 1', - icon: '💡', - items: [mockChatPrompts[0], mockChatPrompts[1]], - }, - { - label: 'Category 2', - icon: undefined, // No icon provided for cat2 in mock data - items: [mockChatPrompts[2]], - }, - ]; - - // Use expect.arrayContaining because the order of categories is not guaranteed - expect(groupedPrompts).toEqual( - expect.arrayContaining([expect.objectContaining(expectedGrouping[0]), expect.objectContaining(expectedGrouping[1])]) - ); - expect(groupedPrompts.length).toBe(expectedGrouping.length); // Ensure no extra categories - }); - }); -}); diff --git a/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx b/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx deleted file mode 100644 index 35ac96c41..000000000 --- a/eventcatalog/src/enterprise/eventcatalog-chat/components/Chat.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import Sidebar from './ChatSidebar'; -import { ChatProvider } from './hooks/ChatProvider'; -import ChatWindowServer from './windows/ChatWindow.server'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import type { ChatPromptCategoryGroup } from '@enterprise/eventcatalog-chat/utils/chat-prompts'; -interface Resource { - id: string; - type: string; - name: string; -} - -/** - * Chat component has two modes: - * - Server: ChatWindow.server.tsx (uses server-side code, bring your own API key) - * - * The mode is determined by the config.output property in the eventcatalog.config.js file. - */ - -const Chat = ({ - chatConfig, - resources, - chatPrompts, - output, -}: { - chatConfig: any; - resources: Resource[]; - chatPrompts: ChatPromptCategoryGroup[]; - output: 'static' | 'server'; -}) => { - const queryClient = new QueryClient(); - - if (output !== 'server') { - return ( - // Message to turn on server side -
    -
    -

    Chat is only supported on server side

    -

    - Please switch to server side by setting the output{' '} - property to server in your{' '} - eventcatalog.config.js file. -

    -
    -
    - ); - } - - return ( - -
    - - - - -
    -
    - ); -}; - -export default Chat; diff --git a/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx b/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx deleted file mode 100644 index 8bb5c581d..000000000 --- a/eventcatalog/src/enterprise/eventcatalog-chat/components/ChatMessage.tsx +++ /dev/null @@ -1,414 +0,0 @@ -import React, { useState } from 'react'; -import ReactMarkdown, { type ExtraProps } from 'react-markdown'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import type { Message } from '@enterprise/eventcatalog-chat/components/hooks/ChatProvider'; -import * as Dialog from '@radix-ui/react-dialog'; -import { Fullscreen, X, Clipboard, Check, ChevronDown, ChevronRight } from 'lucide-react'; // Add Clipboard, Check, ChevronDown, ChevronRight icons - -// Define Resource type locally -interface Resource { - id: string; - type: string; - url: string; - title?: string; - name?: string | null; // Allow null for name -} - -interface ChatMessageProps { - message: Message; -} - -// Function to escape special characters for regex -function escapeRegex(string: string): string { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -// Function to parse and render content with iframes -function parseContentWithIframes(content: string): React.ReactNode[] { - const iframeRegex = /]*>.*?<\/iframe>/gi; - const parts: React.ReactNode[] = []; - let lastIndex = 0; - let match; - - while ((match = iframeRegex.exec(content)) !== null) { - // Add text before iframe as markdown - if (match.index > lastIndex) { - const textBefore = content.slice(lastIndex, match.index); - if (textBefore.trim()) { - parts.push( - -
    - {language} -
    - - {codeString.replace(/\n$/, '')} - -
    - ) : ( - - {codeString.trim()} - - ); - }, - a: ({ node, ...props }) => ( - - ), - p: ({ node, ...props }) =>

    , - }} - > - {textBefore} - - ); - } - } - - // Parse iframe attributes - const iframeHtml = match[0]; - const srcMatch = iframeHtml.match(/src=["']([^"']+)["']/i); - const widthMatch = iframeHtml.match(/width=["']([^"']+)["']/i); - const heightMatch = iframeHtml.match(/height=["']([^"']+)["']/i); - const titleMatch = iframeHtml.match(/title=["']([^"']+)["']/i); - - if (srcMatch) { - parts.push( - ', - ].join('\n'), - inputSchema: z.object({ - id: z.string().describe('The id of the resource to find'), - version: z.string().describe('The version of the resource to find'), - type: z - .enum(['services', 'domains', 'events', 'commands', 'queries', 'flows', 'entities']) - .describe('The type of resource to find'), - }), - execute: async ({ id, version, type }) => { - const text = await getResourceInformation(type, id, version); - return text; - }, - }), - }, - }); - } -} diff --git a/eventcatalog/src/enterprise/eventcatalog-chat/providers/anthropic.ts b/eventcatalog/src/enterprise/eventcatalog-chat/providers/anthropic.ts deleted file mode 100644 index 710e137dd..000000000 --- a/eventcatalog/src/enterprise/eventcatalog-chat/providers/anthropic.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { anthropic } from '@ai-sdk/anthropic'; -import { AIProvider, type AIProviderOptions } from './ai-provider'; - -const AVAILABLE_GOOGLE_MODELS = [ - 'claude-3-7-sonnet-20250219', - 'claude-3-5-sonnet-latest', - 'claude-3-5-sonnet-20241022', - 'claude-3-5-sonnet-20240620', - 'claude-3-5-haiku-latest', - 'claude-3-5-haiku-20241022', - 'claude-3-opus-latest', - 'claude-3-opus-20240229', - 'claude-3-sonnet-20240229', - 'claude-3-haiku-20240307', -] as const; - -export class AnthropicProvider extends AIProvider { - public models: string[] = [...AVAILABLE_GOOGLE_MODELS]; - - constructor(options: AIProviderOptions) { - const languageModel = anthropic(options.modelId || 'claude-3-7-sonnet-20250219'); - - super({ - ...options, - model: languageModel, - }); - } -} diff --git a/eventcatalog/src/enterprise/eventcatalog-chat/providers/google.ts b/eventcatalog/src/enterprise/eventcatalog-chat/providers/google.ts deleted file mode 100644 index 0c794465d..000000000 --- a/eventcatalog/src/enterprise/eventcatalog-chat/providers/google.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { google } from '@ai-sdk/google'; -import { AIProvider, type AIProviderOptions } from './ai-provider'; - -const AVAILABLE_GOOGLE_MODELS = [ - 'gemini-1.5-flash', - 'gemini-1.5-flash-latest', - 'gemini-1.5-flash-001', - 'gemini-1.5-flash-002', - 'gemini-1.5-flash-8b', - 'gemini-1.5-flash-8b-latest', - 'gemini-1.5-flash-8b-001', - 'gemini-1.5-pro', - 'gemini-1.5-pro-latest', - 'gemini-1.5-pro-001', - 'gemini-1.5-pro-002', - 'gemini-2.0-flash', - 'gemini-2.0-flash-001', - 'gemini-2.0-flash-live-001', - 'gemini-2.0-flash-lite', - 'gemini-2.0-pro-exp-02-05', - 'gemini-2.0-flash-thinking-exp-01-21', - 'gemini-2.0-flash-exp', - 'gemini-2.5-pro-exp-03-25', - 'gemini-2.5-pro-preview-05-06', - 'gemini-2.5-flash-preview-04-17', - 'gemini-exp-1206', - 'gemma-3-27b-it', - 'learnlm-1.5-pro-experimental', -] as const; - -export class GoogleProvider extends AIProvider { - public models: string[] = [...AVAILABLE_GOOGLE_MODELS]; - - constructor(options: AIProviderOptions) { - const languageModel = google(options.modelId || 'gemini-1.5-flash'); - super({ - ...options, - model: languageModel, - }); - } -} diff --git a/eventcatalog/src/enterprise/eventcatalog-chat/providers/index.ts b/eventcatalog/src/enterprise/eventcatalog-chat/providers/index.ts deleted file mode 100644 index ac7781c2d..000000000 --- a/eventcatalog/src/enterprise/eventcatalog-chat/providers/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { OpenAIProvider } from './openai'; -import { GoogleProvider } from './google'; -import { AnthropicProvider } from './anthropic'; -import type { AIProviderOptions } from './ai-provider'; - -export function getProvider(provider: string, options: AIProviderOptions) { - switch (provider) { - case 'openai': - return new OpenAIProvider({ - ...options, - modelId: options.modelId, - }); - case 'google': - return new GoogleProvider({ - ...options, - modelId: options.modelId, - }); - case 'anthropic': - return new AnthropicProvider({ - ...options, - modelId: options.modelId, - }); - default: - throw new Error(`Provider ${provider} not supported`); - } -} diff --git a/eventcatalog/src/enterprise/eventcatalog-chat/providers/openai.ts b/eventcatalog/src/enterprise/eventcatalog-chat/providers/openai.ts deleted file mode 100644 index f2ade1d8f..000000000 --- a/eventcatalog/src/enterprise/eventcatalog-chat/providers/openai.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { openai } from '@ai-sdk/openai'; -import { AIProvider, type AIProviderOptions } from './ai-provider'; - -const AVAILABLE_OPENAI_MODELS = [ - 'o1', - 'o1-2024-12-17', - 'o1-mini', - 'o1-mini-2024-09-12', - 'o1-preview', - 'o1-preview-2024-09-12', - 'o3-mini', - 'o3-mini-2025-01-31', - 'o3', - 'o3-2025-04-16', - 'o4-mini', - 'o4-mini-2025-04-16', - 'gpt-4.1', - 'gpt-4.1-2025-04-14', - 'gpt-4.1-mini', - 'gpt-4.1-mini-2025-04-14', - 'gpt-4.1-nano', - 'gpt-4.1-nano-2025-04-14', - 'gpt-4o', - 'gpt-4o-2024-05-13', - 'gpt-4o-2024-08-06', - 'gpt-4o-2024-11-20', - 'gpt-4o-audio-preview', - 'gpt-4o-audio-preview-2024-10-01', - 'gpt-4o-audio-preview-2024-12-17', - 'gpt-4o-search-preview', - 'gpt-4o-search-preview-2025-03-11', - 'gpt-4o-mini-search-preview', - 'gpt-4o-mini-search-preview-2025-03-11', - 'gpt-4o-mini', - 'gpt-4o-mini-2024-07-18', - 'gpt-4-turbo', - 'gpt-4-turbo-2024-04-09', - 'gpt-4-turbo-preview', - 'gpt-4-0125-preview', - 'gpt-4-1106-preview', - 'gpt-4', - 'gpt-4-0613', - 'gpt-4.5-preview', - 'gpt-4.5-preview-2025-02-27', - 'gpt-3.5-turbo-0125', - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-1106', - 'chatgpt-4o-latest', -] as const; - -export class OpenAIProvider extends AIProvider { - public models: string[] = [...AVAILABLE_OPENAI_MODELS]; - - constructor(options: AIProviderOptions) { - const languageModel = openai(options.modelId || 'o4-mini'); - super({ - ...options, - model: languageModel, - }); - } -} diff --git a/eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts b/eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts deleted file mode 100644 index ccfaa7ff9..000000000 --- a/eventcatalog/src/enterprise/eventcatalog-chat/utils/chat-prompts.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; - -export type ChatPrompt = CollectionEntry<'chatPrompts'>; - -// Update cache to store both versions -let cachedChatPrompts: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getChatPrompts = async (): Promise => { - const cacheKey = 'allVersions'; - - // Check if we have cached domains for this specific getAllVersions value - if (cachedChatPrompts[cacheKey].length > 0) { - return cachedChatPrompts[cacheKey]; - } - - const prompts = await getCollection('chatPrompts'); - - return prompts; -}; - -export type ChatPromptCategoryGroup = { - label: string; - icon?: string; - items: ChatPrompt[]; -}; - -export const getChatPromptsGroupedByCategory = async (): Promise => { - const prompts = await getChatPrompts(); - - const grouped = prompts.reduce( - (acc, prompt) => { - const { id, label, icon } = prompt.data.category; - - if (!acc[id]) { - acc[id] = { label, icon, items: [] }; - } - - acc[id].items.push(prompt); - return acc; - }, - {} as Record - ); - - // Convert the grouped object into the desired array format - return Object.values(grouped); -}; diff --git a/eventcatalog/src/env.d.ts b/eventcatalog/src/env.d.ts deleted file mode 100644 index b6af1f927..000000000 --- a/eventcatalog/src/env.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -/// -/// - -declare const __EC_TRAILING_SLASH__: boolean; diff --git a/eventcatalog/src/layouts/DirectoryLayout.astro b/eventcatalog/src/layouts/DirectoryLayout.astro deleted file mode 100644 index 96020c528..000000000 --- a/eventcatalog/src/layouts/DirectoryLayout.astro +++ /dev/null @@ -1,116 +0,0 @@ ---- -import { Table, type TData, type TCollectionTypes } from '@components/Tables/Table'; -import { buildUrl } from '@utils/url-builder'; -import VerticalSideBarLayout from './VerticalSideBarLayout.astro'; -import { User, Users } from 'lucide-react'; -import { getUsers } from '@utils/users'; -import { getTeams } from '@utils/teams'; -import config from '@config'; - -const users = await getUsers(); -const teams = await getTeams(); - -export interface Props { - title: string; - subtitle: string; - data: TData[]; - type: T; -} - -const { title, subtitle, data, type } = Astro.props; -const currentPath = Astro.url.pathname; - -const checkboxLatestId = 'latest-only'; -const checkboxDraftsId = 'drafts-only'; - -// @ts-ignore -const tableConfiguration = config[type as keyof typeof config]?.tableConfiguration ?? { columns: {} }; - -const tabs = [ - { - label: `Users (${users.length})`, - href: buildUrl('/directory/users'), - isActive: currentPath === '/directory/users', - icon: User, - activeColor: 'orange', - enabled: users.length > 0, - visible: users.length > 0, - }, - { - label: `Teams (${teams.length})`, - href: buildUrl('/directory/teams'), - isActive: currentPath === '/directory/teams', - icon: Users, - activeColor: 'blue', - enabled: teams.length > 0, - visible: teams.length > 0, - }, -]; ---- - - -

    - - - -
    -
    -
    -
    -

    {title}

    -

    {subtitle}

    -
    -
    -
    -
    -
    - - - - - - - - - - diff --git a/eventcatalog/src/layouts/DiscoverLayout.astro b/eventcatalog/src/layouts/DiscoverLayout.astro deleted file mode 100644 index d176398ba..000000000 --- a/eventcatalog/src/layouts/DiscoverLayout.astro +++ /dev/null @@ -1,182 +0,0 @@ ---- -import { Table, type TData, type TCollectionTypes } from '@components/Tables/Table'; -import { QueueListIcon, RectangleGroupIcon, BoltIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/outline'; -import ServerIcon from '@heroicons/react/24/outline/ServerIcon'; -import { getCommands } from '@utils/commands'; -import { getDomains } from '@utils/collections/domains'; -import { getFlows } from '@utils/collections/flows'; -import { getEvents } from '@utils/events'; -import { getServices } from '@utils/collections/services'; -import { buildUrl } from '@utils/url-builder'; -import { getQueries } from '@utils/queries'; -import { getContainers } from '@utils/collections/containers'; -import { DatabaseIcon } from 'lucide-react'; -import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'; -import VerticalSideBarLayout from './VerticalSideBarLayout.astro'; -import Checkbox from '@components/Checkbox.astro'; -import config from '@config'; - -const events = await getEvents(); -const queries = await getQueries(); -const commands = await getCommands(); -const services = await getServices(); -const domains = await getDomains(); -const flows = await getFlows(); -const containers = await getContainers(); -export interface Props { - title: string; - subtitle: string; - data: TData[]; - type: T; -} - -const { title, subtitle, data, type } = Astro.props; -const currentPath = Astro.url.pathname; - -const checkboxLatestId = 'latest-only'; -const checkboxDraftsId = 'show-drafts'; - -// @ts-ignore -const tableConfiguration = config[type as keyof typeof config]?.tableConfiguration ?? { columns: {} }; - -const tabs = [ - { - label: `Events (${events.length})`, - href: buildUrl('/discover/events'), - isActive: currentPath === '/discover/events', - icon: BoltIcon, - activeColor: 'orange', - enabled: events.length > 0, - visible: events.length > 0, - }, - { - label: `Commands (${commands.length})`, - href: buildUrl('/discover/commands'), - isActive: currentPath === '/discover/commands', - icon: ChatBubbleLeftIcon, - activeColor: 'blue', - enabled: commands.length > 0, - visible: commands.length > 0, - }, - { - label: `Queries (${queries.length})`, - href: buildUrl('/discover/queries'), - isActive: currentPath === '/discover/queries', - icon: MagnifyingGlassIcon, - activeColor: 'green', - enabled: queries.length > 0, - visible: queries.length > 0, - }, - { - label: `Services (${services.length})`, - href: buildUrl('/discover/services'), - isActive: currentPath === '/discover/services', - icon: ServerIcon, - activeColor: 'pink', - enabled: services.length > 0, - visible: services.length > 0, - }, - { - label: `Domains (${domains.length})`, - href: buildUrl('/discover/domains'), - isActive: currentPath === '/discover/domains', - icon: RectangleGroupIcon, - activeColor: 'yellow', - enabled: domains.length > 0, - visible: domains.length > 0, - }, - { - label: `Data (${containers.length})`, - href: buildUrl('/discover/containers'), - isActive: currentPath === '/discover/containers', - icon: DatabaseIcon, - activeColor: 'blue', - enabled: containers.length > 0, - visible: containers.length > 0, - }, - { - label: `Flows (${flows.length})`, - href: buildUrl('/discover/flows'), - isActive: currentPath === '/discover/flows', - icon: QueueListIcon, - activeColor: 'teal', - enabled: flows.length > 0, - visible: flows.length > 0, - }, -]; ---- - - -
    -
    - -
    - - -
    -
    -
    -
    -

    {title}

    -

    {subtitle}

    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    -
    - - - - - - - - - diff --git a/eventcatalog/src/layouts/Footer.astro b/eventcatalog/src/layouts/Footer.astro deleted file mode 100644 index d1811df8b..000000000 --- a/eventcatalog/src/layouts/Footer.astro +++ /dev/null @@ -1,43 +0,0 @@ ---- -import { buildUrl } from '@utils/url-builder'; -import { showEventCatalogBranding } from '@utils/feature'; -const { className } = Astro.props; ---- - - diff --git a/eventcatalog/src/layouts/VerticalSideBarLayout.astro b/eventcatalog/src/layouts/VerticalSideBarLayout.astro deleted file mode 100644 index 8bce236b9..000000000 --- a/eventcatalog/src/layouts/VerticalSideBarLayout.astro +++ /dev/null @@ -1,518 +0,0 @@ ---- -interface Props { - title: string; - sidebar?: boolean; - description?: string; -} - -import { - BookOpenText, - Workflow, - TableProperties, - House, - BookUser, - BotMessageSquare, - Users, - Sparkles, - Rocket, - FileText, - SquareDashedMousePointerIcon, - PackageSearch, - FileJson, -} from 'lucide-react'; -import Header from '../components/Header.astro'; -import SEO from '../components/Seo.astro'; -import SideNav from '../components/SideNav/SideNav.astro'; -import config from '@config'; -import { getCollection } from 'astro:content'; -import '@fontsource/inter'; -import '@fontsource/inter/400.css'; // Specify weight -import '@fontsource/inter/700.css'; // Specify weight - -import { getCommands } from '@utils/commands'; -import { getDomains } from '@utils/collections/domains'; -import { getEvents } from '@utils/events'; -import { getServices } from '@utils/collections/services'; -import { getFlows } from '@utils/collections/flows'; -import { isCollectionVisibleInCatalog } from '@eventcatalog'; -import { buildUrl } from '@utils/url-builder'; -import { getQueries } from '@utils/queries'; -import { hasLandingPageForDocs } from '@utils/pages'; - -const events = await getEvents({ getAllVersions: false }); -const commands = await getCommands({ getAllVersions: false }); -const queries = await getQueries({ getAllVersions: false }); -const services = await getServices({ getAllVersions: false }); -const domains = await getDomains({ getAllVersions: false }); -const flows = await getFlows({ getAllVersions: false }); -const customDocs = await getCollection('customPages'); - -import { isEventCatalogUpgradeEnabled, isVisualiserEnabled } from '@utils/feature'; - -// Try and load any custom styles if they exist -try { - await import('../../eventcatalog.styles.css'); -} catch (error) {} - -const currentPath = Astro.url.pathname; - -const catalogHasDefaultLandingPageForDocs = await hasLandingPageForDocs(); - -const getDefaultUrl = (route: string, defaultValue: string) => { - if (route === 'docs/custom') { - return customDocs.length > 0 ? buildUrl(`/${route}/${customDocs[0].id.replace('docs', '')}`) : buildUrl(defaultValue); - } - - const collections = [ - { data: domains, key: 'domains' }, - { data: services, key: 'services' }, - { data: events, key: 'events' }, - { data: commands, key: 'commands' }, - { data: queries, key: 'queries' }, - { data: flows, key: 'flows' }, - ]; - - for (const { data, key } of collections) { - if (data.length > 0 && isCollectionVisibleInCatalog(key)) { - // find the first item that has visualiser set to true - const item = data.find((item) => item.data.visualiser !== false); - if (item) { - return buildUrl(`/${route}/${key}/${item.data.id}/${item.data.latestVersion}`); - } else { - continue; - } - } - } - - return buildUrl(defaultValue); -}; - -const userSideBarConfiguration = config.sidebar || []; - -const navigationItems = [ - { - id: '/', - label: 'Home', - icon: House, - href: buildUrl('/'), - current: currentPath === '/', - sidebar: false, - hidden: !!config.landingPage, - }, - { - id: '/docs', - label: 'Architecture Documentation', - icon: BookOpenText, - href: catalogHasDefaultLandingPageForDocs ? buildUrl('/docs') : getDefaultUrl('docs', '/docs'), - current: - (currentPath.includes('/docs') && !currentPath.includes('/docs/custom')) || currentPath.includes('/architecture/docs/'), - sidebar: true, - }, - { - id: '/visualiser', - label: 'Visualiser', - icon: Workflow, - href: getDefaultUrl('visualiser', '/visualiser'), - current: currentPath.includes('/visualiser'), - sidebar: true, - hidden: !isVisualiserEnabled(), - }, - - { - id: '/discover', - label: 'Explore', - icon: TableProperties, - href: buildUrl('/discover/events'), - current: currentPath.includes('/discover/'), - sidebar: false, - }, - { - id: '/schemas', - label: 'Schema Explorer', - icon: FileJson, - href: buildUrl('/schemas'), - current: currentPath.includes('/schemas'), - sidebar: false, - }, - { - id: '/directory', - label: 'Users & Teams', - icon: Users, - href: buildUrl('/directory/users'), - current: currentPath.includes('/directory'), - sidebar: false, - }, - { - id: '/architecture', - label: 'Architecture', - icon: BookUser, - href: buildUrl('/architecture/domains'), - current: currentPath.includes('/architecture/'), - sidebar: false, - hidden: true, - }, -].filter((item) => { - const userSideBarOption = userSideBarConfiguration.find((config: { id: string; visible: boolean }) => config.id === item.id); - return userSideBarOption ? userSideBarOption.visible : true; -}); - -const studioNavigationItem = [ - { - id: '/studio', - label: 'EventCatalog Studio', - icon: SquareDashedMousePointerIcon, - href: buildUrl('/studio'), - current: currentPath.includes('/studio'), - sidebar: false, - }, -].filter((item) => { - const userSideBarOption = userSideBarConfiguration.find((config: { id: string; visible: boolean }) => config.id === item.id); - return userSideBarOption ? userSideBarOption.visible : true; -}); - -const premiumFeatures = [ - { - id: '/docs/custom', - label: 'Custom Documentation', - icon: FileText, - href: getDefaultUrl('docs/custom', '/docs/custom'), - current: currentPath.includes('/docs/custom'), - sidebar: false, - isPremium: true, - }, - { - id: '/chat', - label: 'EventCatalog Chat', - icon: BotMessageSquare, - href: buildUrl('/chat'), - current: currentPath.includes('/chat'), - isPremium: true, - }, -].filter((item) => { - const userSideBarOption = userSideBarConfiguration.find((config: { id: string; visible: boolean }) => config.id === item.id); - return userSideBarOption ? userSideBarOption.visible : true; -}); - -const currentNavigationItem = [...navigationItems, ...studioNavigationItem, ...premiumFeatures].find((item) => item.current); -const { title, description } = Astro.props; - -const showSideBarOnLoad = currentNavigationItem?.sidebar; - -const canPageBeEmbedded = process.env.ENABLE_EMBED === 'true'; ---- - - - - - - - - -
    -
    - -
    - -
    - - - -
    - - - diff --git a/eventcatalog/src/layouts/VisualiserLayout.astro b/eventcatalog/src/layouts/VisualiserLayout.astro deleted file mode 100644 index a54e65ea2..000000000 --- a/eventcatalog/src/layouts/VisualiserLayout.astro +++ /dev/null @@ -1,167 +0,0 @@ ---- -import { getCommands } from '@utils/commands'; -import { getDomains } from '@utils/collections/domains'; -import { getEvents } from '@utils/events'; -import { getFlows } from '@utils/collections/flows'; -import { getQueries } from '@utils/queries'; -import { getServices } from '@utils/collections/services'; -import VerticalSideBarLayout from './VerticalSideBarLayout.astro'; - -const [services, events, commands, queries, domains, flows] = await Promise.all([ - getServices(), - getEvents(), - getCommands(), - getQueries(), - getDomains(), - getFlows(), -]); - -// @ts-ignore for large catalogs https://github.com/event-catalog/eventcatalog/issues/552 -const navItems = [...domains, ...services, ...events, ...commands, ...queries, ...flows]; - -const navigation = navItems - .filter((item) => item.data.version === item.data.latestVersion) - .reduce((acc, item) => { - // @ts-ignore - const group = acc[item.collection] || []; - - return { - ...acc, - [item.collection]: [...group, item], - }; - }, {}) as any; - -const currentPath = Astro.url.pathname; -const { title, description } = Astro.props; - -const getColor = (collection: string) => { - switch (collection) { - case 'services': - return 'green'; - case 'events': - return 'red'; - case 'queries': - return 'green'; - case 'commands': - return 'yellow'; - case 'domains': - return 'blue'; - default: - return 'gray'; - } -}; ---- - - -
    -
    - - -
    - - -
    -
    -
    -
    - - - - - - diff --git a/eventcatalog/src/middleware.ts b/eventcatalog/src/middleware.ts deleted file mode 100644 index 481b96763..000000000 --- a/eventcatalog/src/middleware.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { MiddlewareHandler } from 'astro'; -import { authMiddleware } from './middleware-auth.ts'; -import { sequence } from 'astro:middleware'; -import { join } from 'node:path'; -import { isSSR } from '@utils/feature'; - -// Try to load customer's custom RBAC middleware -let customerRbacMiddleware: MiddlewareHandler | null = null; - -try { - const catalogDirectory = process.env.PROJECT_DIR || process.cwd(); - const customerMiddleware = await import(/* @vite-ignore */ join(catalogDirectory, 'middleware.ts')); - customerRbacMiddleware = customerMiddleware.rbacMiddleware; - - if (!isSSR()) { - // Tell user they need to build in SSR mode - console.log( - '🔴 Found custom middleware.ts file. To use RBAC, you need to build in SSR mode, by setting output: "server" in your eventcatalog.config.js file.' - ); - } else { - console.log('✅ Loaded custom RBAC middleware'); - } -} catch (error) { - // Just silently fail, we don't want to block the app -} - -const errorHandlingMiddleware: MiddlewareHandler = async (context, next) => { - const response = await next(); - - if (response.status === 403) { - const params = new URLSearchParams({ - path: context.url.pathname, - returnTo: context.url.pathname + context.url.search, - }); - - return context.redirect(`/unauthorized?${params.toString()}`); - } - - return response; -}; - -const middlewareChain = customerRbacMiddleware - ? [errorHandlingMiddleware, authMiddleware, customerRbacMiddleware] - : [errorHandlingMiddleware, authMiddleware]; - -export const onRequest = isSSR() ? sequence(...middlewareChain) : undefined; diff --git a/eventcatalog/src/pages/_index.astro b/eventcatalog/src/pages/_index.astro deleted file mode 100644 index 7749c6cf0..000000000 --- a/eventcatalog/src/pages/_index.astro +++ /dev/null @@ -1,278 +0,0 @@ ---- -import { buildUrl } from '@utils/url-builder'; -import { ChatBubbleLeftIcon, RectangleGroupIcon, ServerIcon } from '@heroicons/react/24/outline'; -import config from '@config'; - -import { getMessages } from '@utils/messages'; -import { getDomains } from '@utils/collections/domains'; -import { getServices } from '@utils/collections/services'; -import { getFlows } from '@utils/collections/flows'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { BookOpenText, Workflow, TableProperties, House, BookUser, MessageSquare, BotMessageSquare, Users } from 'lucide-react'; - -const { commands = [], events = [], queries = [] } = await getMessages({ getAllVersions: false }); -const messages = [...events, ...queries, ...commands]; -const domains = await getDomains({ getAllVersions: false }); -const services = await getServices({ getAllVersions: false }); -const flows = await getFlows({ getAllVersions: false }); - -const gettingStartedItems = [ - { - title: 'Add a New Message', - icon: ChatBubbleLeftIcon, - iconBg: 'blue', - description: 'Document a new message in your system with schemas, examples, and relationships.', - links: [ - { - text: 'How to add a message', - href: 'https://www.eventcatalog.dev/docs/messages', - }, - { - text: 'Versioning guide', - href: 'https://www.eventcatalog.dev/docs/development/guides/messages/events/versioning', - }, - { - text: 'Adding schemas', - href: 'https://www.eventcatalog.dev/docs/development/guides/messages/events/adding-schemas', - }, - ], - }, - { - title: 'Document a Service', - icon: ServerIcon, - iconBg: 'green', - description: 'Add details about a service, including its events, APIs, and dependencies.', - links: [ - { - text: 'How to add a service', - href: 'https://www.eventcatalog.dev/docs/services', - }, - { - text: 'Service ownership', - href: 'https://www.eventcatalog.dev/docs/development/guides/services/owners', - }, - { - text: 'Assign specifications to services', - href: 'https://www.eventcatalog.dev/docs/development/guides/services/adding-spec-files-to-services', - }, - ], - }, - { - title: 'Create a Domain', - icon: RectangleGroupIcon, - iconBg: 'purple', - description: 'Organize your services and events into logical business domains.', - links: [ - { - text: 'How to add a domain', - href: 'https://www.eventcatalog.dev/docs/domains', - }, - { - text: 'Adding services to domains', - href: 'https://www.eventcatalog.dev/docs/development/guides/domains/adding-services-to-domains', - }, - { - text: 'Creating a ubiquitous language', - href: 'https://www.eventcatalog.dev/docs/development/guides/domains/adding-ubiquitous-language', - }, - ], - }, -]; - -const getDefaultUrl = (route: string, defaultValue: string) => { - if (domains.length > 0) return buildUrl(`/${route}/domains/${domains[0].data.id}/${domains[0].data.latestVersion}`); - if (services.length > 0) return buildUrl(`/${route}/services/${services[0].data.id}/${services[0].data.latestVersion}`); - if (flows.length > 0) return buildUrl(`/${route}/flows/${flows[0].data.id}/${flows[0].data.latestVersion}`); - return buildUrl(defaultValue); -}; - -const topTiles = [ - { - title: 'Domains', - count: domains.length, - description: 'Business domains defined', - href: buildUrl('/architecture/domains'), - icon: RectangleGroupIcon, - bgColor: 'bg-yellow-100', - textColor: 'text-yellow-600', - arrowColor: 'text-yellow-600', - }, - { - title: 'Services', - count: services.length, - description: 'Services documented in the catalog', - href: buildUrl('/architecture/services'), - icon: ServerIcon, - bgColor: 'bg-pink-100', - textColor: 'text-pink-600', - arrowColor: 'text-pink-600', - }, - { - title: 'Messages', - count: messages.length, - description: 'Messages documented in the catalog', - href: buildUrl('/architecture/messages'), - icon: ChatBubbleLeftIcon, - bgColor: 'bg-blue-100', - textColor: 'text-blue-600', - arrowColor: 'text-blue-600', - }, -]; ---- - - - -
    -
    -

    - {config?.organizationName || 'EventCatalog'} -

    -

    - {config.tagline || 'Comprehensive event-driven architecture documentation covering events, services, domains.'} -

    -
    - -

    Architecture overview

    - - - - -
    -

    Getting Started

    -
    - { - gettingStartedItems.map((item) => ( -
    -
    -
    - -
    -

    {item.title}

    -
    -

    {item.description}

    -
    - {item.links.map((link) => ( - - → {link.text} - - ))} -
    -
    - )) - } -
    -
    -
    - -
    diff --git a/eventcatalog/src/pages/api/catalog.ts b/eventcatalog/src/pages/api/catalog.ts deleted file mode 100644 index 58f504d68..000000000 --- a/eventcatalog/src/pages/api/catalog.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { APIRoute } from 'astro'; -import utils from '@eventcatalog/sdk'; -import config from '@config'; - -/** - * Route the will dump the whole catalog as JSON (without markdown) - * Experimental API - * @param param0 - * @returns - */ -export const GET: APIRoute = async ({ params, request }) => { - const isFullCatalogAPIEnabled = config.api?.fullCatalogAPIEnabled ?? true; - - if (!isFullCatalogAPIEnabled) { - return new Response(JSON.stringify({ error: 'Full catalog API is not enabled' }), { - status: 404, - }); - } - - const { dumpCatalog } = utils(process.env.PROJECT_DIR || ''); - const catalog = await dumpCatalog({ includeMarkdown: false }); - - return new Response(JSON.stringify(catalog), { - headers: { - 'Content-Type': 'application/json', - }, - }); -}; diff --git a/eventcatalog/src/pages/api/schemas/[collection]/[id]/[version]/index.ts b/eventcatalog/src/pages/api/schemas/[collection]/[id]/[version]/index.ts deleted file mode 100644 index f34c3105e..000000000 --- a/eventcatalog/src/pages/api/schemas/[collection]/[id]/[version]/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type { APIRoute } from 'astro'; -import { getCollection } from 'astro:content'; -import path from 'node:path'; -import fs from 'node:fs'; -import { isEventCatalogScaleEnabled } from '@utils/feature'; -import { sortVersioned } from '@utils/collections/util'; - -export async function getStaticPaths() { - const events = await getCollection('events'); - const commands = await getCollection('commands'); - const queries = await getCollection('queries'); - const messages = [...events, ...commands, ...queries]; - - const messagesWithSchemas = messages - .filter((message) => message.data.schemaPath) - .filter((message) => fs.existsSync(path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? ''))); - - // Generate paths for specific versions - const versionedPaths = messagesWithSchemas.map((message) => ({ - params: { collection: message.collection, id: message.data.id, version: message.data.version }, - props: { - pathToSchema: path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? ''), - schema: fs.readFileSync(path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? ''), 'utf8'), - extension: message.data.schemaPath?.split('.').pop(), - }, - })); - - // Group messages by collection and id to find latest versions - const groupedMessages = messagesWithSchemas.reduce( - (acc, message) => { - const key = `${message.collection}:${message.data.id}`; - if (!acc[key]) { - acc[key] = []; - } - acc[key].push(message); - return acc; - }, - {} as Record - ); - - // Generate "latest" paths for each unique collection/id combination - const latestPaths = Object.values(groupedMessages).map((group) => { - // Sort by version (descending) and get the latest - const sorted = sortVersioned(group, (m) => m.data.version); - const latestMessage = sorted[0]; - return { - params: { collection: latestMessage.collection, id: latestMessage.data.id, version: 'latest' }, - props: { - pathToSchema: path.join(path.dirname(latestMessage.filePath ?? ''), latestMessage.data.schemaPath ?? ''), - schema: fs.readFileSync( - path.join(path.dirname(latestMessage.filePath ?? ''), latestMessage.data.schemaPath ?? ''), - 'utf8' - ), - extension: latestMessage.data.schemaPath?.split('.').pop(), - }, - }; - }); - - return [...versionedPaths, ...latestPaths]; -} - -export const GET: APIRoute = async ({ props }) => { - if (!isEventCatalogScaleEnabled()) { - return new Response( - JSON.stringify({ - error: 'feature_not_available_on_server', - message: 'Schema API is not enabled for this deployment and supported in EventCatalog Scale.', - }), - { - status: 501, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', - }, - } - ); - } - - return new Response(props.schema, { - headers: { 'Content-Type': 'text/plain' }, - }); -}; diff --git a/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts b/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts deleted file mode 100644 index 23bfbf285..000000000 --- a/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { APIRoute } from 'astro'; -import { getCollection } from 'astro:content'; -import path from 'node:path'; -import fs from 'node:fs'; -import { getSpecificationsForService } from '@utils/collections/services'; -import { isEventCatalogScaleEnabled } from '@utils/feature'; - -export async function getStaticPaths() { - const services = await getCollection('services'); - const servicesWithSpecifications = services.filter((service) => getSpecificationsForService(service).length > 0); - return servicesWithSpecifications.reduce< - { params: { collection: string; id: string; version: string; specification: string }; props: { schema: string } }[] - >( - (acc, service) => { - const specifications = getSpecificationsForService(service); - return [ - ...acc, - ...specifications.map((specification) => ({ - params: { - collection: service.collection, - id: service.data.id, - version: service.data.version, - specification: specification.type, - }, - props: { - schema: fs.readFileSync(path.join(path.dirname(service.filePath ?? ''), specification.path ?? ''), 'utf8'), - }, - })), - ]; - }, - [] as { params: { collection: string; id: string; version: string; specification: string }; props: { schema: string } }[] - ); -} - -export const GET: APIRoute = async ({ props }) => { - if (!isEventCatalogScaleEnabled()) { - return new Response( - JSON.stringify({ - error: 'feature_not_available_on_server', - message: 'Schema API is not enabled for this deployment and supported in EventCatalog Scale.', - }), - { - status: 501, - headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }, - } - ); - } - return new Response(props.schema, { - headers: { 'Content-Type': 'text/plain' }, - }); -}; diff --git a/eventcatalog/src/pages/architecture/[type]/index.astro b/eventcatalog/src/pages/architecture/[type]/index.astro deleted file mode 100644 index bed361c36..000000000 --- a/eventcatalog/src/pages/architecture/[type]/index.astro +++ /dev/null @@ -1,14 +0,0 @@ ---- -import Architecture from '../architecture.astro'; - -export async function getStaticPaths() { - const VALID_TYPES = ['domains', 'services', 'messages'] as const; - return VALID_TYPES.map((type) => ({ - params: { type }, - })); -} - -const { type } = Astro.params; ---- - - diff --git a/eventcatalog/src/pages/architecture/architecture.astro b/eventcatalog/src/pages/architecture/architecture.astro deleted file mode 100644 index 3eef231d3..000000000 --- a/eventcatalog/src/pages/architecture/architecture.astro +++ /dev/null @@ -1,110 +0,0 @@ ---- -import { getDomains, getMessagesForDomain } from '@utils/collections/domains'; -import { getServices } from '@utils/collections/services'; -import { getContainers } from '@utils/collections/containers'; -import { getMessages } from '@utils/messages'; -import type { ExtendedDomain } from '@components/Grids/DomainGrid'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import DomainGrid from '@components/Grids/DomainGrid'; -import ServiceGrid from '@components/Grids/ServiceGrid'; -import MessageGrid from '@components/Grids/MessageGrid'; -import { removeContentFromCollection } from '@utils/collections/util'; - -import type { CollectionEntry } from 'astro:content'; -import type { CollectionMessageTypes } from '@types'; - -import { isVisualiserEnabled } from '@utils/feature'; - -import { ClientRouter, fade } from 'astro:transitions'; -// Define valid types and their corresponding data fetchers -const VALID_TYPES = ['domains', 'services', 'messages'] as const; -type ValidType = (typeof VALID_TYPES)[number]; - -interface Service extends CollectionEntry<'services'> { - sends: CollectionEntry<'events' | 'commands' | 'queries'>[]; - receives: CollectionEntry<'events' | 'commands' | 'queries'>[]; -} - -const { type, embeded = false } = Astro.props as { type: ValidType; embeded: boolean }; - -// Get data based on type -let items: Service[] | CollectionEntry<'commands'>[] | CollectionEntry[] = []; -let domains: ExtendedDomain[] = []; -let containers: CollectionEntry<'containers'>[] = []; - -const getDomainsForArchitecturePages = async () => { - const domains = await getDomains({ getAllVersions: false }); - - // Get messages for each domain - return Promise.all( - domains.map(async (domain) => { - const messages = await getMessagesForDomain(domain); - // @ts-ignore we have to remove markdown information, as it's all send to the astro components. This reduced the page size. - return { - ...domain, - sends: messages.sends.map((s) => ({ ...s, body: undefined, catalog: undefined })), - receives: messages.receives.map((r) => ({ ...r, body: undefined, catalog: undefined })), - catalog: undefined, - body: undefined, - } as ExtendedDomain; - }) - ); -}; - -if (type === 'domains' || type === 'services') { - domains = await getDomainsForArchitecturePages(); -} - -if (type === 'services') { - const services = await getServices({ getAllVersions: false }); - let filteredServices = services.map((s) => { - // @ts-ignore we have to remove markdown information, as it's all send to the astro components. This reduced the page size. - return { - ...s, - sends: (s.data.sends || []).map((s) => ({ ...s, body: undefined, catalog: undefined })), - receives: (s.data.receives || []).map((r) => ({ ...r, body: undefined, catalog: undefined })), - catalog: undefined, - body: undefined, - } as Service; - }) as unknown as Service[]; - items = filteredServices; -} else if (type === 'messages') { - const { events, commands, queries } = await getMessages({ getAllVersions: false, hydrateServices: false }); - const messages = [...events, ...commands, ...queries]; - items = removeContentFromCollection(messages) as unknown as CollectionEntry[]; - containers = await getContainers({ getAllVersions: false }); -} ---- - - -
    -
    -
    - {type === 'domains' && } - { - type === 'services' && ( - - ) - } - { - type === 'messages' && ( - []} - embeded={embeded} - containers={containers} - isVisualiserEnabled={isVisualiserEnabled()} - client:load - /> - ) - } -
    -
    - -
    -
    diff --git a/eventcatalog/src/pages/architecture/docs/[type]/index.astro b/eventcatalog/src/pages/architecture/docs/[type]/index.astro deleted file mode 100644 index f71565e2d..000000000 --- a/eventcatalog/src/pages/architecture/docs/[type]/index.astro +++ /dev/null @@ -1,14 +0,0 @@ ---- -import Architecture from '../../architecture.astro'; - -export async function getStaticPaths() { - const VALID_TYPES = ['domains', 'services', 'messages'] as const; - return VALID_TYPES.map((type) => ({ - params: { type }, - })); -} - -const { type } = Astro.params; ---- - - diff --git a/eventcatalog/src/pages/auth/login.astro b/eventcatalog/src/pages/auth/login.astro deleted file mode 100644 index fa1745c66..000000000 --- a/eventcatalog/src/pages/auth/login.astro +++ /dev/null @@ -1,280 +0,0 @@ ---- -import config from '@config'; -const { title, logo } = config; -import { join } from 'node:path'; -import { isAuthEnabled, isSSR } from '@utils/feature'; - -const catalogDirectory = process.env.PROJECT_DIR || process.cwd(); - -let hasAuthConfigurationFile = false; -let providers: string[] = []; - -try { - const authConfig = await import(/* @vite-ignore */ join(catalogDirectory, 'eventcatalog.auth.js')); - providers = Object.keys(authConfig.default.providers); - hasAuthConfigurationFile = true; -} catch (error) { - hasAuthConfigurationFile = false; -} - -// Check if we should show login (auth file exists, SSR enabled, auth enabled, and has providers) -const shouldShowLogin = hasAuthConfigurationFile && isSSR() && isAuthEnabled() && providers.length > 0; - -// Check if configuration exists but no providers are set up -const hasConfigButNoProviders = hasAuthConfigurationFile && isSSR() && isAuthEnabled() && providers.length === 0; - -// If we are not in SSR mode, redirect to home -if (!isSSR() || !isAuthEnabled()) { - return Astro.redirect('/'); -} - -// If we are in SSR mode, check if the user is already logged in -if (isSSR() && isAuthEnabled()) { - const { getSession } = await import(/* @vite-ignore */ 'auth-astro/server'); - const session = await getSession(Astro.request); - if (session) { - return Astro.redirect('/'); - } -} - -// Provider configurations -const providerConfig = { - github: { - name: 'GitHub', - icon: ` - - `, - }, - google: { - name: 'Google', - icon: ` - - - - - `, - }, - okta: { - name: 'Okta', - icon: ` - - `, - }, - auth0: { - name: 'Auth0', - icon: ``, - }, - entra: { - name: 'Microsoft', - icon: ``, - }, -}; ---- - - - - - - - - - Sign In | {title} - - - - - - -
    - -
    - { - logo && ( -
    - {logo.alt} -

    {title}

    -
    - ) - } -
    - - { - shouldShowLogin ? ( -
    -
    -
    -

    Sign in to your account

    -
    - -
    - {providers.map((provider) => { - const config = providerConfig[provider as keyof typeof providerConfig]; - if (!config) return null; - - return ( - - ); - })} -
    -
    - -
    -

    - Missing integration? - - Let us know - -

    -
    -
    - ) : hasConfigButNoProviders ? ( -
    -
    -
    -

    No Authentication Providers Configured

    -

    - Authentication is enabled but no providers are configured in your auth configuration file. -

    -
    - -
    -

    To add authentication providers:

    -
      -
    1. - Update your{' '} - eventcatalog.auth.js{' '} - file to include at least one provider (GitHub, Google, Okta, etc.) -
    2. -
    3. Configure the provider with the necessary credentials and settings
    4. -
    5. Restart your EventCatalog server to apply the changes
    6. -
    -
    - - -
    - -
    -

    - Missing integration? - - Let us know - -

    -
    -
    - ) : ( -
    -
    -
    -

    Authentication Not Configured

    -

    - {!hasAuthConfigurationFile - ? 'No authentication configuration file found.' - : 'Authentication is not properly enabled.'} -

    -
    - -
    -

    To enable authentication:

    -
      - {!hasAuthConfigurationFile && ( -
    1. - Create an{' '} - eventcatalog.auth.js{' '} - configuration file in your project root -
    2. - )} - {!isSSR() && ( -
    3. - Enable SSR (Server-Side Rendering) in your{' '} - - eventcatalog.config.js - {' '} - file by setting{' '} - output: 'server' -
    4. - )} - {!isAuthEnabled() && ( -
    5. - Enable authentication in your{' '} - - eventcatalog.config.js - {' '} - file by setting{' '} - - auth: {`{ enabled: true }`} - -
    6. - )} -
    -
    - - -
    - -
    -

    - Missing integration? - - Let us know - -

    -
    -
    - ) - } -
    - - - - diff --git a/eventcatalog/src/pages/chat/feature.astro b/eventcatalog/src/pages/chat/feature.astro deleted file mode 100644 index cf8a0d167..000000000 --- a/eventcatalog/src/pages/chat/feature.astro +++ /dev/null @@ -1,179 +0,0 @@ ---- -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { isEventCatalogChatEnabled as hasEventCatlaogChatLicense, isSSR } from '@utils/feature'; -import { BotMessageSquare } from 'lucide-react'; -const hasChatLicense = hasEventCatlaogChatLicense(); - -if (hasChatLicense) { - return Astro.redirect('/chat'); -} ---- - - - - - - - - - EventCatalog chat? - - - -
    -
    - {/* Hero Section */} -
    -
    -
    - - EventCatalog: Agent -
    -

    Ask. Understand. Ship faster.

    -

    - Get answers about your architecture — instantly. Connect to your own AI models and data. -

    - - -

    Available with EventCatalog Starter or Scale plans

    - - { - !isSSR() && ( -

    - This feature is only available on server side. You can switch to server side by - setting the output property to{' '} - server in your{' '} - eventcatalog.config.js file. -

    - ) - } -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - What services publish order.created? -
    -
    - -
    -
    -

    - The Order Service publishes the order.created event. -

    -

    This event is consumed by:

    -
      -
    • • Payment Service - Initiates payment processing
    • -
    • • Inventory Service - Updates stock levels
    • -
    • • Notification Service - Sends order confirmations
    • -
    -
    -
    -
    -
    -
    -
    -
    - - {/* Features Section */} -
    -
    -
    - - - -
    -

    Direct Answers

    -

    - Ask questions about your catalog and get direct answers, using your own models and API keys. -

    -
    - -
    -
    - - - -
    -

    Smart Insights

    -

    Get intelligent suggestions and insights about your architecture automatically.

    -
    - -
    -
    - - - -
    -

    Privacy First

    -

    - Runs on your own infrastructure, and your own models. Provide your own API keys to get started. -

    -
    -
    - - {/* Bottom Link */} - -
    -
    -
    - - - - diff --git a/eventcatalog/src/pages/chat/index.astro b/eventcatalog/src/pages/chat/index.astro deleted file mode 100644 index d8b049a98..000000000 --- a/eventcatalog/src/pages/chat/index.astro +++ /dev/null @@ -1,10 +0,0 @@ ---- -import ChatPage from '@enterprise/eventcatalog-chat/pages/chat/index.astro'; -import { isEventCatalogChatEnabled } from '@utils/feature'; -import { buildUrl } from '@utils/url-builder'; -if (!isEventCatalogChatEnabled()) { - return Astro.redirect(buildUrl('/chat/feature')); -} ---- - - diff --git a/eventcatalog/src/pages/directory/[type]/_index.data.ts b/eventcatalog/src/pages/directory/[type]/_index.data.ts deleted file mode 100644 index d75e8a0c7..000000000 --- a/eventcatalog/src/pages/directory/[type]/_index.data.ts +++ /dev/null @@ -1,63 +0,0 @@ -// pages/directory/[type]/index.page.ts -import { isSSR } from '@utils/feature'; -import { HybridPage } from '@utils/page-loaders/hybrid-page'; - -export class Page extends HybridPage { - static get prerender(): boolean { - return !isSSR(); - } - - static async getStaticPaths(): Promise> { - const { getUsers } = await import('@utils/users'); - const { getTeams } = await import('@utils/teams'); - - const loaders = { - users: getUsers, - teams: getTeams, - }; - - const itemTypes = ['users', 'teams'] as const; - const allItems = await Promise.all(itemTypes.map((type) => loaders[type]())); - - return allItems.flatMap((items, index) => ({ - params: { - type: itemTypes[index], - }, - props: { - data: items, - type: itemTypes[index], - }, - })); - } - - protected static async fetchData(params: any) { - const { type } = params; - - if (!type) { - return null; - } - - const { getUsers } = await import('@utils/users'); - const { getTeams } = await import('@utils/teams'); - - const loaders = { - users: getUsers, - teams: getTeams, - }; - - // Get all items of the specified type - const items = await loaders[type as keyof typeof loaders](); - - return { - type, - data: items, - }; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'Directory type not found', - }); - } -} diff --git a/eventcatalog/src/pages/directory/[type]/index.astro b/eventcatalog/src/pages/directory/[type]/index.astro deleted file mode 100644 index a25253bf8..000000000 --- a/eventcatalog/src/pages/directory/[type]/index.astro +++ /dev/null @@ -1,55 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; - -import DirectoryLayout, { type Props as DirectoryLayoutProps } from '@layouts/DirectoryLayout.astro'; -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -const { type, data } = await Page.getData(Astro); - -function mapToItem(i: any) { - return { - collection: i.collection, - data: { - id: i.data.id, - name: i.data.name, - version: i.data.version, - }, - }; -} ---- - -) => - ({ - collection: d.collection, - data: { - // @ts-ignore - id: d.data.id, - name: d.data.name, - // @ts-ignore - role: d.data?.role, - // @ts-ignore - avatarUrl: d.data?.avatarUrl, - // @ts-ignore - associatedTeams: d.data?.associatedTeams?.map(mapToItem) ?? [], - // @ts-ignore - ownedCommands: d.data?.ownedCommands?.map(mapToItem) ?? [], - // @ts-ignore - ownedQueries: d.data?.ownedQueries?.map(mapToItem) ?? [], - // @ts-ignore - ownedEvents: d.data?.ownedEvents?.map(mapToItem) ?? [], - // @ts-ignore - ownedServices: d.data?.ownedServices?.map(mapToItem) ?? [], - // @ts-ignore - members: d.data?.members, - }, - }) as DirectoryLayoutProps['data'][0] - )} - type={type} -/> diff --git a/eventcatalog/src/pages/discover/[type]/_index.data.ts b/eventcatalog/src/pages/discover/[type]/_index.data.ts deleted file mode 100644 index a37659f1c..000000000 --- a/eventcatalog/src/pages/discover/[type]/_index.data.ts +++ /dev/null @@ -1,62 +0,0 @@ -// pages/discover/[type]/index.page.ts -import { isSSR } from '@utils/feature'; -import { HybridPage } from '@utils/page-loaders/hybrid-page'; -import { pageDataLoader } from '@utils/page-loaders/page-data-loader'; - -export class Page extends HybridPage { - static get prerender(): boolean { - return !isSSR(); - } - - static async getStaticPaths(): Promise> { - const { getFlows } = await import('@utils/collections/flows'); - - const loaders = { - ...pageDataLoader, - flows: getFlows, - }; - - const itemTypes = ['events', 'commands', 'queries', 'services', 'domains', 'flows', 'containers'] as const; - const allItems = await Promise.all(itemTypes.map((type) => loaders[type]())); - - return allItems.flatMap((items, index) => ({ - params: { - type: itemTypes[index], - }, - props: { - data: items, - type: itemTypes[index], - }, - })); - } - - protected static async fetchData(params: any) { - const { type } = params; - - if (!type) { - return null; - } - - const { getFlows } = await import('@utils/collections/flows'); - - const loaders = { - ...pageDataLoader, - flows: getFlows, - }; - - // @ts-ignore - const items = await loaders[type](); - - return { - type, - data: items, - }; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'Collection type not found', - }); - } -} diff --git a/eventcatalog/src/pages/discover/[type]/index.astro b/eventcatalog/src/pages/discover/[type]/index.astro deleted file mode 100644 index 120235973..000000000 --- a/eventcatalog/src/pages/discover/[type]/index.astro +++ /dev/null @@ -1,63 +0,0 @@ ---- -import type { CollectionEntry } from 'astro:content'; -import type { CollectionTypes } from '@types'; -import DiscoverLayout, { type Props as DiscoverLayoutProps } from '@layouts/DiscoverLayout.astro'; -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -const { type, data } = await Page.getData(Astro); - -let title = `${type} (${data.length})`; - -if (type === 'containers') { - title = `Data (${data.length})`; -} - -function mapToItem(i: any) { - return { - collection: i.collection, - data: { - id: i.data.id, - name: i.data.name, - version: i.data.version, - }, - }; -} ---- - -) => - ({ - collection: d.collection, - data: { - id: d.data.id, - name: d.data.name, - summary: d.data?.summary, - version: d.data.version, - latestVersion: d.data?.latestVersion, - draft: d.data?.draft, - badges: d.data?.badges, - // @ts-ignore - consumers: d.data?.consumers?.map(mapToItem) ?? [], - // @ts-ignore - producers: d.data?.producers?.map(mapToItem) ?? [], - // @ts-ignore - receives: d.data?.receives?.map(mapToItem) ?? [], - // @ts-ignore - sends: d.data?.sends?.map(mapToItem) ?? [], - // @ts-ignore - services: d.data?.services?.map(mapToItem) ?? [], - // @ts-ignore - servicesThatWriteToContainer: d.data?.servicesThatWriteToContainer?.map(mapToItem) ?? [], - // @ts-ignore - servicesThatReadFromContainer: d.data?.servicesThatReadFromContainer?.map(mapToItem) ?? [], - }, - }) as DiscoverLayoutProps['data'][0] - )} - type={type} -/> diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts b/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts deleted file mode 100644 index 43bb00f1c..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts +++ /dev/null @@ -1,59 +0,0 @@ -// This file exposes the markdown for EventCatalog in the Url -// For example http://localhost:3000/docs/events/OrderAmended/0.0.1 loads the Page and http://localhost:3000/docs/events/OrderAmended/0.0.1.md loads the markdown -// This is used for the LLMs to load the markdown for the given item (llms.txt); - -import type { APIRoute } from 'astro'; -import { getCollection } from 'astro:content'; -import { getEntities } from '@utils/entities'; -import config from '@config'; -import fs from 'fs'; - -const events = await getCollection('events'); -const commands = await getCollection('commands'); -const queries = await getCollection('queries'); -const services = await getCollection('services'); -const domains = await getCollection('domains'); -const flows = await getCollection('flows'); -const channels = await getCollection('channels'); -const containers = await getCollection('containers'); -const entities = await getEntities(); -export async function getStaticPaths() { - // Just return empty array if LLMs are not enabled - if (!config.llmsTxt?.enabled) { - return []; - } - - const collections = { - events, - commands, - queries, - services, - domains, - flows, - channels, - containers, - entities, - }; - const paths = Object.keys(collections).map((type) => { - return collections[type as keyof typeof collections].map((item: { data: { id: string; version: string } }) => ({ - params: { type, id: item.data.id, version: item.data.version }, - props: { content: item }, - })); - }); - - return paths.flat(); -} - -export const GET: APIRoute = async ({ params, props }) => { - // Just return empty array if LLMs are not enabled - if (!config.llmsTxt?.enabled) { - return new Response('llms.txt is not enabled for this Catalog.', { status: 404 }); - } - - if (props?.content?.filePath) { - const file = fs.readFileSync(props.content.filePath, 'utf8'); - return new Response(file, { status: 200 }); - } - - return new Response('Not found', { status: 404 }); -}; diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version].mdx.ts b/eventcatalog/src/pages/docs/[type]/[id]/[version].mdx.ts deleted file mode 100644 index c4f12d1a2..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version].mdx.ts +++ /dev/null @@ -1,76 +0,0 @@ -// This file exposes the markdown for EventCatalog in the Url -// For example http://localhost:3000/docs/events/OrderAmended/0.0.1 loads the Page and http://localhost:3000/docs/events/OrderAmended/0.0.1.md loads the markdown -// This is used for the LLMs to load the markdown for the given item (llms.txt); - -import type { APIRoute } from 'astro'; -import { getCollection } from 'astro:content'; -import config from '@config'; -import fs from 'fs'; -import { addSchemaToMarkdown } from '@utils/llms'; -import { isLLMSTxtEnabled, isSSR } from '@utils/feature'; -const events = await getCollection('events'); -const commands = await getCollection('commands'); -const queries = await getCollection('queries'); -const services = await getCollection('services'); -const domains = await getCollection('domains'); -const flows = await getCollection('flows'); -const channels = await getCollection('channels'); -const entities = await getCollection('entities'); - -import utils from '@eventcatalog/sdk'; - -export async function getStaticPaths() { - // Just return empty array if LLMs are not enabled - if (!config.llmsTxt?.enabled) { - return []; - } - const collections = { - events, - commands, - queries, - services, - domains, - flows, - channels, - entities, - }; - const paths = Object.keys(collections).map((type) => { - return collections[type as keyof typeof collections].map((item: { data: { id: string; version: string } }) => ({ - params: { type, id: item.data.id, version: item.data.version }, - props: { content: item }, - })); - }); - - return paths.flat(); -} - -export const GET: APIRoute = async ({ params, props }) => { - // Just return empty array if LLMs are not enabled - if (!isLLMSTxtEnabled()) { - return new Response('llms.txt is not enabled for this Catalog.', { status: 404 }); - } - - if (isSSR()) { - const { getResourcePath } = utils(process.env.PROJECT_DIR ?? ''); - const filePath = await getResourcePath(process.env.PROJECT_DIR ?? '', params.id ?? '', params.version ?? ''); - if (!filePath) { - return new Response('Not found', { status: 404 }); - } - const file = fs.readFileSync(filePath.fullPath, 'utf8'); - return new Response(file, { status: 200 }); - } else { - if (props?.content?.filePath) { - let file = fs.readFileSync(props.content.filePath, 'utf8'); - - try { - file = addSchemaToMarkdown(props.content, file); - } catch (error) { - console.log('Warning: Cant find the schema for', props.content.data.id, props.content.data.version); - } - - return new Response(file, { status: 200 }); - } - } - - return new Response('Not found', { status: 404 }); -}; diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts b/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts deleted file mode 100644 index 9f2eecdd7..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { isSSR } from '@utils/feature'; -import { HybridPage } from '@utils/page-loaders/hybrid-page'; -import type { PageTypes } from '@types'; -import { pageDataLoader } from '@utils/page-loaders/page-data-loader'; - -/** - * Documentation page class for all collection types with versioning - */ -export class Page extends HybridPage { - static async getStaticPaths() { - if (isSSR()) { - return []; - } - - const itemTypes: PageTypes[] = [ - 'events', - 'commands', - 'queries', - 'services', - 'domains', - 'flows', - 'channels', - 'entities', - 'containers', - ]; - const allItems = await Promise.all(itemTypes.map((type) => pageDataLoader[type]())); - - return allItems.flatMap((items, index) => - items.map((item) => ({ - params: { - type: itemTypes[index], - id: item.data.id, - version: item.data.version, - }, - props: { - type: itemTypes[index], - ...item, - }, - })) - ); - } - - protected static async fetchData(params: any) { - const { type, id, version } = params; - - if (!type || !id || !version) { - return null; - } - - // Get all items of the specified type - const items = await pageDataLoader[type as PageTypes](); - - // Find the specific item by id and version - const item = items.find((i) => i.data.id === id && i.data.version === version); - - if (!item) { - return null; - } - - return { - type, - ...item, - }; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'Documentation not found', - }); - } -} diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro b/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro deleted file mode 100644 index ad0c285c7..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +++ /dev/null @@ -1,89 +0,0 @@ ---- -import { createElement } from 'react'; -import { renderToString } from 'react-dom/server'; -import { Parser } from '@asyncapi/parser'; -import { AvroSchemaParser } from '@asyncapi/avro-schema-parser'; -import fs from 'fs'; - -import '@asyncapi/react-component/styles/default.min.css'; -import js from '@asyncapi/react-component/browser/standalone/without-parser.js?url'; -import { AsyncApiComponentWP, type ConfigInterface } from '@asyncapi/react-component'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import Config from '@utils/eventcatalog-config/catalog'; -import { Page } from './_[filename].data'; -import { getAbsoluteFilePathForAstroFile } from '@utils/files'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const { collection, data, filePath, filename, path: relativeSpecPath } = await Page.getData(Astro); - -const fileName = filename || 'asyncapi.yaml'; -const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName); -const fileExists = fs.existsSync(pathToSpec); -const isRemote = relativeSpecPath.includes('https://'); -let content = ''; - -if (fileExists && !isRemote) { - content = fs.readFileSync(pathToSpec, 'utf8'); -} - -if (isRemote) { - // Fetch the content from the remote path - content = await fetch(relativeSpecPath).then((res) => res.text()); -} - -// AsyncAPI parser will parser schemas for users, they can turn this off. -const parseSchemas = Config?.asyncAPI?.renderParsedSchemas ?? true; -const parsed = await new Parser({ schemaParsers: [AvroSchemaParser()] }).parse(content, { parseSchemas }); -const stringified = parsed.document?.json(); -const config: ConfigInterface = { show: { sidebar: true, errors: true } }; - -const component = createElement(AsyncApiComponentWP, { schema: { stringified }, config }); -const renderedComponent = renderToString(component); - -// Capitalize the first letter of a string -const pageTitle = `${collection} | ${data.name} | AsyncApi Spec`.replace(/^\w/, (c) => c.toUpperCase()); - -// Index only the latest version -const pagefindAttributes = - data.version === data.latestVersion - ? { - 'data-pagefind-body': '', - 'data-pagefind-meta': `title:${pageTitle}`, - } - : {}; ---- - - -
    - { - // Currently, Pagefind does not index metadata (such as the title), - // so we need to ensure it is included as text on the page. - // https://github.com/CloudCannon/pagefind/issues/437 - } - -
    -
    - - - - - - diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts b/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts deleted file mode 100644 index 6acd48456..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { isSSR } from '@utils/feature'; -import { HybridPage } from '@utils/page-loaders/hybrid-page'; -import type { PageTypes } from '@types'; - -export class Page extends HybridPage { - static get prerender(): boolean { - return !isSSR(); - } - - static async getStaticPaths(): Promise> { - if (isSSR()) { - return []; - } - - const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader'); - - const itemTypes: PageTypes[] = ['events', 'commands', 'queries', 'services', 'domains', 'flows', 'containers']; - const allItems = await Promise.all(itemTypes.map((type) => pageDataLoader[type]())); - - return allItems.flatMap((items, index) => - items.map((item) => ({ - params: { - type: itemTypes[index], - id: item.data.id, - version: item.data.version, - }, - props: { - type: itemTypes[index], - allCollectionItems: items, - ...item, - }, - })) - ); - } - - protected static async fetchData(params: any) { - const { type, id, version } = params; - - if (!type || !id || !version) { - return null; - } - - const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader'); - - // Get all items of the specified type - const items = await pageDataLoader[type as PageTypes](); - - // Find the specific item by id and version - const item = items.find((i) => i.data.id === id && i.data.version === version); - - if (!item) { - return null; - } - - return { - type, - allCollectionItems: items, - ...item, - }; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'Changelog not found', - }); - } -} diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro b/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro deleted file mode 100644 index 1f9af4f40..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +++ /dev/null @@ -1,225 +0,0 @@ ---- -import Footer from '@layouts/Footer.astro'; -import { getChangeLogs } from '@utils/collections/changelogs'; -import { - RectangleGroupIcon, - ServerIcon, - BoltIcon, - ChatBubbleLeftIcon, - MagnifyingGlassIcon, - QueueListIcon, -} from '@heroicons/react/24/outline'; -import { DatabaseIcon } from 'lucide-react'; -import { render, getEntry } from 'astro:content'; -import mdxComponents from '@components/MDX/components'; -import 'diff2html/bundles/css/diff2html.min.css'; - -import { buildUrl } from '@utils/url-builder'; -import { getPreviousVersion } from '@utils/collections/util'; -import { getDiffsForCurrentAndPreviousVersion } from '@utils/collections/file-diffs'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { ClientRouter } from 'astro:transitions'; -import { isChangelogEnabled } from '@utils/feature'; - -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const props = await Page.getData(Astro); - -if (!isChangelogEnabled()) { - return Astro.redirect('/docs'); -} - -let collectionItem = props; -const logs = await getChangeLogs(props); - -const { data } = props; -const latestVersion = data.latestVersion; - -const renderedLogs = logs.map(async (log) => { - const logEntry = await getEntry('changelogs', log.id); - const { Content } = await render(logEntry as any); - return { - Content, - ...log, - }; -}); - -const logsToRender = await Promise.all(renderedLogs); - -const logListPromise = logsToRender.map(async (log, index) => { - const currentLogVersion = log.data.version; - const previousLogVersion = log.data.version ? getPreviousVersion(log.data.version, data.versions || []) : ''; - return { - id: log.id, - url: buildUrl(`/docs/${props.collection}/${props.data.id}`), - isLatest: log.data.version === latestVersion, - version: log.data.version, - createdAt: log.data.createdAt, - badges: log.data.badges || [], - Content: log.Content, - diffs: - currentLogVersion && previousLogVersion - ? await getDiffsForCurrentAndPreviousVersion( - currentLogVersion, - previousLogVersion, - collectionItem.data.id, - props.allCollectionItems - ) - : [], - }; -}); - -const logList = await Promise.all(logListPromise); - -const getBadge = () => { - if (props.collection === 'services') { - return { backgroundColor: 'pink', textColor: 'pink', content: 'Service', icon: ServerIcon, class: 'text-pink-400' }; - } - if (props.collection === 'events') { - return { backgroundColor: 'orange', textColor: 'orange', content: 'Event', icon: BoltIcon, class: 'text-orange-400' }; - } - if (props.collection === 'commands') { - return { backgroundColor: 'blue', textColor: 'blue', content: 'Command', icon: ChatBubbleLeftIcon, class: 'text-blue-400' }; - } - if (props.collection === 'queries') { - return { backgroundColor: 'green', textColor: 'green', content: 'Query', icon: MagnifyingGlassIcon, class: 'text-blue-400' }; - } - if (props.collection === 'domains') { - return { - backgroundColor: 'yellow', - textColor: 'yellow', - content: 'Domain', - icon: RectangleGroupIcon, - class: 'text-yellow-400', - }; - } - if (props.collection === 'containers') { - return { backgroundColor: 'blue', textColor: 'blue', content: 'Container', icon: DatabaseIcon, class: 'text-blue-400' }; - } - if (props.collection === 'flows') { - return { backgroundColor: 'teal', textColor: 'teal', content: 'Flow', icon: QueueListIcon, class: 'text-teal-400' }; - } -}; - -const badges = [getBadge()]; ---- - - -
    - -
    -
    -

    {props.data.name} (Changelog)

    -

    {props.data.summary}

    - { - badges && ( -
    - {badges.map((badge: any) => ( - - {badge.icon && } - {badge.content} - - ))} -
    - ) - } -
    -
    - { - logList.length === 0 && ( -
    -

    No changelogs found.

    -
    - ) - } -
    -
      - { - logList.map((log, index) => ( -
    • -
      - {index !== logList.length - 1 ? ( -
    • - )) - } -
    -
    -
    -
    - -
    diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro b/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro deleted file mode 100644 index 2aa5b7fdc..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +++ /dev/null @@ -1,183 +0,0 @@ ---- -import { Code } from 'astro-expressive-code/components'; -import fs from 'fs'; - -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import Footer from '@layouts/Footer.astro'; -import { Page } from './_[filename].data'; -import { getAbsoluteFilePathForAstroFile } from '@utils/files'; -import { buildUrl, buildEditUrlForResource } from '@utils/url-builder'; -import Admonition from '@components/MDX/Admonition'; - -import { ServerIcon } from '@heroicons/react/24/outline'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const { collection, data, filePath, filename, path: relativeSpecPath } = await Page.getData(Astro); - -const fileName = filename || 'schema.graphql'; -const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName); -const fileExists = fs.existsSync(pathToSpec); -const isRemote = relativeSpecPath.includes('https://'); -let content = ''; - -if (fileExists && !isRemote) { - content = fs.readFileSync(pathToSpec, 'utf8'); -} - -if (isRemote) { - // Fetch the content from the remote path - content = await fetch(relativeSpecPath).then((res) => res.text()); -} - -// Create comprehensive page title -const pageTitle = `${collection} | ${data.name} | GraphQL Schema`.replace(/^\w/, (c) => c.toUpperCase()); - -const getServiceBadge = () => { - return [{ backgroundColor: 'pink', textColor: 'pink', content: 'Service', icon: ServerIcon, class: 'text-pink-400' }]; -}; - -const getGraphQLBadge = () => { - return [ - { - backgroundColor: 'white', - textColor: 'gray', - content: 'GraphQL Schema', - iconURL: buildUrl('/icons/graphql.svg', true), - class: 'text-black', - id: 'graphql-schema-badge', - }, - ]; -}; - -const badges = [...getServiceBadge(), ...getGraphQLBadge()]; - -// Index only the latest version -const pagefindAttributes = - data.version === data.latestVersion - ? { - 'data-pagefind-body': '', - 'data-pagefind-meta': `title:${pageTitle}`, - } - : {}; ---- - - -
    -
    -
    -
    -
    -
    -
    -

    - {data.name} - (v{data.version}) -

    -

    GraphQL Schema

    -
    -
    - - { - badges && ( -
    - {badges.map((badge: any) => ( - - {badge.icon && } - {badge.iconURL && } - {badge.content} - - ))} -
    - ) - } -
    -
    - -
    - { - data.version !== data.latestVersion && ( -
    -
    -
    - -
    -
    -

    New version found

    -
    -

    - You are looking at a previous version of the service {data.name}.{' '} - - The latest version of this GraphQL schema is - v{data.latestVersion} → - -

    -
    -
    -
    -
    - ) - } -
    - - { - !fileExists && ( - -

    The GraphQL schema file could not be found at the expected location.

    -
    - ) - } - - { - fileExists && content && ( -
    -
    -
    - GraphQL -

    GraphQL Schema

    -
    -

    - This schema defines the GraphQL API structure including types, queries, mutations, and subscriptions for{' '} - {data.name}. -

    -
    - -
    - -
    -
    - ) - } - -
    -
    -
    -
    - - -
    diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro b/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro deleted file mode 100644 index 59cf81312..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +++ /dev/null @@ -1,659 +0,0 @@ ---- -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import Footer from '@layouts/Footer.astro'; -import { marked } from 'marked'; - -import components from '@components/MDX/components'; -import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro'; -import SchemaViewer from '@components/MDX/SchemaViewer/SchemaViewerRoot.astro'; -import Admonition from '@components/MDX/Admonition'; - -import { getSpecificationsForService } from '@utils/collections/services'; - -import { resourceToCollectionMap, collectionToResourceMap } from '@utils/collections/util'; - -// SideBars -import ServiceSideBar from '@components/SideBars/ServiceSideBar.astro'; -import MessageSideBar from '@components/SideBars/MessageSideBar.astro'; -import DomainSideBar from '@components/SideBars/DomainSideBar.astro'; -import ChannelSideBar from '@components/SideBars/ChannelSideBar.astro'; -import FlowSideBar from '@components/SideBars/FlowSideBar.astro'; -import EntitySideBar from '@components/SideBars/EntitySideBar.astro'; -import ContainerSideBar from '@components/SideBars/ContainerSideBar.astro'; -import CopyAsMarkdown from '@components/CopyAsMarkdown'; - -import { - QueueListIcon, - RectangleGroupIcon, - ServerIcon, - BoltIcon, - ChatBubbleLeftIcon, - MagnifyingGlassIcon, - MapIcon, - ClockIcon, -} from '@heroicons/react/24/outline'; -import { ArrowsRightLeftIcon } from '@heroicons/react/20/solid'; -import { Box, Boxes, SquarePenIcon, DatabaseIcon, DatabaseZapIcon, ShieldCheckIcon } from 'lucide-react'; -import type { CollectionTypes } from '@types'; - -import { render } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; - -import { getIcon } from '@utils/badges'; -import { getDeprecatedDetails } from '@utils/collections/util'; -import { buildUrl, buildEditUrlForResource } from '@utils/url-builder'; -import { getSchemasFromResource } from '@utils/collections/schemas'; -import { isEventCatalogChatEnabled, isMarkdownDownloadEnabled, isVisualiserEnabled } from '@utils/feature'; - -import { getMDXComponentsByName } from '@utils/markdown'; - -import config from '@config'; -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const props = await Page.getData(Astro); - -const { Content } = await render(props); - -const pageTitle = `${props.collection} | ${props.data.name}`.replace(/^\w/, (c) => c.toUpperCase()); -const contentBadges = props.data.badges || []; -const editUrl = - props.data.editUrl || (config.editUrl && props?.filePath ? buildEditUrlForResource(config.editUrl, props?.filePath) : ''); - -const getContentBadges = () => - contentBadges.map((badge: any) => ({ - ...badge, - icon: badge.icon ? getIcon(badge.icon) : null, - })); - -const getBadge = () => { - if (props.collection === 'services') { - return [{ backgroundColor: 'pink', textColor: 'pink', content: 'Service', icon: ServerIcon, class: 'text-pink-400' }]; - } - if (props.collection === 'events') { - return [{ backgroundColor: 'orange', textColor: 'orange', content: 'Event', icon: BoltIcon, class: 'text-orange-400' }]; - } - if (props.collection === 'commands') { - return [{ backgroundColor: 'blue', textColor: 'blue', content: 'Command', icon: ChatBubbleLeftIcon, class: 'text-blue-400' }]; - } - if (props.collection === 'queries') { - return [ - { backgroundColor: 'green', textColor: 'green', content: 'Query', icon: MagnifyingGlassIcon, class: 'text-green-400' }, - ]; - } - if (props.collection === 'domains') { - return [ - { - backgroundColor: 'yellow', - textColor: 'yellow', - content: 'Domain', - icon: RectangleGroupIcon, - class: 'text-yellow-400', - }, - ]; - } - - if (props.collection === 'flows') { - return [{ backgroundColor: 'teal', textColor: 'teal', content: 'Flow', icon: QueueListIcon, class: 'text-gray' }]; - } - - if (props.collection === 'channels') { - return [{ backgroundColor: 'teal', textColor: 'teal', content: 'Channel', icon: ArrowsRightLeftIcon, class: 'text-gray' }]; - } - - if (props.collection === 'containers') { - const badges = []; - const content = props.data.container_type?.charAt(0).toUpperCase() + props.data.container_type?.slice(1) || 'Database'; - - badges.push({ backgroundColor: 'blue', textColor: 'blue', content: content, icon: DatabaseIcon, class: 'text-gray' }); - - if (props.data?.technology) { - badges.push({ - backgroundColor: 'indigo', - textColor: 'indigo', - content: `${props.data.technology}`, - icon: DatabaseZapIcon, - }); - } - - if (props.data?.residency) { - badges.push({ - backgroundColor: 'red', - textColor: 'red', - content: `Residency: ${props.data.residency}`, - icon: MapIcon, - }); - } - - if (props.data?.retention) { - badges.push({ - backgroundColor: 'green', - textColor: 'green', - content: `Retention: ${props.data.retention}`, - icon: ClockIcon, - }); - } - - if (props.data?.access_mode) { - badges.push({ - backgroundColor: 'green', - textColor: 'green', - content: `Access Mode: ${props.data.access_mode}`, - icon: ShieldCheckIcon, - }); - } - - return badges; - } - - if (props.collection === 'entities') { - const entityBadges = [{ backgroundColor: 'purple', textColor: 'purple', content: 'Entity', icon: Box, class: 'text-gray' }]; - if (props.data.aggregateRoot) { - entityBadges.push({ - backgroundColor: 'purple', - textColor: 'purple', - content: '(Aggregate Root)', - icon: Boxes, - class: 'text-gray', - }); - } - return entityBadges; - } - - return [{ backgroundColor: 'teal', textColor: 'teal', content: '', icon: QueueListIcon, class: 'text-gray' }]; -}; - -const getSpecificationBadges = () => { - const badges = []; - - const specifications = getSpecificationsForService(props); - - const asyncapiSpecs = specifications.filter((spec) => spec.type === 'asyncapi'); - const openapiSpecs = specifications.filter((spec) => spec.type === 'openapi'); - const graphQLSpecs = specifications.filter((spec) => spec.type === 'graphql'); - - if (openapiSpecs.length > 0) { - for (const spec of openapiSpecs) { - badges.push({ - backgroundColor: 'white', - textColor: 'gray', - content: spec.name || 'OpenAPI Spec', - iconURL: buildUrl('/icons/openapi.svg', true), - class: 'text-black hover:underline', - id: 'open-api-badge', - url: buildUrl(`/docs/${props.collection}/${props.data.id}/${props.data.version}/spec/${spec.filenameWithoutExtension}`), - }); - } - } - - if (asyncapiSpecs.length > 0) { - for (const spec of asyncapiSpecs) { - badges.push({ - backgroundColor: 'white', - textColor: 'gray', - content: spec.name || 'AsyncAPI Spec', - iconURL: buildUrl('/icons/asyncapi.svg', true), - class: 'text-black hover:underline', - id: 'asyncapi-badge', - url: buildUrl( - `/docs/${props.collection}/${props.data.id}/${props.data.version}/asyncapi/${spec.filenameWithoutExtension}` - ), - }); - } - } - - if (graphQLSpecs.length > 0) { - for (const spec of graphQLSpecs) { - badges.push({ - backgroundColor: 'white', - textColor: 'gray', - content: spec.name || 'GraphQL Spec', - iconURL: buildUrl('/icons/graphql.svg', true), - class: 'text-black hover:underline', - id: 'graphql-badge', - url: buildUrl( - `/docs/${props.collection}/${props.data.id}/${props.data.version}/graphql/${spec.filenameWithoutExtension}` - ), - }); - } - } - - return badges; -}; - -const badges = [...getBadge(), ...getContentBadges(), ...getSpecificationBadges()]; - -// Index only the latest version -const pagefindAttributes = - props.data.version === props.data.latestVersion - ? { - 'data-pagefind-body': '', - 'data-pagefind-meta': `title:${pageTitle}`, - } - : {}; - -const { - hasDeprecated, - message: deprecatedMessage, - isMarkedAsDeprecated, - deprecatedDate: formattedDate, -} = getDeprecatedDetails(props as unknown as CollectionEntry); - -let friendlyCollectionName = props.collection.slice(0, props.collection.length - 1); -friendlyCollectionName = friendlyCollectionName === 'querie' ? 'query' : friendlyCollectionName; - -const schemasForResource = getSchemasFromResource(props); - -const generatePromptForResource = (props: any) => { - return ` - Please tell me more about the ${props.collection.slice(0, props.collection.length - 1)} - ${props.data.name} v${props.data.version} in this catalog. - `; -}; - -// Handle node graphs in the markdown -let nodeGraphs = getMDXComponentsByName(props.body || '', 'NodeGraph') || []; - -// Get props for the node graph (when no id is passed, we assume its the current page) -const nodeGraphPropsForPage = nodeGraphs.find((nodeGraph: any) => nodeGraph.id === undefined) || ({} as any); - -// This will render the graph for this page -nodeGraphs.push({ - id: props.data.id, - version: props.data.version, - type: collectionToResourceMap[props.collection as keyof typeof collectionToResourceMap], - ...nodeGraphPropsForPage, - search: nodeGraphPropsForPage?.search ? nodeGraphPropsForPage.search === 'true' : true, - legend: nodeGraphPropsForPage?.legend ? nodeGraphPropsForPage.legend === 'true' : true, -}); ---- - - -
    -
    -
    -
    -
    -
    -

    - {props.data.name} - (v{props.data.version}) -

    - -
    - { - isMarkedAsDeprecated && hasDeprecated && ( - -
    - {!deprecatedMessage && ( -

    - The {friendlyCollectionName} has been marked as deprecated - {formattedDate && ` on ${formattedDate}`}. -

    - )} - {deprecatedMessage &&
    } -
    - - ) - } - { - isMarkedAsDeprecated && !hasDeprecated && ( - -
    - {!deprecatedMessage && ( -

    - The {friendlyCollectionName} will be deprecated on {formattedDate}. -

    - )} - {deprecatedMessage &&
    } -
    - - ) - } - -

    {props.data.summary}

    - { - badges && ( -
    - {badges.map((badge: any) => { - return ( - - - {badge.icon && ( - - )} - {badge.iconURL && ( - - )} - - {badge.content} - - - - ); - })} -
    - ) - } - - { - props.data.draft && ( - -
    - {!props.data.draft.message && ( -

    This resource has been marked as a draft and may still be under development.

    - )} - {props.data.draft.message && ( -
    - )} -
    - - ) - } -
    -
    - -
    - { - props.data.version !== props.data.latestVersion && ( -
    -
    -
    - -
    -
    -

    New version found

    -
    -

    - You are looking at a previous version of the {props.collection.slice(0, props.collection.length - 1)}{' '} - {props.data.name}.{' '} - - The latest version of this {props.collection.slice(0, props.collection.length - 1)} is - v{props.data.latestVersion} → - -

    -
    -
    -
    -
    - ) - } -
    - -
    - -
    -
    - - - - - { - nodeGraphs.length > 0 && - nodeGraphs.map((nodeGraph: any) => { - const collection = resourceToCollectionMap[nodeGraph.type as keyof typeof resourceToCollectionMap]; - return ( - - ); - }) - } -
    -
    - -
    -
    - - - - - - - - -
    diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro b/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro deleted file mode 100644 index ffdab2071..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +++ /dev/null @@ -1,71 +0,0 @@ ---- -import fs from 'node:fs'; -import OpenAPISpec from './_OpenAPI.tsx'; - -import { DocumentMinusIcon } from '@heroicons/react/24/outline'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import './_styles.css'; -import { Page } from './_[filename].data.ts'; -import { getAbsoluteFilePathForAstroFile } from '@utils/files'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const props = await Page.getData(Astro); - -// @ts-ignore -const { collection, data, catalog, filePath, filename, path: relativeSpecPath } = props; -const fileName = filename || 'openapi.yml'; - -const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName); -const fileExists = fs.existsSync(pathToSpec); -const isRemote = relativeSpecPath.includes('https://'); - -let content = ''; - -// Capitalize the first letter of a string -const pageTitle = `${collection} | ${data.name} | OpenAPI Spec`.replace(/^\w/, (c) => c.toUpperCase()); - -// Index only the latest version -const pagefindAttributes = - data.version === data.latestVersion - ? { - 'data-pagefind-body': '', - 'data-pagefind-meta': `title:${pageTitle}`, - } - : {}; - -if (fileExists && !isRemote) { - content = fs.readFileSync(pathToSpec, 'utf8'); -} - -if (isRemote) { - // Fetch the content from the remote path - content = await fetch(relativeSpecPath).then((res) => res.text()); -} ---- - - - { - !fileExists && !isRemote ? ( -
    - -

    No OpenAPI spec file found

    -

    - Could not find OpenAPI file for {data.name} in {`/${catalog.path}`} -

    -
    - ) : ( -
    - { - // Currently, Pagefind does not index metadata (such as the title), - // so we need to ensure it is included as text on the page. - // https://github.com/CloudCannon/pagefind/issues/437 - } - - -
    - ) - } -
    diff --git a/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_OpenAPI.tsx b/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_OpenAPI.tsx deleted file mode 100644 index 0b55c26e6..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_OpenAPI.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useState } from 'react'; -import { ApiReferenceReact } from '@scalar/api-reference-react'; -import '@scalar/api-reference-react/style.css'; -import './_styles.css'; -const OpenAPISpec = ({ spec }: { spec: string }) => { - const [loaded, setLoaded] = useState(false); - return ( -
    - {!loaded &&
    Loading...
    } - { - setLoaded(true); - }, - forceDarkModeState: 'light', - darkMode: false, - defaultOpenAllTags: true, - hideDarkModeToggle: true, - searchHotKey: 'p', - showSidebar: true, - customCss: 'bg-red-500', - }} - /> -
    - ); -}; - -export default OpenAPISpec; diff --git a/eventcatalog/src/pages/docs/[type]/[id]/index.astro b/eventcatalog/src/pages/docs/[type]/[id]/index.astro deleted file mode 100644 index 251fbe6bd..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/index.astro +++ /dev/null @@ -1,70 +0,0 @@ ---- -import Seo from '@components/Seo.astro'; -import { buildUrl } from '@utils/url-builder'; -import { getEvents } from '@utils/events'; -import { getEntities } from '@utils/entities'; -import { getCommands } from '@utils/commands'; -import { getServices } from '@utils/collections/services'; -import { getDomains } from '@utils/collections/domains'; -import type { CollectionEntry } from 'astro:content'; -import type { CollectionTypes } from '@types'; -import { getChannels } from '@utils/channels'; - -export async function getStaticPaths() { - const [events, commands, services, domains, channels, entities] = await Promise.all([ - getEvents(), - getCommands(), - getServices(), - getDomains(), - getChannels(), - getEntities(), - ]); - - const buildPages = (collection: CollectionEntry[]) => { - return collection.map((item) => ({ - params: { - type: item.collection, - id: item.data.id, - }, - props: { - type: item.collection, - ...item, - }, - })); - }; - - return [ - ...buildPages(domains), - ...buildPages(events), - ...buildPages(services), - ...buildPages(commands), - ...buildPages(channels), - ...buildPages(entities), - ]; -} - -const props = Astro.props; - -const pageTitle = `${props.collection} | ${props.data.name}`.replace(/^\w/, (c) => c.toUpperCase()); - -const { pathname } = Astro.url; -const redirectUrl = buildUrl(pathname + '/' + props.data.latestVersion, false, true); ---- - - - - - - - -

    You are being redirected to {redirectUrl}

    - - - - diff --git a/eventcatalog/src/pages/docs/[type]/[id]/language.mdx.ts b/eventcatalog/src/pages/docs/[type]/[id]/language.mdx.ts deleted file mode 100644 index b6305223a..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/language.mdx.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getDomains, getUbiquitousLanguage } from '@utils/collections/domains'; -import type { CollectionEntry } from 'astro:content'; -import type { APIRoute } from 'astro'; -import config from '@config'; -import fs from 'fs'; - -export async function getStaticPaths() { - const domains = await getDomains({ getAllVersions: false }); - - const buildPages = (collection: CollectionEntry<'domains'>[]) => { - return collection.map((item) => ({ - params: { - type: item.collection, - id: item.data.id, - }, - props: { - type: item.collection, - ...item, - }, - })); - }; - - return [...buildPages(domains)]; -} - -export const GET: APIRoute = async ({ params, props }) => { - // Just return empty array if LLMs are not enabled - if (!config.llmsTxt?.enabled) { - return new Response('llms.txt is not enabled for this Catalog.', { status: 404 }); - } - - const ubiquitousLanguages = await getUbiquitousLanguage(props as CollectionEntry<'domains'>); - const ubiquitousLanguage = ubiquitousLanguages[0]; - - if (ubiquitousLanguage?.filePath) { - let file = fs.readFileSync(ubiquitousLanguage.filePath, 'utf8'); - - return new Response(file, { status: 200 }); - } - - return new Response('Not found', { status: 404 }); -}; diff --git a/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro b/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro deleted file mode 100644 index 2328ebb5b..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +++ /dev/null @@ -1,206 +0,0 @@ ---- -import RectangleGroupIcon from '@heroicons/react/24/outline/RectangleGroupIcon'; -import Footer from '@layouts/Footer.astro'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { buildUrl } from '@utils/url-builder'; -import { ClientRouter } from 'astro:transitions'; -import { marked } from 'marked'; -import * as LucideIcons from 'lucide-react'; - -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -const props = await Page.getData(Astro); - -const { ubiquitousLanguage } = props; -const pageTitle = `${props.type} | ${ubiquitousLanguage.name}`.replace(/^\w/, (c) => c.toUpperCase()); - -marked.setOptions({ - breaks: true, - gfm: true, -}); - -const badges = [ - { - backgroundColor: 'yellow', - textColor: 'yellow', - content: props.domain.name, - icon: RectangleGroupIcon, - class: 'text-yellow-400', - href: buildUrl(`/docs/${props.type}/${props.domainId}`), - }, -]; ---- - - -
    -
    -
    - {/* Breadcrumb */} - - -
    -
    -

    - {ubiquitousLanguage.name} -

    -

    {ubiquitousLanguage.summary}

    - - { - badges && ( -
    - {badges.map((badge: any) => { - return ( - - - {badge.icon && } - {badge.iconURL && } - {badge.content} - - - ); - })} -
    - ) - } -
    -
    - - { - ubiquitousLanguage.description && ( -
    - ) - } - - { - !ubiquitousLanguage.description && ( -
    -

    No description for {ubiquitousLanguage.name} available.

    -
    - ) - } - -
    -
    -
    - -
    - - -
    diff --git a/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts b/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts deleted file mode 100644 index 4b26f4ec2..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { isSSR } from '@utils/feature'; -import { HybridPage } from '@utils/page-loaders/hybrid-page'; - -/** - * Domain page class with static methods - */ -export class Page extends HybridPage { - static async getStaticPaths() { - if (isSSR()) { - return []; - } - - const { getDomains } = await import('@utils/collections/domains'); - const domains = await getDomains({ getAllVersions: false }); - - return domains.map((item) => ({ - params: { - type: item.collection, - id: item.data.id, - }, - props: { - type: item.collection, - ...item, - }, - })); - } - - protected static async fetchData(params: any) { - const { getDomains } = await import('@utils/collections/domains'); - const domains = await getDomains({ getAllVersions: false }); - return domains.find((d) => d.data.id === params.id && d.collection === params.type) || null; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'Domain not found', - }); - } -} diff --git a/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro b/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro deleted file mode 100644 index 75b9ab99e..000000000 --- a/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +++ /dev/null @@ -1,382 +0,0 @@ ---- -import Footer from '@layouts/Footer.astro'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { getUbiquitousLanguageWithSubdomains } from '@utils/collections/domains'; -import { buildUrl } from '@utils/url-builder'; -import { ClientRouter } from 'astro:transitions'; -import * as LucideIcons from 'lucide-react'; -import { RectangleGroupIcon } from '@heroicons/react/24/outline'; - -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const props = await Page.getData(Astro); - -const pageTitle = `${props.collection} | ${props.data.name}`.replace(/^\w/, (c) => c.toUpperCase()); -const ubiquitousLanguageData = await getUbiquitousLanguageWithSubdomains(props); -const ubiquitousLanguage = ubiquitousLanguageData.domain; -const { subdomains, duplicateTerms } = ubiquitousLanguageData; ---- - - -
    -
    -
    - {/* Breadcrumb */} - - - {/* Title Section */} -
    -
    -
    -
    -

    Ubiquitous Language Explorer

    -
    -

    - Browse and discover ubiquitous language terms in the {props.data.name} domain{ - subdomains.length > 0 ? ' and its subdomains' : '' - } -

    -
    - -
    -
    - -
    - - - -
    -
    -
    -
    - {/* This will be updated by JavaScript */} -
    -
    -
    -
    -
    - - { - !ubiquitousLanguage && subdomains.length === 0 && ( -
    -

    - This domain does not have any defined ubiquitous language terms yet. Consider adding some terms to help establish - a common vocabulary for your domain. -

    -
    - ) - } - -
    - { - (ubiquitousLanguage || subdomains.some((s) => s.ubiquitousLanguage)) && ( -
    - {/* Domain Language Section */} - {ubiquitousLanguage && ( -
    -

    - {props.data.name} Domain Language -

    -
    - {ubiquitousLanguage?.data?.dictionary?.map((term) => ( -
    -
    - {term.icon && ( -
    - {(() => { - const Icon = LucideIcons[term.icon as keyof typeof LucideIcons]; - //@ts-ignore - return Icon ? : null; - })()} -
    - )} -
    -

    - {term.name} - {duplicateTerms.has(term.name.toLowerCase()) && ( - - Duplicate - - )} -

    -
    -

    - {term.summary} -

    - {term.description && ( - - )} -
    -
    -
    -
    - ))} -
    - -
    - )} - - {/* Subdomain Language Sections */} - {subdomains - .filter((s) => s.ubiquitousLanguage) - .map(({ subdomain, ubiquitousLanguage: subdomainUL }) => ( -
    -
    -

    {subdomain.data.name} Subdomain Language

    - - View Domain → - -
    -
    - {subdomainUL?.data?.dictionary?.map((term) => ( -
    -
    - {term.icon && ( -
    - {(() => { - const Icon = LucideIcons[term.icon as keyof typeof LucideIcons]; - //@ts-ignore - return Icon ? : null; - })()} -
    - )} -
    -

    - {term.name} - {duplicateTerms.has(term.name.toLowerCase()) && ( - - Duplicate - - )} -

    -
    -

    - {term.summary} -

    - {term.description && ( - - )} -
    -
    -
    -
    - ))} -
    - -
    - ))} -
    - ) - } -
    - -
    -
    -
    - -
    - - - - -
    diff --git a/eventcatalog/src/pages/docs/_default-docs.mdx b/eventcatalog/src/pages/docs/_default-docs.mdx deleted file mode 100644 index 403bb3633..000000000 --- a/eventcatalog/src/pages/docs/_default-docs.mdx +++ /dev/null @@ -1,25 +0,0 @@ -# **Welcome to EventCatalog** - -This open-source project is designed to help you and your teams bring discoverability and clarity to your event-driven architectures (EDA). - -This page can be replaced with your own content, but to help you get started, we have created a few guides and resources. - - - - - - - - - - -### **Join the community** - -Our project and community is growing fast. We have over 1000+ members in our [Discord community](https://discord.gg/3rjaZMmrAm). - - - - - - ---- \ No newline at end of file diff --git a/eventcatalog/src/pages/docs/custom/[...path].mdx.ts b/eventcatalog/src/pages/docs/custom/[...path].mdx.ts deleted file mode 100644 index 29eee0097..000000000 --- a/eventcatalog/src/pages/docs/custom/[...path].mdx.ts +++ /dev/null @@ -1,32 +0,0 @@ -// This file exposes the markdown for EventCatalog in the Url -// For example http://localhost:3000/docs/events/OrderAmended/0.0.1 loads the Page and http://localhost:3000/docs/events/OrderAmended/0.0.1.md loads the markdown -// This is used for the LLMs to load the markdown for the given item (llms.txt); - -import type { APIRoute, GetStaticPaths } from 'astro'; -import { getCollection } from 'astro:content'; -import config from '@config'; -import fs from 'fs'; - -export const getStaticPaths = (async () => { - const docs = await getCollection('customPages'); - const paths = docs.map((doc) => ({ - params: { path: doc.id.replace('docs/', '') }, - props: doc, - type: 'custom', - })); - return paths; -}) satisfies GetStaticPaths; - -export const GET: APIRoute = async ({ params, props }) => { - // Just return empty array if LLMs are not enabled - if (!config.llmsTxt?.enabled) { - return new Response('llms.txt is not enabled for this Catalog.', { status: 404 }); - } - - if (props.filePath) { - const file = fs.readFileSync(props.filePath, 'utf8'); - return new Response(file, { status: 200 }); - } - - return new Response('Not found', { status: 404 }); -}; diff --git a/eventcatalog/src/pages/docs/custom/feature.astro b/eventcatalog/src/pages/docs/custom/feature.astro deleted file mode 100644 index d9e18ac60..000000000 --- a/eventcatalog/src/pages/docs/custom/feature.astro +++ /dev/null @@ -1,165 +0,0 @@ ---- -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { BookOpenIcon, FileText } from 'lucide-react'; ---- - - -
    -
    - {/* Hero Section */} -
    -
    -
    - - New: Bring your documentation into EventCatalog -
    -

    - Document Everything. Share Knowledge. Build Better. -

    -

    - Add your own documentation to EventCatalog — from ADRs and system guides to runbooks and onboarding material. Connect - your knowledge to your architecture. -

    - -

    - Available with EventCatalog Starter or Scale plans - Try free for 14 days, no credit card required -

    -
    - -
    -
    -
    -
    -
    -
    - Custom Documentation Preview -
    -
    -
    - - {/* Features Section */} -
    -
    -
    - - - -
    -

    Centralized Knowledge

    -

    - Keep architecture decisions, system guides, and runbooks in one place — easy to access, easy to trust. -

    -
    - -
    -
    - - - -
    -

    Rich Formatting

    -

    - Use Markdown, diagrams, code blocks, and EventCatalog components to create structured, useful documentation. -

    -
    - -
    -
    - - - -
    -

    Version Control

    -

    Track changes and ensure your documentation grows alongside your system.

    -
    - -
    -
    - - - -
    -

    Documentation Ownership

    -

    Assign and track document owners, making it easy to find the right person in seconds.

    -
    - -
    -
    - - - -
    -

    Customizable Sidebars

    -

    - Auto-generated and fully customizable navigation sidebars to organize your documentation perfectly. -

    -
    - -
    -
    - - - -
    -

    EventCatalog Chat

    -

    - Interact with your documentation using AI-powered chat to find answers quickly and efficiently. -

    -
    -
    - - {/* Bottom Link */} - -
    -
    -
    - - diff --git a/eventcatalog/src/pages/docs/custom/index.astro b/eventcatalog/src/pages/docs/custom/index.astro deleted file mode 100644 index 69453f315..000000000 --- a/eventcatalog/src/pages/docs/custom/index.astro +++ /dev/null @@ -1,241 +0,0 @@ ---- -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { Code } from 'astro-expressive-code/components'; -import { isCustomDocsEnabled } from '@utils/feature'; - -// Example for folder structure -const folderStructureExample = `my-catalog/ - └── docs/ - ├── getting-started/ - │ ├── 01-introduction.mdx - │ └── 02-quick-start.mdx - ├── architecture-decisions/ - │ ├── 01-what-are-architecture-decisions.mdx - │ ├── 02-how-to-create-architecture-decisions.mdx - │ ├── published/ - │ │ ├── 01-adr-001-event-driven.mdx - │ │ └── 02-adr-002-api-first.mdx - │ └── drafts/ - │ ├── 01-adr-003-microservices.mdx - │ └── 02-adr-004-monolith.mdx - └`; -// Example MDX file content -const mdxFileExample = `--- -title: Getting Started -summary: How to get started with our event-driven architecture ---- - -# Getting Started with our Event-Driven Architecture - -This guide will help you understand how our services communicate using events. - -## Prerequisites - -- Understanding of basic messaging patterns -- Node.js installed on your machine - -## Key Concepts - -Events are the backbone of our architecture. They represent facts that have happened in our system. -`; - -// Example config file -const configExample = `// eventcatalog.config.js - -module.exports = { - // Your existing config... - - customDocs: { - sidebar: [ - { - label: 'Getting Started', - badge: { - text: 'New', color: 'green' - }, - collapsed: false, - items: [ - { label: 'Introduction', slug: 'getting-started/01-introduction' }, - { label: 'Quick Start', slug: 'getting-started/02-quick-start' } - ] - }, - { - label: 'Architecture Decisions', - badge: { - text: 'New', color: 'green' - }, - collapsed: true, - items: [ - { - label: 'What are Architecture Decisions?', - slug: 'architecture-decisions/01-what-are-architecture-decisions' - }, - { - label: 'How to Create Architecture Decisions', - slug: 'architecture-decisions/02-how-to-create-architecture-decisions' - }, - { - label: 'Published ADRs', - autogenerated: { - directory: 'architecture-decisions/published', - collapsed: true - } - }, - { - label: 'Draft ADRs', - autogenerated: { - directory: 'architecture-decisions/drafts', - collapsed: true - } - } - ] - } - ] - } -}`; - -if (!isCustomDocsEnabled()) { - return Astro.redirect('/docs/custom/feature'); -} ---- - - - -
    -
    -
    -
    -
    -

    Custom Documentation

    -
    - Pro feature -
    -
    -

    - Add custom documentation to EventCatalog to create a unified source of truth for your team. Document your - architecture decisions, patterns, and guidelines. -

    -
    -
    - - Read documentation → - - { - !isCustomDocsEnabled() && ( - - Start 14-day trial - - ) - } -
    -
    -
    - -

    Setup Guide

    -

    - Custom documentation let's you bring any documentation into EventCatalog. This is useful for documenting your architecture - decisions, patterns, and guidelines. Follow these steps to get started: -

    - -
    -
    -
    -
    - 1 -
    -
    -

    Create the content structure

    -

    Create a folder structure in your directory to organize your documentation.

    - -
    -
    -
    - -
    -
    -
    - 2 -
    -
    -

    Add MDX files

    -

    Create MDX files with frontmatter and markdown content.

    - -
    -
    -
    - -
    -
    -
    - 3 -
    -
    -

    Update your eventcatalog.config.js file

    -

    - Add the customDocs configuration to your eventcatalog.config.js file to define your sidebar structure. -

    - -

    This configuration defines the sidebar structure for your custom documentation:

    -
      -
    • - label: The display name for each sidebar section -
    • -
    • - badge: Optional badge to highlight new sections -
    • -
    • - collapsed: Whether the section is collapsed by default -
    • -
    • - autogenerated: Automatically generate sidebar items from a directory -
    • -
    • - slug: Direct link to a specific page -
    • -
    -
    -
    -
    - -
    -
    -
    - 4 -
    -
    -

    Restart EventCatalog

    -

    - After configuring your documentation, restart EventCatalog to see your custom documentation. -

    -
    - -
    -

    - Once restarted, you'll see your custom documentation displayed with the sidebar structure you defined: -

    -
    - Example of custom documentation interface -
    -
    -
    -
    -
    -
    - -
    -) diff --git a/eventcatalog/src/pages/docs/index.astro b/eventcatalog/src/pages/docs/index.astro deleted file mode 100644 index 63b648833..000000000 --- a/eventcatalog/src/pages/docs/index.astro +++ /dev/null @@ -1,33 +0,0 @@ ---- -import Footer from '@layouts/Footer.astro'; -import components from '@components/MDX/page-components'; -import mdxComponents from '@components/MDX/components'; -import { getIndexPage } from '@utils/pages'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { render } from 'astro:content'; - -const page = await getIndexPage(); -let CustomContent = null; - -const props = Astro.props; - -if (page) { - const { Content } = await render(page); - CustomContent = Content; -} else { - CustomContent = await import('./_default-docs.mdx').then((mod) => mod.default); -} ---- - - -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    diff --git a/eventcatalog/src/pages/docs/llm/llms-full.txt.ts b/eventcatalog/src/pages/docs/llm/llms-full.txt.ts deleted file mode 100644 index 60f12507b..000000000 --- a/eventcatalog/src/pages/docs/llm/llms-full.txt.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getCollection, type CollectionEntry } from 'astro:content'; -import type { APIRoute } from 'astro'; -import fs from 'fs'; -import { isCustomDocsEnabled } from '@utils/feature'; -import { addSchemaToMarkdown } from '@utils/llms'; - -type AllowedCollections = - | 'events' - | 'commands' - | 'queries' - | 'services' - | 'domains' - | 'teams' - | 'users' - | 'customPages' - | 'channels' - | 'entities' - | 'flows' - | 'containers'; - -const events = await getCollection('events'); -const commands = await getCollection('commands'); -const queries = await getCollection('queries'); -const services = await getCollection('services'); -const domains = await getCollection('domains'); -const teams = await getCollection('teams'); -const users = await getCollection('users'); -const entities = await getCollection('entities'); -const channels = await getCollection('channels'); -const flows = await getCollection('flows'); -const containers = await getCollection('containers'); -const customDocs = await getCollection('customPages'); - -import { isLLMSTxtEnabled } from '@utils/feature'; - -export const GET: APIRoute = async ({ params, request }) => { - if (!isLLMSTxtEnabled()) { - return new Response('llms.txt is not enabled for this Catalog.', { status: 404 }); - } - - const resources: CollectionEntry[] = [ - ...events, - ...commands, - ...queries, - ...services, - ...domains, - ...teams, - ...users, - ...entities, - ...channels, - ...containers, - ...flows, - ]; - - if (isCustomDocsEnabled()) { - resources.push(...(customDocs as CollectionEntry[])); - } - - const content = resources - .map((item) => { - if (!item.filePath) return ''; - - let file = fs.readFileSync(item.filePath, 'utf8'); - - try { - // Try and add the schemas to the resource - // @ts-ignore - file = addSchemaToMarkdown(item, file); - } catch (error) { - // just skip the resource if it has no schema - } - - return file; - }) - .join('\n'); - - return new Response(content, { - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, - }); -}; diff --git a/eventcatalog/src/pages/docs/llm/llms-services.txt.ts b/eventcatalog/src/pages/docs/llm/llms-services.txt.ts deleted file mode 100644 index 91c458983..000000000 --- a/eventcatalog/src/pages/docs/llm/llms-services.txt.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { type CollectionEntry } from 'astro:content'; -import type { APIRoute } from 'astro'; - -import { getServices } from '@utils/collections/services'; - -const services = await getServices(); - -export const GET: APIRoute = async ({ params, request }) => { - const url = new URL(request.url); - const baseUrl = process.env.LLMS_TXT_BASE_URL || `${url.origin}`; - - const formatServiceWithLinks = (service: CollectionEntry<'services'>) => { - const sends = service.data.sends as unknown as CollectionEntry<'events'>[]; - const receives = service.data.receives as unknown as CollectionEntry<'events'>[]; - const writesTo = service.data.writesTo as unknown as CollectionEntry<'containers'>[]; - const readsFrom = service.data.readsFrom as unknown as CollectionEntry<'containers'>[]; - - const sendsList = - sends.length > 0 - ? sends - .map( - (send) => - `- [${send.data.name} - ${send.data.version}](${baseUrl}/docs/events/${send.data.id}/${send.data.version}.mdx) - ${send.data.summary?.trim() || ''}` - ) - .join('\n') - : '- Does not send any messages'; - - const receivesList = - receives.length > 0 - ? receives - .map( - (receive) => - `- [${receive.data.name} - ${receive.data.version}](${baseUrl}/docs/events/${receive.data.id}/${receive.data.version}.mdx) - ${receive.data.summary?.trim() || ''}` - ) - .join('\n') - : '- Does not receive any messages'; - - const writesToList = - writesTo.length > 0 - ? writesTo - .map( - (write) => - `- [${write.data.name} - ${write.data.version}](${baseUrl}/docs/containers/${write.data.id}/${write.data.version}.mdx) - ${write.data.summary?.trim() || ''}` - ) - .join('\n') - : '- Does not write to any containers'; - - const readsFromList = - readsFrom.length > 0 - ? readsFrom - .map( - (read) => - `- [${read.data.name} - ${read.data.version}](${baseUrl}/docs/containers/${read.data.id}/${read.data.version}.mdx) - ${read.data.summary?.trim() || ''}` - ) - .join('\n') - : '- Does not read from any containers'; - - return `## [${service.data.name} - ${service.data.version}](${baseUrl}/docs/services/${service.data.id}/${service.data.version}.mdx) - -${service.data.summary?.trim() || ''} - -### Sends -${sendsList} - -### Receives -${receivesList} - -### Writes to -${writesToList} - -### Reads from -${readsFromList} -`; - }; - - const content = ['# Services\n', services.map((item) => formatServiceWithLinks(item)).join('\n')].join('\n'); - - return new Response(content, { - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, - }); -}; diff --git a/eventcatalog/src/pages/docs/llm/llms.txt.ts b/eventcatalog/src/pages/docs/llm/llms.txt.ts deleted file mode 100644 index b53289a73..000000000 --- a/eventcatalog/src/pages/docs/llm/llms.txt.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { getCollection } from 'astro:content'; -import config from '@config'; -import type { APIRoute } from 'astro'; - -import { isCustomDocsEnabled } from '@utils/feature'; -import { getUbiquitousLanguage } from '@utils/collections/domains'; - -const events = await getCollection('events'); -const commands = await getCollection('commands'); -const queries = await getCollection('queries'); - -const services = await getCollection('services'); -const domains = await getCollection('domains'); - -const teams = await getCollection('teams'); -const users = await getCollection('users'); - -const flows = await getCollection('flows'); -const channels = await getCollection('channels'); -const containers = await getCollection('containers'); - -const entities = await getCollection('entities'); - -const customDocs = await getCollection('customPages'); - -const ubiquitousLanguages: Record = {}; - -for (const domain of domains) { - const ubiquitousLanguagesForDomain = await getUbiquitousLanguage(domain); - if (ubiquitousLanguagesForDomain.length > 0) { - ubiquitousLanguages[domain.id] = ubiquitousLanguagesForDomain.map((item) => ({ - id: domain.id, - version: domain.data.version, - properties: item.data.dictionary, - })); - } -} - -// Render the Ubiquitous Languages section -const renderUbiquitousLanguages = (baseUrl: string) => { - return Object.entries(ubiquitousLanguages) - .map(([domainId, items]) => { - const domainName = domains.find((domain) => domain.id === domainId)?.data.name || domainId; - const itemsList = items - .map((item) => { - // @ts-ignore - const propertiesList = Object.entries(item.properties) - .map( - ([key, value]: any) => - ` - [${value.name}: - ${value.summary}](${baseUrl}/docs/domains/${domainId.split('-')[0]}/language.mdx)` - ) - .join('\n'); - return propertiesList; - }) - .join('\n'); - return `- ${domainName} Domain\n${itemsList}`; - }) - .join('\n'); -}; - -// render the entities from the domain list -const renderEntities = (baseUrl: string) => { - const domainsWithEntities = domains.filter((domain) => domain.data.entities?.length && domain.data.entities.length > 0); - - if (domainsWithEntities.length === 0) { - return ''; - } - - return domainsWithEntities - .map((domain) => { - const entitiesList = domain.data.entities - ?.map((entity) => { - const entityItem = entities.find((e) => e.data.id === entity.id); - return ` - [${entityItem?.data.name}](${baseUrl}/docs/entities/${entityItem?.data.id}/${entityItem?.data.version}.mdx) - ${entityItem?.data.summary}`; - }) - .join('\n'); - return `- ${domain.data.name} Domain\n${entitiesList || ''}`; - }) - .join('\n'); -}; - -export const GET: APIRoute = async ({ params, request }) => { - const url = new URL(request.url); - const baseUrl = process.env.LLMS_TXT_BASE_URL || `${url.origin}`; - - const formatVersionedItem = (item: any, type: string, extraParams?: string | string[]) => { - const params = Array.isArray(extraParams) ? extraParams.join('&') : extraParams || ''; - return `- [${item.data.name} - ${item.data.id} - ${item.data.version} ${params ? `- ${params}` : ''}](${baseUrl}/docs/${type}/${item.data.id}/${item.data.version}.mdx) ${item.data.summary ? `- ${item.data.summary}` : ''}`; - }; - - const formatSimpleItem = (item: any, type: string) => - `- [${item.id.replace('.mdx', '')}](${baseUrl}/docs/${type}/${item.data.id}.mdx) - ${item.data.name}`; - - const formatCustomDoc = (item: any, route: string) => - `- [${item.data.title}](${baseUrl}/${route}/${item.id.replace('docs\/', '')}.mdx) - ${item.data.summary || ''}`; - - const content = [ - `# ${config.organizationName} EventCatalog Documentation\n`, - `> ${config.tagline}\n`, - '## Events', - events.map((item) => formatVersionedItem(item, 'events')).join(''), - '\n## Commands', - commands.map((item) => formatVersionedItem(item, 'commands')).join(''), - '\n## Queries', - queries.map((item) => formatVersionedItem(item, 'queries')).join(''), - '\n## Services', - services.map((item) => formatVersionedItem(item, 'services')).join(''), - '\n## Domains', - domains.map((item) => formatVersionedItem(item, 'domains')).join(''), - '\n## Flows', - flows.map((item) => formatVersionedItem(item, 'flows')).join('\n'), - '\n## Channels', - channels - .map((item) => - formatVersionedItem(item, 'channels', item.data.protocols?.map((protocol) => `protocol - ${protocol}`).join('&')) - ) - .join(''), - ...(Object.keys(ubiquitousLanguages).length > 0 ? ['## Ubiquitous Language', renderUbiquitousLanguages(baseUrl)] : []), - '\n## Containers (Databases, External Systems)', - containers.map((item) => formatVersionedItem(item, 'containers')).join('\n'), - '\n## Entities', - renderEntities(baseUrl), - '\n## Teams', - teams.map((item) => formatSimpleItem(item, 'teams')).join('\n'), - '\n## Users', - users.map((item) => formatSimpleItem(item, 'users')).join('\n'), - ...(isCustomDocsEnabled() - ? ['\n## Custom Docs', customDocs.map((item) => formatCustomDoc(item, 'docs/custom')).join('\n')] - : []), - ].join('\n'); - - return new Response(content, { - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, - }); -}; diff --git a/eventcatalog/src/pages/docs/llm/schemas.txt.ts b/eventcatalog/src/pages/docs/llm/schemas.txt.ts deleted file mode 100644 index 4323b98c9..000000000 --- a/eventcatalog/src/pages/docs/llm/schemas.txt.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { getCollection } from 'astro:content'; -import config from '@config'; -import type { APIRoute } from 'astro'; -import type { CollectionEntry } from 'astro:content'; -import { getSpecificationsForService } from '@utils/collections/services'; -import { isEventCatalogScaleEnabled } from '@utils/feature'; - -const events = await getCollection('events'); -const commands = await getCollection('commands'); -const queries = await getCollection('queries'); -const services = await getCollection('services'); - -type ServiceWithSchema = { - collection: string; - id: string; - version: string; - specification: string; - summary: string; -}; - -const servicesWithSchemas = services.filter((service) => getSpecificationsForService(service).length > 0); - -const servicesWithSchemasFlat = servicesWithSchemas.reduce((acc, service) => { - return [ - ...acc, - ...getSpecificationsForService(service).map((specification) => ({ - collection: 'services', - id: service.data.id, - version: service.data.version, - specification: specification.type, - summary: service.data.summary?.trim() || '', - })), - ]; -}, []) as ServiceWithSchema[]; - -const messageHasSchema = (message: CollectionEntry<'events' | 'commands' | 'queries'>) => { - return message.data.schemaPath; -}; - -export const GET: APIRoute = async ({ params, request }) => { - if (!isEventCatalogScaleEnabled()) { - return new Response( - JSON.stringify({ - error: 'feature_not_available_on_server', - message: 'Schema API is not enabled for this deployment and supported in EventCatalog Scale.', - }), - { status: 501, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' } } - ); - } - - const url = new URL(request.url); - const baseUrl = process.env.LLMS_TXT_BASE_URL || `${url.origin}`; - - const formatVersionedItem = (item: any, type: string, extraParams?: string | string[]) => { - return `- [${item.data.name} - ${item.data.id} - ${item.data.version}](${baseUrl}/api/schemas/${type}/${item.data.id}/${item.data.version})} ${item.data.summary ? `- ${item.data.summary.trim()}` : ''}`; - }; - - const formatServiceWithSchema = (item: ServiceWithSchema) => { - return `- [${item.id} - ${item.version} - ${item.specification} specification](${baseUrl}/api/schemas/${item.collection}/${item.id}/${item.version}/${item.specification}) ${item.summary ? `- Specification for ${item.summary}` : ''}`; - }; - - const content = [ - `# ${config.organizationName} EventCatalog Schemas`, - `List of schemas for events, commands, queries, and services in EventCatalog.`, - '', - `## Events\n${events - .filter(messageHasSchema) - .map((item) => formatVersionedItem(item, 'events')) - .join('\n')}`, - '', - `## Commands\n${commands - .filter(messageHasSchema) - .map((item) => formatVersionedItem(item, 'commands')) - .join('\n')}`, - '', - `## Queries\n${queries - .filter(messageHasSchema) - .map((item) => formatVersionedItem(item, 'queries')) - .join('\n')}`, - '', - `## Services\n${servicesWithSchemasFlat.map((item: any) => formatServiceWithSchema(item)).join('\n')}`, - ].join('\n'); - return new Response(content, { - headers: { 'Content-Type': 'text/plain; charset=utf-8' }, - }); -}; diff --git a/eventcatalog/src/pages/docs/teams/[id].md.ts b/eventcatalog/src/pages/docs/teams/[id].md.ts deleted file mode 100644 index 7ad407f04..000000000 --- a/eventcatalog/src/pages/docs/teams/[id].md.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file exposes the markdown for EventCatalog in the Url -// For example http://localhost:3000/docs/events/OrderAmended/0.0.1 loads the Page and http://localhost:3000/docs/events/OrderAmended/0.0.1.md loads the markdown -// This is used for the LLMs to load the markdown for the given item (llms.txt); - -import type { APIRoute } from 'astro'; -import { getCollection } from 'astro:content'; -import config from '@config'; -import fs from 'fs'; - -const teams = await getCollection('teams'); - -export async function getStaticPaths() { - // Just return empty array if LLMs are not enabled - if (!config.llmsTxt?.enabled) { - return []; - } - - return teams.map((team) => ({ - params: { type: 'teams', id: team.data.id }, - props: { content: team }, - })); -} - -export const GET: APIRoute = async ({ params, props }) => { - // Just return empty array if LLMs are not enabled - if (!config.llmsTxt?.enabled) { - return new Response('llms.txt is not enabled for this Catalog.', { status: 404 }); - } - - if (props?.content?.filePath) { - const file = fs.readFileSync(props.content.filePath, 'utf8'); - return new Response(file, { status: 200 }); - } - - return new Response('Not found', { status: 404 }); -}; diff --git a/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts b/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts deleted file mode 100644 index 9246a5168..000000000 --- a/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +++ /dev/null @@ -1,46 +0,0 @@ -// pages/teams/[id]/index.page.ts -import { isSSR } from '@utils/feature'; -import { HybridPage } from '@utils/page-loaders/hybrid-page'; - -export class Page extends HybridPage { - static get prerender(): boolean { - return !isSSR(); - } - - static async getStaticPaths(): Promise> { - if (isSSR()) { - return []; - } - - const { getTeams } = await import('@utils/teams'); - const teams = await getTeams(); - - return teams.map((team) => ({ - params: { id: team.data.id }, - props: team, - })); - } - - protected static async fetchData(params: any) { - const { id } = params; - - if (!id) { - return null; - } - - const { getTeams } = await import('@utils/teams'); - const teams = await getTeams(); - - // Find the specific team by id - const team = teams.find((t) => t.data.id === id); - - return team || null; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'Team not found', - }); - } -} diff --git a/eventcatalog/src/pages/docs/teams/[id]/index.astro b/eventcatalog/src/pages/docs/teams/[id]/index.astro deleted file mode 100644 index d1ee91083..000000000 --- a/eventcatalog/src/pages/docs/teams/[id]/index.astro +++ /dev/null @@ -1,176 +0,0 @@ ---- -import components from '@components/MDX/components'; - -// SideBars -import { getEntry, render } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import OwnersList from '@components/Lists/OwnersList'; -import PillListFlat from '@components/Lists/PillListFlat'; -import EnvelopeIcon from '@heroicons/react/16/solid/EnvelopeIcon'; -import { buildUrl } from '@utils/url-builder'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const props = await Page.getData(Astro); - -const { Content } = await render(props); - -const membersRaw = props.data.members || []; -const members = (await Promise.all(membersRaw.map((m: CollectionEntry<'teams'>) => getEntry(m)))).filter(Boolean); - -const domains = props.data.ownedDomains as CollectionEntry<'domains'>[]; -const services = props.data.ownedServices as CollectionEntry<'services'>[]; -const events = props.data.ownedEvents as CollectionEntry<'events'>[]; -const commands = props.data.ownedCommands as CollectionEntry<'commands'>[]; -const queries = props.data.ownedQueries as CollectionEntry<'queries'>[]; - -const membersList = members.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: o.collection === 'users' ? o.data.role : 'Team', - collection: o.collection, - avatarUrl: o.collection === 'users' ? o.data.avatarUrl : '', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const ownedDomainsList = domains.map((p) => ({ - label: `${p.data.name}`, - collection: p.collection, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), - tag: `v${p.data.version}`, -})); - -const ownedServicesList = services.map((p) => ({ - label: `${p.data.name}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), - collection: p.collection, - tag: `v${p.data.version}`, -})); - -const ownedMessagesList = [...events, ...commands, ...queries].map((p) => ({ - label: `${p.data.name}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), - color: p.collection === 'events' ? 'orange' : 'blue', - collection: p.collection, - tag: `v${p.data.version}`, -})); - -const pageTitle = `Team | ${props.data.name}`; ---- - - -
    -
    -
    -
    -
    -
    -
    -

    {props.data.name} (Team)

    -
    -
    - { - props.data.email && ( -
    - - Email -
    - ) - } - { - props.data.slackDirectMessageUrl && ( - - ) - } - { - props.data.msTeamsDirectMessageUrl && ( - - ) - } -
    -
    -
    -

    {props.data.summary}

    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - - diff --git a/eventcatalog/src/pages/docs/users/[id].md.ts b/eventcatalog/src/pages/docs/users/[id].md.ts deleted file mode 100644 index 4a4544358..000000000 --- a/eventcatalog/src/pages/docs/users/[id].md.ts +++ /dev/null @@ -1,36 +0,0 @@ -// This file exposes the markdown for EventCatalog in the Url -// For example http://localhost:3000/docs/events/OrderAmended/0.0.1 loads the Page and http://localhost:3000/docs/events/OrderAmended/0.0.1.md loads the markdown -// This is used for the LLMs to load the markdown for the given item (llms.txt); - -import type { APIRoute } from 'astro'; -import { getCollection } from 'astro:content'; -import config from '@config'; -import fs from 'fs'; - -const users = await getCollection('users'); - -export async function getStaticPaths() { - // Just return empty array if LLMs are not enabled - if (!config.llmsTxt?.enabled) { - return []; - } - - return users.map((user) => ({ - params: { type: 'users', id: user.data.id }, - props: { content: user }, - })); -} - -export const GET: APIRoute = async ({ params, props }) => { - // Just return empty array if LLMs are not enabled - if (!config.llmsTxt?.enabled) { - return new Response('llms.txt is not enabled for this Catalog.', { status: 404 }); - } - - if (props?.content?.filePath) { - const file = fs.readFileSync(props.content?.filePath, 'utf8'); - return new Response(file, { status: 200 }); - } - - return new Response('Not found', { status: 404 }); -}; diff --git a/eventcatalog/src/pages/docs/users/[id]/_index.data.ts b/eventcatalog/src/pages/docs/users/[id]/_index.data.ts deleted file mode 100644 index 14dee2b2d..000000000 --- a/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +++ /dev/null @@ -1,46 +0,0 @@ -// pages/teams/[id]/index.page.ts -import { isSSR } from '@utils/feature'; -import { HybridPage } from '@utils/page-loaders/hybrid-page'; - -export class Page extends HybridPage { - static get prerender(): boolean { - return !isSSR(); - } - - static async getStaticPaths(): Promise> { - if (isSSR()) { - return []; - } - - const { getUsers } = await import('@utils/users'); - const users = await getUsers(); - - return users.map((user) => ({ - params: { id: user.data.id }, - props: user, - })); - } - - protected static async fetchData(params: any) { - const { id } = params; - - if (!id) { - return null; - } - - const { getUsers } = await import('@utils/users'); - const users = await getUsers(); - - // Find the specific team by id - const user = users.find((u) => u.data.id === id); - - return user || null; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'User not found', - }); - } -} diff --git a/eventcatalog/src/pages/docs/users/[id]/index.astro b/eventcatalog/src/pages/docs/users/[id]/index.astro deleted file mode 100644 index 01596a276..000000000 --- a/eventcatalog/src/pages/docs/users/[id]/index.astro +++ /dev/null @@ -1,191 +0,0 @@ ---- -import components from '@components/MDX/components'; - -// SideBars -import { render } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import OwnersList from '@components/Lists/OwnersList'; -import PillListFlat from '@components/Lists/PillListFlat'; -import EnvelopeIcon from '@heroicons/react/16/solid/EnvelopeIcon'; -import { buildUrl } from '@utils/url-builder'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const props = await Page.getData(Astro); - -const { Content } = await render(props); - -const domains = props.data.ownedDomains as CollectionEntry<'domains'>[]; -const services = props.data.ownedServices as CollectionEntry<'services'>[]; -const events = props.data.ownedEvents as CollectionEntry<'events'>[]; -const commands = props.data.ownedCommands as CollectionEntry<'commands'>[]; -const queries = props.data.ownedQueries as CollectionEntry<'queries'>[]; -const teams = props.data.associatedTeams as CollectionEntry<'teams'>[]; - -const ownedDomainsList = domains.map((p) => ({ - label: `${p.data.name}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), - collection: p.collection, - tag: `v${p.data.version}`, -})); - -const ownedServicesList = services.map((p) => ({ - label: `${p.data.name}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), - collection: p.collection, - tag: `v${p.data.version}`, -})); - -const ownedMessageList = [...events, ...commands, ...queries].map((p) => ({ - label: `${p.data.name}`, - badge: p.collection, - color: p.collection === 'events' ? 'orange' : 'blue', - collection: p.collection, - tag: `v${p.data.version}`, - href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`), -})); - -const associatedTeams = teams.map((o) => ({ - label: o.data.name, - type: o.collection, - badge: 'Team', - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), -})); - -const pageTitle = `User | ${props.data.name}`; ---- - - -
    -
    -
    -
    -
    - Profile picture -
    -
    -

    {props.data.name}

    - {props.data.role} -
    -
    - { - props.data.email && ( -
    - - Email -
    - ) - } - { - props.data.slackDirectMessageUrl && ( - - ) - } - { - props.data.msTeamsDirectMessageUrl && ( - - ) - } -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    - - diff --git a/eventcatalog/src/pages/index.astro b/eventcatalog/src/pages/index.astro deleted file mode 100644 index 4666f4d3c..000000000 --- a/eventcatalog/src/pages/index.astro +++ /dev/null @@ -1,77 +0,0 @@ ---- -import Footer from '@layouts/Footer.astro'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import components from '@components/MDX/page-components'; -import mdxComponents from '@components/MDX/components'; -import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro'; -import { getMDXComponentsByName } from '@utils/markdown'; -import { resourceToCollectionMap } from '@utils/collections/util'; -import { buildUrl } from '@utils/url-builder'; -import { isCustomLandingPageEnabled, isVisualiserEnabled } from '@utils/feature'; -import DefaultAstroLandingPage from './_index.astro'; -import config from '@config'; - -let nodeGraphs: any[] = []; -let CustomContent = null; - -import path from 'path'; -import { existsSync, readFileSync } from 'fs'; - -const props = Astro.props; - -const pathToUserDefinedLandingPage = path.join(process.env.PROJECT_DIR || '', 'pages/homepage.astro'); - -if (existsSync(pathToUserDefinedLandingPage) && isCustomLandingPageEnabled()) { - const customPages = import.meta.glob('/**/homepage.astro', { eager: true }); - // @ts-ignore - CustomContent = Object.values(customPages)[0]?.default || null; - - const rawContent = readFileSync(pathToUserDefinedLandingPage, 'utf-8'); - nodeGraphs = getMDXComponentsByName(rawContent, 'NodeGraph') || []; -} - -if (config.landingPage) { - return Astro.redirect(buildUrl(config.landingPage)); -} ---- - -{ - CustomContent ? ( - -
    -
    -
    -
    - -
    -
    -
    -
    - {nodeGraphs.length > 0 && - nodeGraphs.map((nodeGraph: any) => { - const collection = resourceToCollectionMap[nodeGraph.type as keyof typeof resourceToCollectionMap]; - return ( - - ); - })} -
    -
    - ) : ( - - ) -} diff --git a/eventcatalog/src/pages/plans/index.astro b/eventcatalog/src/pages/plans/index.astro deleted file mode 100644 index dc9bb7111..000000000 --- a/eventcatalog/src/pages/plans/index.astro +++ /dev/null @@ -1,292 +0,0 @@ ---- -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { - Rocket, - Users, - Bot, - ScrollText, - Component, - Cpu, - Code, - LifeBuoy, - ExternalLink, - Network, - Github, - Flag, -} from 'lucide-react'; ---- - - -
    -
    - {/* Hero Section */} -
    -
    -
    - - Upgrade your Catalog -
    -

    Supercharge your EventCatalog

    -

    - Unlock advanced features like automated docs (e.g OpenAPI, AsyncAPI), Custom documentation, AI assistant, and catalog - federation — all designed to help you scale without complexity. -

    - - {/* Integration Sources */} -
    -

    Generate documentation from:

    - -
    - - -

    Try free for 14 days, no credit card required

    -
    - -
    - EventCatalog Pro -
    -
    - - {/* Why upgrade section */} -
    -

    Why upgrade EventCatalog?

    -
    -
    -
    - -
    -

    A living source of truth

    -

    Keep docs in sync with your systems, ensuring your team always has accurate information.

    -
    - -
    -
    - -
    -

    Shared system understanding

    -

    Align teams and reduce tribal knowledge with centralized architecture documentation.

    -
    - -
    -
    - -
    -

    Architecture that scales

    -

    Grow without losing context of complexity, making architectural decisions easy to find.

    -
    -
    -
    - - {/* Documentation Journey Section */} -
    -

    Scales with your team and architecture

    -

    - From scattered documentation to a well-governed system, EventCatalog helps you control complexity with well governed - documentation for your teams. Choose the plan that fits your needs. -

    - -
    - {/* Journey Line */} - - -
    - {/* Stage 1 */} -
    -
    - -
    -

    Community Edition

    -

    You're just beginning to document services, domains, and events.

    -
    - - {/* Stage 2 */} -
    -
    - -
    -

    Starter Plan

    -

    For teams formalizing their architecture documentation

    -
    - - {/* Stage 3 */} -
    -
    - -
    -

    Scale Plan

    -

    - Built for teams scaling across domains and integrating with external systems -

    -
    - - {/* Stage 4 */} -
    -
    - -
    -

    Enterprise Plan

    -

    - Designed for organizations building and governing complex event platforms -

    -
    -
    - - {/* Mobile Progress Indicators */} -
    -
    -
    -
    -
    -
    -
    -
    - - {/* Features Section */} -
    -

    Save time with EventCatalog

    -
    -
    -
    - -
    -

    Custom Documentation

    -

    Add ADRs, runbooks, and technical guidelines to create a centralized knowledge hub.

    -
    - -
    -
    - -
    -

    AI Assistant

    -

    Chat with your catalog to quickly find information about your architecture.

    -
    - -
    -
    - -
    -

    Federation

    -

    Connect multiple catalogs into a unified view for centralized visibility across teams.

    -
    - -
    -
    - -
    -

    IDE Integration

    -

    Access EventCatalog directly from your IDE for seamless documentation while coding.

    -
    - -
    -
    - -
    -

    Automated Documentation

    -

    - Generate and maintain documentation automatically with integrations for AsyncAPI and OpenAPI. -

    -
    - -
    -
    - -
    -

    Priority Support

    -

    Get priority email support and assistance from the EventCatalog team.

    -
    -
    -
    - - {/* Questions Section */} -
    -

    Questions about EventCatalog?

    -
    -
    -

    Request a Demo

    -

    See EventCatalog in action with a personalized demo from our team.

    - - Schedule a demo - - -
    - -
    -

    Join the community

    -

    Join our growing community on Discord. Over 1000+ members.

    - - Join the community - - -
    -
    -
    -
    -
    -
    - - diff --git a/eventcatalog/src/pages/schemas/index.astro b/eventcatalog/src/pages/schemas/index.astro deleted file mode 100644 index c0e761b40..000000000 --- a/eventcatalog/src/pages/schemas/index.astro +++ /dev/null @@ -1,175 +0,0 @@ ---- -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { getEvents } from '@utils/events'; -import { getCommands } from '@utils/commands'; -import { getQueries } from '@utils/queries'; -import { getServices, getSpecificationsForService } from '@utils/collections/services'; -import SchemaExplorer from '@components/SchemaExplorer/SchemaExplorer'; -import { isEventCatalogScaleEnabled } from '@utils/feature'; -import { getOwner } from '@utils/collections/owners'; -import { buildUrl } from '@utils/url-builder'; -import fs from 'fs'; -import path from 'path'; - -// Fetch all messages -const events = await getEvents({ getAllVersions: true }); -const commands = await getCommands({ getAllVersions: true }); -const queries = await getQueries({ getAllVersions: true }); - -// Fetch all services -const services = await getServices({ getAllVersions: true }); - -// Combine all messages -const allMessages = [...events, ...commands, ...queries]; - -// Helper function to enrich owners with full details -async function enrichOwners(ownersRaw: any[]) { - if (!ownersRaw || ownersRaw.length === 0) return []; - - const owners = await Promise.all(ownersRaw.map(getOwner)); - const filteredOwners = owners.filter((o) => o !== undefined); - - return filteredOwners.map((o) => ({ - id: o.data.id, - name: o.data.name, - type: o.collection, - href: buildUrl(`/docs/${o.collection}/${o.data.id}`), - })); -} - -// Filter messages with schemas and read schema content - only keep essential data -const messagesWithSchemas = await Promise.all( - allMessages - .filter((message) => message.data.schemaPath) - // Make sure the file exists - .filter((message) => fs.existsSync(path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? ''))) - .map(async (message) => { - try { - // Get the schema file path - const schemaPath = message.data.schemaPath; - const fullSchemaPath = path.join(path.dirname(message.filePath ?? ''), schemaPath ?? ''); - - // Read the schema content - let schemaContent = ''; - if (fs.existsSync(fullSchemaPath)) { - schemaContent = fs.readFileSync(fullSchemaPath, 'utf-8'); - } - - // Get schema file extension - const schemaExtension = path.extname(schemaPath ?? '').slice(1); - - // Enrich owners with full details - const enrichedOwners = await enrichOwners(message.data.owners || []); - - // Only return essential data - strip out markdown, full data objects, etc. - return { - collection: message.collection, - data: { - id: message.data.id, - name: message.data.name, - version: message.data.version, - summary: message.data.summary, - schemaPath: message.data.schemaPath, - producers: message.data.producers || [], - consumers: message.data.consumers || [], - owners: enrichedOwners, - }, - schemaContent, - schemaExtension, - }; - } catch (error) { - console.error(`Error reading schema for ${message.data.id}:`, error); - const enrichedOwners = await enrichOwners(message.data.owners || []); - return { - collection: message.collection, - data: { - id: message.data.id, - name: message.data.name, - version: message.data.version, - summary: message.data.summary, - schemaPath: message.data.schemaPath, - producers: message.data.producers || [], - consumers: message.data.consumers || [], - owners: enrichedOwners, - }, - schemaContent: '', - schemaExtension: 'json', - }; - } - }) -); - -// Filter services with specifications and read spec content - only keep essential data -const servicesWithSpecs = await Promise.all( - services.map(async (service) => { - try { - const specifications = getSpecificationsForService(service); - - // Only include services that have specifications - if (specifications.length === 0) { - return null; - } - - // Process each specification file for this service - return await Promise.all( - specifications.map(async (spec) => { - const specPath = path.join(path.dirname(service.filePath ?? ''), spec.path); - - // Only include if the spec file exists - if (!fs.existsSync(specPath)) { - return null; - } - - const schemaContent = fs.readFileSync(specPath, 'utf-8'); - // Use spec type (openapi, asyncapi) as the extension for proper labeling - const schemaExtension = spec.type; - - // Enrich owners with full details - const enrichedOwners = await enrichOwners(service.data.owners || []); - - // Only return essential data - strip out markdown, sends/receives, entities, etc. - return { - collection: 'services', - data: { - id: `${service.data.id}`, - name: `${service.data.name} - ${spec.name}`, - version: service.data.version, - summary: service.data.summary, - schemaPath: spec.path, - owners: enrichedOwners, - }, - schemaContent, - schemaExtension, - specType: spec.type, - specName: spec.name, - specFilenameWithoutExtension: spec.filenameWithoutExtension, - }; - }) - ); - } catch (error) { - console.error(`Error reading specifications for service ${service.data.id}:`, error); - return null; - } - }) -); - -// Flatten and filter out null values -const flatServicesWithSpecs = servicesWithSpecs.flat().filter((service) => service !== null); - -// Combine messages and services -const allSchemas = [...messagesWithSchemas, ...flatServicesWithSpecs]; - -const apiAccessEnabled = isEventCatalogScaleEnabled(); ---- - - -
    -
    -
    -
    - -
    -
    -
    -
    -
    diff --git a/eventcatalog/src/pages/studio.astro b/eventcatalog/src/pages/studio.astro deleted file mode 100644 index 9623e8b1f..000000000 --- a/eventcatalog/src/pages/studio.astro +++ /dev/null @@ -1,234 +0,0 @@ ---- -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; -import { getEvents } from '@utils/events'; -import { getCommands } from '@utils/commands'; -import { getServices } from '@utils/collections/services'; -import { BoltIcon, ServerIcon, RectangleGroupIcon } from '@heroicons/react/24/outline'; -import { SquareDashedMousePointerIcon, ArrowLeftRightIcon } from 'lucide-react'; -import StudioPageModal from '@components/Studio/StudioPageModal'; -import { getChannels } from '@utils/channels'; - -const [events, commands, services, channels] = await Promise.all([ - getEvents().catch(() => []), - getCommands().catch(() => []), - getServices().catch(() => []), - getChannels().catch(() => []), -]); - -// Get counts -const eventCount = events.length; -const serviceCount = services.length; -const commandCount = commands.length; -const channelCount = channels.length; - -// Get a few sample resources to display -const sampleEvents = events.slice(0, 2); -const sampleServices = services.slice(0, 2); -const sampleChannels = channels.slice(0, 1); - -// Determine which resources to show (fallback if some are missing) -const resourcesToShow = [ - ...sampleEvents.map((event, index) => ({ type: 'event', data: event, index })), - ...sampleServices.map((service, index) => ({ type: 'service', data: service, index: index + 2 })), - ...sampleChannels.map((channel, index) => ({ type: 'channel', data: channel, index: index + 4 })), -].slice(0, 5); // Max 5 resources ---- - - - - - - - - - EventCatalog Studio - - - -
    -
    - {/* Hero Section */} -
    -
    -
    - - EventCatalog Studio -
    -

    Turn your resources into designs

    -

    - Transform your documented messages, services, and domains into architecture diagrams. Drag, drop, and design with - what you already have. -

    -
    - -
    - -

    - {eventCount + serviceCount + channelCount + commandCount} resources ready to design with -

    -
    - -
    -
    - {/* Animation container */} -
    - {/* Resource cards that animate in */} -
    - { - resourcesToShow.map((resource) => { - if (resource.type === 'event') { - return ( -
    - - {resource.data.data.name} -
    - ); - } else if (resource.type === 'service') { - return ( -
    - - {resource.data.data.name} -
    - ); - } else if (resource.type === 'channel') { - return ( -
    - - {resource.data.data.name} -
    - ); - } - }) - } -
    - - {/* Arrow indicator */} -
    - - - -
    - - {/* Design preview */} -
    - Studio Design Preview -
    -
    -
    -
    -
    - - {/* Features Section */} -
    -
    -
    - - - - - -
    -

    Real Resources

    -

    - Drag and drop messages, services, and domains from your catalog. No more copying names or keeping things manually - in sync. -

    -
    - -
    -
    - - - - - -
    -

    Save & Version

    -

    Save designs locally and store in Git. All designs and data is owned by you.

    -
    - -
    -
    - - - - -
    -

    Embed Anywhere

    -

    - Drop diagrams directly into documentation pages. One source of truth for your architecture. -

    -
    -
    -
    -
    - - {/* Studio Modal */} - -
    - - - - - - diff --git a/eventcatalog/src/pages/unauthorized/index.astro b/eventcatalog/src/pages/unauthorized/index.astro deleted file mode 100644 index 360e15f7e..000000000 --- a/eventcatalog/src/pages/unauthorized/index.astro +++ /dev/null @@ -1,84 +0,0 @@ ---- -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; ---- - - -
    -
    -
    -
    - - - - -
    - -

    Access Denied

    - -

    You don't have permission to access this resource.

    - - - - -
    - - - - - - - Go to Dashboard - -
    -
    -
    -
    - - - -
    diff --git a/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts b/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts deleted file mode 100644 index 3a4ded125..000000000 --- a/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +++ /dev/null @@ -1,100 +0,0 @@ -// pages/visualiser/[type]/[id]/[version]/index.page.ts -import { HybridPage } from '@utils/page-loaders/hybrid-page'; -import { isAuthEnabled } from '@utils/feature'; -import type { PageTypes } from '@types'; -import { pageDataLoader } from '@utils/page-loaders/page-data-loader'; -import { isVisualiserEnabled } from '@utils/feature'; - -type PageTypesWithFlows = PageTypes | 'flows'; - -export class Page extends HybridPage { - static async getStaticPaths(): Promise> { - if (isAuthEnabled() || !isVisualiserEnabled()) { - return []; - } - - const { getFlows } = await import('@utils/collections/flows'); - - const loaders = { - ...pageDataLoader, - flows: getFlows, - }; - - const itemTypes: PageTypesWithFlows[] = ['events', 'commands', 'queries', 'services', 'domains', 'flows', 'containers']; - const allItems = await Promise.all(itemTypes.map((type) => loaders[type]())); - - return allItems.flatMap((items, index) => - items - .filter((item) => item.data.visualiser !== false) - .map((item) => ({ - params: { - type: itemTypes[index], - id: item.data.id, - version: item.data.version, - }, - props: { - type: itemTypes[index], - ...item, - }, - })) - ); - } - - protected static async fetchData(params: any) { - const { type, id, version } = params; - - if (!type || !id || !version || !isVisualiserEnabled()) { - return null; - } - - const { getFlows } = await import('@utils/collections/flows'); - - const loaders = { - ...pageDataLoader, - flows: getFlows, - }; - - // Get all items of the specified type - const items = await loaders[type as PageTypesWithFlows](); - - // Find the specific item by id and version - const item = items.find((i) => i.data.id === id && i.data.version === version && i.data.visualiser !== false); - - if (!item) { - return null; - } - - return { - type, - ...item, - }; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'Visualiser page not found', - }); - } - - static get clientAuthScript(): string { - if (!isAuthEnabled() || !isVisualiserEnabled()) { - return ''; - } - - return ` - if (typeof window !== 'undefined' && !import.meta.env.SSR) { - fetch('/api/auth/session') - .then(res => res.json()) - .then(session => { - if (!session?.user) { - window.location.href = '/auth/login?callbackUrl=' + encodeURIComponent(window.location.pathname); - } - }) - .catch(() => { - window.location.href = '/auth/login?callbackUrl=' + encodeURIComponent(window.location.pathname); - }); - } - `; - } -} diff --git a/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts b/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts deleted file mode 100644 index 931c216a3..000000000 --- a/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { HybridPage } from '@utils/page-loaders/hybrid-page'; -import { isAuthEnabled } from '@utils/feature'; -import { getServices, type Service } from '@utils/collections/services'; - -const serviceHasData = (service: Service) => { - return service.data.writesTo?.length || 0 > 0 || service.data.readsFrom?.length || 0 > 0; -}; - -export class Page extends HybridPage { - static async getStaticPaths(): Promise> { - if (isAuthEnabled()) { - return []; - } - - const services = await getServices(); - const servicesWithData = services.filter((service) => serviceHasData(service)); - - return servicesWithData.flatMap((service) => { - return { - params: { - type: 'services', - id: service.data.id, - version: service.data.version, - }, - props: { - type: 'service', - ...service, - }, - }; - }); - } - - protected static async fetchData(params: any) { - const { id, version } = params; - - if (!id || !version) { - return null; - } - - // Get all items of the specified type - const items = await getServices(); - - // Find the specific item by id and version, and only if it has entities - const item = items.find((i) => i.data.id === id && i.data.version === version && serviceHasData(i)); - - if (!item) { - return null; - } - - return item; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'Service data page not found', - }); - } - - static get clientAuthScript(): string { - if (!isAuthEnabled()) { - return ''; - } - - return ` - if (typeof window !== 'undefined' && !import.meta.env.SSR) { - fetch('/api/auth/session') - .then(res => res.json()) - .then(session => { - if (!session?.user) { - window.location.href = '/auth/login?callbackUrl=' + encodeURIComponent(window.location.pathname); - } - }) - .catch(() => { - window.location.href = '/auth/login?callbackUrl=' + encodeURIComponent(window.location.pathname); - }); - } - `; - } -} diff --git a/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro b/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro deleted file mode 100644 index 7a69986aa..000000000 --- a/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +++ /dev/null @@ -1,52 +0,0 @@ ---- -import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro'; -import VisualiserLayout from '@layouts/VisualiserLayout.astro'; -import { buildUrl } from '@utils/url-builder'; -import { ClientRouter } from 'astro:transitions'; - -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const props = await Page.getData(Astro); - -const { - data: { id }, - collection, -} = props; ---- - - -
    -
    - -
    - -
    - - diff --git a/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro b/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro deleted file mode 100644 index ded83521c..000000000 --- a/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +++ /dev/null @@ -1,54 +0,0 @@ ---- -import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro'; -import VisualiserLayout from '@layouts/VisualiserLayout.astro'; -import type { PageTypes } from '@types'; -import { buildUrl } from '@utils/url-builder'; -import { ClientRouter } from 'astro:transitions'; - -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const props = await Page.getData(Astro); - -const { - data: { id }, - collection, -} = props; ---- - - -
    -
    - -
    - -
    - - diff --git a/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro b/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro deleted file mode 100644 index b0c1156a7..000000000 --- a/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +++ /dev/null @@ -1,65 +0,0 @@ ---- -import Seo from '@components/Seo.astro'; -import { buildUrl } from '@utils/url-builder'; -import { getEvents } from '@utils/events'; -import { getCommands } from '@utils/commands'; -import { getServices } from '@utils/collections/services'; -import { getDomains } from '@utils/collections/domains'; -import { getContainers } from '@utils/collections/containers'; -import type { CollectionEntry } from 'astro:content'; -import type { CollectionTypes } from '@types'; - -export async function getStaticPaths() { - const [events, commands, services, domains, containers] = await Promise.all([ - getEvents(), - getCommands(), - getServices(), - getDomains(), - getContainers(), - ]); - - const resources = [...domains, ...events, ...services, ...commands, ...containers]; - const resourcesWithVisualiserEnabled = resources.filter((resource) => resource.data.visualiser !== false); - - const buildPages = (collection: CollectionEntry[]) => { - return collection.map((item) => ({ - params: { - type: item.collection, - id: item.data.id, - }, - props: { - type: item.collection, - ...item, - }, - })); - }; - - return [...buildPages(resourcesWithVisualiserEnabled)]; -} - -const props = Astro.props; -const pageTitle = `${props.collection} | ${props.data.name}`.replace(/^\w/, (c) => c.toUpperCase()); - -const { pathname } = Astro.url; - -const redirectUrl = buildUrl(pathname + '/' + props.data.latestVersion, false, true); ---- - - - - - - - -

    You are being redirected to {redirectUrl}

    - - - - diff --git a/eventcatalog/src/pages/visualiser/context-map/index.astro b/eventcatalog/src/pages/visualiser/context-map/index.astro deleted file mode 100644 index 42de5dbaa..000000000 --- a/eventcatalog/src/pages/visualiser/context-map/index.astro +++ /dev/null @@ -1,30 +0,0 @@ ---- -import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro'; -import { ClientRouter } from 'astro:transitions'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; ---- - - -
    -
    -
    - -
    - -
    diff --git a/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts b/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts deleted file mode 100644 index c7eb16096..000000000 --- a/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { HybridPage } from '@utils/page-loaders/hybrid-page'; -import { isAuthEnabled } from '@utils/feature'; - -export class Page extends HybridPage { - static async getStaticPaths(): Promise> { - if (isAuthEnabled()) { - return []; - } - - const { getDesigns } = await import('@utils/collections/designs'); - - const designs = await getDesigns(); - - return designs.map((item) => ({ - params: { - type: 'designs', - id: item.data.id, - }, - props: { - type: 'designs', - ...item, - }, - })); - } - - protected static async fetchData(params: any) { - const { id } = params; - - if (!id) { - return null; - } - - const { getDesigns } = await import('@utils/collections/designs'); - const designs = await getDesigns(); - - const design = designs.find((design) => design.data.id === id); - - if (!design) { - return null; - } - - return { - type: 'designs', - ...design, - }; - } - - protected static createNotFoundResponse(): Response { - return new Response(null, { - status: 404, - statusText: 'Design not found', - }); - } - - static get clientAuthScript(): string { - if (!isAuthEnabled()) { - return ''; - } - - return ` - if (typeof window !== 'undefined' && !import.meta.env.SSR) { - fetch('/api/auth/session') - .then(res => res.json()) - .then(session => { - if (!session?.user) { - window.location.href = '/auth/login?callbackUrl=' + encodeURIComponent(window.location.pathname); - } - }) - .catch(() => { - window.location.href = '/auth/login?callbackUrl=' + encodeURIComponent(window.location.pathname); - }); - } - `; - } -} diff --git a/eventcatalog/src/pages/visualiser/designs/[id]/index.astro b/eventcatalog/src/pages/visualiser/designs/[id]/index.astro deleted file mode 100644 index f150250a7..000000000 --- a/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +++ /dev/null @@ -1,31 +0,0 @@ ---- -import NodeGraph from '@components/MDX/NodeGraph/NodeGraph'; -import { ClientRouter } from 'astro:transitions'; -import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro'; - -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -const props = Astro.props; - -const { data } = props; ---- - - -
    -
    -
    - -
    - -
    diff --git a/eventcatalog/src/pages/visualiser/domain-integrations/index.astro b/eventcatalog/src/pages/visualiser/domain-integrations/index.astro deleted file mode 100644 index 04aa7236c..000000000 --- a/eventcatalog/src/pages/visualiser/domain-integrations/index.astro +++ /dev/null @@ -1,31 +0,0 @@ ---- -import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro'; -import VisualiserLayout from '@layouts/VisualiserLayout.astro'; -import { buildUrl } from '@utils/url-builder'; -import { ClientRouter } from 'astro:transitions'; ---- - - -
    -
    -
    - -
    - -
    diff --git a/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro b/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro deleted file mode 100644 index 90d913f77..000000000 --- a/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +++ /dev/null @@ -1,52 +0,0 @@ ---- -import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro'; -import VisualiserLayout from '@layouts/VisualiserLayout.astro'; -import { buildUrl } from '@utils/url-builder'; -import { ClientRouter } from 'astro:transitions'; - -import { Page } from './_index.data'; - -export const prerender = Page.prerender; -export const getStaticPaths = Page.getStaticPaths; - -// Get data -const props = await Page.getData(Astro); - -const { - data: { id }, - collection, -} = props; ---- - - -
    -
    - -
    - -
    - - diff --git a/eventcatalog/src/remark-plugins/directives.ts b/eventcatalog/src/remark-plugins/directives.ts deleted file mode 100644 index 848136eb8..000000000 --- a/eventcatalog/src/remark-plugins/directives.ts +++ /dev/null @@ -1,109 +0,0 @@ -// src/remark-plugins/directives.js -import { visit } from 'unist-util-visit'; - -export function remarkDirectives() { - return (tree: any) => { - visit(tree, (node) => { - if (node.type === 'containerDirective') { - const blockTypes = { - info: 'bg-blue-50 border-l-4 border-blue-500', - warning: 'bg-yellow-50 border-l-4 border-yellow-500', - danger: 'bg-red-50 border-l-4 border-red-500', - tip: 'bg-green-50 border-l-4 border-green-500', - note: 'bg-gray-50 border-l-4 border-gray-500', - }; - - // Lucide icon paths - const iconPaths = { - info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z', - warning: - 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z', - danger: - 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z', - tip: 'M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z', - note: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', - }; - - node.data = node.data || {}; - node.data.hName = 'div'; - node.data.hProperties = { - class: `rounded-lg p-4 my-4 ${blockTypes[node.name as keyof typeof blockTypes] || ''}`, - }; - - // Create header div that will contain icon and type - const headerNode = { - type: 'element', - data: { - hName: 'div', - hProperties: { - class: 'flex items-center gap-2 font-semibold mb-2', - }, - }, - children: [ - // Lucide Icon SVG - { - type: 'element', - data: { - hName: 'svg', - hProperties: { - xmlns: 'http://www.w3.org/2000/svg', - width: '26', - height: '26', - viewBox: '0 0 24 24', - fill: 'none', - stroke: 'currentColor', - strokeWidth: '2', - strokeLinecap: 'round', - strokeLinejoin: 'round', - class: 'lucide', - }, - }, - children: [ - { - type: 'element', - data: { - hName: 'path', - hProperties: { - d: iconPaths[node.name as keyof typeof iconPaths] || '', - }, - }, - }, - ], - }, - // Type label - { - type: 'element', - data: { - hName: 'span', - hProperties: { - class: '', - }, - }, - children: [ - { - type: 'text', - value: node.name.charAt(0).toUpperCase() + node.name.slice(1), - }, - ], - }, - ], - }; - - // Create content div for the rest of the children - const contentNode = { - type: 'element', - data: { - hName: 'div', - hProperties: { - class: 'prose prose-md w-full !max-w-none ', - }, - }, - children: node.children, - }; - - // Replace node's children with header and content - node.children = [headerNode, contentNode]; - } - }); - }; -} diff --git a/eventcatalog/src/remark-plugins/mermaid.ts b/eventcatalog/src/remark-plugins/mermaid.ts deleted file mode 100644 index 53892912b..000000000 --- a/eventcatalog/src/remark-plugins/mermaid.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { RemarkPlugin } from '@astrojs/markdown-remark'; -import { visit } from 'unist-util-visit'; - -const escapeMap: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', -}; - -const escapeHtml = (str: string) => str.replace(/[&<>"']/g, (c) => escapeMap[c]); - -export const mermaid: RemarkPlugin<[]> = () => (tree) => { - visit(tree, 'code', (node) => { - if (node.lang !== 'mermaid') return; - - // @ts-ignore test - node.type = 'html'; - node.value = ` -
    -
    -

    Loading graph...

    -
    -
    - `; - }); -}; diff --git a/eventcatalog/src/types/index.ts b/eventcatalog/src/types/index.ts deleted file mode 100644 index ee37f197e..000000000 --- a/eventcatalog/src/types/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -export type CollectionTypes = - | 'commands' - | 'events' - | 'queries' - | 'domains' - | 'services' - | 'flows' - | 'channels' - | 'entities' - | 'containers'; -export type CollectionMessageTypes = 'commands' | 'events' | 'queries'; -export type CollectionUserTypes = 'users'; -export type PageTypes = - | 'events' - | 'commands' - | 'queries' - | 'services' - | 'domains' - | 'channels' - | 'flows' - | 'entities' - | 'containers'; - -export type TableConfiguration = { - columns: { - [key: string]: { - label?: string; - visible?: boolean; - }; - }; -}; diff --git a/eventcatalog/src/types/react-syntax-highlighter.d.ts b/eventcatalog/src/types/react-syntax-highlighter.d.ts deleted file mode 100644 index 1ffa9de38..000000000 --- a/eventcatalog/src/types/react-syntax-highlighter.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'react-syntax-highlighter/dist/esm/styles/prism/material-light'; diff --git a/eventcatalog/src/utils/__tests__/collections/util.spec.ts b/eventcatalog/src/utils/__tests__/collections/util.spec.ts deleted file mode 100644 index e8623d71a..000000000 --- a/eventcatalog/src/utils/__tests__/collections/util.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { CollectionTypes } from '@types'; -import { getDeprecatedDetails, isSameVersion, satisfies, sortStringVersions } from '@utils/collections/util'; -import type { CollectionEntry } from 'astro:content'; -import { describe, it, expect } from 'vitest'; - -describe('Collections - utils', () => { - describe('satisfies', () => { - it.each([ - [{ version: '1.0.0', range: '1.0.0', expected: true }], - [{ version: '1.0.0', range: '^1', expected: true }], - [{ version: '1.0.0', range: '1', expected: true }], - [{ version: '1.0.0', range: 'v1', expected: true }], - [{ version: '1.0.0', range: '>1', expected: false }], - [{ version: '1', range: '1', expected: true }], - [{ version: '1', range: '^1', expected: true }], - [{ version: '1', range: '1.0.0', expected: true }], - [{ version: '1', range: 'v1', expected: true }], - [{ version: '1', range: '<1', expected: false }], - [{ version: '2', range: 'v1', expected: false }], - [{ version: 'v1', range: 'v1', expected: true }], - [{ version: 'v1', range: '1', expected: true }], - [{ version: 'v1', range: '1.0.0', expected: true }], - [{ version: 'v1', range: '^1', expected: true }], - [{ version: 'v1', range: '2', expected: false }], - [{ version: 'v1', range: '>1', expected: false }], - ])('should returns $expected to version as $version and range as $range', ({ expected, version, range }) => { - expect(satisfies(version, range)).toBe(expected); - }); - }); - - describe('sortStringVersions', () => { - it.each([ - [{ versions: ['1', '3', '2'], result: ['3', '2', '1'], latest: '3' }], - [{ versions: ['10', '1', '2', '3'], result: ['10', '3', '2', '1'], latest: '10' }], - [{ versions: ['1.0.1', '1.1.0', '1.0.2'], result: ['1.1.0', '1.0.2', '1.0.1'], latest: '1.1.0' }], - [{ versions: ['a', 'c', 'b'], result: ['c', 'b', 'a'], latest: 'c' }], - [{ versions: [], result: [], latest: undefined }], - ])('should returns $latest as latest version of $versions', ({ versions, result, latest }) => { - expect(sortStringVersions(versions)).toEqual({ versions: result, latestVersion: latest }); - }); - }); - - describe('isSameVersion', () => { - it.each([ - [{ versions: ['1', '2'], result: false }], - [{ versions: ['1', '1'], result: true }], - [{ versions: ['2.0.0', '1.1.0'], result: false }], - [{ versions: ['2.0.0', '2.0.0'], result: true }], - [{ versions: ['a', 'b'], result: false }], - [{ versions: ['a', 'a'], result: true }], - [{ versions: ['1.0.0', undefined], result: false }], - ])('should returns $result when versions is $versions', ({ versions, result }) => { - expect(isSameVersion(versions[0], versions[1])).toBe(result); - }); - }); - - describe('getDeprecatedDetails', () => { - it('returns false when deprecated is false', () => { - const result = getDeprecatedDetails({ data: { deprecated: false } } as unknown as CollectionEntry); - expect(result).toEqual({ hasDeprecated: false, isMarkedAsDeprecated: false, message: '', deprecatedDate: '' }); - }); - - it('returns true when deprecated is true (boolean)', () => { - const result = getDeprecatedDetails({ data: { deprecated: true } } as unknown as CollectionEntry); - expect(result).toEqual({ hasDeprecated: true, isMarkedAsDeprecated: true, message: '', deprecatedDate: '' }); - }); - - it('returns true when deprecated is true (object)', () => { - const result = getDeprecatedDetails({ - data: { deprecated: { date: '2021-01-01', message: 'This is a test message' } }, - } as unknown as CollectionEntry); - expect(result).toEqual({ - hasDeprecated: true, - isMarkedAsDeprecated: true, - message: 'This is a test message', - deprecatedDate: 'January 1, 2021', - }); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/commands/mocks.ts b/eventcatalog/src/utils/__tests__/commands/mocks.ts deleted file mode 100644 index 2bb0cdfbf..000000000 --- a/eventcatalog/src/utils/__tests__/commands/mocks.ts +++ /dev/null @@ -1,170 +0,0 @@ -export const mockServices = [ - { - id: 'OrderService', - slug: 'OrderService', - collection: 'services', - data: { - id: 'OrderService', - version: '0.0.1', - sends: [ - { - id: 'AdjustOrder', - version: '0.0.1', - }, - ], - receives: [ - { - id: 'PlaceOrder', - version: '>1.5.0', - }, - { - id: 'GetOrder', - version: '0.0.1', - from: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - ], - }, - }, - { - id: 'PaymentService', - slug: 'PaymentService', - collection: 'services', - data: { - id: 'PaymentService', - version: '0.0.1', - sends: [ - { - id: 'PlaceOrder', - version: '0.0.1', - to: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - { - id: 'GetOrder', - version: '0.0.1', - to: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - ], - receives: [ - { - id: 'AdjustOrder', - version: '0.0.1', - }, - ], - }, - }, - { - id: 'InventoryService', - slug: 'InventoryService', - collection: 'services', - data: { - id: 'InventoryService', - version: '0.0.1', - sends: [{ id: 'NotifyLowStock' }], - }, - }, - { - id: 'NotificationService', - slug: 'NotificationService', - collection: 'services', - data: { - id: 'NotificationService', - version: '0.0.1', - receives: [{ id: 'NotifyLowStock', version: 'latest' }], - }, - }, - { - id: 'LegacyOrderService', - slug: 'LegacyOrderService', - collection: 'services', - data: { - id: 'LegacyOrderService', - version: '0.0.1', - receives: [{ id: 'GetOrder', version: 'latest' }], - sends: [{ id: 'GetOrder', version: 'latest' }], - }, - }, -]; - -export const mockCommands = [ - { - id: 'AdjustOrder', - slug: 'AdjustOrder', - collection: 'commands', - data: { - id: 'AdjustOrder', - version: '0.0.1', - }, - }, - { - id: 'PlaceOrder', - slug: 'PlaceOrder', - collection: 'commands', - data: { - id: 'PlaceOrder', - version: '1.5.1', - }, - }, - { - id: 'PlaceOrder', - slug: 'PlaceOrder', - collection: 'commands', - data: { - id: 'PlaceOrder', - version: '2.0.1', - }, - }, - { - id: 'NotifyLowStock', - slug: 'NotifyLowStock', - collection: 'commands', - data: { - id: 'NotifyLowStock', - version: '2.0.0', - }, - }, - { - id: 'NotifyLowStock', - slug: 'NotifyLowStock', - collection: 'commands', - data: { - id: 'NotifyLowStock', - version: '2.0.1', - }, - }, - { - id: 'GetOrder', - slug: 'GetOrder', - collection: 'commands', - data: { - id: 'GetOrder', - version: '0.0.1', - }, - }, -]; - -export const mockChannels = [ - { - id: 'EmailChannel', - slug: 'EmailChannel', - collection: 'channels', - data: { - id: 'EmailChannel', - version: '1.0.0', - }, - }, -]; diff --git a/eventcatalog/src/utils/__tests__/commands/node-graph.spec.ts b/eventcatalog/src/utils/__tests__/commands/node-graph.spec.ts deleted file mode 100644 index b2b8567f6..000000000 --- a/eventcatalog/src/utils/__tests__/commands/node-graph.spec.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { MarkerType } from '@xyflow/react'; -import { getNodesAndEdgesForCommands as getNodesAndEdges } from '../../node-graphs/message-node-graph'; -import { expect, describe, it, vi } from 'vitest'; -import { mockCommands, mockServices, mockChannels } from './mocks'; - -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - // this will only affect "foo" outside of the original module - getCollection: (key: string) => { - if (key === 'services') { - return Promise.resolve(mockServices); - } - if (key === 'commands') { - return Promise.resolve(mockCommands); - } - if (key === 'channels') { - return Promise.resolve(mockChannels); - } - return Promise.resolve([]); - }, - }; -}); - -describe('Commands NodeGraph', () => { - describe('getNodesAndEdges', () => { - it('should return nodes and edges for a given command', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'AdjustOrder', version: '0.0.1' }); - - // The middle node itself, the service - const expectedCommandNode = { - id: 'AdjustOrder-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'commands', - }; - - const expectedProducerNode = { - id: 'OrderService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[0].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedConsumerNode = { - id: 'PaymentService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { - title: 'PaymentService', - mode: 'simple', - service: { ...mockServices[1].data }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'OrderService-0.0.1-AdjustOrder-0.0.1', - source: 'OrderService-0.0.1', - target: 'AdjustOrder-0.0.1', - label: 'invokes', - }), - expect.objectContaining({ - id: 'AdjustOrder-0.0.1-PaymentService-0.0.1', - source: 'AdjustOrder-0.0.1', - target: 'PaymentService-0.0.1', - label: 'accepts', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedConsumerNode), - - // The command node itself - expect.objectContaining(expectedCommandNode), - - // Nodes on the right - expect.objectContaining(expectedProducerNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the command is produced and consumed by a service it will render a custom edge', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'GetOrder', version: '0.0.1' }); - - // The middle node itself, the service - const expectedCommandNode = { - id: 'GetOrder-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: expect.anything() }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'commands', - }; - - const expectedProducerNode = { - id: 'LegacyOrderService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[4].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedConsumerNode = { - id: 'LegacyOrderService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { - title: 'LegacyOrderService', - mode: 'simple', - service: { ...mockServices[4].data }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'LegacyOrderService-0.0.1-GetOrder-0.0.1', - source: 'LegacyOrderService-0.0.1', - target: 'GetOrder-0.0.1', - label: 'invokes', - animated: false, - }), - expect.objectContaining({ - id: 'GetOrder-0.0.1-LegacyOrderService-0.0.1', - source: 'GetOrder-0.0.1', - target: 'LegacyOrderService-0.0.1', - label: 'accepts', - animated: false, - }), - expect.objectContaining({ - id: 'GetOrder-0.0.1-LegacyOrderService-0.0.1-both', - source: 'GetOrder-0.0.1', - target: 'LegacyOrderService-0.0.1', - label: 'publishes and subscribes', - animated: false, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 40, - height: 40, - }, - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedConsumerNode), - - // The event node itself - expect.objectContaining(expectedCommandNode), - - // Nodes on the right - expect.objectContaining(expectedProducerNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the consumer of a command has defined a channel, it will render the channel node and edges', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'GetOrder', version: '0.0.1' }); - - const expectedConsumerNode = { - id: 'OrderService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[0].data }, title: 'OrderService' }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedChannelNode = { - sourcePosition: 'right', - targetPosition: 'left', - id: 'EmailChannel-1.0.0', - type: 'channels', - }; - - const expectedCommandNode = { - id: 'GetOrder-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'commands', - }; - - const expectedEdges = expect.arrayContaining([ - // Message to the channel - expect.objectContaining({ - id: 'GetOrder-0.0.1-EmailChannel-1.0.0', - source: 'GetOrder-0.0.1', - target: 'EmailChannel-1.0.0', - }), - // Channel to the consumer - expect.objectContaining({ - id: 'EmailChannel-1.0.0-OrderService-0.0.1', - source: 'EmailChannel-1.0.0', - target: 'OrderService-0.0.1', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedConsumerNode), - - // channel - expect.objectContaining(expectedChannelNode), - - // The command node itself - expect.objectContaining(expectedCommandNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the producer of an event has defined a channel, it will render the channel node and edges', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'GetOrder', version: '0.0.1' }); - - const expectedProducerNode = { - id: 'PaymentService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedChannelNode = { - sourcePosition: 'right', - targetPosition: 'left', - id: 'EmailChannel-1.0.0', - type: 'channels', - }; - - const expectedCommandNode = { - id: 'GetOrder-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'commands', - }; - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'PaymentService-0.0.1-GetOrder-0.0.1', - source: 'PaymentService-0.0.1', - target: 'GetOrder-0.0.1', - label: 'invokes', - animated: false, - }), - // Message to the channel - expect.objectContaining({ - id: 'GetOrder-0.0.1-EmailChannel-1.0.0', - source: 'GetOrder-0.0.1', - target: 'EmailChannel-1.0.0', - label: 'routes to', - animated: false, - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedProducerNode), - - // channel - expect.objectContaining(expectedChannelNode), - - // The command node itself - expect.objectContaining(expectedCommandNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('returns an empty array if no commands are found', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'UnknownCommand', version: '1.0.0' }); - - expect(nodes).toEqual([]); - expect(edges).toEqual([]); - }); - - it('should return nodes and edges for a given event using semver range', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'PlaceOrder', version: '2.0.1' }); - - // The middle node itself, the service - const expectedCommandNode = { - id: 'PlaceOrder-2.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: expect.anything() }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'commands', - }; - - const expectedConsumerNode = { - id: 'OrderService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { - title: 'OrderService', - mode: 'simple', - service: { ...mockServices[0].data }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'PlaceOrder-2.0.1-OrderService-0.0.1', - source: 'PlaceOrder-2.0.1', - target: 'OrderService-0.0.1', - label: 'accepts', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // The command node itself - expect.objectContaining(expectedCommandNode), - - // Nodes on the Right - expect.objectContaining(expectedConsumerNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/containers/mocks.ts b/eventcatalog/src/utils/__tests__/containers/mocks.ts deleted file mode 100644 index a15a3879d..000000000 --- a/eventcatalog/src/utils/__tests__/containers/mocks.ts +++ /dev/null @@ -1,240 +0,0 @@ -export const mockEvents = [ - { - slug: 'PaymentInitiated', - collection: 'events', - data: { - id: 'PaymentInitiated', - version: '0.0.1', - }, - }, - { - slug: 'PaymentProcessed', - collection: 'events', - data: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, -]; - -export const mockServices = [ - { - slug: 'SubscriptionService', - collection: 'services', - data: { - id: 'SubscriptionService', - version: '0.0.1', - }, - }, - { - slug: 'PaymentService', - collection: 'services', - data: { - id: 'PaymentService', - version: '0.0.1', - }, - }, -]; - -export const mockContainers = [ - { - id: 'OrderDatabase', - collection: 'containers', - data: { - id: 'OrderDatabase', - version: '1.0.0', - servicesThatWriteToContainer: [ - { - id: 'SubscriptionService', - version: '0.0.1', - data: { - id: 'SubscriptionService', - version: '0.0.1', - }, - }, - ], - servicesThatReadFromContainer: [ - { - id: 'SubscriptionService', - version: '0.0.1', - data: { - id: 'SubscriptionService', - version: '0.0.1', - }, - }, - ], - }, - }, -]; - -export const mockFlow = [ - { - id: 'Payment/PaymentProcessed/index.mdx', - slug: 'payment/paymentprocessed', - body: '', - collection: 'flows', - data: { - steps: [ - { - id: 1, - type: 'node', - title: 'Order Placed', - next_step: { - id: 2, - label: 'Proceed to payment', - }, - }, - { - id: 2, - title: 'Payment Initiated', - message: { - id: 'PaymentInitiated', - version: '0.0.1', - }, - next_steps: [ - { - id: 3, - label: 'Payment successful', - }, - { - id: 4, - label: 'Payment failed', - }, - ], - }, - { - id: 3, - title: 'Payment Processed', - message: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, - { - id: 4, - type: 'node', - title: 'Payment Failed', - }, - ], - id: 'PaymentFlow', - name: 'Payment Flow for E-commerce', - summary: 'Business flow for processing payments in an e-commerce platform', - version: '1.0.0', - type: 'node', - }, - }, -]; - -export const mockFlowByIds = [ - { - id: 'Payment/PaymentProcessed/index.mdx', - slug: 'payment/paymentprocessed', - body: '', - collection: 'flows', - data: { - steps: [ - { - id: 1, - type: 'node', - title: 'Order Placed', - next_step: 2, - }, - { - id: 2, - title: 'Payment Initiated', - message: { - id: 'PaymentInitiated', - version: '0.0.1', - }, - next_steps: [3, 4], - }, - { - id: 3, - title: 'Payment Processed', - message: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, - { - id: 4, - type: 'node', - title: 'Payment Failed', - }, - ], - id: 'PaymentFlow', - name: 'Payment Flow for E-commerce', - summary: 'Business flow for processing payments in an e-commerce platform', - version: '1.0.0', - type: 'node', - }, - }, - { - id: 'Subscriptions/CancelSubscription/index.mdx', - slug: 'subscriptions/CancelSubscription', - body: '', - collection: 'flows', - data: { - steps: [ - { - id: 'cancel_subscription_initiated', - title: 'Cancels Subscription', - actor: { - name: 'User', - }, - next_step: { - id: 'cancel_subscription_request', - label: 'Initiate subscription cancellation', - }, - }, - { - id: 'cancel_subscription_request', - title: 'Cancel Subscription', - message: { - id: 'CancelSubscription', - version: '0.0.1', - }, - next_step: { - id: 'subscription_service', - label: 'Proceed to subscription service', - }, - }, - { - id: 'subscription_service', - title: 'Subscription Service', - service: { - id: 'SubscriptionService', - version: 'latest', - }, - next_steps: [ - { - id: 'subscription_cancelled', - label: 'Successful cancellation', - }, - { - id: 'subscription_rejected', - label: 'Failed cancellation', - }, - ], - }, - { - id: 'subscription_cancelled', - title: 'Subscription has been cancelled', - message: { - id: 'UserSubscriptionCancelled', - version: '0.0.1', - }, - }, - { - id: 'subscription_rejected', - title: 'Subscription cancellation has been rejected', - }, - ], - id: 'CancelSubscription', - name: 'User Cancels Subscription', - summary: 'Flow for when a user has cancelled a subscription', - version: '1.0.0', - // type: 'node', - }, - }, -]; diff --git a/eventcatalog/src/utils/__tests__/containers/node-graph.spec.ts b/eventcatalog/src/utils/__tests__/containers/node-graph.spec.ts deleted file mode 100644 index c2e8fe2bd..000000000 --- a/eventcatalog/src/utils/__tests__/containers/node-graph.spec.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { getNodesAndEdges } from '../../node-graphs/container-node-graph'; -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import { mockContainers, mockServices } from './mocks'; - -vi.mock('@utils/collections/containers', async (importOriginal) => { - return { - ...(await importOriginal()), - getContainers: () => Promise.resolve(mockContainers), - }; -}); - -describe('Containers NodeGraph', () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - describe('getNodesAndEdges', () => { - it('should return the correct nodes and edges for a given container', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'OrderDatabase', version: '1.0.0' }); - - expect(nodes).toEqual([ - { - id: 'SubscriptionService-0.0.1', - type: undefined, - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[0].data } }, - position: { x: 75, y: 50 }, - }, - { - id: 'OrderDatabase-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', data: { ...mockContainers[0].data } }, - position: { x: 525, y: 50 }, - type: 'data', - }, - { - id: 'SubscriptionService-0.0.1', - sourcePosition: 'left', - targetPosition: 'right', - data: { title: 'SubscriptionService', mode: 'simple', service: { ...mockServices[0].data } }, - position: { x: 75, y: 50 }, - type: undefined, - }, - ]); - - expect(edges).toEqual([ - { - label: 'reads from \n (undefined)', - animated: false, - style: { - strokeWidth: 1, - }, - id: 'SubscriptionService-0.0.1-OrderDatabase-1.0.0', - source: 'OrderDatabase-1.0.0', - target: 'SubscriptionService-0.0.1', - data: { - service: { - id: 'SubscriptionService', - version: '0.0.1', - data: { - id: 'SubscriptionService', - version: '0.0.1', - }, - }, - }, - type: 'multiline', - markerStart: { - type: 'arrowclosed', - width: 40, - height: 40, - }, - }, - { - label: 'read and writes to \n (undefined)', - animated: false, - markerEnd: { - type: 'arrowclosed', - width: 40, - height: 40, - }, - style: { - strokeWidth: 1, - }, - id: 'OrderDatabase-1.0.0-SubscriptionService-0.0.1-both', - source: 'SubscriptionService-0.0.1', - target: 'OrderDatabase-1.0.0', - type: 'multiline', - markerStart: { - type: 'arrowclosed', - width: 40, - height: 40, - }, - }, - ]); - }); - - it('returns empty nodes and edges if no container is found', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'UnknownContainer', version: '1.0.0' }); - - expect(nodes).toEqual([]); - expect(edges).toEqual([]); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/domains/domains.spec.ts b/eventcatalog/src/utils/__tests__/domains/domains.spec.ts deleted file mode 100644 index 41a379521..000000000 --- a/eventcatalog/src/utils/__tests__/domains/domains.spec.ts +++ /dev/null @@ -1,254 +0,0 @@ -import type { ContentCollectionKey } from 'astro:content'; -import { expect, describe, it, vi } from 'vitest'; -import { mockDomains, mockServices, mockEvents, mockCommands, mockUbiquitousLanguages } from './mocks'; -import { - domainHasEntities, - getDomains, - getDomainsForService, - getParentDomains, - getUbiquitousLanguage, - getUbiquitousLanguageWithSubdomains, -} from '../../collections/domains'; -import type { Service } from '@utils/collections/services'; -import type { Domain } from '@utils/collections/domains'; -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - // this will only affect "foo" outside of the original module - getCollection: (key: ContentCollectionKey, filter?: any) => { - switch (key) { - case 'domains': - return Promise.resolve(mockDomains); - case 'services': - return Promise.resolve(mockServices); - case 'events': - return Promise.resolve(mockEvents); - case 'commands': - return Promise.resolve(mockCommands); - case 'ubiquitousLanguages': - let result = mockUbiquitousLanguages; - if (filter) { - result = mockUbiquitousLanguages.filter(filter); - } - return Promise.resolve(result); - default: - return Promise.resolve([]); - } - }, - }; -}); - -describe('Domains', () => { - describe('getDomains', () => { - it('should returns an array of domains with services using semver or latest', async () => { - const domains = await getDomains(); - - const expectedDomains = [ - // Checkout - { - ...mockDomains[1], - data: expect.objectContaining({ services: [mockServices[2], mockServices[3]] }), - }, - // Notification - { - ...mockDomains[2], - data: expect.objectContaining({ services: [] }), - }, - ]; - - expect(domains).toEqual(expect.arrayContaining(expectedDomains.map((d) => expect.objectContaining(d)))); - }); - }); - - describe('ubiquitous-language', () => { - it('should return the ubiquitous-language for a domain', async () => { - const domains = await getDomains(); - const shippingDomain = domains.find((d) => d.data.id === 'Shipping'); - const ubiquitousLanguages = await getUbiquitousLanguage(shippingDomain!); - expect(ubiquitousLanguages).toEqual([ - { - id: 'domains/Shipping/ubiquitous-language.mdx', - slug: 'domains/Shipping/ubiquitous-language', - collection: 'ubiquitousLanguages', - filePath: 'domains/Shipping/ubiquitous-language.mdx', - data: { - id: 'Shipping', - dictionary: [ - { - id: 'Payment', - name: 'Payment', - summary: 'A financial transaction', - }, - { - id: 'Order', - name: 'Order', - summary: 'A customer purchase request', - }, - ], - }, - }, - ]); - }); - }); - - describe('getDomainsForService', () => { - it('should return the domains for a service', async () => { - const domains = await getDomainsForService(mockServices[0] as unknown as Service); - const expectedDomain = mockDomains[0]; - expect(domains[0].data.id).toEqual(expectedDomain.data.id); - expect(domains[0].data.services).toEqual(expect.arrayContaining([mockServices[0]])); - }); - - it('returns an empty array if the service is not found', async () => { - const domains = await getDomainsForService(mockServices[4] as unknown as Service); - expect(domains).toEqual([]); - }); - }); - - describe('getParentDomains', () => { - it('should return the parent domains for a domain', async () => { - const checkoutDomain = mockDomains.find((d) => d.data.id === 'Checkout'); - const domains = await getParentDomains(checkoutDomain as unknown as Domain); - const expectedDomain = mockDomains.find((d) => d.data.id === 'Shipping'); - expect(domains.length).toBeGreaterThan(0); - expect(domains[0].data.id).toEqual(expectedDomain!.data.id); - // The domains array will contain the processed subdomain objects, not the original mock - //@ts-ignore - expect(domains[0].data.domains.some((d: any) => d.data.id === 'Checkout')).toBe(true); - }); - - it('returns an empty array if the domain is not found', async () => { - const notificationDomain = mockDomains.find((d) => d.data.id === 'Notification'); - const domains = await getParentDomains(notificationDomain as unknown as Domain); - expect(domains).toEqual([]); - }); - }); - - describe('getUbiquitousLanguageWithSubdomains', () => { - it('should return domain ubiquitous language with subdomain languages', async () => { - const domains = await getDomains(); - const shippingDomain = domains.find((d) => d.data.id === 'Shipping'); - - const result = await getUbiquitousLanguageWithSubdomains(shippingDomain as Domain); - - expect(result.domain).toEqual({ - id: 'domains/Shipping/ubiquitous-language.mdx', - slug: 'domains/Shipping/ubiquitous-language', - collection: 'ubiquitousLanguages', - filePath: 'domains/Shipping/ubiquitous-language.mdx', - data: { - id: 'Shipping', - dictionary: [ - { id: 'Payment', name: 'Payment', summary: 'A financial transaction' }, - { id: 'Order', name: 'Order', summary: 'A customer purchase request' }, - ], - }, - }); - - expect(result.subdomains).toHaveLength(1); - expect(result.subdomains[0].subdomain.data.id).toBe('Checkout'); - expect(result.subdomains[0].ubiquitousLanguage).toEqual({ - id: 'domains/Checkout/ubiquitous-language.mdx', - slug: 'domains/Checkout/ubiquitous-language', - collection: 'ubiquitousLanguages', - filePath: 'domains/Checkout/ubiquitous-language.mdx', - data: { - id: 'Checkout', - dictionary: [ - { id: 'Payment', name: 'Payment', summary: 'Processing customer payment' }, - { id: 'Cart', name: 'Cart', summary: 'Shopping cart items' }, - ], - }, - }); - }); - - it('should detect duplicate terms across domain and subdomains', async () => { - const domains = await getDomains(); - const shippingDomain = domains.find((d) => d.data.id === 'Shipping'); - - const result = await getUbiquitousLanguageWithSubdomains(shippingDomain as Domain); - - // Payment appears in both Shipping domain and Checkout subdomain, so should be a duplicate - expect(result.duplicateTerms).toContain('payment'); - // Order only appears in Shipping domain, Cart only in Checkout - neither should be duplicates - expect(result.duplicateTerms).not.toContain('order'); - expect(result.duplicateTerms).not.toContain('cart'); - }); - - it('should handle domain with no ubiquitous language', async () => { - const domains = await getDomains(); - const notificationDomain = domains.find((d) => d.data.id === 'Notification'); - - const result = await getUbiquitousLanguageWithSubdomains(notificationDomain as Domain); - - expect(result.domain).toEqual({ - id: 'domains/Notification/ubiquitous-language.mdx', - slug: 'domains/Notification/ubiquitous-language', - collection: 'ubiquitousLanguages', - filePath: 'domains/Notification/ubiquitous-language.mdx', - data: { - id: 'Notification', - dictionary: [{ id: 'Email', name: 'Email', summary: 'Electronic mail notification' }], - }, - }); - expect(result.subdomains).toHaveLength(0); - expect(result.duplicateTerms.size).toBe(0); - }); - - it('should handle domain with subdomains but no ubiquitous language for subdomains', async () => { - const domains = await getDomains(); - const checkoutDomain = domains.find((d) => d.data.id === 'Checkout'); - - const result = await getUbiquitousLanguageWithSubdomains(checkoutDomain as Domain); - - expect(result.domain).toEqual({ - id: 'domains/Checkout/ubiquitous-language.mdx', - slug: 'domains/Checkout/ubiquitous-language', - collection: 'ubiquitousLanguages', - filePath: 'domains/Checkout/ubiquitous-language.mdx', - data: { - id: 'Checkout', - dictionary: [ - { id: 'Payment', name: 'Payment', summary: 'Processing customer payment' }, - { id: 'Cart', name: 'Cart', summary: 'Shopping cart items' }, - ], - }, - }); - expect(result.subdomains).toHaveLength(0); - expect(result.duplicateTerms.size).toBe(0); - }); - - it('should return empty result when domain has no ubiquitous language and no subdomains', async () => { - const mockDomainWithoutLanguage = { - id: 'domains/Empty/index.mdx', - filePath: 'domains/Empty/index.mdx', - data: { - id: 'Empty', - name: 'Empty', - version: '0.0.1', - domains: [], - }, - }; - - //@ts-ignore - const result = await getUbiquitousLanguageWithSubdomains(mockDomainWithoutLanguage as Domain); - - expect(result.domain).toBeNull(); - expect(result.subdomains).toHaveLength(0); - expect(result.duplicateTerms.size).toBe(0); - }); - }); - - describe('domainHasEntities', () => { - it('should return true if the domain has entities', async () => { - const domains = await getDomains(); - domains[0].data.entities = [{ id: 'Order', version: '1.0.0' }]; - expect(domainHasEntities(domains[0])).toBe(true); - }); - - it('should return false if the domain has no entities', async () => { - const domains = await getDomains(); - expect(domainHasEntities(domains[1])).toBe(false); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/domains/mocks.ts b/eventcatalog/src/utils/__tests__/domains/mocks.ts deleted file mode 100644 index 10a4bf998..000000000 --- a/eventcatalog/src/utils/__tests__/domains/mocks.ts +++ /dev/null @@ -1,247 +0,0 @@ -export const mockDomains = [ - { - id: 'domains/Shipping/index.mdx', - slug: 'domains/Shipping', - collection: 'domains', - filePath: 'domains/Shipping/index.mdx', - data: { - id: 'Shipping', - name: 'Shipping', - version: '0.0.1', - services: [{ id: 'LocationService', version: '0.0.1' }], - domains: [{ id: 'Checkout', version: '0.0.1' }], - entities: [{ id: 'Shipment', version: '0.0.1' }], - }, - }, - { - id: 'domains/Checkout/index.mdx', - slug: 'domains/Checkout', - collection: 'domains', - filePath: 'domains/Checkout/index.mdx', - data: { - id: 'Checkout', - name: 'Checkout', - version: '0.0.1', - services: [{ id: 'OrderService' /* version: latest */ }, { id: 'PaymentService', version: '0.0.1' }], - }, - }, - { - id: 'domains/Notification/index.mdx', - slug: 'domains/Notification', - collection: 'domains', - filePath: 'domains/Notification/index.mdx', - data: { - id: 'Notification', - name: 'Notification', - version: '0.0.1', - services: [{ id: 'MailService' }], - }, - }, -]; - -export const mockServices = [ - { - id: 'services/LocationService/index.mdx', - slug: 'services/LocationService', - collection: 'services', - data: { - id: 'LocationService', - version: '0.0.1', - receives: [{ id: 'OrderPlaced', version: '0.0.1' }], - }, - }, - { - id: 'services/OrderService/versioned/001/index.mdx', - slug: 'services/OrderService/versioned/001', - collection: 'services', - data: { - id: 'OrderService', - version: '0.0.1', - receives: [{ id: 'PlaceOrder', version: '>1.5.0' }], - sends: [{ id: 'OrderPlaced', version: '0.0.1' }], - }, - }, - { - id: 'services/OrderService/index.mdx', - slug: 'services/OrderService', - collection: 'services', - data: { - id: 'OrderService', - version: '1.0.0', - receives: [{ id: 'PlaceOrder', version: '>1.5.0' }], - sends: [{ id: 'OrderPlaced', version: '0.0.1' }], - }, - }, - { - id: 'services/PaymentService/index.mdx', - slug: 'services/PaymentService', - collection: 'services', - data: { - id: 'PaymentService', - version: '0.0.1', - receives: [{ id: 'OrderPlaced' }], - sends: [{ id: 'PaymentPaid', version: 'x' }, { id: 'PaymentRefunded' }, { id: 'PaymentFailed', version: '^1.0.0' }], - }, - }, - { - id: 'services/ServiceWithoutDomains/index.mdx', - slug: 'services/ServiceWithoutDomains', - collection: 'services', - data: { - id: 'ServiceWithoutDomains', - version: '0.0.1', - }, - }, -]; - -export const mockCommands = [ - // PlaceOrder - { - id: 'commands/PlaceOrder/versioned/100/index.mdx', - slug: 'commands/PlaceOrder/versoined/100', - collection: 'commands', - data: { - id: 'PlaceOrder', - version: '1.0.0', - }, - }, - { - id: 'commands/PlaceOrder/versioned/150/index.mdx', - slug: 'commands/PlaceOrder/versoined/150', - collection: 'commands', - data: { - id: 'PlaceOrder', - version: '1.5.0', - }, - }, - { - id: 'commands/PlaceOrder/versioned/177/index.mdx', - slug: 'commands/PlaceOrder/versoined/177', - collection: 'commands', - data: { - id: 'PlaceOrder', - version: '1.7.7', - }, - }, -]; - -export const mockEvents = [ - // OrderPlaced - { - id: 'events/OrderPlaced/index.mdx', - slug: 'events/OrderPlaced', - collection: 'events', - data: { - id: 'OrderPlaced', - version: '0.0.1', - }, - }, - - // PaymentPaid - { - id: 'events/PaymentPaid/versioned/001/index.mdx', - slug: 'events/PaymentPaid/versioned/001', - collection: 'events', - data: { - id: 'PaymentPaid', - version: '0.0.1', - }, - }, - { - id: 'events/PaymentPaid/index.mdx', - slug: 'events/PaymentPaid', - collection: 'events', - data: { - id: 'PaymentPaid', - version: '0.0.2', - }, - }, - - // PaymentRefunded - { - id: 'events/PaymentRefunded/index.mdx', - slug: 'events/PaymentRefunded', - collection: 'events', - data: { - id: 'PaymentRefunded', - version: '0.0.1', - }, - }, - { - id: 'events/PaymentRefunded/index.mdx', - slug: 'events/PaymentRefunded', - collection: 'events', - data: { - id: 'PaymentRefunded', - version: '1.0.0', - }, - }, - - // PaymentFailed - { - id: 'events/PaymentFailed/index.mdx', - slug: 'events/PaymentFailed', - collection: 'events', - data: { - id: 'PaymentFailed', - version: '0.0.1', - }, - }, - { - id: 'events/PaymentFailed/index.mdx', - slug: 'events/PaymentFailed', - collection: 'events', - data: { - id: 'PaymentFailed', - version: '1.0.0', - }, - }, - { - id: 'events/PaymentFailed/index.mdx', - slug: 'events/PaymentFailed', - collection: 'events', - data: { - id: 'PaymentFailed', - version: '2.0.0', - }, - }, -]; - -export const mockUbiquitousLanguages = [ - { - id: 'domains/Shipping/ubiquitous-language.mdx', - slug: 'domains/Shipping/ubiquitous-language', - collection: 'ubiquitousLanguages', - filePath: 'domains/Shipping/ubiquitous-language.mdx', - data: { - id: 'Shipping', - dictionary: [ - { id: 'Payment', name: 'Payment', summary: 'A financial transaction' }, - { id: 'Order', name: 'Order', summary: 'A customer purchase request' }, - ], - }, - }, - { - id: 'domains/Checkout/ubiquitous-language.mdx', - slug: 'domains/Checkout/ubiquitous-language', - collection: 'ubiquitousLanguages', - filePath: 'domains/Checkout/ubiquitous-language.mdx', - data: { - id: 'Checkout', - dictionary: [ - { id: 'Payment', name: 'Payment', summary: 'Processing customer payment' }, - { id: 'Cart', name: 'Cart', summary: 'Shopping cart items' }, - ], - }, - }, - { - id: 'domains/Notification/ubiquitous-language.mdx', - slug: 'domains/Notification/ubiquitous-language', - collection: 'ubiquitousLanguages', - filePath: 'domains/Notification/ubiquitous-language.mdx', - data: { - id: 'Notification', - dictionary: [{ id: 'Email', name: 'Email', summary: 'Electronic mail notification' }], - }, - }, -]; diff --git a/eventcatalog/src/utils/__tests__/domains/node-graph.spec.ts b/eventcatalog/src/utils/__tests__/domains/node-graph.spec.ts deleted file mode 100644 index 60729bbaa..000000000 --- a/eventcatalog/src/utils/__tests__/domains/node-graph.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -import type { ContentCollectionKey } from 'astro:content'; -import { expect, describe, it, vi } from 'vitest'; -import { mockDomains, mockServices, mockEvents, mockCommands } from './mocks'; -import { getNodesAndEdges } from '@utils/node-graphs/domains-node-graph'; - -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - // this will only affect "foo" outside of the original module - getCollection: (key: ContentCollectionKey) => { - switch (key) { - case 'domains': - return Promise.resolve(mockDomains); - case 'services': - return Promise.resolve(mockServices); - case 'events': - return Promise.resolve(mockEvents); - case 'commands': - return Promise.resolve(mockCommands); - default: - return Promise.resolve([]); - } - }, - }; -}); - -describe('Domains NodeGraph', () => { - describe('getNodesAndEdges', () => { - it('returns an empty array if no domains are found', async () => { - // @ts-ignore - const { nodes, edges } = await getNodesAndEdges({ id: 'UnknownDomain', version: '1.0.0' }); - - expect(nodes).toEqual([]); - expect(edges).toEqual([]); - }); - - it('should return nodes and edges for a given domain', async () => { - // @ts-ignore - const { nodes, edges } = await getNodesAndEdges({ id: 'Shipping', version: '0.0.1' }); - - const expectedServiceNode = { - id: 'LocationService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[0].data }, group: expect.anything() }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedEventNode = { - id: 'OrderPlaced-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode: 'simple', - message: { ...mockEvents[0].data }, - group: { - type: 'Domain', - value: 'Checkout', - id: 'Checkout', - }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'OrderPlaced-0.0.1-LocationService-0.0.1', - source: 'OrderPlaced-0.0.1', - target: 'LocationService-0.0.1', - label: 'subscribed by', - animated: false, - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedEventNode), - - // The command node itself - expect.objectContaining(expectedServiceNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('should return a list of nodes and edges with a domain has subdomains', async () => { - // @ts-ignore - const { nodes, edges } = await getNodesAndEdges({ id: 'Shipping', version: '0.0.1' }); - - // Expect the orders service to be rendered (it's a service in a subdomain) - const expectedEventNode = { - id: 'OrderService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode: 'simple', - service: expect.objectContaining({ - id: 'OrderService', - version: '1.0.0', - }), - group: { - type: 'Domain', - value: 'Checkout', - id: 'Checkout', - }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - expect(nodes).toEqual(expect.arrayContaining([expect.objectContaining(expectedEventNode)])); - - expect(nodes.length).toEqual(10); - expect(edges.length).toEqual(9); - }); - - it('should return nodes and edges for a given domain with services using semver range or latest version (version undefind)', async () => { - // @ts-ignore - const { nodes, edges } = await getNodesAndEdges({ id: 'Checkout', version: '0.0.1' }); - - const expectedNodes = [ - { - id: 'PlaceOrder-1.7.7', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockCommands[2].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'commands', - }, - { - id: 'OrderService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode: 'simple', - service: { ...mockServices[2].data }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }, - { - id: 'OrderPlaced-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[0].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }, - /** PAYMENT SERVICE */ - { - id: 'PaymentService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode: 'simple', - service: { ...mockServices[3].data }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }, - { - id: 'PaymentPaid-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[1].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }, - { - id: 'PaymentPaid-0.0.2', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[2].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }, - { - id: 'PaymentRefunded-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[4].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }, - { - id: 'PaymentFailed-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[6].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }, - ]; - - expect(nodes).toStrictEqual(expect.arrayContaining(expectedNodes.map((n) => expect.objectContaining(n)))); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/events/mocks.ts b/eventcatalog/src/utils/__tests__/events/mocks.ts deleted file mode 100644 index 1d178cd42..000000000 --- a/eventcatalog/src/utils/__tests__/events/mocks.ts +++ /dev/null @@ -1,184 +0,0 @@ -export const mockServices = [ - { - id: 'OrderService', - slug: 'OrderService', - collection: 'services', - data: { - id: 'OrderService', - version: '0.0.1', - sends: [ - { - id: 'OrderCreatedEvent', - version: '0.0.1', - to: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - ], - }, - }, - { - id: 'PaymentService', - slug: 'PaymentService', - collection: 'services', - data: { - id: 'PaymentService', - version: '0.0.1', - receives: [ - { - id: 'OrderCreatedEvent', - version: '0.0.1', - }, - ], - }, - }, - { - id: 'InventoryService', - slug: 'InventoryService', - collection: 'services', - data: { - id: 'InventoryService', - version: '0.0.1', - sends: [ - { - id: 'InventoryAdjusted', - version: '>1.2.0', - }, - { - id: 'ProductOutOfStock', - }, - { - id: 'ProductDiscontinued', - version: 'latest', - }, - ], - }, - }, - { - id: 'CatalogService', - slug: 'CatalogService', - collection: 'services', - data: { - id: 'CatalogService', - version: '0.0.1', - receives: [ - { - id: 'InventoryAdjusted', - }, - { - id: 'ProductDiscontinued', - version: '*', - }, - ], - }, - }, - { - id: 'NotificationsService', - slug: 'NotificationsService', - collection: 'services', - data: { - id: 'NotificationsService', - version: '0.0.1', - receives: [ - { - id: 'EmailSent', - from: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - ], - sends: [ - { - id: 'EmailSent', - }, - { - id: 'EmailVerified', - }, - ], - }, - }, -]; - -export const mockChannels = [ - { - id: 'EmailChannel', - slug: 'EmailChannel', - collection: 'channels', - data: { - id: 'EmailChannel', - version: '1.0.0', - }, - }, -]; - -export const mockEvents = [ - { - id: 'OrderCreatedEvent', - slug: 'OrderCreatedEvent', - collection: 'events', - data: { - id: 'OrderCreatedEvent', - version: '0.0.1', - }, - }, - { - id: 'InventoryAdjusted', - slug: 'InventoryAdjusted', - collection: 'events', - data: { - id: 'InventoryAdjusted', - version: '1.5.1', - }, - }, - { - id: 'ProductOutOfStock', - slug: 'ProductOutOfStock', - collection: 'events', - data: { - id: 'ProductOutOfStock', - version: '1.0.0', - }, - }, - { - id: 'ProductDiscontinued', - slug: 'ProductDiscontinued', - collection: 'events', - data: { - id: 'ProductDiscontinued', - version: '0.0.1', - }, - }, - { - id: 'ProductDiscontinued', - slug: 'ProductDiscontinued', - collection: 'events', - data: { - id: 'ProductDiscontinued', - version: '1.0.0', - }, - }, - { - id: 'EmailSent', - slug: 'EmailSent', - collection: 'events', - data: { - id: 'EmailSent', - version: '1.0.0', - }, - }, - { - id: 'EmailVerified', - slug: 'EmailVerified', - collection: 'events', - data: { - id: 'EmailVerified', - version: '1.0.0', - }, - }, -]; diff --git a/eventcatalog/src/utils/__tests__/events/node-graph.spec.ts b/eventcatalog/src/utils/__tests__/events/node-graph.spec.ts deleted file mode 100644 index a59689482..000000000 --- a/eventcatalog/src/utils/__tests__/events/node-graph.spec.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { getNodesAndEdgesForEvents as getNodesAndEdges } from '../../node-graphs/message-node-graph'; -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import { mockEvents, mockServices, mockChannels } from './mocks'; - -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - // this will only affect "foo" outside of the original module - getCollection: (key: string) => { - if (key === 'services') { - return Promise.resolve(mockServices); - } - if (key === 'channels') { - return Promise.resolve(mockChannels); - } - if (key === 'events') { - return Promise.resolve(mockEvents); - } - return Promise.resolve([]); - }, - }; -}); - -describe('Events NodeGraph', () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - describe('getNodesAndEdges', () => { - it('should return nodes and edges for a given event', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'OrderCreatedEvent', version: '0.0.1' }); - - // The middle node itself, the service - const expectedEventNode = { - id: 'OrderCreatedEvent-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - // data: { mode: 'simple', message: expect.anything(), showSource: false }, - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }; - - const expectedProducerNode = { - id: 'OrderService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[0].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedConsumerNode = { - id: 'PaymentService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { - title: 'PaymentService', - mode: 'simple', - service: { ...mockServices[1].data }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'OrderService-0.0.1-OrderCreatedEvent-0.0.1', - source: 'OrderService-0.0.1', - target: 'OrderCreatedEvent-0.0.1', - label: 'publishes \nevent', - animated: false, - }), - // The channel to the event - expect.objectContaining({ - id: 'OrderCreatedEvent-0.0.1-EmailChannel-1.0.0', - source: 'OrderCreatedEvent-0.0.1', - target: 'EmailChannel-1.0.0', - label: 'routes to', - animated: false, - }), - // The event to the consumer - expect.objectContaining({ - id: 'OrderCreatedEvent-0.0.1-PaymentService-0.0.1', - source: 'OrderCreatedEvent-0.0.1', - target: 'PaymentService-0.0.1', - label: 'subscribed by', - animated: false, - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedConsumerNode), - - // The event node itself - expect.objectContaining(expectedEventNode), - - // Nodes on the right - expect.objectContaining(expectedProducerNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the event is produced and consumed by a service it will render a custom edge', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'EmailSent', version: '1.0.0' }); - - // The middle node itself, the service - const expectedEventNode = { - id: 'EmailSent-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - // data: { mode: 'simple', message: expect.anything(), showSource: false }, - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }; - - const expectedProducerNode = { - id: 'NotificationsService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[4].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedConsumerNode = { - id: 'NotificationsService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { - title: 'NotificationsService', - mode: 'simple', - service: { ...mockServices[4].data }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedEdges = expect.arrayContaining([ - // Producer to the event - expect.objectContaining({ - id: 'NotificationsService-0.0.1-EmailSent-1.0.0', - source: 'NotificationsService-0.0.1', - target: 'EmailSent-1.0.0', - label: 'publishes \nevent', - }), - // Event to the channel - expect.objectContaining({ - id: 'EmailSent-1.0.0-EmailChannel-1.0.0', - source: 'EmailSent-1.0.0', - target: 'EmailChannel-1.0.0', - label: 'routes to', - animated: false, - }), - expect.objectContaining({ - id: 'EmailSent-1.0.0-NotificationsService-0.0.1-both', - source: 'EmailSent-1.0.0', - target: 'NotificationsService-0.0.1', - label: 'publishes and subscribes', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedConsumerNode), - - // The event node itself - expect.objectContaining(expectedEventNode), - - // Nodes on the right - expect.objectContaining(expectedProducerNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the consumer of an event has defined a channel, it will render the channel node and edges', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'EmailSent', version: '1.0.0' }); - - const expectedConsumerNode = { - id: 'NotificationsService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[4].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedChannelNode = { - sourcePosition: 'right', - targetPosition: 'left', - id: 'EmailChannel-1.0.0', - type: 'channels', - }; - - const expectedEventNode = { - id: 'EmailSent-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'events', - }; - - const expectedEdges = expect.arrayContaining([ - // Message to the channel - expect.objectContaining({ - id: 'EmailSent-1.0.0-EmailChannel-1.0.0', - source: 'EmailSent-1.0.0', - target: 'EmailChannel-1.0.0', - }), - // Channel to the consumer - expect.objectContaining({ - id: 'EmailChannel-1.0.0-NotificationsService-0.0.1', - source: 'EmailChannel-1.0.0', - target: 'NotificationsService-0.0.1', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedConsumerNode), - - // channel - expect.objectContaining(expectedChannelNode), - - // The event node itself - expect.objectContaining(expectedEventNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the producer of an event has defined a channel, it will render the channel node and edges', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'OrderCreatedEvent', version: '0.0.1' }); - - const expectedProducerNode = { - id: 'OrderService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedChannelNode = { - sourcePosition: 'right', - targetPosition: 'left', - id: 'EmailChannel-1.0.0', - type: 'channels', - }; - - const expectedEventNode = { - id: 'OrderCreatedEvent-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'events', - }; - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'OrderService-0.0.1-OrderCreatedEvent-0.0.1', - source: 'OrderService-0.0.1', - target: 'OrderCreatedEvent-0.0.1', - label: 'publishes \nevent', - animated: false, - }), - // Message to the channel - expect.objectContaining({ - id: 'OrderCreatedEvent-0.0.1-EmailChannel-1.0.0', - source: 'OrderCreatedEvent-0.0.1', - target: 'EmailChannel-1.0.0', - label: 'routes to', - animated: false, - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedProducerNode), - - // channel - expect.objectContaining(expectedChannelNode), - - // The event node itself - expect.objectContaining(expectedEventNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('returns empty nodes and edges if no event is found', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'UnknownEvent', version: '1.0.0' }); - - expect(nodes).toEqual([]); - expect(edges).toEqual([]); - }); - - it('should return nodes and edges for a given event using semver range', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'InventoryAdjusted', version: '1.5.1' }); - - // The middle node itself, the service - const expectedEventNode = { - id: 'InventoryAdjusted-1.5.1', - sourcePosition: 'right', - targetPosition: 'left', - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }; - - const expectedProducerNode = { - id: 'InventoryService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[2].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedConsumerNode = { - id: 'CatalogService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { title: 'CatalogService', mode: 'simple', service: { ...mockServices[3].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'InventoryService-0.0.1-InventoryAdjusted-1.5.1', - source: 'InventoryService-0.0.1', - target: 'InventoryAdjusted-1.5.1', - label: 'publishes \nevent', - animated: false, - }), - expect.objectContaining({ - id: 'InventoryAdjusted-1.5.1-CatalogService-0.0.1', - source: 'InventoryAdjusted-1.5.1', - target: 'CatalogService-0.0.1', - label: 'subscribed by', - animated: false, - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedProducerNode), - - // The event node itself - expect.objectContaining(expectedEventNode), - - // Nodes on the right - expect.objectContaining(expectedConsumerNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/flows/mocks.ts b/eventcatalog/src/utils/__tests__/flows/mocks.ts deleted file mode 100644 index 813626732..000000000 --- a/eventcatalog/src/utils/__tests__/flows/mocks.ts +++ /dev/null @@ -1,201 +0,0 @@ -export const mockEvents = [ - { - slug: 'PaymentInitiated', - collection: 'events', - data: { - id: 'PaymentInitiated', - version: '0.0.1', - }, - }, - { - slug: 'PaymentProcessed', - collection: 'events', - data: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, -]; - -export const mockServices = [ - { - slug: 'SubscriptionService', - collection: 'services', - data: { - id: 'SubscriptionService', - version: '0.0.1', - }, - }, -]; - -export const mockFlow = [ - { - id: 'Payment/PaymentProcessed/index.mdx', - slug: 'payment/paymentprocessed', - body: '', - collection: 'flows', - data: { - steps: [ - { - id: 1, - type: 'node', - title: 'Order Placed', - next_step: { - id: 2, - label: 'Proceed to payment', - }, - }, - { - id: 2, - title: 'Payment Initiated', - message: { - id: 'PaymentInitiated', - version: '0.0.1', - }, - next_steps: [ - { - id: 3, - label: 'Payment successful', - }, - { - id: 4, - label: 'Payment failed', - }, - ], - }, - { - id: 3, - title: 'Payment Processed', - message: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, - { - id: 4, - type: 'node', - title: 'Payment Failed', - }, - ], - id: 'PaymentFlow', - name: 'Payment Flow for E-commerce', - summary: 'Business flow for processing payments in an e-commerce platform', - version: '1.0.0', - type: 'node', - }, - }, -]; - -export const mockFlowByIds = [ - { - id: 'Payment/PaymentProcessed/index.mdx', - slug: 'payment/paymentprocessed', - body: '', - collection: 'flows', - data: { - steps: [ - { - id: 1, - type: 'node', - title: 'Order Placed', - next_step: 2, - }, - { - id: 2, - title: 'Payment Initiated', - message: { - id: 'PaymentInitiated', - version: '0.0.1', - }, - next_steps: [3, 4], - }, - { - id: 3, - title: 'Payment Processed', - message: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, - { - id: 4, - type: 'node', - title: 'Payment Failed', - }, - ], - id: 'PaymentFlow', - name: 'Payment Flow for E-commerce', - summary: 'Business flow for processing payments in an e-commerce platform', - version: '1.0.0', - type: 'node', - }, - }, - { - id: 'Subscriptions/CancelSubscription/index.mdx', - slug: 'subscriptions/CancelSubscription', - body: '', - collection: 'flows', - data: { - steps: [ - { - id: 'cancel_subscription_initiated', - title: 'Cancels Subscription', - actor: { - name: 'User', - }, - next_step: { - id: 'cancel_subscription_request', - label: 'Initiate subscription cancellation', - }, - }, - { - id: 'cancel_subscription_request', - title: 'Cancel Subscription', - message: { - id: 'CancelSubscription', - version: '0.0.1', - }, - next_step: { - id: 'subscription_service', - label: 'Proceed to subscription service', - }, - }, - { - id: 'subscription_service', - title: 'Subscription Service', - service: { - id: 'SubscriptionService', - version: 'latest', - }, - next_steps: [ - { - id: 'subscription_cancelled', - label: 'Successful cancellation', - }, - { - id: 'subscription_rejected', - label: 'Failed cancellation', - }, - ], - }, - { - id: 'subscription_cancelled', - title: 'Subscription has been cancelled', - message: { - id: 'UserSubscriptionCancelled', - version: '0.0.1', - }, - }, - { - id: 'subscription_rejected', - title: 'Subscription cancellation has been rejected', - }, - ], - id: 'CancelSubscription', - name: 'User Cancels Subscription', - summary: 'Flow for when a user has cancelled a subscription', - version: '1.0.0', - // type: 'node', - }, - }, -]; diff --git a/eventcatalog/src/utils/__tests__/flows/node-graph.spec.ts b/eventcatalog/src/utils/__tests__/flows/node-graph.spec.ts deleted file mode 100644 index 37fcbe317..000000000 --- a/eventcatalog/src/utils/__tests__/flows/node-graph.spec.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { getNodesAndEdges } from '../../node-graphs/flows-node-graph'; -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import { mockEvents, mockFlow, mockFlowByIds, mockServices } from './mocks'; -import { getCollection } from 'astro:content'; -let expectedNodes: any; - -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - // this will only affect "foo" outside of the original module - getCollection: (key: string) => { - if (key === 'flows') { - return Promise.resolve(mockFlow); - } - if (key === 'events') { - return Promise.resolve(mockEvents); - } - if (key === 'services') { - return Promise.resolve(mockServices); - } - return Promise.resolve([]); - }, - }; -}); - -describe('Flows NodeGraph', () => { - beforeEach(() => { - vi.restoreAllMocks(); - expectedNodes = [ - { - id: 'step-1', - sourcePosition: 'right', - targetPosition: 'left', - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'step', - }, - { - id: 'step-2', - sourcePosition: 'right', - targetPosition: 'left', - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }, - { - id: 'step-3', - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode: 'simple', - step: { - id: 3, - title: 'Payment Processed', - message: { - slug: 'PaymentProcessed', - collection: 'events', - data: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, - type: 'events', - }, - showTarget: true, - showSource: true, - message: { - slug: 'PaymentProcessed', - collection: 'events', - data: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }, - { - id: 'step-4', - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode: 'simple', - step: { - id: 4, - type: 'step', - title: 'Payment Failed', - }, - showTarget: true, - showSource: true, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'step', - }, - ]; - }); - - describe('getNodesAndEdges', () => { - it('should return the correct nodes and edges for a given flow', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'PaymentFlow', version: '1.0.0' }); - - const expectedEdges = [ - { - id: 'step-1-step-2', - source: 'step-1', - target: 'step-2', - type: 'flow-edge', - animated: true, - markerEnd: expect.objectContaining({ type: 'arrowclosed' }), - style: expect.any(Object), - }, - ]; - - expect(nodes).toEqual(expect.arrayContaining(expectedNodes)); - - expect(edges).toEqual(expect.arrayContaining([expect.objectContaining(expectedEdges[0])])); - }); - - it('should resolves the correct node when it is a service with version as latest', async () => { - const { nodes } = await getNodesAndEdges({ id: 'CancelSubscription', version: '1.0.0' }); - - expect(nodes).toContainEqual( - expect.objectContaining({ - data: expect.objectContaining({ - step: expect.objectContaining({ - type: 'services', - service: expect.objectContaining({ - data: { - id: 'SubscriptionService', - version: '0.0.1', - }, - }), - }), - }), - }) - ); - }); - - describe('when steps are referenced only by id', () => { - it('should return the correct nodes and edges', async () => { - // TODO: This mock seems to be overriding the first mock... - // Mock - vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - // this will only affect "foo" outside of the original module - getCollection: (key: string) => { - if (key === 'flows') { - return Promise.resolve(mockFlowByIds); - } - if (key === 'events') { - return Promise.resolve(mockEvents); - } - if (key === 'services') { - return Promise.resolve(mockServices); - } - return Promise.resolve([]); - }, - }; - }); - - const { nodes, edges } = await getNodesAndEdges({ id: 'PaymentFlow', version: '1.0.0' }); - - const expectedNodes = [ - { - id: 'step-1', - sourcePosition: 'right', - targetPosition: 'left', - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'step', - }, - { - id: 'step-2', - sourcePosition: 'right', - targetPosition: 'left', - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }, - { - id: 'step-3', - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode: 'simple', - step: { - id: 3, - title: 'Payment Processed', - message: { - slug: 'PaymentProcessed', - collection: 'events', - data: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, - type: 'events', - }, - showTarget: true, - showSource: true, - message: { - slug: 'PaymentProcessed', - collection: 'events', - data: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }, - { - id: 'step-4', - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode: 'simple', - step: { - id: 4, - type: 'step', - title: 'Payment Failed', - }, - showTarget: true, - showSource: true, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'step', - }, - ]; - - const expectedEdges = [ - { - id: 'step-1-step-2', - source: 'step-1', - target: 'step-2', - type: 'flow-edge', - animated: true, - markerEnd: expect.objectContaining({ type: 'arrowclosed' }), - style: expect.any(Object), - }, - ]; - - expect(nodes).toEqual(expect.arrayContaining(expectedNodes)); - expect(edges).toEqual(expect.arrayContaining([expect.objectContaining(expectedEdges[0])])); - }); - }); - - it('returns empty nodes and edges if no flow is found', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'UnknownFlow', version: '1.0.0' }); - - expect(nodes).toEqual([]); - expect(edges).toEqual([]); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/markdown.spec.ts b/eventcatalog/src/utils/__tests__/markdown.spec.ts deleted file mode 100644 index bbcadad81..000000000 --- a/eventcatalog/src/utils/__tests__/markdown.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getMDXComponentsByName } from '@utils/markdown'; - -describe('markdown', () => { - describe('getMDXComponentsByName', () => { - it('takes given markdown and returns the components and their props', () => { - const markdown = ` - - - `; - const components = getMDXComponentsByName(markdown, 'SchemaViewer'); - expect(components).toEqual([{ id: 'test' }, { id: 'test2' }]); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/messages/catalog/channels/EventBridgeChannel/index.mdx b/eventcatalog/src/utils/__tests__/messages/catalog/channels/EventBridgeChannel/index.mdx deleted file mode 100644 index 4f2f4a44c..000000000 --- a/eventcatalog/src/utils/__tests__/messages/catalog/channels/EventBridgeChannel/index.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: EventBridgeChannel -name: EventBridge Channel -version: 1.0.0 -routes: - - id: SQSChannel - version: 1.0.0 ---- -## EventBridge Channel diff --git a/eventcatalog/src/utils/__tests__/messages/catalog/channels/SNSChannel/index.mdx b/eventcatalog/src/utils/__tests__/messages/catalog/channels/SNSChannel/index.mdx deleted file mode 100644 index 567b79c63..000000000 --- a/eventcatalog/src/utils/__tests__/messages/catalog/channels/SNSChannel/index.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -id: SNSChannel -name: SNS Channel -version: 1.0.0 ---- -## SNS Channel diff --git a/eventcatalog/src/utils/__tests__/messages/catalog/channels/SQSChannel/index.mdx b/eventcatalog/src/utils/__tests__/messages/catalog/channels/SQSChannel/index.mdx deleted file mode 100644 index b620b6e0a..000000000 --- a/eventcatalog/src/utils/__tests__/messages/catalog/channels/SQSChannel/index.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: SQSChannel -name: SQS Channel -version: 1.0.0 -routes: - - id: SNSChannel - version: 1.0.0 ---- -## SQS Channel diff --git a/eventcatalog/src/utils/__tests__/messages/catalog/events/PaymentProcessed/index.mdx b/eventcatalog/src/utils/__tests__/messages/catalog/events/PaymentProcessed/index.mdx deleted file mode 100644 index f96042c6a..000000000 --- a/eventcatalog/src/utils/__tests__/messages/catalog/events/PaymentProcessed/index.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -id: PaymentProcessed -name: Payment Processed -version: 0.0.1 ---- -## Payment Processed diff --git a/eventcatalog/src/utils/__tests__/messages/catalog/services/OrderService/index.mdx b/eventcatalog/src/utils/__tests__/messages/catalog/services/OrderService/index.mdx deleted file mode 100644 index edaf0f2b7..000000000 --- a/eventcatalog/src/utils/__tests__/messages/catalog/services/OrderService/index.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -id: OrderService -name: Order Service -version: 1.0.0 -receives: - - id: PaymentProcessed - version: 0.0.1 - from: - - id: SNSChannel - version: 1.0.0 ---- -## Order Service diff --git a/eventcatalog/src/utils/__tests__/messages/catalog/services/PaymentService/index.mdx b/eventcatalog/src/utils/__tests__/messages/catalog/services/PaymentService/index.mdx deleted file mode 100644 index 1a2f21672..000000000 --- a/eventcatalog/src/utils/__tests__/messages/catalog/services/PaymentService/index.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -id: PaymentService -name: Payment Service -version: 1.0.0 -sends: - - id: PaymentProcessed - version: 0.0.1 - to: - - id: EventBridgeChannel - version: 1.0.0 ---- -## Payment Service diff --git a/eventcatalog/src/utils/__tests__/messages/messages.spec.ts b/eventcatalog/src/utils/__tests__/messages/messages.spec.ts deleted file mode 100644 index d26084d47..000000000 --- a/eventcatalog/src/utils/__tests__/messages/messages.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { CollectionEntry, ContentCollectionKey } from 'astro:content'; -import { expect, describe, it, vi } from 'vitest'; -import { mockCommands, mockEvents, mockQueries, mockServices, mockChannels } from './mocks'; -import { getMessages } from '@utils/messages'; - -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - async getCollection(key: T, filterFn: (entry: CollectionEntry) => boolean = () => true) { - switch (key) { - case 'services': - return mockServices.filter(filterFn as any); - case 'events': - return mockEvents.filter(filterFn as any); - case 'commands': - return mockCommands.filter(filterFn as any); - case 'channels': - return mockChannels.filter(filterFn as any); - case 'queries': - return mockQueries.filter(filterFn); - } - }, - }; -}); - -describe('getMessages', () => { - it('should return all versions of messages', async () => { - await expect(getMessages({ getAllVersions: true })).resolves.toEqual( - expect.objectContaining({ - events: expect.arrayContaining([ - expect.objectContaining({ - collection: 'events', - data: expect.objectContaining({ - id: 'PaymentProcessed', - version: '0.0.1', - }), - }), - expect.objectContaining({ - collection: 'events', - data: expect.objectContaining({ - id: 'PaymentProcessed', - version: '0.0.2', - }), - }), - expect.objectContaining({ - collection: 'events', - data: expect.objectContaining({ - id: 'PaymentProcessed', - version: '0.1.0', - }), - }), - ]), - }) - ); - }); - - it('should return current versions of messages', async () => { - await expect(getMessages({ getAllVersions: false })).resolves.toEqual( - expect.objectContaining({ - commands: expect.arrayContaining([ - expect.objectContaining({ - collection: 'commands', - data: expect.objectContaining({ - id: 'ProcessPayment', - version: '0.1.0', - }), - }), - ]), - events: expect.arrayContaining([ - expect.objectContaining({ - collection: 'events', - data: expect.objectContaining({ - id: 'PaymentProcessed', - version: '0.1.0', - }), - }), - ]), - }) - ); - }); - - it('should return current versions even if the first call is to get all versions', async () => { - await getMessages({ getAllVersions: true }); - - const messages = await getMessages({ getAllVersions: false }); - - expect(messages).toEqual( - expect.objectContaining({ - queries: [], - commands: expect.arrayContaining([ - expect.objectContaining({ - collection: 'commands', - data: expect.objectContaining({ - id: 'ProcessPayment', - version: '0.1.0', - }), - }), - ]), - events: expect.arrayContaining([ - expect.objectContaining({ - collection: 'events', - data: expect.objectContaining({ - id: 'PaymentProcessed', - version: '0.1.0', - }), - }), - ]), - }) - ); - }); - - it('should return all version even if the first call is to get current versions', async () => { - await getMessages({ getAllVersions: false }); - - const messages = await getMessages({ getAllVersions: true }); - - expect(messages).toEqual( - expect.objectContaining({ - commands: expect.arrayContaining([ - expect.objectContaining({ - collection: 'commands', - data: expect.objectContaining({ - id: 'ProcessPayment', - version: '0.0.1', - }), - }), - expect.objectContaining({ - collection: 'commands', - data: expect.objectContaining({ - id: 'ProcessPayment', - version: '0.0.2', - }), - }), - expect.objectContaining({ - collection: 'commands', - data: expect.objectContaining({ - id: 'ProcessPayment', - version: '0.1.0', - }), - }), - ]), - }) - ); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/messages/mocks.ts b/eventcatalog/src/utils/__tests__/messages/mocks.ts deleted file mode 100644 index b054cf957..000000000 --- a/eventcatalog/src/utils/__tests__/messages/mocks.ts +++ /dev/null @@ -1,160 +0,0 @@ -export const mockCommands = [ - { - id: 'ProcessPayment', - collection: 'commands', - data: { - id: 'ProcessPayment', - version: '0.0.1', - pathToFile: 'commands/ProcessPayment/versioned/0.0.1/index.md', - }, - }, - { - id: 'ProcessPayment', - collection: 'commands', - data: { - id: 'ProcessPayment', - version: '0.0.2', - pathToFile: 'commands/ProcessPayment/versioned/0.0.2/index.md', - }, - }, - { - id: 'ProcessPayment', - collection: 'commands', - data: { - id: 'ProcessPayment', - version: '0.1.0', - pathToFile: 'commands/ProcessPayment/index.md', - }, - }, -]; - -export const mockEvents = [ - { - id: 'PaymentProcessed', - collection: 'events', - data: { - id: 'PaymentProcessed', - version: '0.0.1', - pathToFile: 'events/PaymentProcessed/versioned/0.0.1/index.md', - }, - }, - { - id: 'PaymentProcessed', - collection: 'events', - data: { - id: 'PaymentProcessed', - version: '0.0.2', - pathToFile: 'events/PaymentProcessed/versioned/0.0.2/index.md', - }, - }, - { - id: 'PaymentProcessed', - collection: 'events', - data: { - id: 'PaymentProcessed', - version: '0.1.0', - pathToFile: 'events/PaymentProcessed/index.md', - }, - }, -]; - -export const mockQueries = []; - -export const mockServices = [ - { - id: 'OrdersService', - collection: 'services', - data: { - id: 'OrdersService', - version: '1.0.0', - pathToFile: 'services/OrdersService/index.md', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - }, - ], - }, - }, - { - id: 'ServiceThatReceivesMessagesFromAChannel', - collection: 'services', - data: { - id: 'ServiceThatReceivesMessagesFromAChannel', - version: '1.0.0', - pathToFile: 'services/OrdersService/index.md', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - from: [{ id: 'EmailChannel', version: '1.0.0' }], - }, - ], - }, - }, - { - id: 'ServiceThatProducesMessages', - collection: 'services', - data: { - id: 'ServiceThatProducesMessages', - version: '1.0.0', - pathToFile: 'services/ServiceThatProducesMessages/index.md', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - }, - ], - }, - }, - { - id: 'ServiceThatProducesMessagesOverManyChannels', - collection: 'services', - data: { - id: 'ServiceThatProducesMessagesOverManyChannels', - version: '1.0.0', - pathToFile: 'services/ServiceThatProducesMessagesOverManyChannels/index.md', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - to: [ - { id: 'EmailChannel', version: '1.0.0' }, - { id: 'SNSChannel', version: '1.0.0' }, - { id: 'SQSChannel', version: '1.0.0' }, - ], - }, - ], - }, - }, -]; - -export const mockChannels = [ - { - id: 'EmailChannel', - collection: 'channels', - data: { - id: 'EmailChannel', - version: '1.0.0', - pathToFile: 'channels/EmailChannel/index.md', - }, - }, - { - id: 'SNSChannel', - collection: 'channels', - data: { - id: 'SNSChannel', - version: '1.0.0', - pathToFile: 'channels/SmsChannel/index.md', - }, - }, - { - id: 'SQSChannel', - collection: 'channels', - data: { - id: 'SQSChannel', - version: '1.0.0', - pathToFile: 'channels/SQSChannel/index.md', - }, - }, -]; diff --git a/eventcatalog/src/utils/__tests__/messages/node-graph.spec.ts b/eventcatalog/src/utils/__tests__/messages/node-graph.spec.ts deleted file mode 100644 index 97feeb054..000000000 --- a/eventcatalog/src/utils/__tests__/messages/node-graph.spec.ts +++ /dev/null @@ -1,1476 +0,0 @@ -import { getNodesAndEdgesForConsumedMessage, getNodesAndEdgesForProducedMessage } from '../../node-graphs/message-node-graph'; -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import { mockEvents, mockServices, mockChannels } from './mocks'; -import type { CollectionMessageTypes } from '@types'; -import type { CollectionEntry } from 'astro:content'; -import utils from '@eventcatalog/sdk'; -import path from 'path'; -import fs from 'fs'; - -const CATALOG_FOLDER = path.join(__dirname, 'catalog'); - -const toAstroCollection = (item: any, collection: string) => { - return { - id: item.id, - collection: collection, - data: item, - }; -}; - -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - // this will only affect "foo" outside of the original module - getCollection: (key: string) => { - if (key === 'services') { - return Promise.resolve(mockServices); - } - if (key === 'channels') { - return Promise.resolve(mockChannels); - } - if (key === 'events') { - return Promise.resolve(mockEvents); - } - return Promise.resolve([]); - }, - }; -}); - -describe('Message NodeGraph', () => { - beforeEach(() => { - vi.restoreAllMocks(); - // Clear the catalog folder but not the folder itself - fs.rmSync(CATALOG_FOLDER, { recursive: true, force: true }); - fs.mkdirSync(CATALOG_FOLDER, { recursive: true }); - }); - - describe('getNodesAndEdgesForConsumedMessage', () => { - describe('when the message is consumed by the consumer and the producer and consumer have no channels defined', () => { - it('takes a given message, renders the producer and connects the message to the consumer directly', async () => { - const message = mockEvents[0] as unknown as CollectionEntry; - - const { nodes, edges } = await getNodesAndEdgesForConsumedMessage({ - message, - services: mockServices as any, - channels: mockChannels as any, - currentNodes: [], - target: mockServices[0] as any, - targetChannels: [], - }); - - // The message node itself - const expectedEventNode = { - id: 'PaymentProcessed-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }; - - // The producer node itself - const expectedProducerNode = { - id: 'ServiceThatProducesMessages-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - // The message should be connected to the given target directly - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-OrdersService-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'OrdersService-1.0.0', - }), - expect.objectContaining({ - id: 'ServiceThatProducesMessages-1.0.0-PaymentProcessed-0.0.1', - source: 'ServiceThatProducesMessages-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // The producer that produces the message - expect.objectContaining(expectedProducerNode), - // The message node itself - expect.objectContaining(expectedEventNode), - ]) - ); - expect(edges).toEqual(expectedEdges); - }); - }); - - describe('producer with a channel defined', () => { - it('when the producer sends a message over a channel but the consumer does not read from that channel, no channel nodes are created, and the message is connected to the consumer directly', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // Email Channel - await writeChannel({ - id: 'SQSChannel', - name: 'SQS Channel', - version: '1.0.0', - markdown: '## SQS Channel', - }); - - // The Producer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - to: [{ id: 'SQSChannel', version: '1.0.0' }], - }, - ], - }); - - // The Consumer - await writeService({ - id: 'PaymentService', - name: 'Service That Receives Messages Without Channels', - version: '1.0.0', - markdown: '## Service That Receives Messages Without Channels', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - }, - ], - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - const channels = await getChannels().then( - (channels) => - channels.map((channel) => toAstroCollection(channel, 'channels')) as unknown as CollectionEntry<'channels'>[] - ); - - const target = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForConsumedMessage({ - message, - services: services, - channels: channels, - currentNodes: [], - target, - }); - - const channelNodes = nodes.filter((node) => node.type === 'channels'); - - expect(channelNodes).toEqual([]); - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'OrderService-1.0.0-PaymentProcessed-0.0.1', - source: 'OrderService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-PaymentService-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'PaymentService-1.0.0', - }), - ]); - - expect(edges).toEqual(expectedEdges); - }); - - it('when the producer sends a message over many channels only the matching channels are rendered', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // SQS Channel - await writeChannel({ - id: 'SQSChannel', - name: 'SQS Channel', - version: '1.0.0', - markdown: '## SQS Channel', - }); - - // SNS Channel - await writeChannel({ - id: 'SNSChannel', - name: 'SNS Channel', - version: '1.0.0', - markdown: '## SNS Channel', - }); - - // The Producer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - to: [ - { id: 'SQSChannel', version: '1.0.0' }, - { id: 'SNSChannel', version: '1.0.0' }, - ], - }, - ], - }); - - // The Consumer - await writeService({ - id: 'PaymentService', - name: 'Service That Receives Messages Without Channels', - version: '1.0.0', - markdown: '## Service That Receives Messages Without Channels', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - from: [{ id: 'SQSChannel', version: '1.0.0' }], - }, - ], - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - const channels = await getChannels().then( - (channels) => - channels.map((channel) => toAstroCollection(channel, 'channels')) as unknown as CollectionEntry<'channels'>[] - ); - - const target = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForConsumedMessage({ - message, - services: services, - channels: channels, - currentNodes: [], - target, - }); - - const expectedChannelNode = expect.objectContaining({ - id: 'SQSChannel-1.0.0', - type: 'channels', - position: { x: expect.any(Number), y: expect.any(Number) }, - data: expect.anything(), - sourcePosition: 'right', - targetPosition: 'left', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'OrderService-1.0.0-PaymentProcessed-0.0.1', - source: 'OrderService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // Message to the channel - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-SQSChannel-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'SQSChannel-1.0.0', - }), - // Channel to the consumer - expect.objectContaining({ - id: 'SQSChannel-1.0.0-PaymentService-1.0.0', - source: 'SQSChannel-1.0.0', - target: 'PaymentService-1.0.0', - }), - ]); - - expect(nodes).toEqual(expect.arrayContaining([expectedChannelNode])); - - const allChannelNodes = nodes.filter((node) => node.type === 'channels'); - expect(allChannelNodes).toHaveLength(1); - - expect(edges).toEqual(expectedEdges); - }); - - it('when the producer sends a message over a channel and the channel chains to the target channel, the channel chain is rendered', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // EventBridge Channel - await writeChannel({ - id: 'EventBridgeChannel', - name: 'EventBridge Channel', - version: '1.0.0', - markdown: '## EventBridge Channel', - routes: [ - { - id: 'SNSChannel', - version: '1.0.0', - }, - ], - }); - - // SNS Channel - await writeChannel({ - id: 'SNSChannel', - name: 'SNS Channel', - version: '1.0.0', - markdown: '## SNS Channel', - routes: [ - { - id: 'SQSChannel', - version: '1.0.0', - }, - ], - }); - - // SQS Channel - await writeChannel({ - id: 'SQSChannel', - name: 'SQS Channel', - version: '1.0.0', - markdown: '## SQS Channel', - }); - - // The Producer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - to: [{ id: 'EventBridgeChannel', version: '1.0.0' }], - }, - ], - }); - - // The Consumer - await writeService({ - id: 'PaymentService', - name: 'Service That Receives Messages Without Channels', - version: '1.0.0', - markdown: '## Service That Receives Messages Without Channels', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - from: [{ id: 'SQSChannel', version: '1.0.0' }], - }, - ], - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - const channels = await getChannels().then( - (channels) => - channels.map((channel) => toAstroCollection(channel, 'channels')) as unknown as CollectionEntry<'channels'>[] - ); - - const target = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForConsumedMessage({ - message, - services: services, - channels: channels, - currentNodes: [], - target, - }); - - const channelNodes = nodes.filter((node) => node.type === 'channels'); - expect(channelNodes).toHaveLength(3); - - const expectedEventBusChannelNode = expect.objectContaining({ - id: 'EventBridgeChannel-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'channels', - }); - - const expectedSNSChannelNode = expect.objectContaining({ - id: 'SNSChannel-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'channels', - }); - - const expectedSQSChannelNode = expect.objectContaining({ - id: 'SQSChannel-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'channels', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the event first.... - expect.objectContaining({ - id: 'OrderService-1.0.0-PaymentProcessed-0.0.1', - source: 'OrderService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // The message to the first channel - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-EventBridgeChannel-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'EventBridgeChannel-1.0.0', - }), - // // The next channel in the chain - expect.objectContaining({ - id: 'EventBridgeChannel-1.0.0-SNSChannel-1.0.0', - source: 'EventBridgeChannel-1.0.0', - target: 'SNSChannel-1.0.0', - }), - // The next channel in the chain - expect.objectContaining({ - id: 'SNSChannel-1.0.0-SQSChannel-1.0.0', - source: 'SNSChannel-1.0.0', - target: 'SQSChannel-1.0.0', - }), - // Final channel to the consumer - expect.objectContaining({ - id: 'SQSChannel-1.0.0-PaymentService-1.0.0', - source: 'SQSChannel-1.0.0', - target: 'PaymentService-1.0.0', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Expected channel nodes - expectedEventBusChannelNode, - expectedSNSChannelNode, - expectedSQSChannelNode, - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - }); - - describe('target with channel defined', () => { - it('when the producer does not send the message to a channel, but the target has a channel defined, the message is connected to the channel node and the channel node to the target directly', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // Producer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - }, - ], - }); - - // Target - await writeService({ - id: 'PaymentService', - name: 'Payment Service', - version: '1.0.0', - markdown: '## Payment Service', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - from: [{ id: 'SQSChannel', version: '1.0.0' }], - }, - ], - }); - - await writeChannel({ - id: 'SQSChannel', - name: 'SQS Channel', - version: '1.0.0', - markdown: '## SQS Channel', - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - const channels = await getChannels().then( - (channels) => - channels.map((channel) => toAstroCollection(channel, 'channels')) as unknown as CollectionEntry<'channels'>[] - ); - - const target = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForConsumedMessage({ - message, - services: services, - channels: channels, - currentNodes: [], - target, - targetChannels: [{ id: 'SQSChannel', version: '1.0.0' }], - }); - - const expectedChannelNode = expect.objectContaining({ - id: 'SQSChannel-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'channels', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'OrderService-1.0.0-PaymentProcessed-0.0.1', - source: 'OrderService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // Message to the channel - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-SQSChannel-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'SQSChannel-1.0.0', - }), - // // Channel to the target - // expect.objectContaining({ - // id: 'SQSChannel-1.0.0-PaymentService-1.0.0', - // source: 'SQSChannel-1.0.0', - // target: 'PaymentService-1.0.0', - // }), - ]); - - expect(nodes).toEqual(expect.arrayContaining([expectedChannelNode])); - - expect(edges).toEqual(expectedEdges); - }); - }); - }); - - describe('getNodesAndEdgesForProducedMessage', () => { - describe('when the message is produced by the producer and the producer and consumer have no channels defined', () => { - it('renders the producer publishing the message directly to the consumer', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // The producer - await writeService({ - id: 'PaymentService', - name: 'Payment Service', - version: '1.0.0', - markdown: '## Payment Service', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - }, - ], - }); - - // The Consumer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - }, - ], - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - - const source = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForProducedMessage({ - message, - services: services, - channels: [], - currentNodes: [], - source, - currentEdges: [], - }); - - const expectedProducerNode = expect.objectContaining({ - id: 'PaymentService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedMessageNode = expect.objectContaining({ - id: 'PaymentProcessed-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'events', - }); - - const expectedConsumerNode = expect.objectContaining({ - id: 'OrderService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'PaymentService-1.0.0-PaymentProcessed-0.0.1', - source: 'PaymentService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // Message to the consumer - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-OrderService-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'OrderService-1.0.0', - }), - ]); - - expect(edges).toEqual(expectedEdges); - - expect(nodes).toEqual(expect.arrayContaining([expectedProducerNode, expectedMessageNode, expectedConsumerNode])); - - expect(edges).toEqual(expectedEdges); - }); - }); - - describe('when the message is produced by the producer and the producer defines a channel', () => { - it('renders the producer publishing the message to the channel', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // The producer - await writeService({ - id: 'PaymentService', - name: 'Payment Service', - version: '1.0.0', - markdown: '## Payment Service', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - to: [{ id: 'SQSChannel', version: '1.0.0' }], - }, - ], - }); - - // The channel - await writeChannel({ - id: 'SQSChannel', - name: 'SQS Channel', - version: '1.0.0', - markdown: '## SQS Channel', - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - const channels = await getChannels().then( - (channels) => - channels.map((channel) => toAstroCollection(channel, 'channels')) as unknown as CollectionEntry<'channels'>[] - ); - - const source = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForProducedMessage({ - message, - services: services, - channels: channels, - currentNodes: [], - sourceChannels: [{ id: 'SQSChannel', version: '1.0.0' }], - source, - currentEdges: [], - }); - - const expectedProducerNode = expect.objectContaining({ - id: 'PaymentService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedMessageNode = expect.objectContaining({ - id: 'PaymentProcessed-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'events', - }); - - const expectedChannelNode = expect.objectContaining({ - id: 'SQSChannel-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'channels', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'PaymentService-1.0.0-PaymentProcessed-0.0.1', - source: 'PaymentService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // Message to the channel - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-SQSChannel-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'SQSChannel-1.0.0', - }), - ]); - - expect(nodes).toEqual(expect.arrayContaining([expectedProducerNode, expectedMessageNode, expectedChannelNode])); - - expect(edges).toEqual(expectedEdges); - }); - }); - - describe('when the message is produced by the producer without a channel, but the consumer defines a channel', () => { - it('renders the producer publishing the message to the consumers channel', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // The producer - await writeService({ - id: 'PaymentService', - name: 'Payment Service', - version: '1.0.0', - markdown: '## Payment Service', - }); - - // The channel - await writeChannel({ - id: 'SQSChannel', - name: 'SQS Channel', - version: '1.0.0', - markdown: '## SQS Channel', - }); - - // The consumer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - from: [{ id: 'SQSChannel', version: '1.0.0' }], - }, - ], - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - const channels = await getChannels().then( - (channels) => - channels.map((channel) => toAstroCollection(channel, 'channels')) as unknown as CollectionEntry<'channels'>[] - ); - - const source = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForProducedMessage({ - message, - services: services, - channels: channels, - currentNodes: [], - sourceChannels: [{ id: 'SQSChannel', version: '1.0.0' }], - source, - currentEdges: [], - }); - - const expectedProducerNode = expect.objectContaining({ - id: 'PaymentService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedMessageNode = expect.objectContaining({ - id: 'PaymentProcessed-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'events', - }); - - const expectedChannelNode = expect.objectContaining({ - id: 'SQSChannel-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'channels', - }); - - const expectedConsumerNode = expect.objectContaining({ - id: 'OrderService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'PaymentService-1.0.0-PaymentProcessed-0.0.1', - source: 'PaymentService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // Message to the channel - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-SQSChannel-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'SQSChannel-1.0.0', - }), - // Channel to the consumer - expect.objectContaining({ - id: 'SQSChannel-1.0.0-OrderService-1.0.0', - source: 'SQSChannel-1.0.0', - target: 'OrderService-1.0.0', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([expectedProducerNode, expectedMessageNode, expectedChannelNode, expectedConsumerNode]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the consumer defines a channel that does not exist in EventCatalog, the message is connected to the consumer directly', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // The producer - await writeService({ - id: 'PaymentService', - name: 'Payment Service', - version: '1.0.0', - markdown: '## Payment Service', - }); - - // The consumer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - from: [{ id: 'NonExistentChannel', version: '1.0.0' }], - }, - ], - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - - const source = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForProducedMessage({ - message, - services: services, - channels: [], - currentNodes: [], - sourceChannels: [{ id: 'SQSChannel', version: '1.0.0' }], - source, - currentEdges: [], - }); - - const expectedProducerNode = expect.objectContaining({ - id: 'PaymentService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedMessageNode = expect.objectContaining({ - id: 'PaymentProcessed-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'events', - }); - - const expectedConsumerNode = expect.objectContaining({ - id: 'OrderService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'PaymentService-1.0.0-PaymentProcessed-0.0.1', - source: 'PaymentService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // Message to the consumer directly - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-OrderService-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'OrderService-1.0.0', - }), - ]); - - expect(nodes).toEqual(expect.arrayContaining([expectedProducerNode, expectedMessageNode, expectedConsumerNode])); - - expect(edges).toEqual(expectedEdges); - }); - }); - - describe('When the message is produced with a channel, but the consumer does not define a channel?', () => { - it('renders the producer publishing the message to the channel, but the consumer consumes the message directly from the producer', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // The producer - await writeService({ - id: 'PaymentService', - name: 'Payment Service', - version: '1.0.0', - markdown: '## Payment Service', - }); - - // The channel - await writeChannel({ - id: 'SQSChannel', - name: 'SQS Channel', - version: '1.0.0', - markdown: '## SQS Channel', - }); - - // The consumer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - }, - ], - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - const channels = await getChannels().then( - (channels) => - channels.map((channel) => toAstroCollection(channel, 'channels')) as unknown as CollectionEntry<'channels'>[] - ); - - const source = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForProducedMessage({ - message, - services: services, - channels: channels, - currentNodes: [], - sourceChannels: [{ id: 'SQSChannel', version: '1.0.0' }], - source, - currentEdges: [], - }); - - const expectedProducerNode = expect.objectContaining({ - id: 'PaymentService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedMessageNode = expect.objectContaining({ - id: 'PaymentProcessed-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'events', - }); - - const expectedChannelNode = expect.objectContaining({ - id: 'SQSChannel-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'channels', - }); - - const expectedConsumerNode = expect.objectContaining({ - id: 'OrderService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'PaymentService-1.0.0-PaymentProcessed-0.0.1', - source: 'PaymentService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // Message to the channel - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-SQSChannel-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'SQSChannel-1.0.0', - }), - // Message to the consumer directly - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-OrderService-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'OrderService-1.0.0', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([expectedProducerNode, expectedMessageNode, expectedChannelNode, expectedConsumerNode]) - ); - - expect(edges).toEqual(expectedEdges); - }); - }); - - describe('when the message is produced by the producer and the producer defines a channel and the consumer also defines the same channel (consuming from the channel)', () => { - it('renders the producer publishing the message to the channel and the consumer consuming the message from the channel', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // The producer - await writeService({ - id: 'PaymentService', - name: 'Payment Service', - version: '1.0.0', - markdown: '## Payment Service', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - to: [{ id: 'SQSChannel', version: '1.0.0' }], - }, - ], - }); - - // The channel - await writeChannel({ - id: 'SQSChannel', - name: 'SQS Channel', - version: '1.0.0', - markdown: '## SQS Channel', - }); - - // The consumer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - from: [{ id: 'SQSChannel', version: '1.0.0' }], - }, - ], - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - const channels = await getChannels().then( - (channels) => - channels.map((channel) => toAstroCollection(channel, 'channels')) as unknown as CollectionEntry<'channels'>[] - ); - - const source = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForProducedMessage({ - message, - services: services, - channels: channels, - currentNodes: [], - sourceChannels: [{ id: 'SQSChannel', version: '1.0.0' }], - source, - currentEdges: [], - }); - - const expectedProducerNode = expect.objectContaining({ - id: 'PaymentService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedMessageNode = expect.objectContaining({ - id: 'PaymentProcessed-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'events', - }); - - const expectedChannelNode = expect.objectContaining({ - id: 'SQSChannel-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'channels', - }); - - const expectedConsumerNode = expect.objectContaining({ - id: 'OrderService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'PaymentService-1.0.0-PaymentProcessed-0.0.1', - source: 'PaymentService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // Message to the channel - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-SQSChannel-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'SQSChannel-1.0.0', - }), - // Channel to the consumer - expect.objectContaining({ - id: 'SQSChannel-1.0.0-OrderService-1.0.0', - source: 'SQSChannel-1.0.0', - target: 'OrderService-1.0.0', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([expectedProducerNode, expectedMessageNode, expectedChannelNode, expectedConsumerNode]) - ); - - expect(edges).toEqual(expectedEdges); - }); - }); - - describe('when the message is produced by the producer and the producer defines a channel and the consumer also defines a channel but its a chain of channels', () => { - it('renders the producer publishing the message to the channel and the consumer consuming the message from the channel chain', async () => { - const { writeService, writeEvent, getServices, writeChannel, getEvent, getChannels, getService } = utils(CATALOG_FOLDER); - - // The message itself - await writeEvent({ - id: 'PaymentProcessed', - name: 'Payment Processed', - version: '0.0.1', - markdown: '## Payment Processed', - }); - - // The producer - await writeService({ - id: 'PaymentService', - name: 'Payment Service', - version: '1.0.0', - markdown: '## Payment Service', - sends: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - to: [{ id: 'EventBridgeChannel', version: '1.0.0' }], - }, - ], - }); - - // The first channel - await writeChannel({ - id: 'EventBridgeChannel', - name: 'EventBridge Channel', - version: '1.0.0', - markdown: '## EventBridge Channel', - routes: [{ id: 'SQSChannel', version: '1.0.0' }], - }); - - // The second channel - await writeChannel({ - id: 'SQSChannel', - name: 'SQS Channel', - version: '1.0.0', - markdown: '## SQS Channel', - routes: [{ id: 'SNSChannel', version: '1.0.0' }], - }); - - // The third channel - await writeChannel({ - id: 'SNSChannel', - name: 'SNS Channel', - version: '1.0.0', - markdown: '## SNS Channel', - }); - - // The consumer - await writeService({ - id: 'OrderService', - name: 'Order Service', - version: '1.0.0', - markdown: '## Order Service', - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - from: [{ id: 'SNSChannel', version: '1.0.0' }], - }, - ], - }); - - const message = toAstroCollection( - await getEvent('PaymentProcessed'), - 'events' - ) as unknown as CollectionEntry; - const services = await getServices().then( - (services) => - services.map((service) => toAstroCollection(service, 'services')) as unknown as CollectionEntry<'services'>[] - ); - const channels = await getChannels().then( - (channels) => - channels.map((channel) => toAstroCollection(channel, 'channels')) as unknown as CollectionEntry<'channels'>[] - ); - - const source = toAstroCollection( - await getService('PaymentService'), - 'services' - ) as unknown as CollectionEntry<'services'>; - - const { nodes, edges } = await getNodesAndEdgesForProducedMessage({ - message, - services: services, - channels: channels, - currentNodes: [], - sourceChannels: [{ id: 'SQSChannel', version: '1.0.0' }], - source, - currentEdges: [], - }); - - const expectedProducerNode = expect.objectContaining({ - id: 'PaymentService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedMessageNode = expect.objectContaining({ - id: 'PaymentProcessed-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'events', - }); - - const expectedChannelNode = expect.objectContaining({ - id: 'SQSChannel-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'channels', - }); - - const expectedConsumerNode = expect.objectContaining({ - id: 'OrderService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - }); - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'PaymentService-1.0.0-PaymentProcessed-0.0.1', - source: 'PaymentService-1.0.0', - target: 'PaymentProcessed-0.0.1', - }), - // Message to the first channel - expect.objectContaining({ - id: 'PaymentProcessed-0.0.1-EventBridgeChannel-1.0.0', - source: 'PaymentProcessed-0.0.1', - target: 'EventBridgeChannel-1.0.0', - }), - // First channel to the second channel - expect.objectContaining({ - id: 'EventBridgeChannel-1.0.0-SQSChannel-1.0.0', - source: 'EventBridgeChannel-1.0.0', - target: 'SQSChannel-1.0.0', - }), - // Second channel to the third channel - expect.objectContaining({ - id: 'SQSChannel-1.0.0-SNSChannel-1.0.0', - source: 'SQSChannel-1.0.0', - target: 'SNSChannel-1.0.0', - }), - // Third channel to the consumer - expect.objectContaining({ - id: 'SNSChannel-1.0.0-OrderService-1.0.0', - source: 'SNSChannel-1.0.0', - target: 'OrderService-1.0.0', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([expectedProducerNode, expectedMessageNode, expectedChannelNode, expectedConsumerNode]) - ); - - expect(edges).toEqual(expectedEdges); - }); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/queries/mocks.ts b/eventcatalog/src/utils/__tests__/queries/mocks.ts deleted file mode 100644 index aadf023ab..000000000 --- a/eventcatalog/src/utils/__tests__/queries/mocks.ts +++ /dev/null @@ -1,179 +0,0 @@ -export const mockServices = [ - { - id: 'OrderService', - slug: 'OrderService', - collection: 'services', - data: { - id: 'OrderService', - version: '0.0.1', - sends: [ - { - id: 'GetLatestOrder', - version: '0.0.1', - }, - ], - receives: [ - { - id: 'GetProductStatus', - version: '0.0.1', - from: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - ], - }, - }, - { - id: 'PaymentService', - slug: 'PaymentService', - collection: 'services', - data: { - id: 'PaymentService', - version: '0.0.1', - receives: [ - { - id: 'GetLatestOrder', - version: '0.0.1', - }, - ], - }, - }, - { - id: 'InventoryService', - slug: 'InventoryService', - collection: 'services', - data: { - id: 'InventoryService', - version: '0.0.1', - sends: [ - { - id: 'GetInventoryItem', - version: '>1.2.0', - }, - { - id: 'GetProductStatus', - version: '0.0.1', - from: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - { - id: 'GetStockStatus', - version: 'latest', - }, - ], - }, - }, - { - id: 'CatalogService', - slug: 'CatalogService', - collection: 'services', - data: { - id: 'CatalogService', - version: '0.0.1', - receives: [ - { - id: 'GetInventoryItem', - }, - { - id: 'GetStockStatus', - version: '*', - }, - ], - }, - }, - { - id: 'LegacyOrderService', - slug: 'LegacyOrderService', - collection: 'services', - data: { - id: 'LegacyOrderService', - version: '0.0.1', - receives: [ - { - id: 'GetOrderLegacy', - }, - ], - sends: [ - { - id: 'GetOrderLegacy', - }, - ], - }, - }, -]; - -export const mockQueries = [ - { - id: 'GetLatestOrder', - slug: 'GetLatestOrder', - collection: 'queries', - data: { - id: 'GetLatestOrder', - version: '0.0.1', - }, - }, - { - id: 'GetInventoryItem', - slug: 'GetInventoryItem', - collection: 'queries', - data: { - id: 'GetInventoryItem', - version: '1.5.1', - }, - }, - { - id: 'GetStockStatus', - slug: 'GetStockStatus', - collection: 'queries', - data: { - id: 'GetStockStatus', - version: '0.0.1', - }, - }, - { - id: 'GetStockStatus', - slug: 'GetStockStatus', - collection: 'queries', - data: { - id: 'GetStockStatus', - version: '1.0.0', - }, - }, - { - id: 'GetProductStatus', - slug: 'GetProductStatus', - collection: 'queries', - data: { - id: 'GetProductStatus', - version: '0.0.1', - }, - }, - { - id: 'GetOrderLegacy', - slug: 'GetOrderLegacy', - collection: 'queries', - data: { - id: 'GetOrderLegacy', - version: '0.0.1', - }, - }, -]; - -export const mockChannels = [ - { - id: 'EmailChannel', - slug: 'EmailChannel', - collection: 'channels', - data: { - id: 'EmailChannel', - version: '1.0.0', - }, - }, -]; diff --git a/eventcatalog/src/utils/__tests__/queries/node-graph.spec.ts b/eventcatalog/src/utils/__tests__/queries/node-graph.spec.ts deleted file mode 100644 index 5466cd1a6..000000000 --- a/eventcatalog/src/utils/__tests__/queries/node-graph.spec.ts +++ /dev/null @@ -1,362 +0,0 @@ -import { MarkerType } from '@xyflow/react'; -import { getNodesAndEdgesForQueries as getNodesAndEdges } from '../../node-graphs/message-node-graph'; -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import { mockQueries, mockServices, mockChannels } from './mocks'; - -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - // this will only affect "foo" outside of the original module - getCollection: (key: string) => { - if (key === 'services') { - return Promise.resolve(mockServices); - } - if (key === 'queries') { - return Promise.resolve(mockQueries); - } - if (key === 'channels') { - return Promise.resolve(mockChannels); - } - return Promise.resolve([]); - }, - }; -}); - -describe('Queries NodeGraph', () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - describe('getNodesAndEdges', () => { - it('should return nodes and edges for a given query', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'GetLatestOrder', version: '0.0.1' }); - - // The middle node itself, the service - const expectedQueryNode = { - id: 'GetLatestOrder-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - // data: { mode: 'simple', message: expect.anything(), showTarget: false, showSource: false }, - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'queries', - }; - - const expectedProducerNode = { - id: 'OrderService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[0].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedConsumerNode = { - id: 'PaymentService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { - title: 'PaymentService', - mode: 'simple', - service: { ...mockServices[1].data }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'OrderService-0.0.1-GetLatestOrder-0.0.1', - source: 'OrderService-0.0.1', - target: 'GetLatestOrder-0.0.1', - label: 'requests', - }), - expect.objectContaining({ - id: 'GetLatestOrder-0.0.1-PaymentService-0.0.1', - source: 'GetLatestOrder-0.0.1', - target: 'PaymentService-0.0.1', - label: 'accepts', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedConsumerNode), - - // The event node itself - expect.objectContaining(expectedQueryNode), - - // Nodes on the right - expect.objectContaining(expectedProducerNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the query is produced and consumed by a service it will render a custom edge', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'GetOrderLegacy', version: '0.0.1' }); - - // The middle node itself, the service - const expectedQueryNode = { - id: 'GetOrderLegacy-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: expect.anything() }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'queries', - }; - - const expectedProducerNode = { - id: 'LegacyOrderService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[4].data }, title: 'LegacyOrderService' }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedConsumerNode = { - id: 'LegacyOrderService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { - title: 'LegacyOrderService', - mode: 'simple', - service: { ...mockServices[4].data }, - }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'LegacyOrderService-0.0.1-GetOrderLegacy-0.0.1', - source: 'LegacyOrderService-0.0.1', - target: 'GetOrderLegacy-0.0.1', - label: 'requests', - }), - expect.objectContaining({ - id: 'GetOrderLegacy-0.0.1-LegacyOrderService-0.0.1', - source: 'GetOrderLegacy-0.0.1', - target: 'LegacyOrderService-0.0.1', - label: 'accepts', - }), - expect.objectContaining({ - id: 'GetOrderLegacy-0.0.1-LegacyOrderService-0.0.1-both', - source: 'GetOrderLegacy-0.0.1', - target: 'LegacyOrderService-0.0.1', - label: 'publishes and subscribes', - animated: false, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 40, - height: 40, - }, - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedConsumerNode), - - // The query node itself - expect.objectContaining(expectedQueryNode), - - // Nodes on the right - expect.objectContaining(expectedProducerNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the consumer of a query has defined a channel, it will render the channel node and edges', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'GetProductStatus', version: '0.0.1' }); - - const expectedConsumerNode = { - id: 'OrderService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[0].data }, title: 'OrderService' }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedChannelNode = { - sourcePosition: 'right', - targetPosition: 'left', - id: 'EmailChannel-1.0.0', - type: 'channels', - }; - - const expectedQueryNode = { - id: 'GetProductStatus-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'queries', - }; - - const expectedEdges = expect.arrayContaining([ - // Message to the channel - expect.objectContaining({ - id: 'GetProductStatus-0.0.1-EmailChannel-1.0.0', - source: 'GetProductStatus-0.0.1', - target: 'EmailChannel-1.0.0', - }), - // Channel to the consumer - expect.objectContaining({ - id: 'EmailChannel-1.0.0-OrderService-0.0.1', - source: 'EmailChannel-1.0.0', - target: 'OrderService-0.0.1', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedConsumerNode), - - // channel - expect.objectContaining(expectedChannelNode), - - // The query node itself - expect.objectContaining(expectedQueryNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if the producer of a query has defined a channel, it will render the channel node and edges', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'GetProductStatus', version: '0.0.1' }); - - const expectedProducerNode = { - id: 'InventoryService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedChannelNode = { - sourcePosition: 'right', - targetPosition: 'left', - id: 'EmailChannel-1.0.0', - type: 'channels', - }; - - const expectedQueryNode = { - id: 'GetProductStatus-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - type: 'queries', - }; - - const expectedEdges = expect.arrayContaining([ - // Producer to the message - expect.objectContaining({ - id: 'InventoryService-0.0.1-GetProductStatus-0.0.1', - source: 'InventoryService-0.0.1', - target: 'GetProductStatus-0.0.1', - label: 'requests', - animated: false, - }), - // Message to the channel - expect.objectContaining({ - id: 'GetProductStatus-0.0.1-EmailChannel-1.0.0', - source: 'GetProductStatus-0.0.1', - target: 'EmailChannel-1.0.0', - label: 'routes to', - animated: false, - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedProducerNode), - - // channel - expect.objectContaining(expectedChannelNode), - - // The query node itself - expect.objectContaining(expectedQueryNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('returns empty nodes and edges if no query is found', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'UnknownQuery', version: '1.0.0' }); - - expect(nodes).toEqual([]); - expect(edges).toEqual([]); - }); - - it('should return nodes and edges for a given query using semver range', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'GetInventoryItem', version: '1.5.1' }); - - // The middle node itself, the service - const expectedQueryNode = { - id: 'GetInventoryItem-1.5.1', - sourcePosition: 'right', - targetPosition: 'left', - data: expect.anything(), - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'queries', - }; - - const expectedProducerNode = { - id: 'InventoryService-0.0.1', - type: 'services', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[2].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedConsumerNode = { - id: 'CatalogService-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { title: 'CatalogService', mode: 'simple', service: { ...mockServices[3].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'InventoryService-0.0.1-GetInventoryItem-1.5.1', - source: 'InventoryService-0.0.1', - target: 'GetInventoryItem-1.5.1', - label: 'requests', - }), - expect.objectContaining({ - id: 'GetInventoryItem-1.5.1-CatalogService-0.0.1', - source: 'GetInventoryItem-1.5.1', - target: 'CatalogService-0.0.1', - label: 'accepts', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedProducerNode), - - // The event node itself - expect.objectContaining(expectedQueryNode), - - // Nodes on the right - expect.objectContaining(expectedConsumerNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/schemas/schemas.spec.ts b/eventcatalog/src/utils/__tests__/schemas/schemas.spec.ts deleted file mode 100644 index 22580bbac..000000000 --- a/eventcatalog/src/utils/__tests__/schemas/schemas.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { getSchemasFromResource } from '@utils/collections/schemas'; - -import { getSchemaURL, getSchemaFormatFromURL } from '@utils/collections/schemas'; - -describe('schemas', () => { - const originalEnv = process.env; - - beforeEach(() => { - // Reset the environment before each test - // @ts-ignore - global.__EC_TRAILING_SLASH__ = false; - process.env = { ...originalEnv }; - }); - - describe('getSchemaURL', () => { - it('returns the schema URL for a given resource', () => { - const schemaURL = getSchemaURL({ - collection: 'services', - data: { - id: 'MyService', - version: '1.0.0', - name: 'MyService', - schemaPath: 'schemas/service', - }, - // @ts-ignore - catalog: { - publicPath: '/catalog/services/MyService', - }, - }); - expect(schemaURL).toEqual('/catalog/services/MyService/schemas/service'); - }); - }); - - describe('getSchemaFormatFromURL', () => { - it('returns the format of a given schema URL', () => { - const format = getSchemaFormatFromURL('random-url.com/schemas/service.json'); - expect(format).toEqual('json'); - }); - }); - - describe('getSchemasFromResource', () => { - describe('services', () => { - it('returns an openapi, and asyncapi schema if both are present', () => { - const schemas = getSchemasFromResource({ - collection: 'services', - data: { - id: 'MyService', - version: '1.0.0', - name: 'MyService', - schemaPath: 'schemas/service', - specifications: { - asyncapiPath: 'schemas/service.asyncapi.json', - openapiPath: 'schemas/service.openapi.json', - }, - }, - // @ts-ignore - catalog: { publicPath: '/catalog/services/MyService' }, - }); - expect(schemas).toEqual([ - { url: '/catalog/services/MyService/schemas/service.asyncapi.json', format: 'asyncapi' }, - { url: '/catalog/services/MyService/schemas/service.openapi.json', format: 'openapi' }, - ]); - }); - - it('returns an asyncapi schema if only asyncapi is present', () => { - const schemas = getSchemasFromResource({ - collection: 'services', - data: { - id: 'MyService', - version: '1.0.0', - name: 'MyService', - schemaPath: 'schemas/service', - specifications: { - asyncapiPath: 'schemas/service.asyncapi.json', - }, - }, - // @ts-ignore - catalog: { publicPath: '/catalog/services/MyService' }, - }); - expect(schemas).toEqual([{ url: '/catalog/services/MyService/schemas/service.asyncapi.json', format: 'asyncapi' }]); - }); - - it('returns an openapi schema if only openapi is present', () => { - const schemas = getSchemasFromResource({ - collection: 'services', - data: { - id: 'MyService', - version: '1.0.0', - name: 'MyService', - schemaPath: 'schemas/service', - specifications: { - openapiPath: 'schemas/service.openapi.json', - }, - }, - // @ts-ignore - catalog: { publicPath: '/catalog/services/MyService' }, - }); - expect(schemas).toEqual([{ url: '/catalog/services/MyService/schemas/service.openapi.json', format: 'openapi' }]); - }); - - it('returns an empty array if no schemas are present', () => { - const schemas = getSchemasFromResource({ - collection: 'services', - data: { - id: 'MyService', - version: '1.0.0', - name: 'MyService', - schemaPath: 'schemas/service', - }, - // @ts-ignore - catalog: { publicPath: '/catalog/services/MyService' }, - }); - expect(schemas).toEqual([]); - }); - }); - - describe('all resources (excluding services)', () => { - it('returns a single schema if only one is present', () => { - const schemas = getSchemasFromResource({ - collection: 'events', - data: { - id: 'MyEvent', - version: '1.0.0', - name: 'MyEvent', - schemaPath: 'schemas/event.json', - }, - // @ts-ignore - catalog: { publicPath: '/catalog/events/MyEvent' }, - }); - expect(schemas).toEqual([{ url: '/catalog/events/MyEvent/schemas/event.json', format: 'json' }]); - }); - - it('returns an empty array if no schemas are present', () => { - const schemas = getSchemasFromResource({ - collection: 'events', - data: { - id: 'MyEvent', - version: '1.0.0', - name: 'MyEvent', - }, - // @ts-ignore - catalog: { publicPath: '/catalog/events/MyEvent' }, - }); - expect(schemas).toEqual([]); - }); - }); - }); -}); diff --git a/eventcatalog/src/utils/__tests__/services/mocks.ts b/eventcatalog/src/utils/__tests__/services/mocks.ts deleted file mode 100644 index 2a24ae5b3..000000000 --- a/eventcatalog/src/utils/__tests__/services/mocks.ts +++ /dev/null @@ -1,325 +0,0 @@ -export const mockServices = [ - { - id: 'services/Order/OrderService/index.mdx', - slug: 'services/Order/OrderService', - collection: 'services', - data: { - id: 'OrderService', - version: '1.0.0', - specifications: [ - { - type: 'asyncapi', - path: 'asyncapi.yml', - name: 'AsyncAPI Custom Name', - }, - { - type: 'openapi', - path: 'openapi.yml', - name: 'OpenAPI Custom Name', - }, - ], - sends: [ - { - id: 'OrderCreatedEvent', - version: '0.0.1', - }, - ], - receives: [ - { - id: 'PaymentProcessed', - version: '0.0.1', - }, - ], - writesTo: [ - { - id: 'OrderDatabase', - version: '1.0.0', - }, - ], - readsFrom: [ - { - id: 'PaymentDatabase', - version: '1.0.0', - }, - ], - }, - }, - { - id: 'services/Inventory/InventoryService/index.mdx', - slug: 'services/Inventory/InventoryService', - collection: 'services', - data: { - id: 'InventoryService', - version: '1.0.0', - specifications: { - asyncapiPath: 'asyncapi.yml', - openapiPath: 'openapi.yml', - }, - receives: [ - { - id: 'OrderCreatedEvent', - version: '^1.3.0', - }, - ], - sends: [ - { - id: 'InventoryAdjusted', - version: '~2', - }, - ], - }, - }, - { - id: 'services/Payment/PaymentService/index.mdx', - slug: 'services/Payment/PaymentService', - collection: 'services', - data: { - id: 'PaymentService', - version: '1.0.0', - receives: [ - { - id: 'OrderCreatedEvent', - }, - { - id: 'OrderDeletedEvent', - from: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - ], - sends: [ - { - id: 'PaymentPaid', - to: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - { - id: 'PaymentFailed', - version: 'latest', - }, - { - id: 'EmailVerified', - version: '1.0.0', - }, - ], - }, - }, - { - id: 'services/Notifications/NotificationsService/index.mdx', - slug: 'services/Notifications/NotificationsService', - collection: 'services', - data: { - id: 'NotificationsService', - version: '1.0.0', - receives: [ - { - id: 'OrderCreatedEvent', - }, - ], - sends: [ - { - id: 'OrderCreatedEvent', - }, - ], - }, - }, -]; - -export const mockEvents = [ - { - slug: 'OrderCreatedEvent', - collection: 'events', - data: { - id: 'OrderCreatedEvent', - version: '0.0.1', - }, - }, - { - slug: 'OrderCreatedEvent', - collection: 'events', - data: { - id: 'OrderCreatedEvent', - version: '1.0.0', - }, - }, - { - slug: 'OrderCreatedEvent', - collection: 'events', - data: { - id: 'OrderCreatedEvent', - version: '1.3.9', - }, - }, - { - slug: 'OrderCreatedEvent', - collection: 'events', - data: { - id: 'OrderCreatedEvent', - version: '2.0.0', - }, - }, - { - slug: 'OrderDeletedEvent', - collection: 'events', - data: { - id: 'OrderDeletedEvent', - version: '2.0.0', - channels: [ - { - id: 'OrderChannel', - version: '1.0.0', - }, - ], - }, - }, - { - slug: 'InventoryAdjusted', - collection: 'events', - data: { - id: 'InventoryAdjusted', - version: '0.0.1', - }, - }, - { - slug: 'InventoryAdjusted', - collection: 'events', - data: { - id: 'InventoryAdjusted', - version: '1.0.0', - }, - }, - { - slug: 'InventoryAdjusted', - collection: 'events', - data: { - id: 'InventoryAdjusted', - version: '2.0.0', - }, - }, - // 7 - { - slug: 'PaymentPaid', - collection: 'events', - data: { - id: 'PaymentPaid', - version: '1.0.0', - }, - }, - // 9 - { - slug: 'PaymentPaid', - collection: 'events', - data: { - id: 'PaymentPaid', - version: '2.0.0', - }, - }, - // 10 - { - slug: 'PaymentFailed', - collection: 'events', - data: { - id: 'PaymentFailed', - version: '1.0.0', - }, - }, - // 11 - { - slug: 'PaymentFailed', - collection: 'events', - data: { - id: 'PaymentFailed', - version: '1.2.3', - }, - }, - // 12 - { - id: 'EmailVerified', - slug: 'EmailVerified', - collection: 'events', - data: { - id: 'EmailVerified', - version: '1.0.0', - channels: [ - { - id: 'EmailChannel', - version: '1.0.0', - }, - ], - }, - }, -]; - -export const mockCommands = [ - { - slug: 'PaymentProcessed', - collection: 'commands', - data: { - id: 'PaymentProcessed', - version: '0.0.1', - }, - }, -]; -export const mockQueries = [ - { - slug: 'GetOrder', - collection: 'queries', - data: { - id: 'GetOrder', - version: '0.0.1', - }, - }, -]; - -export const mockContainers = [ - { - id: 'OrderDatabase', - collection: 'containers', - data: { - id: 'OrderDatabase', - version: '1.0.0', - }, - }, - { - id: 'PaymentDatabase', - collection: 'containers', - data: { - id: 'PaymentDatabase', - version: '1.0.0', - }, - }, -]; - -export const mockChannels = [ - { - id: 'EmailChannel', - slug: 'EmailChannel', - collection: 'channels', - data: { - id: 'EmailChannel', - version: '1.0.0', - messages: [ - { - id: 'OrderCreatedEvent', - version: '0.0.1', - }, - ], - }, - }, - { - id: 'OrderChannel', - slug: 'OrderChannel', - collection: 'channels', - data: { - id: 'OrderChannel', - version: '1.0.0', - }, - }, -]; diff --git a/eventcatalog/src/utils/__tests__/services/node-graph.spec.ts b/eventcatalog/src/utils/__tests__/services/node-graph.spec.ts deleted file mode 100644 index 4a28aa44f..000000000 --- a/eventcatalog/src/utils/__tests__/services/node-graph.spec.ts +++ /dev/null @@ -1,404 +0,0 @@ -import { MarkerType } from '@xyflow/react'; -import { getNodesAndEdges } from '../../node-graphs/services-node-graph'; -import { expect, describe, it, vi, beforeEach } from 'vitest'; -import { mockCommands, mockEvents, mockQueries, mockServices, mockChannels, mockContainers } from './mocks'; -import type { ContentCollectionKey } from 'astro:content'; - -vi.mock('astro:content', async (importOriginal) => { - return { - ...(await importOriginal()), - // this will only affect "foo" outside of the original module - getCollection: (key: ContentCollectionKey) => { - switch (key) { - case 'services': - return Promise.resolve(mockServices); - case 'channels': - return Promise.resolve(mockChannels); - case 'events': - return Promise.resolve(mockEvents); - case 'commands': - return Promise.resolve(mockCommands); - case 'queries': - return Promise.resolve(mockQueries); - case 'containers': - return Promise.resolve(mockContainers); - } - }, - }; -}); - -describe('Services NodeGraph', () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - describe('getNodesAndEdges', () => { - it('should return nodes and edges for a given service', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'OrderService', version: '1.0.0' }); - - // The middle node itself, the service - const expectedServiceNode = { - id: 'OrderService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[0].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - // Nodes coming into the service (left) - const expectedRecivesNode = { - id: 'PaymentProcessed-0.0.1', - type: 'commands', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockCommands[0].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - // Nodes going out of the service (right) - const expectedSendsNode = { - id: 'OrderCreatedEvent-0.0.1', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[0].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }; - - const expectedOrderDatabaseNode = { - id: 'OrderDatabase-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', data: { ...mockContainers[0].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'data', - }; - - const expectedPaymentDatabaseNode = { - id: 'PaymentDatabase-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', data: { ...mockContainers[1].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'data', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - label: 'accepts', - animated: false, - id: 'PaymentProcessed-0.0.1-OrderService-1.0.0-warning', - source: 'PaymentProcessed-0.0.1', - target: 'OrderService-1.0.0', - }), - expect.objectContaining({ - id: 'OrderService-1.0.0-OrderDatabase-1.0.0', - source: 'OrderService-1.0.0', - target: 'OrderDatabase-1.0.0', - type: 'multiline', - }), - expect.objectContaining({ - label: 'reads from \n (undefined)', - id: 'OrderService-1.0.0-PaymentDatabase-1.0.0', - source: 'PaymentDatabase-1.0.0', - target: 'OrderService-1.0.0', - type: 'multiline', - }), - expect.objectContaining({ - label: 'publishes \nevent', - animated: false, - id: 'OrderService-1.0.0-OrderCreatedEvent-0.0.1', - source: 'OrderService-1.0.0', - target: 'OrderCreatedEvent-0.0.1', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedRecivesNode), - - // The data node - expect.objectContaining(expectedOrderDatabaseNode), - expect.objectContaining(expectedPaymentDatabaseNode), - - // The service node itself - expect.objectContaining(expectedServiceNode), - - // Nodes on the right - expect.objectContaining(expectedSendsNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('if a message is sent and received by the same service it will render a custom edge', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'NotificationsService', version: '1.0.0' }); - - // The middle node itself, the service - const expectedServiceNode = { - id: 'NotificationsService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[3].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - // Nodes coming into the service (left) - const expectedRecivesNode = { - id: 'OrderCreatedEvent-2.0.0', - type: 'events', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[3].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - // Nodes going out of the service (right) - const expectedSendsNode = { - id: 'OrderCreatedEvent-2.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[3].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'OrderCreatedEvent-2.0.0-NotificationsService-1.0.0', - source: 'OrderCreatedEvent-2.0.0', - target: 'NotificationsService-1.0.0', - }), - expect.objectContaining({ - id: 'NotificationsService-1.0.0-OrderCreatedEvent-2.0.0', - source: 'NotificationsService-1.0.0', - target: 'OrderCreatedEvent-2.0.0', - }), - expect.objectContaining({ - id: 'NotificationsService-1.0.0-OrderCreatedEvent-2.0.0-both', - source: 'NotificationsService-1.0.0', - target: 'OrderCreatedEvent-2.0.0', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedRecivesNode), - - // The service node itself - expect.objectContaining(expectedServiceNode), - - // Nodes on the right - expect.objectContaining(expectedSendsNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('returns empty nodes and edges if no service is found', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'UnknownService', version: '1.0.0' }); - - expect(nodes).toEqual([]); - expect(edges).toEqual([]); - }); - - it('should return nodes and edges for a given service using semver range', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'InventoryService', version: '1.0.0' }); - - // The middle node itself, the service - const expectedServiceNode = { - id: 'InventoryService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[1].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - // Nodes coming into the service (left) - const expectedRecivesNode = { - id: 'OrderCreatedEvent-1.3.9', - type: 'events', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[2].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - // Nodes going out of the service (right) - const expectedSendsNode = { - id: 'InventoryAdjusted-2.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: { ...mockEvents[7].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'events', - }; - - const expectedEdges = expect.arrayContaining([ - expect.objectContaining({ - id: 'OrderCreatedEvent-1.3.9-InventoryService-1.0.0', - source: 'OrderCreatedEvent-1.3.9', - target: 'InventoryService-1.0.0', - }), - expect.objectContaining({ - id: 'InventoryService-1.0.0-InventoryAdjusted-2.0.0', - source: 'InventoryService-1.0.0', - target: 'InventoryAdjusted-2.0.0', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // Nodes on the left - expect.objectContaining(expectedRecivesNode), - - // The service node itself - expect.objectContaining(expectedServiceNode), - - // Nodes on the right - expect.objectContaining(expectedSendsNode), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('creates a channel node between the service and message if the service defined a channel', async () => { - const { nodes, edges } = await getNodesAndEdges({ id: 'PaymentService', version: '1.0.0' }); - - // The middle node itself, the service - const expectedServiceNode = { - id: 'PaymentService-1.0.0', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', service: { ...mockServices[2].data } }, - position: { x: expect.any(Number), y: expect.any(Number) }, - type: 'services', - }; - - const expectedConsumedMessageWithoutChannel = { - id: 'OrderCreatedEvent-2.0.0', - type: 'events', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: expect.anything() }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedConsumedMessageWithChannel = { - id: 'OrderDeletedEvent-2.0.0', - type: 'events', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: expect.anything() }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedChannelNode = { - id: 'EmailChannel-1.0.0', - type: 'channels', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', channel: expect.anything() }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - // Message coming into the service with a channel - const expectedProducedMessage = { - id: 'PaymentPaid-2.0.0', - type: 'events', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', message: expect.anything() }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - // Message coming into the service with a channel - const expectedChannelAfterProducedMessage = { - id: 'EmailChannel-1.0.0', - type: 'channels', - sourcePosition: 'right', - targetPosition: 'left', - data: { mode: 'simple', channel: expect.anything() }, - position: { x: expect.any(Number), y: expect.any(Number) }, - }; - - const expectedEdges = expect.arrayContaining([ - // The service to the message (it publishes) - expect.objectContaining({ - id: 'PaymentService-1.0.0-PaymentPaid-2.0.0', - source: 'PaymentService-1.0.0', - target: 'PaymentPaid-2.0.0', - }), - // The Message to the channel defined by the producing service - expect.objectContaining({ - id: 'PaymentPaid-2.0.0-EmailChannel-1.0.0', - source: 'PaymentPaid-2.0.0', - target: 'EmailChannel-1.0.0', - }), - - // The consumed message with no channel defined - // We expect to to connect to the service - expect.objectContaining({ - id: 'OrderCreatedEvent-2.0.0-PaymentService-1.0.0', - source: 'OrderCreatedEvent-2.0.0', - target: 'PaymentService-1.0.0', - }), - - // The consume message with a channel defined - expect.objectContaining({ - id: 'OrderDeletedEvent-2.0.0-EmailChannel-1.0.0', - source: 'OrderDeletedEvent-2.0.0', - target: 'EmailChannel-1.0.0', - }), - ]); - - expect(nodes).toEqual( - expect.arrayContaining([ - // message the service consumes - expect.objectContaining(expectedConsumedMessageWithoutChannel), - - // message the service consumes with a channel - expect.objectContaining(expectedConsumedMessageWithChannel), - - // The channel node - expect.objectContaining(expectedChannelNode), - - // The service node itself - expect.objectContaining(expectedServiceNode), - - // message on the right of the service - expect.objectContaining(expectedProducedMessage), - - expect.objectContaining(expectedChannelAfterProducedMessage), - ]) - ); - - expect(edges).toEqual(expectedEdges); - }); - - it('when `renderMessages` is false it should not render any messages or channels', async () => { - const { nodes } = await getNodesAndEdges({ id: 'PaymentService', version: '1.0.0', renderMessages: false }); - - const hasMessages = nodes.some((node) => node.type === 'events' || node.type === 'commands' || node.type === 'queries'); - const hasChannels = nodes.some((node) => node.type === 'channels'); - - expect(hasMessages).toBe(false); - expect(hasChannels).toBe(false); - }); - - it('will render any container (data) that the service writes to or reads from', async () => { - const { nodes } = await getNodesAndEdges({ id: 'OrderService', version: '1.0.0' }); - - const hasContainers = nodes.some((node) => node.type === 'data'); - expect(hasContainers).toBe(true); - }); - }); -}); diff --git a/eventcatalog/src/utils/channels.ts b/eventcatalog/src/utils/channels.ts deleted file mode 100644 index 04e9de0d2..000000000 --- a/eventcatalog/src/utils/channels.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; -import { getItemsFromCollectionByIdAndSemverOrLatest, getVersionForCollectionItem, satisfies } from './collections/util'; -import { getMessages } from './messages'; -import type { CollectionMessageTypes } from '@types'; -import utils from '@eventcatalog/sdk'; - -const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd(); - -type Channel = CollectionEntry<'channels'> & { - catalog: { - path: string; - filePath: string; - type: string; - }; -}; - -interface Props { - getAllVersions?: boolean; -} - -// cache for build time -let cachedChannels: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getChannels = async ({ getAllVersions = true }: Props = {}): Promise => { - const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions'; - - if (cachedChannels[cacheKey].length > 0) { - return cachedChannels[cacheKey]; - } - - const channels = await getCollection('channels', (query) => { - return (getAllVersions || !query.filePath?.includes('versioned')) && query.data.hidden !== true; - }); - - const { commands, events, queries } = await getMessages(); - const allMessages = [...commands, ...events, ...queries]; - - cachedChannels[cacheKey] = await Promise.all( - channels.map(async (channel) => { - const { latestVersion, versions } = getVersionForCollectionItem(channel, channels); - - const messagesForChannel = allMessages.filter((message) => { - return message.data.channels?.some((messageChannel) => { - if (messageChannel.id != channel.data.id) return false; - if (messageChannel.version == 'latest' || messageChannel.version == undefined) - return channel.data.version == latestVersion; - return satisfies(channel.data.version, messageChannel.version); - }); - }); - - const messages = messagesForChannel.map((message: CollectionEntry) => { - return { id: message.data.id, name: message.data.name, version: message.data.version, collection: message.collection }; - }); - - const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? ''); - const folderName = await getResourceFolderName( - process.env.PROJECT_DIR ?? '', - channel.data.id, - channel.data.version.toString() - ); - const channelFolderName = folderName ?? channel.id.replace(`-${channel.data.version}`, ''); - - return { - ...channel, - data: { - ...channel.data, - versions, - latestVersion, - messages, - }, - catalog: { - path: path.join(channel.collection, channel.id.replace('/index.mdx', '')), - absoluteFilePath: path.join(PROJECT_DIR, channel.collection, channel.id.replace('/index.mdx', '/index.md')), - astroContentFilePath: path.join(process.cwd(), 'src', 'content', channel.collection, channel.id), - filePath: path.join(process.cwd(), 'src', 'catalog-files', channel.collection, channel.id.replace('/index.mdx', '')), - publicPath: path.join('/generated', channel.collection, channelFolderName), - type: 'event', - }, - }; - }) - ); - - // order them by the name of the channel - cachedChannels[cacheKey].sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedChannels[cacheKey]; -}; - -// Could be recursive, we need to keep going until we find a loop or until we reach the target channel -export const isChannelsConnected = ( - sourceChannel: CollectionEntry<'channels'>, - targetChannel: CollectionEntry<'channels'>, - channels: CollectionEntry<'channels'>[], - visited: Set = new Set() -) => { - // Create a unique key for this channel (id + version to handle multiple versions) - const channelKey = `${sourceChannel.data.id}:${sourceChannel.data.version}`; - - // Base case: we've reached the target channel - if (sourceChannel.data.id === targetChannel.data.id) { - return true; - } - - // Prevent infinite loops by tracking visited channels - if (visited.has(channelKey)) { - return false; - } - - // Mark this channel as visited - visited.add(channelKey); - - const routes = sourceChannel.data.routes ?? []; - for (const route of routes) { - const routeChannel = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - route.id, - route.version - )[0] as CollectionEntry<'channels'>; - - if (routeChannel) { - // Pass the visited set to the recursive call - if (isChannelsConnected(routeChannel, targetChannel, channels, visited)) { - return true; - } - } - } - return false; -}; - -// Go from the source to the target channel and return the channel chain -export const getChannelChain = ( - sourceChannel: CollectionEntry<'channels'>, - targetChannel: CollectionEntry<'channels'>, - channels: CollectionEntry<'channels'>[] -): CollectionEntry<'channels'>[] => { - // Base case: we've reached the target channel - if (sourceChannel.data.id === targetChannel.data.id && sourceChannel.data.version === targetChannel.data.version) { - return [sourceChannel]; - } - - const routes = sourceChannel.data.routes ?? []; - - if (routes.length > 0 && isChannelsConnected(sourceChannel, targetChannel, channels)) { - // Need to check every route and see if any of them are connected to the target channel - for (const route of routes) { - const routeChannel = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - route.id, - route.version - )[0] as CollectionEntry<'channels'>; - if (routeChannel) { - if (isChannelsConnected(routeChannel, targetChannel, channels)) { - return [sourceChannel, ...getChannelChain(routeChannel, targetChannel, channels)]; - } - } - } - } - return []; -}; diff --git a/eventcatalog/src/utils/collections/changelogs.ts b/eventcatalog/src/utils/collections/changelogs.ts deleted file mode 100644 index 84b3a9570..000000000 --- a/eventcatalog/src/utils/collections/changelogs.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { CollectionTypes } from '@types'; -import { getCollection, type CollectionEntry } from 'astro:content'; -import path from 'node:path'; - -export type ChangeLog = CollectionEntry<'changelogs'>; - -export const getChangeLogs = async (item: CollectionEntry): Promise => { - const { collection, data, filePath } = item; - - // Get all logs for collection type and filter by given collection - const logs = await getCollection('changelogs', (log) => { - const collectionDirectory = path.dirname(item?.filePath || ''); - const isRootChangeLog = path.join(collectionDirectory, 'changelog.mdx') === log.filePath; - // Ensure the path follows /versioned//changelog.mdx - const versionedPathPattern = new RegExp(`${collectionDirectory}/versioned/[^/]+/changelog\\.mdx$`); - const isVersionedChangeLog = versionedPathPattern.test(log.filePath!); - return log.id.includes(`${collection}/`) && (isRootChangeLog || isVersionedChangeLog); - }); - - const hydratedLogs = logs.map((log) => { - // Check if there is a version in the url - const isVersioned = log.id.includes('versioned'); - - const parts = log.filePath!.split('/'); - // hack to get the version of the id (url) - const version = parts[parts.length - 2]; - return { - ...log, - data: { - ...log.data, - version: isVersioned ? version : data.latestVersion || 'latest', - }, - }; - }); - - // Order by version string - return hydratedLogs.sort((a, b) => { - return b.data.version.localeCompare(a.data.version); - }); -}; diff --git a/eventcatalog/src/utils/collections/containers.ts b/eventcatalog/src/utils/collections/containers.ts deleted file mode 100644 index 5956765cc..000000000 --- a/eventcatalog/src/utils/collections/containers.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; -import { getVersionForCollectionItem, satisfies } from './util'; -import utils from '@eventcatalog/sdk'; - -const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd(); - -export type Entity = CollectionEntry<'containers'> & { - catalog: { - path: string; - filePath: string; - type: string; - }; -}; - -interface Props { - getAllVersions?: boolean; -} - -// cache for build time -let cachedEntities: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getContainers = async ({ getAllVersions = true }: Props = {}): Promise => { - const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions'; - - if (cachedEntities[cacheKey].length > 0) { - return cachedEntities[cacheKey]; - } - - const containers = await getCollection('containers', (container) => { - return (getAllVersions || !container.filePath?.includes('versioned')) && container.data.hidden !== true; - }); - - const services = await getCollection('services'); - - cachedEntities[cacheKey] = await Promise.all( - containers.map(async (container) => { - const { latestVersion, versions } = getVersionForCollectionItem(container, containers); - - const servicesThatReferenceContainer = services.filter((service) => { - const references = [...(service.data.writesTo || []), ...(service.data.readsFrom || [])]; - return references.some((item) => { - if (item.id != container.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return container.data.version == latestVersion; - return satisfies(container.data.version, item.version); - }); - }); - - const servicesThatWriteToContainer = services.filter((service) => { - return service.data?.writesTo?.some((item) => { - if (item.id != container.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return container.data.version == latestVersion; - return satisfies(container.data.version, item.version); - }); - }); - - const servicesThatReadFromContainer = services.filter((service) => { - return service.data?.readsFrom?.some((item) => { - if (item.id != container.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return container.data.version == latestVersion; - return satisfies(container.data.version, item.version); - }); - }); - - const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? ''); - const folderName = await getResourceFolderName( - process.env.PROJECT_DIR ?? '', - container.data.id, - container.data.version.toString() - ); - const containerFolderName = folderName ?? container.id.replace(`-${container.data.version}`, ''); - - return { - ...container, - data: { - ...container.data, - versions, - latestVersion, - services: servicesThatReferenceContainer, - servicesThatWriteToContainer, - servicesThatReadFromContainer, - }, - catalog: { - path: path.join(container.collection, container.id.replace('/index.mdx', '')), - absoluteFilePath: path.join(PROJECT_DIR, container.collection, container.id.replace('/index.mdx', '/index.md')), - astroContentFilePath: path.join(process.cwd(), 'src', 'content', container.collection, container.id), - filePath: path.join( - process.cwd(), - 'src', - 'catalog-files', - container.collection, - container.id.replace('/index.mdx', '') - ), - publicPath: path.join('/generated', container.collection, containerFolderName), - type: 'container', - }, - }; - }) - ); - - // order them by the name of the event - cachedEntities[cacheKey].sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedEntities[cacheKey]; -}; diff --git a/eventcatalog/src/utils/collections/domains.ts b/eventcatalog/src/utils/collections/domains.ts deleted file mode 100644 index d9fcef801..000000000 --- a/eventcatalog/src/utils/collections/domains.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { getItemsFromCollectionByIdAndSemverOrLatest, getVersionForCollectionItem } from '@utils/collections/util'; -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; -import type { CollectionMessageTypes } from '@types'; -import type { Service } from './services'; -import utils from '@eventcatalog/sdk'; - -const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd(); - -export type Domain = CollectionEntry<'domains'>; -export type UbiquitousLanguage = CollectionEntry<'ubiquitousLanguages'>; -interface Props { - getAllVersions?: boolean; -} - -// Update cache to store both versions -let cachedDomains: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getDomains = async ({ getAllVersions = true }: Props = {}): Promise => { - const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions'; - - // Check if we have cached domains for this specific getAllVersions value - if (cachedDomains[cacheKey].length > 0) { - return cachedDomains[cacheKey]; - } - - // Get all the domains that are not versioned - const domains = await getCollection('domains', (domain) => { - return (getAllVersions || !domain.filePath?.includes('versioned')) && domain.data.hidden !== true; - }); - - // Get all the services that are not versioned - const servicesCollection = await getCollection('services'); - const entitiesCollection = await getCollection('entities'); - - // @ts-ignore // TODO: Fix this type - cachedDomains[cacheKey] = await Promise.all( - domains.map(async (domain) => { - const { latestVersion, versions } = getVersionForCollectionItem(domain, domains); - - // const receives = service.data.receives || []; - const servicesInDomain = domain.data.services || []; - const subDomainsInDomain = domain.data.domains || []; - const entitiesInDomain = domain.data.entities || []; - const subDomains = subDomainsInDomain - .map((_subDomain: { id: string; version: string | undefined }) => - getItemsFromCollectionByIdAndSemverOrLatest(domains, _subDomain.id, _subDomain.version) - ) - .flat() - // Stop circular references - .filter((subDomain) => subDomain.data.id !== domain.data.id); - - // Services in the sub domains - const subdomainServices = subDomains.flatMap((subDomain) => subDomain.data.services || []); - - const services = [...servicesInDomain, ...subdomainServices] - .map((_service: { id: string; version: string | undefined }) => - getItemsFromCollectionByIdAndSemverOrLatest(servicesCollection, _service.id, _service.version) - ) - .flat(); - - const entities = [...entitiesInDomain] - .map((_entity: { id: string; version: string | undefined }) => - getItemsFromCollectionByIdAndSemverOrLatest(entitiesCollection, _entity.id, _entity.version) - ) - .flat(); - - const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? ''); - const folderName = await getResourceFolderName( - process.env.PROJECT_DIR ?? '', - domain.data.id, - domain.data.version.toString() - ); - const domainFolderName = folderName ?? domain.id.replace(`-${domain.data.version}`, ''); - - return { - ...domain, - data: { - ...domain.data, - services: services, - domains: subDomains, - entities: entities, - latestVersion, - versions, - }, - catalog: { - path: path.join(domain.collection, domain.id.replace('/index.mdx', '')), - absoluteFilePath: path.join(PROJECT_DIR, domain.collection, domain.id.replace('/index.mdx', '/index.md')), - astroContentFilePath: path.join(process.cwd(), 'src', 'content', domain.collection, domain.id), - filePath: path.join(process.cwd(), 'src', 'catalog-files', domain.collection, domain.id.replace('/index.mdx', '')), - publicPath: path.join('/generated', domain.collection, domainFolderName), - type: 'service', - }, - }; - }) - ); - - // order them by the name of the domain - cachedDomains[cacheKey].sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedDomains[cacheKey]; -}; - -export const getMessagesForDomain = async ( - domain: Domain -): Promise<{ sends: CollectionEntry[]; receives: CollectionEntry[] }> => { - // We already have the services from the domain - const services = domain.data.services as unknown as CollectionEntry<'services'>[]; - - const events = await getCollection('events'); - const commands = await getCollection('commands'); - const queries = await getCollection('queries'); - - const allMessages = [...events, ...commands, ...queries]; - - const sends = services.flatMap((service) => service.data.sends || []); - const receives = services.flatMap((service) => service.data.receives || []); - - const sendsMessages = sends.map((send) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, send.id, send.version)); - const receivesMessages = receives.map((receive) => - getItemsFromCollectionByIdAndSemverOrLatest(allMessages, receive.id, receive.version) - ); - - return { - sends: sendsMessages.flat(), - receives: receivesMessages.flat(), - }; -}; - -export const getUbiquitousLanguage = async (domain: Domain): Promise => { - const ubiquitousLanguages = await getCollection('ubiquitousLanguages', (ubiquitousLanguage: UbiquitousLanguage) => { - const domainFolder = path.dirname(domain.filePath || ''); - const ubiquitousLanguageFolder = path.dirname(ubiquitousLanguage.filePath || ''); - return domainFolder === ubiquitousLanguageFolder; - }); - - return ubiquitousLanguages; -}; - -export const getUbiquitousLanguageWithSubdomains = async ( - domain: Domain -): Promise<{ - domain: UbiquitousLanguage | null; - subdomains: Array<{ subdomain: Domain; ubiquitousLanguage: UbiquitousLanguage | null }>; - duplicateTerms: Set; -}> => { - // Get domain's own ubiquitous language - const domainUbiquitousLanguage = await getUbiquitousLanguage(domain); - const domainUL = domainUbiquitousLanguage[0] || null; - - // Get all subdomains - const subdomains = (domain.data.domains as unknown as Domain[]) || []; - - // Get ubiquitous language for each subdomain - const subdomainULs = await Promise.all( - subdomains.map(async (subdomain) => { - const subdomainUL = await getUbiquitousLanguage(subdomain); - return { - subdomain, - ubiquitousLanguage: subdomainUL[0] || null, - }; - }) - ); - - // Find duplicate terms across domain and subdomains - const duplicateTerms = new Set(); - const termCounts = new Map(); - - // Count terms from domain - if (domainUL?.data?.dictionary) { - domainUL.data.dictionary.forEach((term) => { - const termName = term.name.toLowerCase(); - termCounts.set(termName, (termCounts.get(termName) || 0) + 1); - }); - } - - // Count terms from subdomains - subdomainULs.forEach(({ ubiquitousLanguage }) => { - if (ubiquitousLanguage?.data?.dictionary) { - ubiquitousLanguage.data.dictionary.forEach((term) => { - const termName = term.name.toLowerCase(); - termCounts.set(termName, (termCounts.get(termName) || 0) + 1); - }); - } - }); - - // Identify duplicates - termCounts.forEach((count, termName) => { - if (count > 1) { - duplicateTerms.add(termName); - } - }); - - return { - domain: domainUL, - subdomains: subdomainULs, - duplicateTerms, - }; -}; - -export const getParentDomains = async (domain: Domain): Promise => { - const domains = await getDomains({ getAllVersions: false }); - return domains.filter((d) => { - const subDomains = (d.data.domains as unknown as Domain[]) || []; - return subDomains.some((d) => d.data.id === domain.data.id); - }); -}; - -export const getDomainsForService = async (service: Service): Promise => { - const domains = await getDomains({ getAllVersions: false }); - return domains.filter((d) => { - const services = d.data.services as unknown as Service[]; - return services.some((s) => s.data.id === service.data.id); - }); -}; - -export const domainHasEntities = (domain: Domain): boolean => { - return (domain.data.entities && domain.data.entities.length > 0) || false; -}; diff --git a/eventcatalog/src/utils/collections/flows.ts b/eventcatalog/src/utils/collections/flows.ts deleted file mode 100644 index d70553c83..000000000 --- a/eventcatalog/src/utils/collections/flows.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { getItemsFromCollectionByIdAndSemverOrLatest, getVersionForCollectionItem } from '@utils/collections/util'; -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; - -const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd(); - -export type Flow = CollectionEntry<'flows'>; - -interface Props { - getAllVersions?: boolean; -} - -// Cache for build time -let cachedFlows: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getFlows = async ({ getAllVersions = true }: Props = {}): Promise => { - const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions'; - - if (cachedFlows[cacheKey].length > 0) { - return cachedFlows[cacheKey]; - } - - // Get flows that are not versioned - const flows = await getCollection('flows', (flow) => { - return (getAllVersions || !flow.filePath?.includes('versioned')) && flow.data.hidden !== true; - }); - - const events = await getCollection('events'); - const commands = await getCollection('commands'); - - const allMessages = [...events, ...commands]; - - // @ts-ignore // TODO: Fix this type - cachedFlows[cacheKey] = flows.map((flow) => { - // @ts-ignore - const { latestVersion, versions } = getVersionForCollectionItem(flow, flows); - const steps = flow.data.steps || []; - - const hydrateSteps = steps.map((step) => { - if (!step.message) return { ...flow, data: { ...flow.data, type: 'node' } }; - const message = getItemsFromCollectionByIdAndSemverOrLatest(allMessages, step.message.id, step.message.version); - return { - ...step, - type: 'message', - message: message, - }; - }); - - return { - ...flow, - data: { - ...flow.data, - steps: hydrateSteps, - versions, - latestVersion, - }, - catalog: { - path: path.join(flow.collection, flow.id.replace('/index.mdx', '')), - absoluteFilePath: path.join(PROJECT_DIR, flow.collection, flow.id.replace('/index.mdx', '/index.md')), - astroContentFilePath: path.join(process.cwd(), 'src', 'content', flow.collection, flow.id), - filePath: path.join(process.cwd(), 'src', 'catalog-files', flow.collection, flow.id.replace('/index.mdx', '')), - publicPath: path.join('/generated', flow.collection), - type: 'flow', - }, - }; - }); - - // order them by the name of the flow - cachedFlows[cacheKey].sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedFlows[cacheKey]; -}; diff --git a/eventcatalog/src/utils/collections/icons.ts b/eventcatalog/src/utils/collections/icons.ts deleted file mode 100644 index a31e8dbd2..000000000 --- a/eventcatalog/src/utils/collections/icons.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - ServerIcon, - RectangleGroupIcon, - BoltIcon, - ChatBubbleLeftIcon, - MagnifyingGlassIcon, - QueueListIcon, - UserGroupIcon, - UserIcon, - ArrowsRightLeftIcon, - VariableIcon, - MapIcon, -} from '@heroicons/react/24/outline'; -import { BookText, Box, DatabaseIcon } from 'lucide-react'; - -export const getIconForCollection = (collection: string) => { - switch (collection) { - case 'domains': - return RectangleGroupIcon; - case 'services': - return ServerIcon; - case 'events': - return BoltIcon; - case 'commands': - return ChatBubbleLeftIcon; - case 'queries': - return MagnifyingGlassIcon; - case 'flows': - return QueueListIcon; - case 'teams': - return UserGroupIcon; - case 'users': - return UserIcon; - case 'channels': - return ArrowsRightLeftIcon; - case 'channels-parameter': - return VariableIcon; - case 'ubiquitousLanguages': - return BookText; - case 'bounded-context-map': - return MapIcon; - case 'entities': - return Box; - case 'containers': - return DatabaseIcon; - default: - return ServerIcon; - } -}; - -export const getColorAndIconForCollection = (collection: string) => { - const icon = getIconForCollection(collection); - - switch (collection) { - case 'events': - return { color: 'orange', Icon: icon }; - case 'commands': - return { color: 'blue', Icon: icon }; - case 'queries': - return { color: 'green', Icon: icon }; - case 'flows': - return { color: 'teal', Icon: icon }; - case 'teams': - return { color: 'red', Icon: icon }; - case 'users': - return { color: 'gray', Icon: icon }; - case 'channels': - return { color: 'purple', Icon: icon }; - case 'ubiquitousLanguages': - return { color: 'green', Icon: icon }; - case 'entities': - return { color: 'purple', Icon: icon }; - case 'domains': - return { color: 'yellow', Icon: icon }; - case 'services': - return { color: 'pink', Icon: icon }; - default: - return { color: 'gray', Icon: icon }; - } -}; diff --git a/eventcatalog/src/utils/collections/owners.ts b/eventcatalog/src/utils/collections/owners.ts deleted file mode 100644 index 7b88f8ff0..000000000 --- a/eventcatalog/src/utils/collections/owners.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { getCollection, type CollectionEntry } from 'astro:content'; - -const getOwners = (function () { - type Owners = CollectionEntry<'users' | 'teams'>; - let cachedOwners: Map | null = null; - let initializingPromise: Promise> | null = null; - - /** - * Initializes and caches the owners by fetching from the 'users' and 'teams' collections. - */ - async function init() { - const ownersMap = new Map>(); - - const owners = await Promise.all([ - getCollection('users', (entry) => entry.data.hidden !== true), - getCollection('teams', (entry) => entry.data.hidden !== true), - ]); - - for (const owner of owners.flat()) { - ownersMap.set(owner.data.id, owner); - } - - cachedOwners = ownersMap; - initializingPromise = null; - - return cachedOwners; - } - - return () => - cachedOwners || // Return cached owners if already initialized - initializingPromise || // Return the promise if initialization is in progress - (initializingPromise = init()); // Initialize if neither cache nor promise exists -})(); - -export async function getOwner(lookup: { id: string }): Promise | undefined> { - const lookupId = typeof lookup === 'string' ? lookup : lookup.id; - - const owner = (await getOwners()).get(lookupId); - - if (!owner) console.warn(`Entry ${lookupId} not found in "teams"/"users" collections.`); - - return owner; -} diff --git a/eventcatalog/src/utils/collections/schemas.ts b/eventcatalog/src/utils/collections/schemas.ts deleted file mode 100644 index d20c64cd7..000000000 --- a/eventcatalog/src/utils/collections/schemas.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { CollectionEntry } from 'astro:content'; -import type { PageTypes } from '@types'; -import path from 'path'; -import { buildUrl } from '@utils/url-builder'; - -export type Schema = { - url: string; - format: string; -}; - -export const getSchemaURL = (resource: CollectionEntry) => { - // @ts-ignore - const publicPath = resource?.catalog?.publicPath; - const schemaFilePath = resource?.data?.schemaPath; - - if (!publicPath || !schemaFilePath) { - return; - } - - // new URL - return path.join(publicPath, schemaFilePath); -}; - -export const getSchemaFormatFromURL = (url: string) => { - const pathParts = url.split('.'); - const format = pathParts[pathParts.length - 1]; - return format; -}; - -export const getSchemasFromResource = (resource: CollectionEntry): Schema[] => { - const schemaPublicPath = getSchemaURL(resource); - - if (!schemaPublicPath) { - return []; - } - - if (resource.collection === 'services') { - const specifications = resource?.data?.specifications; - const asyncapiPath = Array.isArray(specifications) - ? specifications.find((spec) => spec.type === 'asyncapi')?.path - : specifications?.asyncapiPath; - const openapiPath = Array.isArray(specifications) - ? specifications.find((spec) => spec.type === 'openapi')?.path - : specifications?.openapiPath; - // @ts-ignore - const publicPath = resource?.catalog?.publicPath; - const schemas = []; - - if (asyncapiPath) { - const asyncapiUrl = path.join(publicPath, asyncapiPath); - schemas.push({ url: buildUrl(asyncapiUrl), format: 'asyncapi' }); - } - - if (openapiPath) { - const openapiUrl = path.join(publicPath, openapiPath); - schemas.push({ url: buildUrl(openapiUrl), format: 'openapi' }); - } - - return schemas; - } else { - const pathParts = schemaPublicPath.split('.'); - const format = pathParts[pathParts.length - 1]; - return [{ url: buildUrl(schemaPublicPath), format }]; - } -}; diff --git a/eventcatalog/src/utils/collections/services.ts b/eventcatalog/src/utils/collections/services.ts deleted file mode 100644 index 58e3bbcaf..000000000 --- a/eventcatalog/src/utils/collections/services.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { getItemsFromCollectionByIdAndSemverOrLatest, getVersionForCollectionItem } from '@utils/collections/util'; -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; -import semver from 'semver'; -import type { CollectionTypes } from '@types'; -const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd(); -import utils from '@eventcatalog/sdk'; - -export type Service = CollectionEntry<'services'>; - -interface Props { - getAllVersions?: boolean; -} - -// Cache for build time -let cachedServices: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getServices = async ({ getAllVersions = true }: Props = {}): Promise => { - const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions'; - - // Check if we have cached domains for this specific getAllVersions value - if (cachedServices[cacheKey].length > 0) { - return cachedServices[cacheKey]; - } - - // Get services that are not versioned - const services = await getCollection('services', (service) => { - return (getAllVersions || !service.filePath?.includes('versioned')) && service.data.hidden !== true; - }); - - const events = await getCollection('events'); - const commands = await getCollection('commands'); - const queries = await getCollection('queries'); - const entities = await getCollection('entities'); - const containers = await getCollection('containers'); - const allMessages = [...events, ...commands, ...queries]; - - // @ts-ignore // TODO: Fix this type - cachedServices[cacheKey] = await Promise.all( - services.map(async (service) => { - const { latestVersion, versions } = getVersionForCollectionItem(service, services); - - const sendsMessages = service.data.sends || []; - const receivesMessages = service.data.receives || []; - const serviceEntities = service.data.entities || []; - const serviceWritesTo = service.data.writesTo || []; - const serviceReadsFrom = service.data.readsFrom || []; - - const sends = sendsMessages - .map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version)) - .flat() - .filter((e: any) => e !== undefined); - - const receives = receivesMessages - .map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version)) - .flat() - .filter((e: any) => e !== undefined); - - const mappedEntities = serviceEntities - .map((entity: any) => getItemsFromCollectionByIdAndSemverOrLatest(entities, entity.id, entity.version)) - .flat() - .filter((e: any) => e !== undefined); - - const mappedWritesTo = serviceWritesTo - .map((container: any) => getItemsFromCollectionByIdAndSemverOrLatest(containers, container.id, container.version)) - .flat() - .filter((e: any) => e !== undefined); - - const mappedReadsFrom = serviceReadsFrom - .map((container: any) => getItemsFromCollectionByIdAndSemverOrLatest(containers, container.id, container.version)) - .flat() - .filter((e: any) => e !== undefined); - - const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? ''); - const folderName = await getResourceFolderName( - process.env.PROJECT_DIR ?? '', - service.data.id, - service.data.version.toString() - ); - const serviceFolderName = folderName ?? service.id.replace(`-${service.data.version}`, ''); - - return { - ...service, - data: { - ...service.data, - writesTo: mappedWritesTo, - readsFrom: mappedReadsFrom, - receives, - sends, - versions, - latestVersion, - entities: mappedEntities, - }, - // TODO: verify if it could be deleted. - nodes: { - receives, - sends, - }, - catalog: { - // TODO: avoid use string replace at path due to win32 - path: path.join(service.collection, service.id.replace('/index.mdx', '')), - absoluteFilePath: path.join(PROJECT_DIR, service.collection, service.id.replace('/index.mdx', '/index.md')), - astroContentFilePath: path.join(process.cwd(), 'src', 'content', service.collection, service.id), - filePath: path.join(process.cwd(), 'src', 'catalog-files', service.collection, service.id.replace('/index.mdx', '')), - // service will be MySerive-0.0.1 remove the version - publicPath: path.join('/generated', service.collection, serviceFolderName), - type: 'service', - }, - }; - }) - ); - - // order them by the name of the service - cachedServices[cacheKey].sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedServices[cacheKey]; -}; - -export const getProducersOfMessage = (services: Service[], message: CollectionEntry<'events' | 'commands' | 'queries'>) => { - return services.filter((service) => { - return service.data.sends?.some((send) => { - const idMatch = send.id === message.data.id; - - // If no version specified in send, treat as 'latest' - if (!send.version) return idMatch; - - // If version is 'latest', match any version - if (send.version === 'latest') return idMatch; - - // Use semver to compare versions - return idMatch && semver.satisfies(message.data.version, send.version); - }); - }); -}; - -export const getConsumersOfMessage = (services: Service[], message: CollectionEntry<'events' | 'commands' | 'queries'>) => { - return services.filter((service) => { - return service.data.receives?.some((receive) => { - const idMatch = receive.id === message.data.id; - - // If no version specified in send, treat as 'latest' - if (!receive.version) return idMatch; - - // If version is 'latest', match any version - if (receive.version === 'latest') return idMatch; - - // Use semver to compare versions - return idMatch && semver.satisfies(message.data.version, receive.version); - }); - }); -}; - -export const getSpecificationsForService = (service: CollectionEntry) => { - const specifications = Array.isArray(service.data.specifications) ? service.data.specifications : []; - - if (service.data.specifications && !Array.isArray(service.data.specifications)) { - if (service.data.specifications.asyncapiPath) { - specifications.push({ - type: 'asyncapi', - path: service.data.specifications.asyncapiPath, - name: 'AsyncAPI', - }); - } - if (service.data.specifications.openapiPath) { - specifications.push({ - type: 'openapi', - path: service.data.specifications.openapiPath, - name: 'OpenAPI', - }); - } - } - - return specifications.map((spec) => ({ - ...spec, - name: spec.name || (spec.type === 'asyncapi' ? 'AsyncAPI' : 'OpenAPI'), - filename: path.basename(spec.path), - filenameWithoutExtension: path.basename(spec.path, path.extname(spec.path)), - })); -}; -// Get services for channel -export const getProducersAndConsumersForChannel = async (channel: CollectionEntry<'channels'>) => { - const messages = channel.data.messages ?? []; - const services = await getServices({ getAllVersions: false }); - - const producers = services.filter((service) => { - const sends = service.data.sends ?? []; - return sends.some((send) => { - // @ts-ignore - return messages.some((m) => m.id === send.data.id); - }); - }); - - const consumers = services.filter((service) => { - const receives = service.data.receives ?? []; - return receives.some((receive) => { - // @ts-ignore - return messages.some((m) => m.id === receive.data.id); - }); - }); - - return { - producers: producers ?? [], - consumers: consumers ?? [], - }; -}; diff --git a/eventcatalog/src/utils/collections/util.ts b/eventcatalog/src/utils/collections/util.ts deleted file mode 100644 index d8699af3e..000000000 --- a/eventcatalog/src/utils/collections/util.ts +++ /dev/null @@ -1,167 +0,0 @@ -import type { CollectionTypes } from '@types'; -import type { CollectionEntry } from 'astro:content'; -import { coerce, compare, eq, satisfies as satisfiesRange } from 'semver'; - -export const getPreviousVersion = (version: string, versions: string[]) => { - const index = versions.indexOf(version); - return index === -1 ? null : versions[index + 1]; -}; - -export const getVersions = (data: CollectionEntry[]) => { - const allVersions = data.map((item) => item.data.version); - const versions = [...new Set(allVersions)]; - return sortStringVersions(versions); -}; - -export function isSameVersion(v1: string | undefined, v2: string | undefined) { - const semverV1 = coerce(v1); - const semverV2 = coerce(v2); - - if (semverV1 != null && semverV2 != null) { - return eq(semverV1, semverV2); - } - - return v1 === v2; -} - -/** - * Sorts versioned items. Latest version first. - */ -export function sortVersioned(versioned: T[], versionExtractor: (e: T) => string): T[] { - // try to coerce semver versions from input version - const semverVersions = versioned.map((v) => ({ original: v, semver: coerce(versionExtractor(v)) })); - - // if all versions are semver'ish, use semver to sort them - if (semverVersions.every((v) => v.semver != null)) { - const sorted = semverVersions.sort((a, b) => compare(b.semver!, a.semver!)); - - return sorted.map((v) => v.original); - } else { - // fallback to default sort - return versioned.sort((a, b) => versionExtractor(b).localeCompare(versionExtractor(a))); - } -} - -// Takes a collection and a id of a resource, and checks if the version is the latest version in the collection -export const getLatestVersionInCollectionById = (collection: CollectionEntry[], id: string) => { - const items = collection.filter((i) => i.data.id === id); - const sortedVersions = sortVersioned(items, (v) => v.data.version); - return sortedVersions[0]?.data.version ?? id; -}; - -export const getVersionForCollectionItem = ( - item: CollectionEntry, - collection: CollectionEntry[] -) => { - const allVersionsForItem = collection.filter((i) => i.data.id === item.data.id); - - return getVersions(allVersionsForItem); -}; - -export function sortStringVersions(versions: string[]) { - const sorted = sortVersioned(versions, (v) => v); - - return { latestVersion: sorted[0], versions: sorted }; -} - -/** - * @param {string} version A valid version (number | v{\d+} | semver) - * @param {string} range A semver range or exact version to compare - * @returns {boolean} Returns true if the version satisfies the range. - */ -export const satisfies = (version: string, range: string): boolean => { - const coercedVersion = coerce(version); - if (!coercedVersion) return false; - return satisfiesRange(coercedVersion, range); -}; - -export const getItemsFromCollectionByIdAndSemverOrLatest = ( - collection: T[], - id: string, - version?: string -): T[] => { - const filteredCollection = collection.filter((c) => c.data.id == id); - - if (version && version != 'latest') { - return filteredCollection.filter((c) => satisfies(c.data.version, version)); - } - - // Order by version - const sorted = sortVersioned(filteredCollection, (item) => item.data.version); - - // latest version - return sorted[0] != null ? [sorted[0]] : []; -}; - -export const findMatchingNodes = ( - nodesA: CollectionEntry<'events' | 'commands' | 'queries' | 'services' | 'containers'>[], - nodesB: CollectionEntry<'events' | 'commands' | 'queries' | 'services' | 'containers'>[] -) => { - // Track messages that are both sent and received - return nodesA.filter((nodeA) => { - return nodesB.some((nodeB) => { - return nodeB.data.id === nodeA.data.id && nodeB.data.version === nodeA.data.version; - }); - }); -}; - -export const resourceToCollectionMap = { - service: 'services', - event: 'events', - command: 'commands', - query: 'queries', - domain: 'domains', - flow: 'flows', - channel: 'channels', - user: 'users', - team: 'teams', - container: 'containers', -} as const; - -export const collectionToResourceMap = { - services: 'service', - events: 'event', - commands: 'command', - queries: 'query', - domains: 'domain', - flows: 'flow', - channels: 'channel', - users: 'user', - teams: 'team', - containers: 'container', -} as const; - -export const getDeprecatedDetails = (item: CollectionEntry) => { - let options = { - isMarkedAsDeprecated: false, - hasDeprecated: false, - message: '', - deprecatedDate: '', - }; - - if (!item.data?.deprecated) return options; - - if (typeof item.data.deprecated === 'boolean') { - options.hasDeprecated = item.data.deprecated; - options.isMarkedAsDeprecated = item.data.deprecated; - } - - if (typeof item.data.deprecated === 'object') { - options.isMarkedAsDeprecated = true; - options.hasDeprecated = item.data.deprecated.date ? new Date(item.data.deprecated.date) < new Date() : false; - options.message = item.data.deprecated.message ?? ''; - options.deprecatedDate = item.data.deprecated.date - ? new Date(item.data.deprecated.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) - : ''; - } - - return options; -}; - -export const removeContentFromCollection = (collection: CollectionEntry[]) => { - return collection.map((item) => ({ - ...item, - body: undefined, - catalog: undefined, - })); -}; diff --git a/eventcatalog/src/utils/collections/versions.ts b/eventcatalog/src/utils/collections/versions.ts deleted file mode 100644 index f5d072a92..000000000 --- a/eventcatalog/src/utils/collections/versions.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { CollectionTypes } from '@types'; -import type { CollectionEntry } from 'astro:content'; -import { satisfies, validRange } from 'semver'; - -export const getVersionFromCollection = ( - collection: CollectionEntry[], - id: string, - version?: string -): CollectionEntry[] => { - const data = collection; - const semverRange = validRange(version); - - if (semverRange) { - return data.filter((msg) => msg.data.id == id).filter((msg) => satisfies(msg.data.version, semverRange)); - } - - const filteredEvents = data.filter((event) => event.data.id === id); - - // Order by version - const sorted = filteredEvents.sort((a, b) => { - return a.data.version.localeCompare(b.data.version); - }); - - // latest version - return [sorted[sorted.length - 1]]; -}; diff --git a/eventcatalog/src/utils/commands.ts b/eventcatalog/src/utils/commands.ts deleted file mode 100644 index 1ce0c2344..000000000 --- a/eventcatalog/src/utils/commands.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; -import { getVersionForCollectionItem, satisfies } from './collections/util'; -import utils from '@eventcatalog/sdk'; - -const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd(); - -type Command = CollectionEntry<'commands'> & { - catalog: { - path: string; - filePath: string; - type: string; - }; -}; - -interface Props { - getAllVersions?: boolean; - hydrateServices?: boolean; -} - -// cache for build time -let cachedCommands: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getCommands = async ({ getAllVersions = true, hydrateServices = true }: Props = {}): Promise => { - const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions'; - - if (cachedCommands[cacheKey].length > 0 && hydrateServices) { - return cachedCommands[cacheKey]; - } - - const commands = await getCollection('commands', (command) => { - return (getAllVersions || !command.filePath?.includes('versioned')) && command.data.hidden !== true; - }); - - const services = await getCollection('services'); - const allChannels = await getCollection('channels'); - - // @ts-ignore - cachedCommands[cacheKey] = await Promise.all( - commands.map(async (command) => { - const { latestVersion, versions } = getVersionForCollectionItem(command, commands); - - const producers = services - .filter((service) => { - return service.data.sends?.some((item) => { - if (item.id != command.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return command.data.version == latestVersion; - return satisfies(command.data.version, item.version); - }); - }) - .map((service) => { - if (!hydrateServices) return { id: service.data.id, version: service.data.version }; - return service; - }); - - const consumers = services - .filter((service) => { - return service.data.receives?.some((item) => { - if (item.id != command.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return command.data.version == latestVersion; - return satisfies(command.data.version, item.version); - }); - }) - .map((service) => { - if (!hydrateServices) return { id: service.data.id, version: service.data.version }; - return service; - }); - - const messageChannels = command.data.channels || []; - const channelsForCommand = allChannels.filter((c) => messageChannels.some((channel) => c.data.id === channel.id)); - - const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? ''); - const folderName = await getResourceFolderName( - process.env.PROJECT_DIR ?? '', - command.data.id, - command.data.version.toString() - ); - const commandFolderName = folderName ?? command.id.replace(`-${command.data.version}`, ''); - - return { - ...command, - data: { - ...command.data, - messageChannels: channelsForCommand, - producers, - consumers, - versions, - latestVersion, - }, - catalog: { - path: path.join(command.collection, command.id.replace('/index.mdx', '')), - absoluteFilePath: path.join(PROJECT_DIR, command.collection, command.id.replace('/index.mdx', '/index.md')), - astroContentFilePath: path.join(process.cwd(), 'src', 'content', command.collection, command.id), - filePath: path.join(process.cwd(), 'src', 'catalog-files', command.collection, command.id.replace('/index.mdx', '')), - publicPath: path.join('/generated', command.collection, commandFolderName), - type: 'command', - }, - }; - }) - ); - - // order them by the name of the command - cachedCommands[cacheKey].sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedCommands[cacheKey]; -}; diff --git a/eventcatalog/src/utils/entities.ts b/eventcatalog/src/utils/entities.ts deleted file mode 100644 index 36a421e62..000000000 --- a/eventcatalog/src/utils/entities.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; -import { getVersionForCollectionItem, satisfies } from './collections/util'; -import utils from '@eventcatalog/sdk'; - -const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd(); - -export type Entity = CollectionEntry<'entities'> & { - catalog: { - path: string; - filePath: string; - type: string; - }; -}; - -interface Props { - getAllVersions?: boolean; -} - -// cache for build time -let cachedEntities: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getEntities = async ({ getAllVersions = true }: Props = {}): Promise => { - const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions'; - - if (cachedEntities[cacheKey].length > 0) { - return cachedEntities[cacheKey]; - } - - const entities = await getCollection('entities', (entity) => { - return (getAllVersions || !entity.filePath?.includes('versioned')) && entity.data.hidden !== true; - }); - - const services = await getCollection('services'); - const domains = await getCollection('domains'); - - cachedEntities[cacheKey] = await Promise.all( - entities.map(async (entity) => { - const { latestVersion, versions } = getVersionForCollectionItem(entity, entities); - - const servicesThatReferenceEntity = services.filter((service) => - service.data.entities?.some((item) => { - if (item.id != entity.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return entity.data.version == latestVersion; - return satisfies(entity.data.version, item.version); - }) - ); - - const domainsThatReferenceEntity = domains.filter((domain) => - domain.data.entities?.some((item) => { - if (item.id != entity.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return entity.data.version == latestVersion; - return satisfies(entity.data.version, item.version); - }) - ); - - const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? ''); - const folderName = await getResourceFolderName( - process.env.PROJECT_DIR ?? '', - entity.data.id, - entity.data.version.toString() - ); - const entityFolderName = folderName ?? entity.id.replace(`-${entity.data.version}`, ''); - - return { - ...entity, - data: { - ...entity.data, - versions, - latestVersion, - services: servicesThatReferenceEntity, - domains: domainsThatReferenceEntity, - }, - catalog: { - path: path.join(entity.collection, entity.id.replace('/index.mdx', '')), - absoluteFilePath: path.join(PROJECT_DIR, entity.collection, entity.id.replace('/index.mdx', '/index.md')), - astroContentFilePath: path.join(process.cwd(), 'src', 'content', entity.collection, entity.id), - filePath: path.join(process.cwd(), 'src', 'catalog-files', entity.collection, entity.id.replace('/index.mdx', '')), - publicPath: path.join('/generated', entity.collection, entityFolderName), - type: 'entity', - }, - }; - }) - ); - - // order them by the name of the event - cachedEntities[cacheKey].sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedEntities[cacheKey]; -}; diff --git a/eventcatalog/src/utils/eventcatalog-config/catalog.ts b/eventcatalog/src/utils/eventcatalog-config/catalog.ts deleted file mode 100644 index 584c130f7..000000000 --- a/eventcatalog/src/utils/eventcatalog-config/catalog.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as config from '@config'; - -type SideBarItemConfig = { - visible?: boolean; -}; - -export type CatalogConfig = { - docs: { - sidebar: { - showPageHeadings?: boolean; - domains?: SideBarItemConfig; - flows?: SideBarItemConfig; - services?: SideBarItemConfig; - messages?: SideBarItemConfig; - teams?: SideBarItemConfig; - users?: SideBarItemConfig; - }; - }; -}; - -const getConfigValue = (obj: any, key: string, defaultValue: any) => { - return obj?.[key] ?? defaultValue; -}; - -export const isCollectionVisibleInCatalog = (collection: string) => { - const sidebarConfig = config?.default?.docs?.sidebar || {}; - const collections = [ - 'events', - 'commands', - 'queries', - 'domains', - 'channels', - 'flows', - 'services', - 'teams', - 'users', - 'customDocs', - ]; - - if (!collections.includes(collection)) return false; - - const collectionConfig = - sidebarConfig[collection === 'events' || collection === 'commands' || collection === 'queries' ? 'messages' : collection]; - return getConfigValue(collectionConfig, 'visible', true); -}; - -export default config.default; diff --git a/eventcatalog/src/utils/events.ts b/eventcatalog/src/utils/events.ts deleted file mode 100644 index 5f8951ea1..000000000 --- a/eventcatalog/src/utils/events.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; -import { getVersionForCollectionItem, satisfies } from './collections/util'; -import utils from '@eventcatalog/sdk'; - -const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd(); - -type Event = CollectionEntry<'events'> & { - catalog: { - path: string; - filePath: string; - type: string; - }; -}; - -interface Props { - getAllVersions?: boolean; - hydrateServices?: boolean; -} - -// cache for build time -let cachedEvents: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getEvents = async ({ getAllVersions = true, hydrateServices = true }: Props = {}): Promise => { - const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions'; - - if (cachedEvents[cacheKey].length > 0 && hydrateServices) { - return cachedEvents[cacheKey]; - } - - const events = await getCollection('events', (event) => { - return (getAllVersions || !event.filePath?.includes('versioned')) && event.data.hidden !== true; - }); - - const services = await getCollection('services'); - const allChannels = await getCollection('channels'); - - // @ts-ignore - cachedEvents[cacheKey] = await Promise.all( - events.map(async (event) => { - const { latestVersion, versions } = getVersionForCollectionItem(event, events); - - const producers = services - .filter((service) => - service.data.sends?.some((item) => { - if (item.id != event.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return event.data.version == latestVersion; - return satisfies(event.data.version, item.version); - }) - ) - .map((service) => { - if (!hydrateServices) return { id: service.data.id, version: service.data.version }; - return service; - }); - - const consumers = services - .filter((service) => - service.data.receives?.some((item) => { - if (item.id != event.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return event.data.version == latestVersion; - return satisfies(event.data.version, item.version); - }) - ) - .map((service) => { - if (!hydrateServices) return { id: service.data.id, version: service.data.version }; - return service; - }); - - const messageChannels = event.data.channels || []; - const channelsForEvent = allChannels.filter((c) => messageChannels.some((channel) => c.data.id === channel.id)); - - const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? ''); - const folderName = await getResourceFolderName(process.env.PROJECT_DIR ?? '', event.data.id, event.data.version.toString()); - const eventFolderName = folderName ?? event.id.replace(`-${event.data.version}`, ''); - - return { - ...event, - data: { - ...event.data, - messageChannels: channelsForEvent, - producers, - consumers, - versions, - latestVersion, - }, - catalog: { - path: path.join(event.collection, event.id.replace('/index.mdx', '')), - absoluteFilePath: path.join(PROJECT_DIR, event.collection, event.id.replace('/index.mdx', '/index.md')), - astroContentFilePath: path.join(process.cwd(), 'src', 'content', event.collection, event.id), - filePath: path.join(process.cwd(), 'src', 'catalog-files', event.collection, event.id.replace('/index.mdx', '')), - publicPath: path.join('/generated', event.collection, eventFolderName), - type: 'event', - }, - }; - }) - ); - - // order them by the name of the event - cachedEvents[cacheKey].sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedEvents[cacheKey]; -}; diff --git a/eventcatalog/src/utils/feature.ts b/eventcatalog/src/utils/feature.ts deleted file mode 100644 index 45a0e605f..000000000 --- a/eventcatalog/src/utils/feature.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * ⚠️ WARNING: IMPORTANT LICENSE NOTICE ⚠️ - * - * Manually setting environment variables (EVENTCATALOG_STARTER or EVENTCATALOG_SCALE) to 'true' - * or modifying these functions without a valid license is strictly prohibited and constitutes - * a violation of EventCatalog's terms of use and license agreement. - * - * To access premium features legally: - * 1. Visit https://www.eventcatalog.dev/pricing - * 2. Purchase an appropriate license - * 3. Follow the official activation instructions - */ - -import config from '@config'; -import fs from 'fs'; -import { join } from 'path'; - -// These functions check for valid, legally obtained access to premium features -export const isEventCatalogStarterEnabled = () => process.env.EVENTCATALOG_STARTER === 'true'; -export const isEventCatalogScaleEnabled = () => process.env.EVENTCATALOG_SCALE === 'true'; - -export const isPrivateRemoteSchemaEnabled = () => isEventCatalogScaleEnabled() || isEventCatalogStarterEnabled(); - -export const showEventCatalogBranding = () => { - const override = process.env.EVENTCATALOG_SHOW_BRANDING; - // if any value we return true - if (override) { - return true; - } - return !isEventCatalogStarterEnabled() && !isEventCatalogScaleEnabled(); -}; - -export const showCustomBranding = () => { - return isEventCatalogStarterEnabled() || isEventCatalogScaleEnabled(); -}; - -export const isChangelogEnabled = () => config?.changelog?.enabled ?? true; - -export const isCustomDocsEnabled = () => isEventCatalogStarterEnabled() || isEventCatalogScaleEnabled(); -export const isEventCatalogChatEnabled = () => { - const isFeatureEnabledFromPlan = isEventCatalogStarterEnabled() || isEventCatalogScaleEnabled(); - return isFeatureEnabledFromPlan && config?.chat?.enabled && isSSR(); -}; - -export const isEventCatalogUpgradeEnabled = () => !isEventCatalogStarterEnabled() && !isEventCatalogScaleEnabled(); -export const isCustomLandingPageEnabled = () => isEventCatalogStarterEnabled() || isEventCatalogScaleEnabled(); - -export const isMarkdownDownloadEnabled = () => config?.llmsTxt?.enabled ?? false; -export const isLLMSTxtEnabled = () => (config?.llmsTxt?.enabled || isEventCatalogChatEnabled()) ?? false; - -export const isAuthEnabled = () => { - const directory = process.env.PROJECT_DIR || process.cwd(); - const hasAuthConfig = fs.existsSync(join(directory, 'eventcatalog.auth.js')); - return (hasAuthConfig && isSSR() && isEventCatalogScaleEnabled()) || false; -}; - -export const isSSR = () => config?.output === 'server'; - -export const isVisualiserEnabled = () => config?.visualiser?.enabled ?? true; diff --git a/eventcatalog/src/utils/generators/index.ts b/eventcatalog/src/utils/generators/index.ts deleted file mode 100644 index 918e129b4..000000000 --- a/eventcatalog/src/utils/generators/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Should really only be used on the server side -// If users are using path or fs in the eventcatalog.config.js file, it will break the build (for now) - -import config from '@config'; - -export const getConfigurationForGivenGenerator = (generator: string) => { - const generators = config.generators ?? []; - const generatorConfig = generators.find((g: any) => g[0] === generator); - return generatorConfig?.[1]; -}; diff --git a/eventcatalog/src/utils/llms.ts b/eventcatalog/src/utils/llms.ts deleted file mode 100644 index 4b08a860c..000000000 --- a/eventcatalog/src/utils/llms.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { CollectionTypes } from '@types'; -import type { CollectionEntry } from 'astro:content'; -import { dirname } from 'path'; -import { join } from 'path'; -import fs from 'fs'; - -export const addSchemaToMarkdown = (collection: CollectionEntry, file: string) => { - const resourceData = collection?.data; - const fileToResource = collection.filePath ?? ''; - let schemas: string[] = []; - - if (!resourceData?.specifications && !resourceData?.schemaPath) { - return file; - } - - if (Array.isArray(resourceData?.specifications)) { - schemas = resourceData?.specifications.map((specification: any) => specification.path); - } else { - schemas = [resourceData?.schemaPath ?? '']; - } - - const filteredSchemas = schemas.filter((schema: any) => schema !== undefined); - - // attach the schema if it has it - if (filteredSchemas.length > 0) { - for (const pathToSchema of filteredSchemas) { - const directory = dirname(fileToResource); - const schemaPath = join(directory, pathToSchema); - const schemaFile = fs.readFileSync(schemaPath, 'utf8'); - file = `${file}\n\n ## Raw Schema:${pathToSchema}\n\n${schemaFile}`; - } - } - - return file; -}; diff --git a/eventcatalog/src/utils/markdown.ts b/eventcatalog/src/utils/markdown.ts deleted file mode 100644 index 408ba1317..000000000 --- a/eventcatalog/src/utils/markdown.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Method returns MDX components and there props in markdown files -// rarely used, but useful for components that need to know how many times -// the user wants to render a component in a markdown file -export const getMDXComponentsByName = (document: string, componentName: string) => { - // Define regex pattern to match - const pattern = new RegExp(`<${componentName}\\s+([^>]*)\\/>`, 'g'); - - // Find all matches of the pattern - const matches = [...document.matchAll(pattern)]; - - // Extract the properties of each SchemaViewer - const components = matches.map((match) => { - const propsString = match[1]; - const props = {}; - - // Use regex to extract key-value pairs from propsString - const propsPattern = /(\w+)=["']([^"']+)["']/g; - let propMatch; - while ((propMatch = propsPattern.exec(propsString)) !== null) { - const key = propMatch[1]; - const value = propMatch[2]; - // @ts-ignore - props[key] = value; - } - - return props; - }); - - return components; -}; diff --git a/eventcatalog/src/utils/messages.ts b/eventcatalog/src/utils/messages.ts deleted file mode 100644 index 5c8984bda..000000000 --- a/eventcatalog/src/utils/messages.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Exporting getCommands and getEvents directly -import { getCommands } from '@utils/commands'; -import { getEvents } from '@utils/events'; -import { getQueries } from './queries'; -import type { CollectionEntry } from 'astro:content'; -export { getCommands } from '@utils/commands'; -export { getEvents } from '@utils/events'; - -interface Props { - getAllVersions?: boolean; - hydrateServices?: boolean; -} - -type Messages = { - commands: CollectionEntry<'commands'>[]; - events: CollectionEntry<'events'>[]; - queries: CollectionEntry<'queries'>[]; -}; - -// Main function that uses the imported functions -export const getMessages = async ({ getAllVersions = true, hydrateServices = true }: Props = {}): Promise => { - const [commands, events, queries] = await Promise.all([ - getCommands({ getAllVersions, hydrateServices }), - getEvents({ getAllVersions, hydrateServices }), - getQueries({ getAllVersions, hydrateServices }), - ]); - - return { - commands, - events, - queries, - }; -}; diff --git a/eventcatalog/src/utils/node-graphs/container-node-graph.ts b/eventcatalog/src/utils/node-graphs/container-node-graph.ts deleted file mode 100644 index a7ccc9b14..000000000 --- a/eventcatalog/src/utils/node-graphs/container-node-graph.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { CollectionEntry } from 'astro:content'; -import dagre from 'dagre'; -import { calculatedNodes, createDagreGraph, createEdge, generatedIdForEdge, generateIdForNode } from './utils/utils'; -import { MarkerType } from '@xyflow/react'; -import { findMatchingNodes } from '@utils/collections/util'; -import { getContainers } from '@utils/collections/containers'; - -type DagreGraph = any; - -interface Props { - id: string; - version: string; - defaultFlow?: DagreGraph; - mode?: 'simple' | 'full'; - channelRenderMode?: 'flat' | 'single'; - collection?: CollectionEntry<'containers'>[]; -} - -export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simple', channelRenderMode = 'flat' }: Props) => { - const containers = await getContainers(); - - const flow = defaultFlow || createDagreGraph({ ranksep: 300, nodesep: 50 }); - const nodes = [] as any, - edges = [] as any; - - const container = containers.find((container) => container.data.id === id && container.data.version === version); - - // Nothing found... - if (!container) { - return { - nodes: [], - edges: [], - }; - } - - const servicesThatWriteToContainer = (container.data.servicesThatWriteToContainer as CollectionEntry<'services'>[]) || []; - const servicesThatReadFromContainer = (container.data.servicesThatReadFromContainer as CollectionEntry<'services'>[]) || []; - - // Track nodes that are bth sent and received - const bothSentAndReceived = findMatchingNodes(servicesThatWriteToContainer, servicesThatReadFromContainer); - - servicesThatWriteToContainer.forEach((service) => { - nodes.push({ - id: generateIdForNode(service), - type: service?.collection, - sourcePosition: 'right', - targetPosition: 'left', - data: { mode, service: { ...service.data } }, - position: { x: 250, y: 0 }, - }); - - if (!bothSentAndReceived.includes(service)) { - edges.push({ - id: generatedIdForEdge(service, container), - source: generateIdForNode(service), - target: generateIdForNode(container), - label: 'writes to', - data: { service }, - animated: false, - type: 'default', - style: { - strokeWidth: 1, - }, - }); - } - }); - - // The message itself - nodes.push({ - id: generateIdForNode(container), - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode, - data: { - ...container.data, - }, - }, - position: { x: 0, y: 0 }, - type: 'data', - }); - - // // The messages the service sends - servicesThatReadFromContainer.forEach((service) => { - nodes.push({ - id: generateIdForNode(service), - sourcePosition: 'left', - targetPosition: 'right', - data: { title: service?.data.id, mode, service: { ...service.data } }, - position: { x: 0, y: 0 }, - type: service?.collection, - }); - - if (!bothSentAndReceived.includes(service)) { - edges.push( - createEdge({ - id: generatedIdForEdge(service, container), - source: generateIdForNode(container), - target: generateIdForNode(service), - label: `reads from \n (${container.data.technology})`, - data: { service }, - type: 'multiline', - // type: 'animatedData', - markerStart: { - type: MarkerType.ArrowClosed, - width: 40, - height: 40, - }, - markerEnd: undefined, - }) - ); - } - }); - - // Handle messages that are both sent and received - bothSentAndReceived.forEach((_service) => { - if (container) { - edges.push( - createEdge({ - id: generatedIdForEdge(container, _service) + '-both', - source: generateIdForNode(_service), - target: generateIdForNode(container), - label: `read and writes to \n (${container.data.technology})`, - type: 'multiline', - markerStart: { - type: MarkerType.ArrowClosed, - width: 40, - height: 40, - }, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 40, - height: 40, - }, - }) - ); - } - }); - - nodes.forEach((node: any) => { - flow.setNode(node.id, { width: 150, height: 100 }); - }); - - edges.forEach((edge: any) => { - flow.setEdge(edge.source, edge.target); - }); - - // Render the diagram in memory getting hte X and Y - dagre.layout(flow); - - return { - nodes: calculatedNodes(flow, nodes), - edges, - }; -}; diff --git a/eventcatalog/src/utils/node-graphs/domains-node-graph.ts b/eventcatalog/src/utils/node-graphs/domains-node-graph.ts deleted file mode 100644 index 469f3ebe9..000000000 --- a/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +++ /dev/null @@ -1,314 +0,0 @@ -import { getCollection } from 'astro:content'; -import { - createDagreGraph, - calculatedNodes, - generateIdForNode, - getEdgeLabelForServiceAsTarget, - generatedIdForEdge, - createEdge, -} from '@utils/node-graphs/utils/utils'; -import { getNodesAndEdges as getServicesNodeAndEdges } from './services-node-graph'; -import merge from 'lodash.merge'; -import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util'; -import type { Node } from '@xyflow/react'; -import { getProducersOfMessage } from '@utils/collections/services'; - -type DagreGraph = any; - -interface Props { - defaultFlow?: DagreGraph; - channelRenderMode?: 'single' | 'flat'; -} - -export const getNodesAndEdgesForDomainContextMap = async ({ defaultFlow = null }: Props) => { - const flow = defaultFlow ?? createDagreGraph({ ranksep: 360, nodesep: 50, edgesep: 50 }); - let nodes = [] as any, - edges = [] as any; - - const allDomains = await getCollection('domains'); - const domains = allDomains.filter((domain) => !domain.id.includes('/versioned')); - const services = await getCollection('services'); - - const events = await getCollection('events'); - const commands = await getCollection('commands'); - const queries = await getCollection('queries'); - - const messages = [...events, ...commands, ...queries]; - - domains.forEach((domain, index) => { - const nodeId = generateIdForNode(domain); - const rawServices = domain.data.services ?? []; - const domainServices = rawServices - .map((service) => getItemsFromCollectionByIdAndSemverOrLatest(services, service.id, service.version)) - .flat() - .filter((e) => e !== undefined); - - // Calculate domain node size based on services - const servicesCount = domainServices.length; - const SERVICES_PER_ROW = 1; - const SERVICE_WIDTH = 330; - const SERVICE_HEIGHT = 100; - const PADDING = 40; - const TITLE_HEIGHT = 20; - - const rows = Math.ceil(servicesCount / SERVICES_PER_ROW); - const domainWidth = SERVICE_WIDTH * SERVICES_PER_ROW; - const domainHeight = SERVICE_HEIGHT * rows + PADDING * 4; - - // Position domains in a grid layout - const DOMAINS_PER_ROW = 2; - const rowIndex = Math.floor(index / DOMAINS_PER_ROW); - const colIndex = index % DOMAINS_PER_ROW; - - const test = servicesCount * SERVICE_HEIGHT + PADDING * 2; - - nodes.push({ - id: nodeId, - type: 'group', - position: { - x: colIndex * (domainWidth + 400), // Increased from 100 to 400px gap between domains - y: rowIndex * (domainHeight + 300), // Increased from 100 to 300px gap between rows - }, - style: { - width: domainWidth, - height: domainHeight, - backgroundColor: 'transparent', - borderRadius: '8px', - border: '1px solid #ddd', - 'box-shadow': '0 0 10px 0 rgba(0, 0, 0, 0.1)', - }, - data: { - label: domain.data.name, - domain, - }, - }); - - nodes.push({ - id: `domain-context-map-title-${domain.data.name}`, - data: { label: `Bounded Context: ${domain.data.name}` }, - position: { x: 0, y: 0 }, - style: { - height: 40, - backgroundColor: 'transparent', - border: 'none', - color: 'black', - width: domainWidth, - }, - extent: 'parent', - parentId: nodeId, - connectable: false, - sourcePosition: 'left', - targetPosition: 'right', - draggable: false, - } as Node); - - // Position services in a grid within the domain - if (domainServices) { - domainServices.forEach((service, serviceIndex) => { - const row = Math.floor(serviceIndex / SERVICES_PER_ROW); - const col = serviceIndex % SERVICES_PER_ROW; - const serviceNodeId = `service-${domain.id}-${service.id}`; - - // Add spacing between services - const SERVICE_MARGIN = 25; - const xPosition = PADDING + col * (SERVICE_WIDTH + SERVICE_MARGIN) + 20; - const yPosition = PADDING + row * (SERVICE_HEIGHT + SERVICE_MARGIN) + TITLE_HEIGHT; - - nodes.push({ - id: generateIdForNode(service), - sourcePosition: 'right', - targetPosition: 'left', - type: 'services', - position: { - x: xPosition, - y: yPosition, - }, - parentId: nodeId, - extent: 'parent', - draggable: false, - data: { - mode: 'full', - service, - }, - }); - - // Edges - const rawReceives = service.data.receives ?? []; - const rawSends = service.data.sends ?? []; - - const receives = rawReceives - .map((receive) => getItemsFromCollectionByIdAndSemverOrLatest(messages, receive.id, receive.version)) - .flat(); - const sends = rawSends.map((send) => getItemsFromCollectionByIdAndSemverOrLatest(messages, send.id, send.version)).flat(); - - for (const receive of receives) { - const producers = getProducersOfMessage(services, receive); - - for (const producer of producers) { - const isSameDomain = domainServices.some((domainService) => domainService.data.id === producer.data.id); - - if (!isSameDomain) { - // WIP... adding messages? - // edges.push(createEdge({ - // id: generatedIdForEdge(receive, service), - // source: generateIdForNode(receive), - // target: generateIdForNode(service), - // label: getEdgeLabelForServiceAsTarget(receive), - // zIndex: 1000, - // })); - - // Find the producer and consumer nodes to get their positions - // const producerNode = nodes.find(n => n.id === generateIdForNode(producer)); - // const consumerNode = nodes.find(n => n.id === generateIdForNode(service)); - - edges.push( - createEdge({ - id: generatedIdForEdge(producer, service), - source: generateIdForNode(producer), - target: generateIdForNode(service), - label: getEdgeLabelForServiceAsTarget(receive), - zIndex: 1000, - }) - ); - - // // Calculate middle position between producer and consumer - // const messageX = (producerNode?.position?.x ?? 0) + - // ((consumerNode?.position?.x ?? 0) - (producerNode?.position?.x ?? 0)) / 2; - // const messageY = (producerNode?.position?.y ?? 0) + - // ((consumerNode?.position?.y ?? 0) - (producerNode?.position?.y ?? 0)) / 2; - - // nodes.push({ - // id: generateIdForNode(receive), - // type: receive.collection, - // sourcePosition: 'right', - // targetPosition: 'left', - // data: { - // message: receive, - // mode: 'full', - // }, - // position: { x: messageX, y: messageY }, - // }); - - // edges.push(createEdge({ - // id: generatedIdForEdge(producer, receive), - // source: generateIdForNode(producer), - // target: generateIdForNode(receive), - // label: getEdgeLabelForServiceAsTarget(receive), - // zIndex: 1000, - // })); - } - } - } - }); - } - }); - - return { - nodes, - edges, - }; -}; - -interface NodesAndEdgesProps { - id: string; - version: string; - defaultFlow?: DagreGraph; - mode: 'simple' | 'full'; - group?: boolean; - channelRenderMode?: 'single' | 'flat'; -} - -export const getNodesAndEdges = async ({ - id, - version, - defaultFlow, - mode = 'simple', - group = false, - channelRenderMode = 'flat', -}: NodesAndEdgesProps) => { - const flow = defaultFlow || createDagreGraph({ ranksep: 360, nodesep: 50, edgesep: 50 }); - let nodes = new Map(), - edges = new Map(); - - const domains = await getCollection('domains'); - - const domain = domains.find((service) => service.data.id === id && service.data.version === version); - - // Nothing found... - if (!domain) { - return { - nodes: [], - edges: [], - }; - } - - const rawServices = domain?.data.services || []; - const rawSubDomains = domain?.data.domains || []; - - const servicesCollection = await getCollection('services'); - - const domainServicesWithVersion = rawServices - .map((service) => getItemsFromCollectionByIdAndSemverOrLatest(servicesCollection, service.id, service.version)) - .flat() - .map((svc) => ({ id: svc.data.id, version: svc.data.version })); - - const domainSubDomainsWithVersion = rawSubDomains - .map((subDomain) => getItemsFromCollectionByIdAndSemverOrLatest(domains, subDomain.id, subDomain.version)) - .flat() - .map((svc) => ({ id: svc.data.id, version: svc.data.version })); - - // Get all the nodes for everyhing - - for (const service of domainServicesWithVersion) { - const { nodes: serviceNodes, edges: serviceEdges } = await getServicesNodeAndEdges({ - id: service.id, - version: service.version, - defaultFlow: flow, - mode, - renderAllEdges: true, - channelRenderMode, - }); - serviceNodes.forEach((n) => { - /** - * A message could be sent by one service and received by another service on the same domain. - * So, we need deep merge the message to keep the `showSource` and `showTarget` as true. - * - * Let's see an example: - * Take an `OrderPlaced` event sent by the `OrderService` `{ showSource: true }` and - * received by `PaymentService` `{ showTarget: true }`. - */ - nodes.set(n.id, nodes.has(n.id) ? merge(nodes.get(n.id), n) : n); - }); - // @ts-ignore - serviceEdges.forEach((e) => edges.set(e.id, e)); - } - - for (const subDomain of domainSubDomainsWithVersion) { - const { nodes: subDomainNodes, edges: subDomainEdges } = await getNodesAndEdges({ - id: subDomain.id, - version: subDomain.version, - defaultFlow: flow, - mode, - group: true, - channelRenderMode, - }); - subDomainNodes.forEach((n) => { - nodes.set(n.id, nodes.has(n.id) ? merge(nodes.get(n.id), n) : n); - }); - - subDomainEdges.forEach((e) => edges.set(e.id, e)); - } - - // Add group node to the graph first before calculating positions - if (group) { - // Update the data of the node to add the group name and color - nodes.forEach((n) => { - nodes.set(n.id, { ...n, data: { ...n.data, group: { type: 'Domain', value: domain?.data.name, id: domain?.data.id } } }); - }); - } - - return { - nodes: calculatedNodes(flow, Array.from(nodes.values())), - edges: [...edges.values()], - }; -}; diff --git a/eventcatalog/src/utils/node-graphs/export-node-graph.ts b/eventcatalog/src/utils/node-graphs/export-node-graph.ts deleted file mode 100644 index a11427b9c..000000000 --- a/eventcatalog/src/utils/node-graphs/export-node-graph.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { ReactFlowInstance, Node, ReactFlowJsonObject, Edge } from '@xyflow/react'; - -// Define the structure of resource data that can be found in nodes -interface ResourceData { - id?: string; - name?: string; - summary?: string; - version?: string; -} - -// Define the possible node data structure -interface NodeDataWithResources { - [key: string]: any; - channel?: ResourceData; - custom?: ResourceData; - data?: ResourceData; - domain?: ResourceData; - entity?: ResourceData; - message?: ResourceData; - flow?: ResourceData; - service?: ResourceData; - step?: ResourceData; - user?: ResourceData; -} - -export const exportNodeGraphForStudio = (data: ReactFlowJsonObject) => { - const dataTypes: (keyof NodeDataWithResources)[] = [ - 'channel', - 'custom', - 'data', - 'domain', - 'entity', - 'message', - 'flow', - 'service', - 'step', - 'user', - ]; - - // try and remove unwanted data for studio - const nodes = data.nodes.map((node: Node) => { - let nodeData = node.data as NodeDataWithResources; - const dataProperties = Object.keys(nodeData) as (keyof NodeDataWithResources)[]; - - // If we find a match..... - const hasCustomDataType = dataProperties.find((property) => dataTypes.includes(property)); - - if (hasCustomDataType && nodeData[hasCustomDataType]) { - const resourceData = nodeData[hasCustomDataType] as ResourceData; - nodeData = { - ...nodeData, - [hasCustomDataType]: { - id: resourceData?.id, - name: resourceData?.name, - summary: resourceData?.summary, - version: resourceData?.version, - }, - }; - } - - return { - ...node, - data: { - ...nodeData, - // We dont need these for studio - source: undefined, - target: undefined, - // Studio wants all nodes in full mode - mode: 'full', - }, - }; - }); - - const edges = data.edges.map((edge: Edge) => { - return { - ...edge, - data: undefined, - type: 'animatedMessage', - }; - }); - - return { - ...data, - nodes, - edges, - }; -}; diff --git a/eventcatalog/src/utils/node-graphs/flows-node-graph.ts b/eventcatalog/src/utils/node-graphs/flows-node-graph.ts deleted file mode 100644 index 3a5a39513..000000000 --- a/eventcatalog/src/utils/node-graphs/flows-node-graph.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { getCollection, type CollectionEntry } from 'astro:content'; -import dagre from 'dagre'; -import { createDagreGraph, calculatedNodes } from '@utils/node-graphs/utils/utils'; -import { MarkerType } from '@xyflow/react'; -import type { Node as NodeType } from '@xyflow/react'; -import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util'; - -type DagreGraph = any; - -interface Props { - id: string; - version: string; - defaultFlow?: DagreGraph; - mode?: 'simple' | 'full'; - renderAllEdges?: boolean; -} - -const getServiceNode = (step: any, services: CollectionEntry<'services'>[]) => { - const servicesForVersion = getItemsFromCollectionByIdAndSemverOrLatest(services, step.service.id, step.service.version); - const service = servicesForVersion?.[0]; - return { - ...step, - type: service ? service.collection : 'step', - service, - }; -}; - -const getFlowNode = (step: any, flows: CollectionEntry<'flows'>[]) => { - const flowsForVersion = getItemsFromCollectionByIdAndSemverOrLatest(flows, step.flow.id, step.flow.version); - const flow = flowsForVersion?.[0]; - return { - ...step, - type: flow ? flow.collection : 'step', - flow, - }; -}; - -const getMessageNode = (step: any, messages: CollectionEntry<'events' | 'commands' | 'queries'>[]) => { - const messagesForVersion = getItemsFromCollectionByIdAndSemverOrLatest(messages, step.message.id, step.message.version); - const message = messagesForVersion[0]; - return { - ...step, - type: message ? message.collection : 'step', - message, - }; -}; - -export const getNodesAndEdges = async ({ id, defaultFlow, version, mode = 'simple', renderAllEdges = false }: Props) => { - const graph = defaultFlow || createDagreGraph({ ranksep: 360, nodesep: 200 }); - const nodes = [] as any, - edges = [] as any; - - const flows = await getCollection('flows'); - const flow = flows.find((flow) => flow.data.id === id && flow.data.version === version); - - // Nothing found... - if (!flow) { - return { - nodes: [], - edges: [], - }; - } - - const events = await getCollection('events'); - const commands = await getCollection('commands'); - const queries = await getCollection('queries'); - const services = await getCollection('services'); - - const messages = [...events, ...commands, ...queries]; - - const steps = flow?.data.steps || []; - - // Hydrate the steps with information they may need. - const hydratedSteps = steps.map((step: any) => { - if (step.service) return getServiceNode(step, services); - if (step.flow) return getFlowNode(step, flows); - if (step.message) return getMessageNode(step, messages); - if (step.actor) return { ...step, type: 'actor', actor: step.actor }; - if (step.custom) return { ...step, type: 'custom', custom: step.custom }; - if (step.externalSystem) return { ...step, type: 'externalSystem', externalSystem: step.externalSystem }; - return { ...step, type: 'step' }; - }); - - // Create nodes - hydratedSteps.forEach((step: any, index: number) => { - const node = { - id: `step-${step.id}`, - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode, - step: { ...step, ...step.data }, - showTarget: true, - showSource: true, - }, - position: { x: 250, y: index * 150 }, - type: step.type, - } as NodeType; - - if (step.service) node.data.service = { ...step.service, ...step.service.data }; - if (step.flow) node.data.flow = { ...step.flow, ...step.flow.data }; - if (step.message) node.data.message = { ...step.message, ...step.message.data }; - if (step.actor) node.data.actor = { ...step.actor, ...step.actor.data }; - if (step.externalSystem) node.data.externalSystem = { ...step.externalSystem, ...step.externalSystem.data }; - if (step.custom) node.data.custom = { ...step.custom, ...step.custom.data }; - nodes.push(node); - }); - - // Create Edges - hydratedSteps.forEach((step: any, index: number) => { - let paths = step.next_steps || []; - - if (step.next_step) { - // If its a string or number - if (!step.next_step?.id) { - paths = [{ id: step.next_step }]; - } else { - paths = [step.next_step]; - } - } - - paths = paths.map((path: any) => { - if (typeof path === 'string') { - return { id: path }; - } - return path; - }); - - paths.forEach((path: any) => { - edges.push({ - id: `step-${step.id}-step-${path.id}`, - source: `step-${step.id}`, - target: `step-${path.id}`, - type: 'flow-edge', - label: path.label, - animated: true, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 20, - height: 20, - color: '#666', - }, - style: { - strokeWidth: 2, - stroke: '#ccc', - }, - }); - }); - }); - - nodes.forEach((node: any) => { - graph.setNode(node.id, { width: 150, height: 100 }); - }); - - edges.forEach((edge: any) => { - graph.setEdge(edge.source, edge.target); - }); - - // Render the diagram in memory getting hte X and Y - dagre.layout(graph); - - return { - nodes: calculatedNodes(graph, nodes), - edges: edges, - }; -}; diff --git a/eventcatalog/src/utils/node-graphs/message-node-graph.ts b/eventcatalog/src/utils/node-graphs/message-node-graph.ts deleted file mode 100644 index cff002722..000000000 --- a/eventcatalog/src/utils/node-graphs/message-node-graph.ts +++ /dev/null @@ -1,1009 +0,0 @@ -// import { getColor } from '@utils/colors'; -import { getEvents } from '@utils/events'; -import type { CollectionEntry } from 'astro:content'; -import dagre from 'dagre'; -import { - calculatedNodes, - createDagreGraph, - createEdge, - generatedIdForEdge, - generateIdForNode, - getColorFromString, - getEdgeLabelForMessageAsSource, - getEdgeLabelForServiceAsTarget, -} from './utils/utils'; -import { MarkerType, type Node, type Edge } from '@xyflow/react'; -import { - findMatchingNodes, - getItemsFromCollectionByIdAndSemverOrLatest, - getLatestVersionInCollectionById, -} from '@utils/collections/util'; -import type { CollectionMessageTypes } from '@types'; -import { getCommands } from '@utils/commands'; -import { getQueries } from '@utils/queries'; -import { createNode } from './utils/utils'; -import { getConsumersOfMessage, getProducersOfMessage } from '@utils/collections/services'; -import { getNodesAndEdgesForChannelChain } from './channel-node-graph'; -import { getChannelChain, isChannelsConnected } from '@utils/channels'; -import { getChannels } from '@utils/channels'; - -type DagreGraph = any; - -interface Props { - id: string; - version: string; - defaultFlow?: DagreGraph; - mode?: 'simple' | 'full'; - channelRenderMode?: 'flat' | 'single'; - collection?: CollectionEntry[]; - channels?: CollectionEntry<'channels'>[]; -} - -const getNodesAndEdges = async ({ - id, - version, - defaultFlow, - mode = 'simple', - channelRenderMode = 'flat', - collection = [], - channels = [], -}: Props) => { - const flow = defaultFlow || createDagreGraph({ ranksep: 300, nodesep: 50 }); - const nodes = [] as any, - edges = [] as any; - - const message = collection.find((message) => { - return message.data.id === id && message.data.version === version; - }); - - // Nothing found... - if (!message) { - return { - nodes: [], - edges: [], - }; - } - - // We always render the message itself - nodes.push({ - id: generateIdForNode(message), - sourcePosition: 'right', - targetPosition: 'left', - data: { - mode, - message: { - ...message.data, - }, - }, - position: { x: 0, y: 0 }, - type: message.collection, - }); - - const producers = (message.data.producers as CollectionEntry<'services'>[]) || []; - const consumers = (message.data.consumers as CollectionEntry<'services'>[]) || []; - - // Track nodes that are both sent and received - const bothSentAndReceived = findMatchingNodes(producers, consumers); - - for (const producer of producers) { - // Create the producer node - nodes.push({ - id: generateIdForNode(producer), - type: producer?.collection, - sourcePosition: 'right', - targetPosition: 'left', - data: { mode, service: { ...producer.data } }, - position: { x: 250, y: 0 }, - }); - - // Is the producer sending this message to a channel? - const producerConfigurationForMessage = producer.data.sends?.find((send) => send.id === message.data.id); - const producerChannelConfiguration = producerConfigurationForMessage?.to ?? []; - - const producerHasChannels = producerChannelConfiguration?.length > 0; - - const rootSourceAndTarget = { - source: { id: generateIdForNode(producer), collection: producer.collection }, - target: { id: generateIdForNode(message), collection: message.collection }, - }; - - // If the producer does not have any channels defined, then we just connect the producer to the event directly - if (!producerHasChannels) { - edges.push({ - id: generatedIdForEdge(producer, message), - source: generateIdForNode(producer), - target: generateIdForNode(message), - label: getEdgeLabelForServiceAsTarget(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - animated: false, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 40, - height: 40, - }, - }); - continue; - } - - // If the producer has channels defined, we need to render them - for (const producerChannel of producerChannelConfiguration) { - const channel = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - producerChannel.id, - producerChannel.version - )[0] as CollectionEntry<'channels'>; - - // If we cannot find the channel in EventCatalog, we just connect the producer to the event directly - if (!channel) { - edges.push( - createEdge({ - id: generatedIdForEdge(producer, message), - source: generateIdForNode(producer), - target: generateIdForNode(message), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - continue; - } - - // We render the channel node - nodes.push( - createNode({ - id: generateIdForNode(channel), - type: channel.collection, - data: { mode, channel: { ...channel.data } }, - position: { x: 0, y: 0 }, - }) - ); - - // Connect the producer to the message - edges.push( - createEdge({ - id: generatedIdForEdge(producer, message), - source: generateIdForNode(producer), - target: generateIdForNode(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - label: getEdgeLabelForServiceAsTarget(message), - }) - ); - - // Connect the message to the channel - edges.push( - createEdge({ - id: generatedIdForEdge(message, channel), - source: generateIdForNode(message), - target: generateIdForNode(channel), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - label: 'routes to', - }) - ); - } - } - - // The messages the service sends - for (const consumer of consumers) { - // Render the consumer node - nodes.push({ - id: generateIdForNode(consumer), - sourcePosition: 'right', - targetPosition: 'left', - data: { title: consumer?.data.id, mode, service: { ...consumer.data } }, - position: { x: 0, y: 0 }, - type: consumer?.collection, - }); - - // Is the consumer receiving this message from a channel? - const consumerConfigurationForMessage = consumer.data.receives?.find((receive) => receive.id === message.data.id); - const consumerChannelConfiguration = consumerConfigurationForMessage?.from ?? []; - - const consumerHasChannels = consumerChannelConfiguration.length > 0; - - const rootSourceAndTarget = { - source: { id: generateIdForNode(message), collection: message.collection }, - target: { id: generateIdForNode(consumer), collection: consumer.collection }, - }; - - // If the consumer does not have any channels defined, connect the consumer to the event directly - if (!consumerHasChannels) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, consumer), - source: generateIdForNode(message), - target: generateIdForNode(consumer), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - } - - // If the consumer has channels defined, we try and render them - for (const consumerChannel of consumerChannelConfiguration) { - const channel = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - consumerChannel.id, - consumerChannel.version - )[0] as CollectionEntry<'channels'>; - - // If we cannot find the channel in EventCatalog, we connect the message directly to the consumer - if (!channel) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, consumer), - source: generateIdForNode(message), - target: generateIdForNode(consumer), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - continue; - } - - // Can any of the consumer channels be linked to any of the producer channels? - const producerChannels = producers - .map((producer) => producer.data.sends?.find((send) => send.id === message.data.id)?.to ?? []) - .flat(); - const consumerChannels = consumer.data.receives?.find((receive) => receive.id === message.data.id)?.from ?? []; - - for (const producerChannel of producerChannels) { - const producerChannelValue = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - producerChannel.id, - producerChannel.version - )[0] as CollectionEntry<'channels'>; - - for (const consumerChannel of consumerChannels) { - const consumerChannelValue = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - consumerChannel.id, - consumerChannel.version - )[0] as CollectionEntry<'channels'>; - const channelChainToRender = getChannelChain(producerChannelValue, consumerChannelValue, channels); - - // If there is a chain between them we need to render them al - if (channelChainToRender.length > 0) { - const { nodes: channelNodes, edges: channelEdges } = getNodesAndEdgesForChannelChain({ - source: message, - target: consumer, - channelChain: channelChainToRender, - mode, - }); - - nodes.push(...channelNodes); - edges.push(...channelEdges); - } else { - // There is no chain found, we need to render the channel between message and the consumers - nodes.push( - createNode({ - id: generateIdForNode(channel), - type: channel.collection, - data: { mode, channel: { ...channel.data } }, - position: { x: 0, y: 0 }, - }) - ); - edges.push( - createEdge({ - id: generatedIdForEdge(message, channel), - source: generateIdForNode(message), - target: generateIdForNode(channel), - label: 'routes to', - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - edges.push( - createEdge({ - id: generatedIdForEdge(channel, consumer), - source: generateIdForNode(channel), - target: generateIdForNode(consumer), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - } - } - } - - // If producer does not have a any channels defined, we need to connect the message to the consumer directly - if (producerChannels.length === 0 && channel) { - // Create the channel node - nodes.push( - createNode({ - id: generateIdForNode(channel), - type: channel.collection, - data: { mode, channel: { ...channel.data } }, - position: { x: 0, y: 0 }, - }) - ); - - // Connect the message to the channel - edges.push( - createEdge({ - id: generatedIdForEdge(message, channel), - source: generateIdForNode(message), - target: generateIdForNode(channel), - label: 'routes to', - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - - // Connect the channel to the consumer - edges.push( - createEdge({ - id: generatedIdForEdge(channel, consumer), - source: generateIdForNode(channel), - target: generateIdForNode(consumer), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - } - } - } - - // Handle messages that are both sent and received - bothSentAndReceived.forEach((_message) => { - if (message) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, _message) + '-both', - source: generateIdForNode(message), - target: generateIdForNode(_message), - label: 'publishes and subscribes', - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget: { source: message, target: _message } }, - }) - ); - } - }); - - nodes.forEach((node: any) => { - flow.setNode(node.id, { width: 150, height: 100 }); - }); - - edges.forEach((edge: any) => { - flow.setEdge(edge.source, edge.target); - }); - - // Render the diagram in memory getting hte X and Y - dagre.layout(flow); - - return { - nodes: calculatedNodes(flow, nodes), - edges, - }; -}; - -export const getNodesAndEdgesForQueries = async ({ - id, - version, - defaultFlow, - mode = 'simple', - channelRenderMode = 'flat', -}: Props) => { - const queries = await getQueries(); - const channels = await getChannels(); - return getNodesAndEdges({ id, version, defaultFlow, mode, channelRenderMode, collection: queries, channels }); -}; - -export const getNodesAndEdgesForCommands = async ({ - id, - version, - defaultFlow, - mode = 'simple', - channelRenderMode = 'flat', -}: Props) => { - const commands = await getCommands(); - const channels = await getChannels(); - return getNodesAndEdges({ id, version, defaultFlow, mode, channelRenderMode, collection: commands, channels }); -}; - -export const getNodesAndEdgesForEvents = async ({ - id, - version, - defaultFlow, - mode = 'simple', - channelRenderMode = 'flat', -}: Props) => { - const events = await getEvents(); - const channels = await getChannels(); - return getNodesAndEdges({ id, version, defaultFlow, mode, channelRenderMode, collection: events, channels }); -}; - -export const getNodesAndEdgesForConsumedMessage = ({ - message, - targetChannels = [], - services, - channels, - currentNodes = [], - target, - mode = 'simple', -}: { - message: CollectionEntry; - targetChannels?: { id: string; version: string }[]; - services: CollectionEntry<'services'>[]; - channels: CollectionEntry<'channels'>[]; - currentNodes: Node[]; - target: CollectionEntry<'services'>; - mode?: 'simple' | 'full'; -}) => { - let nodes = [] as Node[], - edges = [] as any; - - const messageId = generateIdForNode(message); - - const rootSourceAndTarget = { - source: { id: generateIdForNode(message), collection: message.collection }, - target: { id: generateIdForNode(target), collection: target.collection }, - }; - - // Render the message node - nodes.push( - createNode({ - id: messageId, - type: message.collection, - data: { mode, message: { ...message.data } }, - position: { x: 0, y: 0 }, - }) - ); - - // Render the target node - nodes.push( - createNode({ - id: generateIdForNode(target), - type: target.collection, - data: { mode, service: { ...target.data } }, - position: { x: 0, y: 0 }, - }) - ); - - const targetMessageConfiguration = target.data.receives?.find((receive) => receive.id === message.data.id); - const channelsFromMessageToTarget = targetMessageConfiguration?.from ?? []; - const hydratedChannelsFromMessageToTarget = channelsFromMessageToTarget - .map((channel) => getItemsFromCollectionByIdAndSemverOrLatest(channels, channel.id, channel.version)[0]) - .filter((channel) => channel !== undefined); - - // Now we get the producers of the message and create nodes and edges for them - const producers = getProducersOfMessage(services, message); - - const hasProducers = producers.length > 0; - const targetHasDefinedChannels = targetChannels.length > 0; - - const isMessageEvent = message.collection === 'events'; - - // Warning edge if no producers or target channels are defined - if (!hasProducers && !targetHasDefinedChannels) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, target) + '-warning', - source: messageId, - target: generateIdForNode(target), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - } - - // If the target defined channels they consume the message from, we need to create the channel nodes and edges - if (targetHasDefinedChannels) { - for (const targetChannel of targetChannels) { - const channel = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - targetChannel.id, - targetChannel.version - )[0] as CollectionEntry<'channels'>; - - if (!channel) { - // No channe found, we just connect the message to the target directly - edges.push( - createEdge({ - id: generatedIdForEdge(message, target), - source: messageId, - target: generateIdForNode(target), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - continue; - } - - const channelId = generateIdForNode(channel); - - // Create the channel node - nodes.push( - createNode({ - id: channelId, - type: channel.collection, - data: { mode, channel: { ...channel.data, ...channel, id: channel.data.id } }, - position: { x: 0, y: 0 }, - }) - ); - - // Connect the channel to the target - edges.push( - createEdge({ - id: generatedIdForEdge(channel, target), - source: channelId, - target: generateIdForNode(target), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - - // If we dont have any producers, we will connect the message to the channel directly - if (producers.length === 0) { - const isEvent = message.collection === 'events'; - - edges.push( - createEdge({ - id: generatedIdForEdge(message, channel), - source: messageId, - target: channelId, - label: 'routes to', - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - } - } - } - - // Process the producers for the message - for (const producer of producers) { - const producerId = generateIdForNode(producer); - - // Create the producer node - nodes.push( - createNode({ - id: producerId, - type: producer.collection, - data: { mode, service: { ...producer.data } }, - position: { x: 0, y: 0 }, - }) - ); - - // The message is always connected directly to the producer - edges.push( - createEdge({ - id: generatedIdForEdge(producer, message), - source: producerId, - target: messageId, - label: getEdgeLabelForServiceAsTarget(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - - // Check if the producer is sending the message to a channel - const producerConfigurationForMessage = producer.data.sends?.find((send) => send.id === message.data.id); - const producerChannelConfiguration = producerConfigurationForMessage?.to ?? []; - - const producerHasChannels = producerChannelConfiguration.length > 0; - const targetHasChannels = hydratedChannelsFromMessageToTarget.length > 0; - - // If the producer or target (consumer) has no channels defined, we just connect the message to the consumer directly - // of the target has no channels defined, we just connect the message to the target directly - if ((!producerHasChannels && !targetHasChannels) || !targetHasChannels) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, target), - source: messageId, - target: generateIdForNode(target), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - continue; - } - - // If the target has channels but the producer does not - // We then connect the message to the channels directly - if (targetHasChannels && !producerHasChannels) { - for (const targetChannel of hydratedChannelsFromMessageToTarget) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, targetChannel), - source: messageId, - target: generateIdForNode(targetChannel), - label: 'routes to', - data: { - customColor: getColorFromString(message.data.id), - rootSourceAndTarget: { - source: { id: generateIdForNode(message), collection: message.collection }, - target: { id: generateIdForNode(target), collection: target.collection }, - }, - }, - }) - ); - } - continue; - } - - // Process each producer channel configuration - for (const producerChannel of producerChannelConfiguration) { - const channel = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - producerChannel.id, - producerChannel.version - )[0] as CollectionEntry<'channels'>; - - // If we cannot find the channel in EventCatalog, we just connect the message to the target directly - if (!channel) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, target), - source: messageId, - target: generateIdForNode(target), - label: getEdgeLabelForMessageAsSource(message), - data: { - customColor: getColorFromString(message.data.id), - rootSourceAndTarget: { - source: { id: generateIdForNode(message), collection: message.collection }, - target: { id: generateIdForNode(target), collection: target.collection }, - }, - }, - }) - ); - continue; - } - - // Does the producer have any channels defined? If not, we just connect the message to the target directly - if (!producerHasChannels) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, target), - source: messageId, - target: generateIdForNode(target), - label: getEdgeLabelForMessageAsSource(message), - data: { - customColor: getColorFromString(message.data.id), - rootSourceAndTarget: { - source: { id: generateIdForNode(message), collection: message.collection }, - target: { id: generateIdForNode(target), collection: target.collection }, - }, - }, - }) - ); - continue; - } - - // The producer does have channels defined, we need to try and work out the path the message takes to the target - for (const targetChannel of hydratedChannelsFromMessageToTarget) { - const channelChainToRender = getChannelChain(channel, targetChannel, channels); - if (channelChainToRender.length > 0) { - const { nodes: channelNodes, edges: channelEdges } = getNodesAndEdgesForChannelChain({ - source: message, - target: target, - channelChain: channelChainToRender, - mode, - }); - - nodes.push(...channelNodes); - edges.push(...channelEdges); - - break; - } else { - // No chain found create the channel, and connect the message to the target channel directly - nodes.push( - createNode({ - id: generateIdForNode(targetChannel), - type: targetChannel.collection, - data: { mode, channel: { ...targetChannel.data, ...targetChannel } }, - position: { x: 0, y: 0 }, - }) - ); - edges.push( - createEdge({ - id: generatedIdForEdge(message, targetChannel), - source: messageId, - target: generateIdForNode(targetChannel), - label: 'routes to', - data: { - rootSourceAndTarget: { - source: { id: generateIdForNode(message), collection: message.collection }, - target: { id: generateIdForNode(targetChannel), collection: targetChannel.collection }, - }, - }, - }) - ); - } - } - } - } - - // Remove any nodes that are already in the current nodes (already on the UI) - nodes = nodes.filter((node) => !currentNodes.find((n) => n.id === node.id)); - - // Make sure all nodes are unique - const uniqueNodes = nodes.filter((node, index, self) => index === self.findIndex((t) => t.id === node.id)); - - const uniqueEdges = edges.filter( - (edge: any, index: number, self: any[]) => index === self.findIndex((t: any) => t.id === edge.id) - ); - - return { nodes: uniqueNodes, edges: uniqueEdges }; -}; - -export const getNodesAndEdgesForProducedMessage = ({ - message, - sourceChannels, - services, - channels, - currentNodes = [], - currentEdges = [], - source, - mode = 'simple', -}: { - message: CollectionEntry; - sourceChannels?: { id: string; version: string }[]; - services: CollectionEntry<'services'>[]; - channels: CollectionEntry<'channels'>[]; - currentNodes: Node[]; - currentEdges: Edge[]; - source: CollectionEntry<'services'>; - mode?: 'simple' | 'full'; -}) => { - let nodes = [] as Node[], - edges = [] as any; - - const messageId = generateIdForNode(message); - - const rootSourceAndTarget = { - source: { id: generateIdForNode(source), collection: source.collection }, - target: { id: generateIdForNode(message), collection: message.collection }, - }; - - // Render the message node - nodes.push( - createNode({ - id: messageId, - type: message.collection, - data: { mode, message: { ...message.data } }, - position: { x: 0, y: 0 }, - }) - ); - - // Render the producer node - nodes.push( - createNode({ - id: generateIdForNode(source), - type: source.collection, - data: { mode, service: { ...source.data } }, - position: { x: 0, y: 0 }, - }) - ); - - // Render the edge from the producer to the message - edges.push( - createEdge({ - id: generatedIdForEdge(source, message), - source: generateIdForNode(source), - target: messageId, - label: getEdgeLabelForServiceAsTarget(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - - const sourceMessageConfiguration = source.data.sends?.find((send) => send.id === message.data.id); - const channelsFromSourceToMessage = sourceMessageConfiguration?.to ?? []; - - const hydratedChannelsFromSourceToMessage = channelsFromSourceToMessage - .map((channel) => getItemsFromCollectionByIdAndSemverOrLatest(channels, channel.id, channel.version)[0]) - .filter((channel) => channel !== undefined); - - // If the source defined channels they send the message to, we need to create the channel nodes and edges - if (sourceChannels && sourceChannels.length > 0) { - for (const sourceChannel of sourceChannels) { - const channel = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - sourceChannel.id, - sourceChannel.version - )[0] as CollectionEntry<'channels'>; - - if (!channel) { - // No channel found, we just connect the message to the source directly - edges.push( - createEdge({ - id: generatedIdForEdge(message, source), - source: messageId, - target: generateIdForNode(source), - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - continue; - } - - const channelId = generateIdForNode(channel); - - // Create the channel node - nodes.push( - createNode({ - id: channelId, - type: channel.collection, - data: { mode, channel: { ...channel.data, ...channel, mode, id: channel.data.id } }, - position: { x: 0, y: 0 }, - }) - ); - - // Connect the produced message to the channel - edges.push( - createEdge({ - id: generatedIdForEdge(message, channel), - source: messageId, - target: channelId, - label: 'routes to', - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - } - } - - // Now we get the producers of the message and create nodes and edges for them - const consumers = getConsumersOfMessage(services, message); - - // TODO: Make this a UI Switch in the future.... - const latestConsumers = consumers.filter( - (consumer) => getLatestVersionInCollectionById(services, consumer.data.id) === consumer.data.version - ); - - // Process the consumers for the message - for (const consumer of latestConsumers) { - const consumerId = generateIdForNode(consumer); - - // Create the consumer node - nodes.push( - createNode({ - id: consumerId, - type: consumer.collection, - data: { mode, service: { ...consumer.data } }, - position: { x: 0, y: 0 }, - }) - ); - - // Check if the consumer is consuming the message from a channel - const consumerConfigurationForMessage = consumer.data.receives?.find((receive) => receive.id === message.data.id); - const consumerChannelConfiguration = consumerConfigurationForMessage?.from ?? []; - - const consumerHasChannels = consumerChannelConfiguration.length > 0; - const producerHasChannels = hydratedChannelsFromSourceToMessage.length > 0; - - // If the consumer and producer have no channels defined, - // or the consumer has no channels defined, we just connect the message to the consumer directly - if ((!consumerHasChannels && !producerHasChannels) || !consumerHasChannels) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, consumer), - source: messageId, - target: consumerId, - label: getEdgeLabelForMessageAsSource(message), - data: { customColor: getColorFromString(message.data.id), rootSourceAndTarget }, - }) - ); - continue; - } - - // Process each consumer channel configuration - for (const consumerChannel of consumerChannelConfiguration) { - const channel = getItemsFromCollectionByIdAndSemverOrLatest( - channels, - consumerChannel.id, - consumerChannel.version - )[0] as CollectionEntry<'channels'>; - - const edgeProps = { customColor: getColorFromString(message.data.id), rootSourceAndTarget }; - - // If the channel cannot be found in EventCatalog, we just connect the message to the consumer directly - // as a fallback, rather than just an empty node floating around - if (!channel) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, consumer), - source: messageId, - target: consumerId, - label: 'consumes', - data: edgeProps, - }) - ); - continue; - } - - // We always add the consumer channel to be rendered - nodes.push( - createNode({ - id: generateIdForNode(channel), - type: channel.collection, - data: { mode, channel: { ...channel.data, ...channel } }, - position: { x: 0, y: 0 }, - }) - ); - - // If the producer does not have any channels defined, we connect the message to the consumers channel directly - if (!producerHasChannels) { - edges.push( - createEdge({ - id: generatedIdForEdge(message, channel), - source: messageId, - target: generateIdForNode(channel), - label: 'routes to', - data: edgeProps, - }) - ); - edges.push( - createEdge({ - id: generatedIdForEdge(channel, consumer), - source: generateIdForNode(channel), - target: generateIdForNode(consumer), - label: getEdgeLabelForMessageAsSource(message), - data: { - ...edgeProps, - rootSourceAndTarget: { - source: { id: generateIdForNode(message), collection: message.collection }, - target: { id: generateIdForNode(consumer), collection: consumer.collection }, - }, - }, - }) - ); - continue; - } - - // The producer has channels defined, we need to try and work out the path the message takes to the consumer - for (const sourceChannel of hydratedChannelsFromSourceToMessage) { - const channelChainToRender = getChannelChain(sourceChannel, channel, channels); - - if (channelChainToRender.length > 0) { - const { nodes: channelNodes, edges: channelEdges } = getNodesAndEdgesForChannelChain({ - source: message, - target: consumer, - channelChain: channelChainToRender, - mode, - }); - - nodes.push(...channelNodes); - edges.push(...channelEdges); - } else { - // No chain found, we need to connect to the message to the channel - // And the channel to the consumer - edges.push( - createEdge({ - id: generatedIdForEdge(message, channel), - source: messageId, - target: generateIdForNode(channel), - label: 'routes to', - data: { - ...edgeProps, - rootSourceAndTarget: { - source: { id: generateIdForNode(message), collection: message.collection }, - target: { id: generateIdForNode(consumer), collection: consumer.collection }, - }, - }, - }) - ); - edges.push( - createEdge({ - id: generatedIdForEdge(channel, consumer), - source: generateIdForNode(channel), - target: generateIdForNode(consumer), - label: `${getEdgeLabelForMessageAsSource(message, true)} \n ${message.data.name}`, - data: { - ...edgeProps, - rootSourceAndTarget: { - source: { id: generateIdForNode(message), collection: message.collection }, - target: { id: generateIdForNode(consumer), collection: consumer.collection }, - }, - }, - }) - ); - } - } - } - } - - // Remove any nodes that are already in the current nodes (already on the UI) - nodes = nodes.filter((node) => !currentNodes.find((n) => n.id === node.id)); - - // Make sure all nodes are unique - const uniqueNodes = nodes.filter((node, index, self) => index === self.findIndex((t) => t.id === node.id)); - - const uniqueEdges = edges.filter( - (edge: any, index: number, self: any[]) => index === self.findIndex((t: any) => t.id === edge.id) - ); - - return { nodes: uniqueNodes, edges: uniqueEdges }; -}; diff --git a/eventcatalog/src/utils/node-graphs/services-node-graph.ts b/eventcatalog/src/utils/node-graphs/services-node-graph.ts deleted file mode 100644 index a01ce1d1a..000000000 --- a/eventcatalog/src/utils/node-graphs/services-node-graph.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { getCollection, type CollectionEntry } from 'astro:content'; -import dagre from 'dagre'; -import { - createDagreGraph, - generateIdForNode, - generatedIdForEdge, - calculatedNodes, - createEdge, -} from '@utils/node-graphs/utils/utils'; - -import { findMatchingNodes, getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util'; -import { MarkerType } from '@xyflow/react'; -import type { CollectionMessageTypes } from '@types'; -import { getNodesAndEdgesForConsumedMessage, getNodesAndEdgesForProducedMessage } from './message-node-graph'; - -type DagreGraph = any; - -interface Props { - id: string; - version: string; - defaultFlow?: DagreGraph; - mode?: 'simple' | 'full'; - renderAllEdges?: boolean; - channelRenderMode?: 'single' | 'flat'; - renderMessages?: boolean; -} - -const getSendsMessageByMessageType = (messageType: string) => { - switch (messageType) { - case 'events': - return 'publishes event'; - case 'commands': - return 'invokes command'; - case 'queries': - return 'requests'; - default: - return 'invokes message'; - } -}; - -const getReceivesMessageByMessageType = (messageType: string) => { - switch (messageType) { - case 'events': - return 'receives event'; - case 'commands': - case 'queries': - return 'accepts'; - default: - return 'accepts message'; - } -}; - -export const getNodesAndEdges = async ({ - id, - defaultFlow, - version, - mode = 'simple', - renderAllEdges = false, - channelRenderMode = 'flat', - renderMessages = true, -}: Props) => { - const flow = defaultFlow || createDagreGraph({ ranksep: 300, nodesep: 50 }); - let nodes = [] as any, - edges = [] as any; - - const services = await getCollection('services'); - - const service = services.find((service) => service.data.id === id && service.data.version === version); - - // Nothing found... - if (!service) { - return { - nodes: [], - edges: [], - }; - } - - const receivesRaw = service?.data.receives || []; - const sendsRaw = service?.data.sends || []; - const writesToRaw = service?.data.writesTo || []; - const readsFromRaw = service?.data.readsFrom || []; - - const events = await getCollection('events'); - const commands = await getCollection('commands'); - const queries = await getCollection('queries'); - const channels = await getCollection('channels'); - const containers = await getCollection('containers'); - - const messages = [...events, ...commands, ...queries]; - - const receivesHydrated = receivesRaw - .map((message) => getItemsFromCollectionByIdAndSemverOrLatest(messages, message.id, message.version)) - .flat() - .filter((e) => e !== undefined); - - const sendsHydrated = sendsRaw - .map((message) => getItemsFromCollectionByIdAndSemverOrLatest(messages, message.id, message.version)) - .flat() - .filter((e) => e !== undefined); - - const writesToHydrated = writesToRaw - .map((container) => getItemsFromCollectionByIdAndSemverOrLatest(containers, container.id, container.version)) - .flat() - .filter((e) => e !== undefined); - - const readsFromHydrated = readsFromRaw - .map((container) => getItemsFromCollectionByIdAndSemverOrLatest(containers, container.id, container.version)) - .flat() - .filter((e) => e !== undefined); - - const receives = (receivesHydrated as CollectionEntry[]) || []; - const sends = (sendsHydrated as CollectionEntry[]) || []; - const writesTo = (writesToHydrated as CollectionEntry<'containers'>[]) || []; - const readsFrom = (readsFromHydrated as CollectionEntry<'containers'>[]) || []; - - // Track messages that are both sent and received - const bothSentAndReceived = findMatchingNodes(receives, sends); - const bothReadsAndWrites = findMatchingNodes(readsFrom, writesTo); - - if (renderMessages) { - // All the messages the service receives - receives.forEach((receive) => { - const targetChannels = receivesRaw.find((receiveRaw) => receiveRaw.id === receive.data.id)?.from; - - const { nodes: consumedMessageNodes, edges: consumedMessageEdges } = getNodesAndEdgesForConsumedMessage({ - message: receive, - targetChannels: targetChannels, - services, - currentNodes: nodes, - target: service, - mode, - channels, - }); - - nodes.push(...consumedMessageNodes); - edges.push(...consumedMessageEdges); - }); - } - - // The service itself - nodes.push({ - id: generateIdForNode(service), - sourcePosition: 'right', - targetPosition: 'left', - // data: { mode, service: { ...service, ...service.data } }, - data: { mode, service: { ...service.data } }, - type: service.collection, - }); - - // Any containers the service writes to - writesTo.forEach((writeTo) => { - nodes.push({ - id: generateIdForNode(writeTo), - sourcePosition: 'right', - targetPosition: 'left', - data: { mode, data: { ...writeTo.data } }, - type: 'data', - }); - - // If its not in the reads/and writes to, we need to add the edge - if (!bothReadsAndWrites.includes(writeTo)) { - edges.push( - createEdge({ - id: generatedIdForEdge(service, writeTo), - source: generateIdForNode(service), - target: generateIdForNode(writeTo), - label: `writes to \n (${writeTo.data?.technology})`, - type: 'multiline', - markerEnd: { - type: MarkerType.ArrowClosed, - color: '#666', - width: 40, - height: 40, - }, - }) - ); - } - }); - - // Any containers the service reads from - readsFrom.forEach((readFrom) => { - nodes.push({ - id: generateIdForNode(readFrom), - sourcePosition: 'right', - targetPosition: 'left', - data: { mode, data: { ...readFrom.data } }, - type: 'data', - }); - - // If its not in the reads/and writes to, we need to add the edge - if (!bothReadsAndWrites.includes(readFrom)) { - edges.push( - createEdge({ - id: generatedIdForEdge(service, readFrom), - source: generateIdForNode(readFrom), - target: generateIdForNode(service), - label: `reads from \n (${readFrom.data?.technology})`, - type: 'multiline', - markerStart: { - type: MarkerType.ArrowClosed, - color: '#666', - width: 40, - height: 40, - }, - markerEnd: undefined, - }) - ); - } - }); - - if (renderMessages) { - sends.forEach((send) => { - const sourceChannels = sendsRaw.find((sendRaw) => sendRaw.id === send.data.id)?.to; - - const { nodes: producedMessageNodes, edges: producedMessageEdges } = getNodesAndEdgesForProducedMessage({ - message: send, - sourceChannels: sourceChannels, - services, - currentNodes: nodes, - source: service, - currentEdges: edges, - mode, - channels, - }); - - nodes.push(...producedMessageNodes); - edges.push(...producedMessageEdges); - }); - - // Handle messages that are both sent and received - bothSentAndReceived.forEach((message) => { - if (message) { - edges.push({ - id: generatedIdForEdge(service, message) + '-both', - source: generateIdForNode(service), - target: generateIdForNode(message), - label: `${getSendsMessageByMessageType(message?.collection)} & ${getReceivesMessageByMessageType(message?.collection)}`, - animated: false, - data: { message: { ...message.data } }, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 40, - height: 40, - }, - style: { - strokeWidth: 1, - }, - }); - } - }); - } - - bothReadsAndWrites.forEach((container) => { - edges.push({ - id: generatedIdForEdge(service, container) + '-both', - source: generateIdForNode(service), - target: generateIdForNode(container), - type: 'multiline', - // @ts-ignore - label: `reads from \n and writes to \n (${container.data.technology})`, - markerStart: { - type: MarkerType.ArrowClosed, - color: '#666', - width: 40, - height: 40, - }, - markerEnd: { - type: MarkerType.ArrowClosed, - color: '#666', - width: 40, - height: 40, - }, - }); - }); - - nodes.forEach((node: any) => { - flow.setNode(node.id, { width: 150, height: 100 }); - }); - - edges.forEach((edge: any) => { - flow.setEdge(edge.source, edge.target); - }); - - // Render the diagram in memory getting the X and Y - dagre.layout(flow); - - // Find any duplicated edges, and merge them into one edge - const uniqueEdges = edges.reduce((acc: any[], edge: any) => { - const existingEdge = acc.find((e: any) => e.id === edge.id); - if (existingEdge) { - existingEdge.label = `${existingEdge.label} & ${edge.label}`; - // Add the custom colors to the existing edge which can be an array of strings - const value = Array.isArray(edge.data.customColor) ? edge.data.customColor : [edge.data.customColor]; - const existingValue = Array.isArray(existingEdge.data.customColor) - ? existingEdge.data.customColor - : [existingEdge.data.customColor]; - existingEdge.data.customColor = [...value, ...existingValue]; - } else { - acc.push(edge); - } - return acc; - }, []); - - return { - nodes: calculatedNodes(flow, nodes), - edges: uniqueEdges, - }; -}; diff --git a/eventcatalog/src/utils/node-graphs/utils/utils.ts b/eventcatalog/src/utils/node-graphs/utils/utils.ts deleted file mode 100644 index ecde5b3a8..000000000 --- a/eventcatalog/src/utils/node-graphs/utils/utils.ts +++ /dev/null @@ -1,147 +0,0 @@ -// Can't use the CollectionEntry type from astro:content because a client component is using this util - -import { MarkerType, Position, type Edge, type Node } from '@xyflow/react'; -import dagre from 'dagre'; -import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util'; -interface BaseCollectionData { - id: string; - version: string; -} - -interface CollectionItem { - collection: string; - data: BaseCollectionData; -} - -interface MessageCollectionItem extends CollectionItem { - collection: 'commands' | 'events' | 'queries'; -} - -export const generateIdForNode = (node: CollectionItem) => { - return `${node.data.id}-${node.data.version}`; -}; -export const generateIdForNodes = (nodes: any) => { - return nodes.map((node: any) => `${node.data.id}-${node.data.version}`).join('-'); -}; -export const generatedIdForEdge = (source: CollectionItem, target: CollectionItem) => { - return `${source.data.id}-${source.data.version}-${target.data.id}-${target.data.version}`; -}; - -export const getColorFromString = (id: string) => { - // Takes the given id (string) and returns a custom hex color based on the id - // Create a hash from the string - let hash = 0; - for (let i = 0; i < id.length; i++) { - hash = id.charCodeAt(i) + ((hash << 5) - hash); - } - - // Convert the hash into a hex color - let color = '#'; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xff; - color += value.toString(16).padStart(2, '0'); - } - - return color; -}; - -export const getEdgeLabelForServiceAsTarget = (data: MessageCollectionItem) => { - const type = data.collection; - switch (type) { - case 'commands': - return 'invokes'; - case 'events': - return 'publishes \nevent'; - case 'queries': - return 'requests'; - default: - return 'sends to'; - } -}; -export const getEdgeLabelForMessageAsSource = (data: MessageCollectionItem, throughChannel = false) => { - const type = data.collection; - switch (type) { - case 'commands': - return 'accepts'; - case 'events': - return throughChannel ? 'subscribed to' : 'subscribed by'; - case 'queries': - return 'accepts'; - default: - return 'sends to'; - } -}; - -export const calculatedNodes = (flow: dagre.graphlib.Graph, nodes: Node[]) => { - return nodes.map((node: any) => { - const { x, y } = flow.node(node.id); - return { ...node, position: { x, y } }; - }); -}; - -// Creates a new dagre graph -export const createDagreGraph = ({ ranksep = 180, nodesep = 50, ...rest }: any) => { - const graph = new dagre.graphlib.Graph({ compound: true }); - graph.setGraph({ rankdir: 'LR', ranksep, nodesep, ...rest }); - graph.setDefaultEdgeLabel(() => ({})); - return graph; -}; - -export const createEdge = (edgeOptions: Edge) => { - return { - label: 'subscribed by', - animated: false, - // markerStart: { - // type: MarkerType.Arrow, - // width: 40, - // height: 40, - // }, - markerEnd: { - type: MarkerType.ArrowClosed, - width: 40, - height: 40, - }, - style: { - strokeWidth: 1, - }, - ...edgeOptions, - }; -}; - -export const createNode = (values: Node): Node => { - return { - sourcePosition: Position.Right, - targetPosition: Position.Left, - ...values, - }; -}; - -type DagreGraph = any; - -export const getNodesAndEdgesFromDagre = ({ - nodes, - edges, - defaultFlow, -}: { - nodes: Node[]; - edges: Edge[]; - defaultFlow?: DagreGraph; -}) => { - const flow = defaultFlow || createDagreGraph({ ranksep: 300, nodesep: 50 }); - - nodes.forEach((node: any) => { - flow.setNode(node.id, { width: 150, height: 100 }); - }); - - edges.forEach((edge: any) => { - flow.setEdge(edge.source, edge.target); - }); - - // Render the diagram in memory getting hte X and Y - dagre.layout(flow); - - return { - nodes: calculatedNodes(flow, nodes), - edges, - }; -}; diff --git a/eventcatalog/src/utils/page-loaders/page-data-loader.ts b/eventcatalog/src/utils/page-loaders/page-data-loader.ts deleted file mode 100644 index 31b1aff58..000000000 --- a/eventcatalog/src/utils/page-loaders/page-data-loader.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { CollectionTypes, PageTypes } from '@types'; -import { getChannels } from '@utils/channels'; -import { getDomains } from '@utils/collections/domains'; -import { getCommands, getEvents } from '@utils/messages'; -import { getQueries } from '@utils/queries'; -import { getServices } from '@utils/collections/services'; -import { getFlows } from '@utils/collections/flows'; -import { getEntities } from '@utils/entities'; -import { getContainers } from '@utils/collections/containers'; -import type { CollectionEntry } from 'astro:content'; - -export const pageDataLoader: Record Promise[]>> = { - events: getEvents, - commands: getCommands, - queries: getQueries, - services: getServices, - domains: getDomains, - channels: getChannels, - flows: getFlows, - entities: getEntities, - containers: getContainers, -}; diff --git a/eventcatalog/src/utils/queries.ts b/eventcatalog/src/utils/queries.ts deleted file mode 100644 index 6a8c62594..000000000 --- a/eventcatalog/src/utils/queries.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; -import utils from '@eventcatalog/sdk'; -import { getVersionForCollectionItem, satisfies } from './collections/util'; - -const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd(); - -type Query = CollectionEntry<'queries'> & { - catalog: { - path: string; - filePath: string; - type: string; - }; -}; - -interface Props { - getAllVersions?: boolean; - hydrateServices?: boolean; -} - -// Cache for build time -let cachedQueries: Record = { - allVersions: [], - currentVersions: [], -}; - -export const getQueries = async ({ getAllVersions = true, hydrateServices = true }: Props = {}): Promise => { - const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions'; - - if (cachedQueries[cacheKey].length > 0 && hydrateServices) { - return cachedQueries[cacheKey]; - } - - const queries = await getCollection('queries', (query) => { - return (getAllVersions || !query.filePath?.includes('versioned')) && query.data.hidden !== true; - }); - - const services = await getCollection('services'); - const allChannels = await getCollection('channels'); - - // @ts-ignore - cachedQueries[cacheKey] = await Promise.all( - queries.map(async (query) => { - const { latestVersion, versions } = getVersionForCollectionItem(query, queries); - - const producers = services - .filter((service) => - service.data.sends?.some((item) => { - if (item.id != query.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return query.data.version == latestVersion; - return satisfies(query.data.version, item.version); - }) - ) - .map((service) => { - if (!hydrateServices) return { id: service.data.id, version: service.data.version }; - return service; - }); - - const consumers = services - .filter((service) => - service.data.receives?.some((item) => { - if (item.id != query.data.id) return false; - if (item.version == 'latest' || item.version == undefined) return query.data.version == latestVersion; - return satisfies(query.data.version, item.version); - }) - ) - .map((service) => { - if (!hydrateServices) return { id: service.data.id, version: service.data.version }; - return service; - }); - - const messageChannels = query.data.channels || []; - const channelsForQuery = allChannels.filter((c) => messageChannels.some((channel) => c.data.id === channel.id)); - - const { getResourceFolderName } = utils(process.env.PROJECT_DIR ?? ''); - const folderName = await getResourceFolderName(process.env.PROJECT_DIR ?? '', query.data.id, query.data.version.toString()); - const queryFolderName = folderName ?? query.id.replace(`-${query.data.version}`, ''); - - return { - ...query, - data: { - ...query.data, - messageChannels: channelsForQuery, - producers, - consumers, - versions, - latestVersion, - }, - catalog: { - path: path.join(query.collection, query.id.replace('/index.mdx', '')), - absoluteFilePath: path.join(PROJECT_DIR, query.collection, query.id.replace('/index.mdx', '/index.md')), - astroContentFilePath: path.join(process.cwd(), 'src', 'content', query.collection, query.id), - filePath: path.join(process.cwd(), 'src', 'catalog-files', query.collection, query.id.replace('/index.mdx', '')), - publicPath: path.join('/generated', query.collection, queryFolderName), - type: 'event', - }, - }; - }) - ); - - // order them by the name of the query - cachedQueries[cacheKey].sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedQueries[cacheKey]; -}; diff --git a/eventcatalog/src/utils/teams.ts b/eventcatalog/src/utils/teams.ts deleted file mode 100644 index ce844bba3..000000000 --- a/eventcatalog/src/utils/teams.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; - -export type Team = CollectionEntry<'teams'>; - -// Cache for build time -let cachedTeams: Team[] = []; - -export const getTeams = async (): Promise => { - if (cachedTeams.length > 0) { - return cachedTeams; - } - - // Get services that are not versioned - const teams = await getCollection('teams', (team) => { - return team.data.hidden !== true; - }); - // What do they own? - const domains = await getCollection('domains'); - // What do they own? - const services = await getCollection('services'); - // What do they own? - const events = await getCollection('events'); - const commands = await getCollection('commands'); - const queries = await getCollection('queries'); - cachedTeams = teams.map((team) => { - const ownedDomains = domains.filter((domain) => { - return domain.data.owners?.find((owner) => owner.id === team.data.id); - }); - - const ownedServices = services.filter((service) => { - return service.data.owners?.find((owner) => owner.id === team.data.id); - }); - - const ownedEvents = events.filter((event) => { - return event.data.owners?.find((owner) => owner.id === team.data.id); - }); - - const ownedCommands = commands.filter((command) => { - return command.data.owners?.find((owner) => owner.id === team.data.id); - }); - - const ownedQueries = queries.filter((query) => { - return query.data.owners?.find((owner) => owner.id === team.data.id); - }); - - return { - ...team, - data: { - ...team.data, - ownedDomains, - ownedServices, - ownedCommands, - ownedQueries, - ownedEvents, - }, - catalog: { - path: path.join(team.collection, team.id.replace('/index.mdx', '')), - filePath: path.join(process.cwd(), 'src', 'catalog-files', team.collection, team.id.replace('/index.mdx', '')), - type: 'team', - }, - }; - }); - - // order them by the name of the team - cachedTeams.sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return cachedTeams; -}; diff --git a/eventcatalog/src/utils/url-builder.ts b/eventcatalog/src/utils/url-builder.ts deleted file mode 100644 index 7a52d2800..000000000 --- a/eventcatalog/src/utils/url-builder.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Don't import config here, as it breaks in the client, as path cannot be resolved. - -const cleanUrl = (url: string) => { - return url.replace(/\/+/g, '/'); -}; - -// Custom URL builder as Astro does not support this stuff out the box -export const buildUrl = (url: string, ignoreTrailingSlash = false, urlAlreadyIncludesBaseUrl = false) => { - // Should a trailingSlash be added to urls? - const trailingSlash = __EC_TRAILING_SLASH__; - - let newUrl = url; - - // If the base URL is not the root, we need to append it - if (import.meta.env.BASE_URL !== '/' && !urlAlreadyIncludesBaseUrl) { - newUrl = `${import.meta.env.BASE_URL}${url}`; - } - - // Should we add a trailing slash to the url? - if (trailingSlash && !ignoreTrailingSlash) { - if (url.endsWith('/')) return newUrl; - return cleanUrl(`${newUrl}/`); - } - - return cleanUrl(newUrl); -}; - -// Helper function to build URLs with query parameters -export const buildUrlWithParams = (baseUrl: string, params: Record) => { - // Filter out undefined values and empty strings - const validParams = Object.entries(params) - .filter(([_, value]) => value !== undefined && value !== '') - .reduce>((acc, [key, value]) => ({ ...acc, [key]: value as string }), {}); - - // If no valid params, just return the base URL - if (Object.keys(validParams).length === 0) { - return buildUrl(baseUrl); - } - - // Build query string with encoded values - const queryString = Object.entries(validParams) - .map(([key, value]) => `${key}=${value}`) - .join('&'); - - return `${buildUrl(baseUrl)}?${queryString}`; -}; - -export const buildEditUrlForResource = (editUrl: string, filePath: string) => { - // filepath may have ../ or ./ in it, so we need to remove it - const cleanFilePath = filePath.replace(/^\.\.?\//g, ''); - return `${editUrl}/${cleanFilePath}`; -}; - -// Takes a given url and returns the .mdx url -export const toMarkdownUrl = (url: string) => { - const trailingSlash = __EC_TRAILING_SLASH__; - - if (trailingSlash) { - const urlWithoutTrailingSlash = url.replace(/\/$/, ''); - return urlWithoutTrailingSlash + '.mdx/'; - } - - return url + '.mdx'; -}; diff --git a/eventcatalog/src/utils/users.ts b/eventcatalog/src/utils/users.ts deleted file mode 100644 index 49eaa783a..000000000 --- a/eventcatalog/src/utils/users.ts +++ /dev/null @@ -1,72 +0,0 @@ -import type { CollectionTypes } from '@types'; -import { getCollection } from 'astro:content'; -import type { CollectionEntry } from 'astro:content'; -import path from 'path'; - -export type User = CollectionEntry<'users'>; - -export const getUsers = async (): Promise => { - // Get services that are not versioned - const users = await getCollection('users', (user) => { - return user.data.hidden !== true; - }); - - // What do they own? - const domains = await getCollection('domains'); - const services = await getCollection('services'); - const events = await getCollection('events'); - const commands = await getCollection('commands'); - const queries = await getCollection('queries'); - - const teams = await getCollection('teams', (team) => { - return team.data.hidden !== true; - }); - - const mappedUsers = users.map((user) => { - const associatedTeams = teams.filter((team) => { - return team.data.members?.some((member) => member.id === user.data.id); - }); - - const ownedDomains = domains.filter((domain) => { - return domain.data.owners?.find((owner) => owner.id === user.data.id); - }); - - const isOwnedByUserOrAssociatedTeam = (item: CollectionEntry) => { - const associatedTeamsId: string[] = associatedTeams.map((team) => team.data.id); - return item.data.owners?.some((owner) => owner.id === user.data.id || associatedTeamsId.includes(owner.id)); - }; - - const ownedServices = services.filter(isOwnedByUserOrAssociatedTeam); - - const ownedEvents = events.filter(isOwnedByUserOrAssociatedTeam); - - const ownedCommands = commands.filter(isOwnedByUserOrAssociatedTeam); - - const ownedQueries = queries.filter(isOwnedByUserOrAssociatedTeam); - - return { - ...user, - data: { - ...user.data, - ownedDomains, - ownedServices, - ownedEvents, - ownedCommands, - ownedQueries, - associatedTeams, - }, - catalog: { - path: path.join(user.collection, user.id.replace('/index.mdx', '')), - filePath: path.join(process.cwd(), 'src', 'catalog-files', user.collection, user.id.replace('/index.mdx', '')), - type: 'user', - }, - }; - }); - - // order them by the name of the user - mappedUsers.sort((a, b) => { - return (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id); - }); - - return mappedUsers; -}; diff --git a/eventcatalog/tailwind.config.mjs b/eventcatalog/tailwind.config.mjs deleted file mode 100644 index b498e6254..000000000 --- a/eventcatalog/tailwind.config.mjs +++ /dev/null @@ -1,69 +0,0 @@ -import config from './eventcatalog.config.js'; -import typography from '@tailwindcss/typography'; - -const HEADER_HEIGHT = '4rem'; -const theme = config.theme || {}; - -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], - theme: { - extend: { - fontFamily: { - sans: ['Inter', 'sans-serif'], - }, - height: { - header: HEADER_HEIGHT, - content: `calc(100vh - ${HEADER_HEIGHT})`, - }, - spacing: { - header: HEADER_HEIGHT, - content: `calc(100vh - ${HEADER_HEIGHT})`, - }, - screens: { - xxl: '1990px', - }, - colors: { - primary: { - DEFAULT: '#a855f7', - }, - secondary: { - light: '#ff9980', - DEFAULT: '#ff6633', - dark: '#cc3300', - }, - ...theme, - }, - }, - }, - safelist: [ - { pattern: /border-.*-(200|400|500)/ }, - { pattern: /bg-.*-(100|200|400|500)/ }, - { pattern: /from-.*-(100|200|300|400|500|600|700)/ }, - { pattern: /to-.*-(100|200|300|400|500|600|700)/ }, - { pattern: /text-.*-(400|500|800)/ }, - 'border-blue-200', - 'border-green-300', - 'bg-blue-600', - 'bg-orange-600', - 'bg-red-50', - 'bg-yellow-50', - 'bg-pink-50', - 'bg-green-50', - 'bg-blue-50', - 'bg-indigo-50', - 'border-l-red-500', - 'border-l-yellow-500', - 'border-l-blue-500', - 'bg-yellow-100', - 'bg-pink-100', - 'bg-green-100', - 'bg-blue-100', - 'bg-indigo-100', - 'text-[5px]', - 'text-[9px]', - 'min-h-[100px]', - ], - - plugins: [typography], -}; diff --git a/eventcatalog/tsconfig.json b/eventcatalog/tsconfig.json deleted file mode 100644 index 602a1429d..000000000 --- a/eventcatalog/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "extends": "astro/tsconfigs/strict", - "compilerOptions": { - "strict": true, - "baseUrl": ".", - "paths": { - "@config": ["./eventcatalog.config.js"], - "@eventcatalog": ["src/utils/eventcatalog-config/catalog.ts"], - "@icons/*": ["src/icons/*"], - "@components/*": ["src/components/*"], - "@catalog/components/*": ["src/custom-defined-components/*"], - "@catalog/snippets/*": ["src/snippets/*"], - "@types": ["src/types/index.ts"], - "@utils/*": ["src/utils/*"], - "@layouts/*": ["src/layouts/*"], - "@enterprise/*": ["src/enterprise/*"], - "@ai/*": ["src/generated-ai/*"], - "auth:config": ["./auth.config.ts"] - }, - "jsx": "react-jsx", - "jsxImportSource": "react", - "types": ["vitest/globals"] - } -} diff --git a/examples/default/.env-example b/examples/default/.env-example index e69de29bb..4f37f2873 100644 --- a/examples/default/.env-example +++ b/examples/default/.env-example @@ -0,0 +1,12 @@ +# EventCatalog Scale (optional) — enables Scale plan features +# EVENTCATALOG_SCALE=true +# EVENTCATALOG_SCALE_LICENSE_KEY= + +# AI Chat (optional) +# OPENAI_API_KEY= +# ANTHROPIC_API_KEY= + +# Auth (optional) +# AUTH_SECRET= +# GITHUB_CLIENT_ID= +# GITHUB_CLIENT_SECRET= diff --git a/examples/default/.gitignore b/examples/default/.gitignore index 3739f8846..4250cb55b 100644 --- a/examples/default/.gitignore +++ b/examples/default/.gitignore @@ -1,4 +1,4 @@ generated-ai/ public/ai/ -public/pagefind/ \ No newline at end of file +public/pagefind/ diff --git a/examples/default/_data/visualizer-layouts/domains-systems-context/catalog/1.0.0.json b/examples/default/_data/visualizer-layouts/domains-systems-context/catalog/1.0.0.json new file mode 100644 index 000000000..9f7b5070d --- /dev/null +++ b/examples/default/_data/visualizer-layouts/domains-systems-context/catalog/1.0.0.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "savedAt": "2026-06-29T11:03:00.194Z", + "resourceKey": "domains-systems-context/catalog/1.0.0", + "positions": { + "product-catalog-system-1.0.0": { + "x": 654.4139077853363, + "y": 77.21012849584278 + }, + "search-system-1.0.0": { + "x": 1145, + "y": 335 + }, + "actor-merchandiser": { + "x": 90, + "y": 60 + }, + "actor-catalog-admin": { + "x": 90, + "y": 260 + }, + "actor-shopper": { + "x": 595, + "y": 360 + }, + "actor-storefront-team": { + "x": 595, + "y": 560 + } + } +} \ No newline at end of file diff --git a/examples/default/_data/visualizer-layouts/domains-systems-context/customer/1.0.0.json b/examples/default/_data/visualizer-layouts/domains-systems-context/customer/1.0.0.json new file mode 100644 index 000000000..e39d05811 --- /dev/null +++ b/examples/default/_data/visualizer-layouts/domains-systems-context/customer/1.0.0.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "savedAt": "2026-06-26T12:15:29.856Z", + "resourceKey": "domains-systems-context/customer/1.0.0", + "positions": { + "customer-management-system-1.0.0": { + "x": 652.3279365079366, + "y": 338.73238095238094 + }, + "identity-provider-1.0.0": { + "x": 745.6812698412698, + "y": 71.1025396825397 + }, + "actor-customer": { + "x": 90, + "y": 122.5 + }, + "actor-support-agent": { + "x": 90, + "y": 322.5 + } + } +} \ No newline at end of file diff --git a/examples/default/_data/visualizer-layouts/system-context-map/system-context-map/1.0.0.json b/examples/default/_data/visualizer-layouts/system-context-map/system-context-map/1.0.0.json new file mode 100644 index 000000000..1248cc73b --- /dev/null +++ b/examples/default/_data/visualizer-layouts/system-context-map/system-context-map/1.0.0.json @@ -0,0 +1,91 @@ +{ + "version": 1, + "savedAt": "2026-06-29T08:55:18.430Z", + "resourceKey": "system-context-map/system-context-map/1.0.0", + "positions": { + "carrier-1.0.0": { + "x": 1423.6053719008264, + "y": 1259.6570247933885 + }, + "shipping-system-1.0.0": { + "x": 1622.4194214876034, + "y": 1702.4814049586778 + }, + "cart-system-1.0.0": { + "x": 595, + "y": 60 + }, + "promotion-system-1.0.0": { + "x": 1145, + "y": 60 + }, + "actor-shopper": { + "x": 56.553719008264444, + "y": 178.41322314049586 + }, + "checkout-system-1.0.0": { + "x": 595, + "y": 260 + }, + "order-management-system-1.0.0": { + "x": 1145, + "y": 260 + }, + "customer-management-system-1.0.0": { + "x": 812.667132867133, + "y": 836.9146853146854 + }, + "identity-provider-1.0.0": { + "x": 1145, + "y": 1085 + }, + "actor-customer": { + "x": 90, + "y": 910 + }, + "actor-support-agent": { + "x": 90, + "y": 1110 + }, + "fraud-detection-1.0.0": { + "x": -664.9235537190083, + "y": 1282.7169421487602 + }, + "payment-processing-system-1.0.0": { + "x": -145.58264462809922, + "y": 1486.1818181818182 + }, + "inventory-system-1.0.0": { + "x": 366.3367768595041, + "y": 1716.0785123966941 + }, + "warehouse-system-1.0.0": { + "x": 955.3574380165288, + "y": 1529.3367768595042 + }, + "stripe-1.0.0": { + "x": 318.0144628099173, + "y": 1260.4194214876034 + }, + "product-catalog-system-1.0.0": { + "x": 1145, + "y": 497.5 + }, + "search-system-1.0.0": { + "x": 440.8853146853147, + "y": 723.5524475524476 + }, + "actor-merchandiser": { + "x": 447.2405594405594, + "y": 530.0979020979021 + }, + "actor-catalog-admin": { + "x": 561.634965034965, + "y": 418.6909090909091 + }, + "actor-storefront-team": { + "x": -349.5522886204704, + "y": 775.1666878575969 + } + } +} \ No newline at end of file diff --git a/examples/default/_data/visualizer-layouts/systems-context/customer-management-system/1.0.0.json b/examples/default/_data/visualizer-layouts/systems-context/customer-management-system/1.0.0.json new file mode 100644 index 000000000..0d4b87545 --- /dev/null +++ b/examples/default/_data/visualizer-layouts/systems-context/customer-management-system/1.0.0.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "savedAt": "2026-06-29T11:26:03.901Z", + "resourceKey": "systems-context/customer-management-system/1.0.0", + "positions": { + "customer-management-system-1.0.0": { + "x": 593.5226571767497, + "y": -13.867141162514821 + }, + "identity-provider-1.0.0": { + "x": 1145, + "y": 85 + }, + "actor-customer": { + "x": 90, + "y": 60 + }, + "actor-support-agent": { + "x": 90, + "y": 260 + } + } +} \ No newline at end of file diff --git a/examples/default/channels/event-buses/cross-account-bus/index.mdx b/examples/default/channels/event-buses/cross-account-bus/index.mdx deleted file mode 100644 index 45413d6e0..000000000 --- a/examples/default/channels/event-buses/cross-account-bus/index.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -id: cross-account-bus -name: Organization Cross Account Bus -version: 1.0.0 -summary: | - Amazon Organization Cross Account Bus -owners: - - dboyne -address: cross-account-bus.amazonaws.com -protocols: - - aws-cross-account-bus -parameters: - accountId: - description: 'The AWS account ID to use' -routes: - - id: registrations-domain-eventbus - - id: shipping-domain-eventbus - # - id: orders-domain-eventbus ---- - -{/* Information about the AWS cross account eventbridge bus */} - -Full this.... diff --git a/examples/default/channels/event-buses/inventory-eventbus/index.mdx b/examples/default/channels/event-buses/inventory-eventbus/index.mdx deleted file mode 100644 index 802dd8cd7..000000000 --- a/examples/default/channels/event-buses/inventory-eventbus/index.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -id: inventory-domain-eventbus -name: Inventory Domain EventBus -version: 1.0.0 -summary: | - The event bus for the Inventory domain. It is used to send events to and from the Inventory domain. Also events can be consumed from this bus by other domains. -owners: - - dboyne -routes: - - id: cross-account-bus ---- - - diff --git a/examples/default/channels/event-buses/registrations-domain-eventbus/index.mdx b/examples/default/channels/event-buses/registrations-domain-eventbus/index.mdx deleted file mode 100644 index 2aa6f3e0d..000000000 --- a/examples/default/channels/event-buses/registrations-domain-eventbus/index.mdx +++ /dev/null @@ -1,10 +0,0 @@ ---- -id: registrations-domain-eventbus -name: Registrations Domain EventBus -version: 1.0.0 -summary: | - Amazon Registrations Domain EventBus -owners: - - dboyne ---- - diff --git a/examples/default/channels/event-buses/shipping-domain-eventbus/index.mdx b/examples/default/channels/event-buses/shipping-domain-eventbus/index.mdx deleted file mode 100644 index 956b2f43f..000000000 --- a/examples/default/channels/event-buses/shipping-domain-eventbus/index.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: shipping-domain-eventbus -name: Shipping Domain EventBus -version: 1.0.0 -summary: | - Amazon Shipping Domain EventBus -owners: - - dboyne -routes: - - id: cross-account-bus ---- diff --git a/examples/default/channels/inventory.{env}.events/index.mdx b/examples/default/channels/inventory.{env}.events/index.mdx deleted file mode 100644 index f95bc8ba2..000000000 --- a/examples/default/channels/inventory.{env}.events/index.mdx +++ /dev/null @@ -1,157 +0,0 @@ ---- -id: inventory.{env}.events -name: Inventory Events Channel -version: 1.0.0 -summary: | - Central event stream for all inventory-related events including stock updates, allocations, and adjustments -owners: - - dboyne -address: inventory.{env}.events -protocols: - - kafka - -parameters: - env: - enum: - - dev - - sit - - prod - description: 'Environment to use' ---- - -### Overview -The Inventory Events channel is the central stream for all inventory-related events across the system. This includes stock level changes, inventory allocations, adjustments, and stocktake events. Events for a specific SKU are guaranteed to be processed in sequence when using productId as the partition key. - - - -### Publishing and Subscribing to Events - -#### Publishing Example -```python -from kafka import KafkaProducer -import json -from datetime import datetime - -# Kafka configuration -bootstrap_servers = ['localhost:9092'] -topic = f'inventory.{env}.events' - -# Create a Kafka producer -producer = KafkaProducer( - bootstrap_servers=bootstrap_servers, - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# Example inventory update event -inventory_event = { - "eventType": "STOCK_LEVEL_CHANGED", - "timestamp": datetime.utcnow().isoformat(), - "version": "1.0", - "payload": { - "productId": "PROD-456", - "locationId": "WH-123", - "previousQuantity": 100, - "newQuantity": 95, - "changeReason": "ORDER_FULFILLED", - "unitOfMeasure": "EACH", - "batchInfo": { - "batchId": "BATCH-789", - "expiryDate": "2025-12-31" - } - }, - "metadata": { - "source": "warehouse_system", - "correlationId": "inv-xyz-123", - "userId": "john.doe" - } -} - -# Send the message - using productId as key for partitioning -producer.send( - topic, - key=inventory_event['payload']['productId'].encode('utf-8'), - value=inventory_event -) -producer.flush() - -print(f"Inventory event sent to topic {topic}") - -``` - -### Subscription example - -```python -from kafka import KafkaConsumer -import json -from datetime import datetime - -class InventoryEventConsumer: - def __init__(self): - # Kafka configuration - self.topic = f'inventory.{env}.events' - self.consumer = KafkaConsumer( - self.topic, - bootstrap_servers=['localhost:9092'], - group_id='inventory-processor-group', - auto_offset_reset='earliest', - enable_auto_commit=False, - value_deserializer=lambda x: json.loads(x.decode('utf-8')), - key_deserializer=lambda x: x.decode('utf-8') if x else None - ) - - def process_event(self, event): - """Process individual inventory events based on type""" - event_type = event.get('eventType') - - if event_type == 'STOCK_LEVEL_CHANGED': - self.handle_stock_level_change(event) - elif event_type == 'LOW_STOCK_ALERT': - self.handle_low_stock_alert(event) - # Add more event type handlers as needed - - def handle_stock_level_change(self, event): - """Handle stock level change events""" - payload = event['payload'] - print(f"Stock level change detected for product {payload['productId']}") - print(f"New quantity: {payload['newQuantity']}") - # Add your business logic here - - def handle_low_stock_alert(self, event): - """Handle low stock alert events""" - payload = event['payload'] - print(f"Low stock alert for product {payload['productId']}") - print(f"Current quantity: {payload['currentQuantity']}") - # Add your business logic here - - def start_consuming(self): - """Start consuming messages from the topic""" - try: - print(f"Starting consumption from topic: {self.topic}") - for message in self.consumer: - try: - # Process the message - event = message.value - print(f"Received event: {event['eventType']} for product: {event['payload']['productId']}") - - # Process the event - self.process_event(event) - - # Commit the offset after successful processing - self.consumer.commit() - - except Exception as e: - print(f"Error processing message: {str(e)}") - # Implement your error handling logic here - # You might want to send to a DLQ (Dead Letter Queue) - - except Exception as e: - print(f"Consumer error: {str(e)}") - finally: - # Clean up - self.consumer.close() - -if __name__ == "__main__": - # Create and start the consumer - consumer = InventoryEventConsumer() - consumer.start_consuming() - ``` \ No newline at end of file diff --git a/examples/default/channels/orders.{env}.events/index.mdx b/examples/default/channels/orders.{env}.events/index.mdx deleted file mode 100644 index 4cfd70802..000000000 --- a/examples/default/channels/orders.{env}.events/index.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -id: orders.{env}.events -name: Order Events Channel -version: 1.0.1 -summary: | - Central event stream for all order-related events in the order processing lifecycle -owners: - - dboyne -address: orders.{env}.events -protocols: - - azure-servicebus -sidebar: - badge: 'ServiceBus' -routes: - - id: sns-channel -parameters: - env: - enum: - - dev - - sit - - prod - description: 'Environment to use' ---- - -### Overview -The Orders Events channel is the central stream for all order-related events across the order processing lifecycle. This includes order creation, updates, payment status, fulfillment status, and customer communications. All events related to a specific order are guaranteed to be processed in sequence when using orderId as the partition key. - - - -### Publishing a message using Azure Service Bus - -Here is an example of how to publish an order event using Azure Service Bus: - -```python -import json -from datetime import datetime -from azure.servicebus import ServiceBusClient, ServiceBusMessage - -# --- Azure Service Bus Configuration --- -# Replace with your actual connection string and queue/topic name -CONNECTION_STR = "YOUR_AZURE_SERVICE_BUS_CONNECTION_STRING" -QUEUE_NAME = "orders" # Or your specific queue/topic name e.g., f"orders.{env}.events" - -# --- Example Order Event Data --- -# This is the same event structure as before -order_event_data = { - "eventType": "ORDER_CREATED", - "timestamp": datetime.utcnow().isoformat() + "Z", # ISO 8601 format with Z for UTC - "version": "1.0", - "payload": { - "orderId": "12345", - "customerId": "CUST-789", - "items": [ - { - "productId": "PROD-456", - "quantity": 2, - "price": 29.99 - } - ], - "totalAmount": 59.98, - "shippingAddress": { - "street": "123 Main St", - "city": "Springfield", - "country": "US" - } - }, - "metadata": { - "source": "web_checkout", - "correlationId": "abc-xyz-123" - # Potentially add a message ID if not automatically handled or for specific tracking - # "messageId": str(uuid.uuid4()) # Requires import uuid - } -} - -def send_order_event_to_service_bus(connection_string, queue_name, event_data): - # Create a ServiceBusClient object - with ServiceBusClient.from_connection_string(conn_str=connection_string) as servicebus_client: - # Create a sender for the queue - # For a topic, use: servicebus_client.get_topic_sender(topic_name=queue_name) - sender = servicebus_client.get_queue_sender(queue_name=queue_name) - with sender: - # Serialize the event data to a JSON string - event_json = json.dumps(event_data) - # Create a ServiceBusMessage object - message = ServiceBusMessage(event_json) - - # Set properties if needed, e.g., message_id or correlation_id - # message.message_id = event_data["metadata"].get("messageId") - message.correlation_id = event_data["metadata"]["correlationId"] - - # Send the message - sender.send_messages(message) - print(f"Sent order event (ID: {event_data['payload']['orderId']}) to Azure Service Bus queue: {queue_name}") - -if __name__ == "__main__": - # Example of how to call the function - # Ensure azure-servicebus package is installed: pip install azure-servicebus - - # Basic error handling for placeholders - if CONNECTION_STR == "YOUR_AZURE_SERVICE_BUS_CONNECTION_STRING" or QUEUE_NAME == "YOUR_QUEUE_NAME": - print("Please update CONNECTION_STR and QUEUE_NAME with your actual Azure Service Bus details.") - else: - try: - send_order_event_to_service_bus(CONNECTION_STR, QUEUE_NAME, order_event_data) - except Exception as e: - print(f"An error occurred: {e}") - print("Ensure your Azure Service Bus connection string and queue name are correct,") - print("and the 'azure-servicebus' package is installed ('pip install azure-servicebus').") \ No newline at end of file diff --git a/examples/default/channels/payment.{env}.events/index.mdx b/examples/default/channels/payment.{env}.events/index.mdx deleted file mode 100644 index 2aa739645..000000000 --- a/examples/default/channels/payment.{env}.events/index.mdx +++ /dev/null @@ -1,88 +0,0 @@ ---- -id: payments.{env}.events -name: Payment Events Channel -version: 1.0.0 -summary: | - All events contain payment ID for traceability and ordered processing. -owners: - - dboyne -address: payments.{env}.events -protocols: - - kafka - -parameters: - env: - enum: - - dev - - sit - - prod - description: 'Environment to use for payment events' ---- - -### Overview -The Payments Events channel is the central stream for all payment lifecycle events. This includes payment initiation, authorization, capture, completion and failure scenarios. Events for a specific payment are guaranteed to be processed in sequence when using paymentId as the partition key. - - - -### Publishing Events Using Kafka - -Here's an example of publishing a payment event: - -```python -from kafka import KafkaProducer -import json -from datetime import datetime - -# Kafka configuration -bootstrap_servers = ['localhost:9092'] -topic = f'payments.{env}.events' - -# Create Kafka producer -producer = KafkaProducer( - bootstrap_servers=bootstrap_servers, - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# Example payment processed event -payment_event = { - "eventType": "PAYMENT_PROCESSED", - "timestamp": datetime.utcnow().isoformat(), - "version": "1.0", - "payload": { - "paymentId": "PAY-123-456", - "orderId": "ORD-789", - "amount": { - "value": 99.99, - "currency": "USD" - }, - "status": "SUCCESS", - "paymentMethod": { - "type": "CREDIT_CARD", - "last4": "4242", - "expiryMonth": "12", - "expiryYear": "2025", - "network": "VISA" - }, - "transactionDetails": { - "processorId": "stripe_123xyz", - "authorizationCode": "AUTH123", - "captureId": "CAP456" - } - }, - "metadata": { - "correlationId": "corr-123-abc", - "merchantId": "MERCH-456", - "source": "payment_service", - "environment": "prod", - "idempotencyKey": "PAY-123-456-2024-11-11-99.99" - } -} - -# Send message - using paymentId as key for partitioning -producer.send( - topic, - key=payment_event['payload']['paymentId'].encode('utf-8'), - value=payment_event -) -producer.flush() -``` \ No newline at end of file diff --git a/examples/default/chat-prompts/code/create-an-example-schema.mdx b/examples/default/chat-prompts/code/create-an-example-schema.mdx deleted file mode 100644 index 274161ace..000000000 --- a/examples/default/chat-prompts/code/create-an-example-schema.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Generate a JSON Schema Following FlowMart Standards -category: - id: Code - label: Code - icon: Code -inputs: - - id: event-name - label: What is the name of the event? - type: text ---- - -Generate a JSON schema for an event called `{{event-name}}`. This schema must follow FlowMart's payload standards. - -**FlowMart Payload Standards:** - -* Every payload MUST include a top-level `metadata` field. -* The `metadata` field MUST contain: - * `correlationId`: A string, preferably in UUID format. - * `createdDate`: A string in ISO 8601 format (e.g., "2023-11-02T14:29:55Z"). -* The main event details should be within a top-level `data` field. - -**Example `UserSignedUp` Event Payload:** - -```json -{ - "metadata": { - "correlationId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "createdDate": "2023-11-02T14:29:55Z" - }, - "data": { - "userId": "user-789", - "email": "test@example.com", - "signUpMethod": "Google" - } -} -``` - -**Invalid Example (Missing `metadata`):** -```json -{ - "data": { - "userId": "user-123", - "email": "invalid@example.com" - } -} -``` - -**Invalid Example (Missing `correlationId` in `metadata`):** -```json -{ - "metadata": { - "createdDate": "2023-11-01T10:00:00Z" - }, - "data": { - "userId": "user-456", - "email": "incomplete@example.com" - } -} -``` - -**Task:** - -Create the JSON schema definition for the `UserSignedUp` event described above, ensuring it enforces the FlowMart payload standards. - - - diff --git a/examples/default/chat-prompts/code/create-consumer-code-for-event.mdx b/examples/default/chat-prompts/code/create-consumer-code-for-event.mdx deleted file mode 100644 index 964d8de8c..000000000 --- a/examples/default/chat-prompts/code/create-consumer-code-for-event.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Create Kafka Consumer Code for Event -category: - id: Code - label: Code - icon: Code -inputs: - - id: event-name - label: Select the event you want to create a consumer for - type: resource-list-events - - id: code-language - label: What is the programming language of the code? - type: select - options: - - Python - - Java - - TypeScript - - Go - - Ruby ---- - -Generate Kafka consumer code in `{{code-language}}` that consumes the event `{{event-name}}`. - -**Kafka Cluster Details:** - -* **Bootstrap Servers:** `kafka-prod-1.flowmart.internal:9092,kafka-prod-2.flowmart.internal:9092,kafka-prod-3.flowmart.internal:9092` -* **Topic Name:** The topic name follows the pattern `fm.events.` (e.g., `fm.events.UserSignedUp` for the `UserSignedUp` event). -* **Consumer Group ID:** Use a descriptive consumer group ID, like `consumer-{{event-name}}-service`. -* **Security Protocol:** SASL_SSL -* **SASL Mechanism:** PLAIN - -**Best Practices to Follow:** - -1. **Deserialization:** Assume the event payload is JSON and follows FlowMart's payload standards (with `metadata` and `data` fields). Deserialize the message payload into an appropriate data structure or class. -2. **Error Handling:** Implement robust error handling for connection issues, deserialization failures, and message processing errors. Use a dead-letter queue (DLQ) pattern for messages that cannot be processed after retries. The DLQ topic name is `dlq.fm.events.`. -3. **Configuration:** Externalize Kafka connection details and consumer group ID (don't hardcode them directly in the main logic). -4. **Logging:** Add basic logging for successful message consumption and any errors encountered. -5. **Commit Strategy:** Use manual commits (`enable.auto.commit=false`) to ensure messages are only marked as processed after successful handling. -6. **Idempotency:** Briefly mention how the consumer logic should handle potential duplicate messages (e.g., by checking a database record based on `metadata.correlationId` before processing). - -**Task:** - -Provide the complete, runnable Kafka consumer code snippet in `{{code-language}}` for the `{{event-name}}` event, incorporating the cluster details and best practices mentioned above. Include necessary imports and basic setup. - -If you use any external libraries, please include the import statements and how to install them, step by step, make sure dependencies are listed first. - - - diff --git a/examples/default/chat-prompts/code/create-consumer-code-for-eventbridge-event.mdx b/examples/default/chat-prompts/code/create-consumer-code-for-eventbridge-event.mdx deleted file mode 100644 index 981cfc65b..000000000 --- a/examples/default/chat-prompts/code/create-consumer-code-for-eventbridge-event.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Create EventBridge Rule for an Event -category: - id: CloudFormation - label: CloudFormation - icon: CloudFormation -inputs: - - id: event-name - label: Select the event you want to create a rule for - type: resource-list-events - - id: rule-name - label: Enter a name for the EventBridge rule - type: text ---- - -Generate an AWS CloudFormation template snippet (YAML) to create an EventBridge rule. This rule should trigger based on the selected event `{{event-name}}` from the default EventBus and invoke the specified Lambda function `{{target-lambda-arn}}`. - -**Rule Details:** - -* **Rule Name:** `{{rule-name}}` (e.g., `trigger-lambda-on-{{event-name}}`) -* **Event Bus:** Use the `default` event bus. -* **Event Pattern:** The rule should match events where the `detail-type` is exactly `{{event-name}}`. -* **Target:** The primary target is the Lambda function with ARN `{{target-lambda-arn}}`. -* **Permissions:** Ensure the necessary permissions are granted for EventBridge to invoke the target Lambda function. - -**Best Practices to Follow:** - -1. **Clarity:** Use clear and descriptive names for resources. -2. **Specificity:** Define the event pattern precisely to avoid unintended triggers. -3. **Permissions:** Include the `AWS::Lambda::Permission` resource to grant invocation rights. -4. **Least Privilege:** Grant only the necessary permissions. - -**Task:** - -Provide the complete, runnable CloudFormation YAML snippet for the `{{rule-name}}` EventBridge rule, incorporating the details and best practices mentioned above. Include the `AWSTemplateFormatVersion` and `Resources` sections. - - - diff --git a/examples/default/chat-prompts/code/create-lambda-function-to-consume-event.mdx b/examples/default/chat-prompts/code/create-lambda-function-to-consume-event.mdx deleted file mode 100644 index 7d9561480..000000000 --- a/examples/default/chat-prompts/code/create-lambda-function-to-consume-event.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Create AWS Lambda Function to Consume EventBridge Event -category: - id: Code - label: Code - icon: Code -inputs: - - id: event-name - label: Select the EventBridge event the Lambda will consume - type: resource-list-events - - id: code-language - label: What is the programming language for the Lambda function? - type: select - options: - - Python - - Java - - TypeScript - - Go - - Ruby - # Add other languages supported by Powertools/Lambda as needed ---- - -Generate an AWS Lambda function handler in `{{code-language}}` that consumes the EventBridge event `{{event-name}}`. - -**Event Details:** - -* **Event Source:** AWS EventBridge -* **Event Name:** `{{event-name}}` -* **Payload Location:** The actual event payload (`{{event-name}}`) is located within the `detail` field of the incoming EventBridge event object. -* **Payload Format:** Assume the event payload within `detail` is JSON and follows FlowMart's payload standards (containing `metadata` and `data` fields). - -**Best Practices to Follow:** - -1. **Parsing & Validation:** Safely parse the incoming EventBridge event object. Extract and validate the `detail` field. Deserialize the JSON payload within `detail` into an appropriate data structure or class. -2. **AWS Lambda Powertools:** Utilize AWS Lambda Powertools for `{{code-language}}` to enhance observability and implement best practices: - * **Logging:** Implement structured logging. Include key information like the EventBridge event ID and `metadata.correlationId` in logs. - * **Tracing:** Enable AWS X-Ray tracing integration for request tracing across services. - * **Metrics:** Emit custom business or operational metrics (e.g., successful processing count, processing latency). - * **(Optional) Event Handler:** Consider using Powertools' Event Handler utility (if available for `{{code-language}}`) for simplified event parsing, validation, and routing based on event content. - * **(Optional) Idempotency:** Implement idempotency using Powertools' Idempotency utility to safely handle potential duplicate event deliveries from EventBridge. Use the EventBridge event `id` or `detail.metadata.correlationId` as the idempotency key. -3. **Error Handling:** Implement robust error handling within the function code. For transient errors, allow the function to error out to leverage Lambda's built-in retry mechanism. Configure a Dead-Letter Queue (DLQ) on the Lambda function itself (via AWS console, CLI, or IaC) to capture events that fail permanently after retries. *Do not implement DLQ logic within the function code.* -4. **Configuration:** Use environment variables for any external configuration (e.g., API endpoints, table names). Do not hardcode configuration values. -5. **Permissions:** Note the essential IAM permissions required for the Lambda execution role (e.g., `logs:CreateLogGroup`, `logs:CreateLogStream`, `logs:PutLogEvents`, `xray:PutTraceSegments`, permissions for any AWS services the function interacts with, and potentially KMS permissions if using encryption). -6. **Dependencies:** Clearly list all external dependencies (like AWS Lambda Powertools) and provide instructions on how to install and package them with the Lambda function (e.g., using requirements.txt for Python, package.json for TypeScript, pom.xml/build.gradle for Java). - -**Task:** - -Provide the complete, runnable AWS Lambda function handler code snippet in `{{code-language}}` for the `{{event-name}}` event. The code should: -* Include necessary imports and initialization for AWS SDK and Powertools. -* Demonstrate parsing the EventBridge event and deserializing the `detail` payload. -* Incorporate Powertools for logging, tracing, and metrics as described. -* Include basic error handling. -* Be ready to be deployed as an AWS Lambda function. - -Also, provide the necessary dependency management file content (e.g., `requirements.txt`, `package.json`) and brief packaging instructions. - - - diff --git a/examples/default/chat-prompts/code/create-producer-code-for-event.mdx b/examples/default/chat-prompts/code/create-producer-code-for-event.mdx deleted file mode 100644 index 6222fd334..000000000 --- a/examples/default/chat-prompts/code/create-producer-code-for-event.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Create Kafka Producer Code for Event -category: - id: Code - label: Code - icon: Code -inputs: - - id: event-name - label: Select the event you want to create a producer for - type: resource-list-events - - id: code-language - label: What is the programming language of the code? - type: select - options: - - Python - - Java - - TypeScript - - Go - - Ruby ---- - -Generate Kafka producer code in `{{code-language}}` that produces the event `{{event-name}}`. - -**Kafka Cluster Details:** - -* **Bootstrap Servers:** `kafka-prod-1.flowmart.internal:9092,kafka-prod-2.flowmart.internal:9092,kafka-prod-3.flowmart.internal:9092` -* **Topic Name:** The topic name follows the pattern `fm.events.` (e.g., `fm.events.UserSignedUp` for the `UserSignedUp` event). -* **Security Protocol:** SASL_SSL -* **SASL Mechanism:** PLAIN -* **Required Acknowledgements (acks):** `all` (Ensure highest durability). - -**Best Practices to Follow:** - -1. **Serialization:** Assume the event payload needs to be serialized to JSON and follow FlowMart's payload standards (with `metadata` and `data` fields). The `metadata` should include a unique `eventId` (UUID) and `timestamp` (ISO 8601). -2. **Error Handling:** Implement robust error handling for connection issues, serialization failures, and send failures. Consider retry mechanisms with backoff for transient errors. -3. **Configuration:** Externalize Kafka connection details (don't hardcode them directly in the main logic). -4. **Logging:** Add basic logging for successful message production and any errors encountered. -5. **Asynchronous Sending:** Use asynchronous send calls for better performance, but ensure proper handling of send callbacks/futures for error checking and logging. -6. **Partitioning:** Briefly mention how to set a message key (e.g., using `metadata.correlationId` or a relevant business ID) if specific partitioning is desired for ordering guarantees within a partition. - -**Task:** - -Provide the complete, runnable Kafka producer code snippet in `{{code-language}}` for the `{{event-name}}` event, incorporating the cluster details and best practices mentioned above. Include necessary imports, basic setup, and an example of how to construct and send an event message. - -If you use any external libraries, please include the import statements and how to install them, step by step, make sure dependencies are listed first. - - - diff --git a/examples/default/chat-prompts/learning/tell-me-more-about-this-event.mdx b/examples/default/chat-prompts/learning/tell-me-more-about-this-event.mdx deleted file mode 100644 index fc1e37834..000000000 --- a/examples/default/chat-prompts/learning/tell-me-more-about-this-event.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Tell me more about the given event -category: - id: Learning - label: Learning - icon: GraduationCap -inputs: - - id: event-name - label: Select the event you want to know more about - type: resource-list-events ---- - -Tell me more about the given event `{{event-name}}`. diff --git a/examples/default/chat-prompts/learning/tell-me-more-about-this-service.mdx b/examples/default/chat-prompts/learning/tell-me-more-about-this-service.mdx deleted file mode 100644 index c9ce404d3..000000000 --- a/examples/default/chat-prompts/learning/tell-me-more-about-this-service.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: Tell me more about the given service -category: - id: Learning - label: Learning - icon: GraduationCap -inputs: - - id: service-name - label: Select the service you want to know more about - type: resource-list-services ---- - -Tell me more about the given service `{{service-name}}`. diff --git a/examples/default/chat-prompts/learning/validate-this-schema.mdx b/examples/default/chat-prompts/learning/validate-this-schema.mdx deleted file mode 100644 index ee7e74732..000000000 --- a/examples/default/chat-prompts/learning/validate-this-schema.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Validate schema against best practices -category: - id: Learning - label: Learning - icon: GraduationCap -inputs: - - id: schema - label: Schema - type: code ---- - -Please review the following event schema: - -```json -{{schema}} -``` - -Validate this schema against the FlowMart EDA best practices listed below and provide feedback on any areas where the schema does not align or could be improved based on these guidelines. - ---- - -## FlowMart EDA Best Practices for Reference: - -### Event Naming and Structure - -* **Past Tense Naming:** All event names MUST be in the past tense (e.g., `OrderCreated`, `UserLoggedIn`, `PaymentProcessed`). This reflects that an event represents something that has already occurred. -* **Correlation IDs:** Every event MUST include a `correlationId`. This allows us to track related events across different services and flows, which is crucial for debugging and observability. -* **Clear Event Payloads:** Event payloads should be well-defined, documented, and contain only the necessary information relevant to the event itself. Avoid overly large or generic payloads. - -### Contract Management and Ownership - -* **Producer Ownership:** The service that produces an event owns its contract (schema). Consumers should rely on this defined contract. -* **Public vs. Private Events:** Events MUST be explicitly marked as either `public` or `private`. - * `Public` events have strong backward compatibility guarantees. - * `Private` events are typically for internal service use and may have less stringent compatibility requirements, but changes should still be coordinated. -* **No Breaking Changes (Public Events):** We strive to avoid breaking changes in `public` event contracts. If a change is necessary, it must be additive or managed through versioning. -* **Semantic Versioning (SemVer):** Event schemas SHOULD follow Semantic Versioning (e.g., `1.0.0`, `1.1.0`, `2.0.0`). - * MAJOR version bumps indicate breaking changes (avoided for public events). - * MINOR version bumps indicate backward-compatible additions. - * PATCH version bumps indicate backward-compatible fixes. - -### Design Principles - -* **Idempotent Consumers:** Consumers should be designed to be idempotent, meaning they can safely process the same event multiple times without unintended side effects. This handles scenarios like message replays or network issues. -* **Asynchronous Communication:** Favor asynchronous communication patterns. Events decouple services, allowing them to operate independently. -* **Domain-Driven Design (DDD):** Align events with bounded contexts and domain concepts from DDD. This helps create meaningful, cohesive events. -* **Schema Registry:** Utilize a schema registry (like the one provided by EventCatalog!) to manage, validate, and evolve event schemas centrally. - -### Documentation and Discovery - -* **EventCatalog:** All events, schemas, producers, and consumers MUST be documented in our EventCatalog. This serves as the single source of truth for our EDA landscape. -* **Clear Descriptions:** Provide clear, concise descriptions for each event, explaining its purpose and context. - -By following these practices, we aim to build a resilient and understandable event-driven ecosystem at FlowMart. diff --git a/examples/default/chat-prompts/learning/what-are-the-best-practices-for-eda-in-the-company.mdx b/examples/default/chat-prompts/learning/what-are-the-best-practices-for-eda-in-the-company.mdx deleted file mode 100644 index 3b35a364d..000000000 --- a/examples/default/chat-prompts/learning/what-are-the-best-practices-for-eda-in-the-company.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: What are the best practices for EDA in the company? -category: - id: Learning - label: Learning - icon: GraduationCap ---- - - - -At FlowMart, we adhere to several best practices to ensure our Event-Driven Architecture (EDA) is robust, scalable, and maintainable. Here are the key guidelines: - -### Event Naming and Structure - -* **Past Tense Naming:** All event names MUST be in the past tense (e.g., `OrderCreated`, `UserLoggedIn`, `PaymentProcessed`). This reflects that an event represents something that has already occurred. -* **Correlation IDs:** Every event MUST include a `correlationId`. This allows us to track related events across different services and flows, which is crucial for debugging and observability. -* **Clear Event Payloads:** Event payloads should be well-defined, documented, and contain only the necessary information relevant to the event itself. Avoid overly large or generic payloads. - -### Contract Management and Ownership - -* **Producer Ownership:** The service that produces an event owns its contract (schema). Consumers should rely on this defined contract. -* **Public vs. Private Events:** Events MUST be explicitly marked as either `public` or `private`. - * `Public` events have strong backward compatibility guarantees. - * `Private` events are typically for internal service use and may have less stringent compatibility requirements, but changes should still be coordinated. -* **No Breaking Changes (Public Events):** We strive to avoid breaking changes in `public` event contracts. If a change is necessary, it must be additive or managed through versioning. -* **Semantic Versioning (SemVer):** Event schemas SHOULD follow Semantic Versioning (e.g., `1.0.0`, `1.1.0`, `2.0.0`). - * MAJOR version bumps indicate breaking changes (avoided for public events). - * MINOR version bumps indicate backward-compatible additions. - * PATCH version bumps indicate backward-compatible fixes. - -### Design Principles - -* **Idempotent Consumers:** Consumers should be designed to be idempotent, meaning they can safely process the same event multiple times without unintended side effects. This handles scenarios like message replays or network issues. -* **Asynchronous Communication:** Favor asynchronous communication patterns. Events decouple services, allowing them to operate independently. -* **Domain-Driven Design (DDD):** Align events with bounded contexts and domain concepts from DDD. This helps create meaningful, cohesive events. -* **Schema Registry:** Utilize a schema registry (like the one provided by EventCatalog!) to manage, validate, and evolve event schemas centrally. - -### Documentation and Discovery - -* **EventCatalog:** All events, schemas, producers, and consumers MUST be documented in our EventCatalog. This serves as the single source of truth for our EDA landscape. -* **Clear Descriptions:** Provide clear, concise descriptions for each event, explaining its purpose and context. - -By following these practices, we aim to build a resilient and understandable event-driven ecosystem at FlowMart. diff --git a/examples/default/chat-prompts/learning/what-events-are-relevant-to-this-feature.mdx b/examples/default/chat-prompts/learning/what-events-are-relevant-to-this-feature.mdx deleted file mode 100644 index 6f6328e28..000000000 --- a/examples/default/chat-prompts/learning/what-events-are-relevant-to-this-feature.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: What events are relevant to this feature? -category: - id: Learning - label: Learning - icon: GraduationCap -inputs: - - id: feature-description - label: Feature Description - type: text-area ---- - - -What events are relevant to this feature `{{feature-description}}`? \ No newline at end of file diff --git a/examples/default/chat-prompts/quick-feedback/validate-this-schema.mdx b/examples/default/chat-prompts/quick-feedback/validate-this-schema.mdx deleted file mode 100644 index f002950b3..000000000 --- a/examples/default/chat-prompts/quick-feedback/validate-this-schema.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Validate schema against FlowMart EDA best practices -category: - id: Code Review - label: Code Review - icon: MessageSquareText -inputs: - - id: schema - label: Schema - type: code ---- - -Please review the following event schema: - -```json -{{schema}} -``` - -Validate this schema against the FlowMart EDA best practices listed below and provide feedback on any areas where the schema does not align or could be improved based on these guidelines. - ---- - -## FlowMart EDA Best Practices for Reference: - -### Event Naming and Structure - -* **Past Tense Naming:** All event names MUST be in the past tense (e.g., `OrderCreated`, `UserLoggedIn`, `PaymentProcessed`). This reflects that an event represents something that has already occurred. -* **Correlation IDs:** Every event MUST include a `correlationId`. This allows us to track related events across different services and flows, which is crucial for debugging and observability. -* **Clear Event Payloads:** Event payloads should be well-defined, documented, and contain only the necessary information relevant to the event itself. Avoid overly large or generic payloads. - -### Contract Management and Ownership - -* **Producer Ownership:** The service that produces an event owns its contract (schema). Consumers should rely on this defined contract. -* **Public vs. Private Events:** Events MUST be explicitly marked as either `public` or `private`. - * `Public` events have strong backward compatibility guarantees. - * `Private` events are typically for internal service use and may have less stringent compatibility requirements, but changes should still be coordinated. -* **No Breaking Changes (Public Events):** We strive to avoid breaking changes in `public` event contracts. If a change is necessary, it must be additive or managed through versioning. -* **Semantic Versioning (SemVer):** Event schemas SHOULD follow Semantic Versioning (e.g., `1.0.0`, `1.1.0`, `2.0.0`). - * MAJOR version bumps indicate breaking changes (avoided for public events). - * MINOR version bumps indicate backward-compatible additions. - * PATCH version bumps indicate backward-compatible fixes. - -### Design Principles - -* **Idempotent Consumers:** Consumers should be designed to be idempotent, meaning they can safely process the same event multiple times without unintended side effects. This handles scenarios like message replays or network issues. -* **Asynchronous Communication:** Favor asynchronous communication patterns. Events decouple services, allowing them to operate independently. -* **Domain-Driven Design (DDD):** Align events with bounded contexts and domain concepts from DDD. This helps create meaningful, cohesive events. -* **Schema Registry:** Utilize a schema registry (like the one provided by EventCatalog!) to manage, validate, and evolve event schemas centrally. - -### Documentation and Discovery - -* **EventCatalog:** All events, schemas, producers, and consumers MUST be documented in our EventCatalog. This serves as the single source of truth for our EDA landscape. -* **Clear Descriptions:** Provide clear, concise descriptions for each event, explaining its purpose and context. - -By following these practices, we aim to build a resilient and understandable event-driven ecosystem at FlowMart. diff --git a/examples/default/components/footer.astro b/examples/default/components/footer.astro index 680e6c1b4..7284e2724 100644 --- a/examples/default/components/footer.astro +++ b/examples/default/components/footer.astro @@ -3,6 +3,5 @@ import config from '@config'; ---
    - Event-driven architecture documentation: {config.organizationName} + Event-driven architecture documentation: {config.organizationName}
    - diff --git a/examples/default/docs/domains/01-catalog-and-search.mdx b/examples/default/docs/domains/01-catalog-and-search.mdx new file mode 100644 index 000000000..4ccfb3ed3 --- /dev/null +++ b/examples/default/docs/domains/01-catalog-and-search.mdx @@ -0,0 +1,46 @@ +--- +title: Catalog and search +summary: How product data is owned by the Product Catalog System and projected into Search for customer discovery. +owners: + - product-platform +badges: + - content: Domain guide + backgroundColor: blue + textColor: blue +--- + +[[domain|catalog]] owns the product information that customers browse and buy. It is split into two systems with different consistency needs. + +## System context map + + + +## Product Catalog System + +[[system|product-catalog-system]] is the source of truth. [[service|product-api]] handles product commands and reads from [[container|product-database]]. Product changes are captured through an outbox and published by [[service|product-search-publisher]]. + +Key contracts: + +- [[command|create-product]] +- [[command|update-product]] +- [[command|delete-product]] +- [[query|get-product]] +- [[event|product-created]] +- [[event|product-updated]] +- [[event|product-deleted]] + +## Search System + +[[system|search-system]] serves product discovery through [[service|search-api]] and [[query|search-products]]. It maintains [[container|search-index]] from product events. + +The Search System should be treated as eventually consistent with the Product Catalog System. If a merchandiser updates a product, the product database is immediately authoritative; search visibility follows after indexing. + +## Decision context + +The product catalog has several architecture decisions documented directly on the system: + +- [[adr|adr-001-use-transactional-outbox]] +- [[adr|adr-002-postgres-as-system-of-record]] +- [[adr|adr-003-offload-async-work-to-worker]] + +These decisions explain why product events are published asynchronously and why Postgres remains the system of record. diff --git a/examples/default/docs/domains/02-shopping-and-promotions.mdx b/examples/default/docs/domains/02-shopping-and-promotions.mdx new file mode 100644 index 000000000..442e045a1 --- /dev/null +++ b/examples/default/docs/domains/02-shopping-and-promotions.mdx @@ -0,0 +1,38 @@ +--- +title: Shopping and promotions +summary: How carts, checkout intent and discount calculation are modelled in the Shopping domain. +owners: + - shopping-platform +badges: + - content: Domain guide + backgroundColor: blue + textColor: blue +--- + +[[domain|shopping]] owns the customer's active path to purchase before an order exists. The core state is the cart, not the order. + +## System context map + + + +## Cart System + +[[system|cart-system]] owns cart state in [[container|cart-database]]. [[service|cart-api]] handles: + +- [[command|add-item-to-cart]] +- [[command|remove-item-from-cart]] +- [[command|checkout-cart]] + +When a cart is checked out, it publishes [[event|cart-checked-out]]. That event is the handoff from Shopping to Ordering. + +## Promotion System + +[[system|promotion-system]] owns promotion rules in [[container|promotion-database]]. It handles [[command|calculate-discount]] and publishes [[event|discount-calculated]]. + +Discount calculation is deliberately separate from cart persistence. The Cart System owns what the customer is buying; the Promotion System owns how discounts are evaluated. + +## Boundary with Ordering + +Shopping does not create orders. Once [[event|cart-checked-out]] is published, [[system|checkout-system]] takes responsibility for coordinating inventory, payment and order creation. + +This boundary keeps cart behavior and order lifecycle behavior independent. It also gives checkout a clear recovery point if inventory or payment fails. diff --git a/examples/default/docs/domains/03-ordering-and-checkout.mdx b/examples/default/docs/domains/03-ordering-and-checkout.mdx new file mode 100644 index 000000000..8d14abc88 --- /dev/null +++ b/examples/default/docs/domains/03-ordering-and-checkout.mdx @@ -0,0 +1,52 @@ +--- +title: Ordering and checkout +summary: How the Ordering domain coordinates checkout and owns the order lifecycle. +owners: + - ordering-platform +badges: + - content: Domain guide + backgroundColor: red + textColor: red +--- + +[[domain|ordering]] is the core revenue domain. It turns a checked-out cart into a durable order and owns that order through completion or cancellation. + +## System context map + + + +## Checkout System + +[[system|checkout-system]] owns orchestration. [[service|checkout-api]] receives checkout intent, and [[service|checkout-orchestrator]] runs the [[flow|checkout-saga]]. + +The saga coordinates: + +1. [[command|reserve-inventory]] +2. [[command|authorize-payment]] +3. [[command|create-order]] + +If a step fails, the orchestrator compensates rather than leaving partial work behind. + + + +## Order Management System + +[[system|order-management-system]] owns order state in [[container|order-database]]. [[service|order-service]] handles [[command|create-order]], [[command|cancel-order]] and [[query|get-order]]. + +It publishes: + +- [[event|order-created]] +- [[event|order-completed]] +- [[event|order-cancelled]] + +## Operating guidance + +Do not add payment or inventory state directly to orders unless it is a snapshot needed for audit or customer support. The owning systems remain [[system|payment-processing-system]] and [[system|inventory-system]]. + +When checkout behavior changes, update the [[flow|place-an-order]] and [[flow|checkout-saga]] pages as well as the command schemas. + + + Review a proposed Ordering domain change for Acme Inc. Identify whether it changes checkout orchestration, order state, payment + or inventory boundaries, customer-visible status, command schemas, event schemas or downstream fulfilment behavior. Return the + affected catalog resources and the teams that should review the change. + diff --git a/examples/default/docs/domains/04-payments-and-fraud.mdx b/examples/default/docs/domains/04-payments-and-fraud.mdx new file mode 100644 index 000000000..cda063532 --- /dev/null +++ b/examples/default/docs/domains/04-payments-and-fraud.mdx @@ -0,0 +1,39 @@ +--- +title: Payments and fraud +summary: How payment authorization, Stripe integration, refunds and fraud screening are represented in the Payments domain. +owners: + - payments-platform +badges: + - content: Domain guide + backgroundColor: red + textColor: red +--- + +[[domain|payments]] owns payment intent and payment outcomes. It integrates with external providers but keeps Acme's internal payment state in [[container|payment-database]]. + +## System context map + + + +## Payment Processing System + +[[system|payment-processing-system]] is composed of [[service|payment-api]] and [[service|payment-worker]]. + +[[service|payment-api]] receives [[command|authorize-payment]] from checkout and records the intent. [[service|payment-worker]] drives the external charge/refund process and records outcomes. + +Important events: + +- [[event|payment-requested]] +- [[event|payment-succeeded]] +- [[event|payment-failed]] +- [[event|refund-requested]] + +## External providers + +[[system|stripe]] is the external payment processor. It reports outcomes through [[service|stripe-webhook-endpoint]] events such as [[event|payment-succeeded]], [[event|payment-failed]] and [[event|refund-processed]]. + +[[system|fraud-detection]] screens payment attempts and reports [[event|fraud-check-passed]] or [[event|fraud-check-failed]]. + +## Failure handling + +Payments cross external boundaries, so every step should be durable and retryable. Do not assume webhook delivery order. Payment state transitions should be idempotent and traceable through the payment database. diff --git a/examples/default/docs/domains/05-fulfilment-and-shipping.mdx b/examples/default/docs/domains/05-fulfilment-and-shipping.mdx new file mode 100644 index 000000000..758489dab --- /dev/null +++ b/examples/default/docs/domains/05-fulfilment-and-shipping.mdx @@ -0,0 +1,36 @@ +--- +title: Fulfilment and shipping +summary: How stock reservation, warehouse packing and carrier handoff work after an order is completed. +owners: + - fulfilment-platform +badges: + - content: Domain guide + backgroundColor: blue + textColor: blue +--- + +[[domain|fulfilment]] gets confirmed orders to customers. It owns the operational handoff from stock reservation through warehouse work and carrier delivery. + +## System context map + + + +## Inventory System + +[[system|inventory-system]] owns [[container|inventory-database]]. [[service|inventory-service]] handles [[command|reserve-inventory]], [[command|release-inventory]] and [[query|get-stock-level]]. + +It publishes [[event|inventory-reserved]] or [[event|inventory-unavailable]] during checkout. + +## Warehouse System + +[[system|warehouse-system]] owns picking and packing in [[container|warehouse-database]]. [[service|warehouse-service]] reacts to [[event|order-completed]] and publishes [[event|order-ready-for-shipping]]. + +[[service|picking-worker]] handles packing work and publishes [[event|order-packed]]. + +## Shipping System and Carrier + +[[system|shipping-system]] creates shipments through [[command|create-shipment]]. [[system|carrier]] is the external delivery provider and reports [[event|shipment-created]], [[event|shipment-delivered]] or [[event|shipment-failed]]. + +## Boundary with Ordering + +Ordering decides that an order exists. Fulfilment decides whether and how it can be delivered. Keep these lifecycles separate so cancellation, refunds and shipment exceptions can be handled independently. diff --git a/examples/default/docs/domains/06-customer-and-identity.mdx b/examples/default/docs/domains/06-customer-and-identity.mdx new file mode 100644 index 000000000..f805771ec --- /dev/null +++ b/examples/default/docs/domains/06-customer-and-identity.mdx @@ -0,0 +1,36 @@ +--- +title: Customer and identity +summary: How customer profiles and authentication are separated across Customer Management and Identity Provider systems. +owners: + - customer-platform +badges: + - content: Domain guide + backgroundColor: blue + textColor: blue +--- + +[[domain|customer]] owns customer profile data and customer authentication. The catalog separates profile management from identity management so customer details and credentials have different operational boundaries. + +## System context map + + + +## Customer Management System + +[[system|customer-management-system]] owns [[container|customer-database]]. [[service|customer-api]] handles: + +- [[command|register-customer]] +- [[command|update-customer]] +- [[query|get-customer]] + +It publishes [[event|customer-registered]] and [[event|customer-updated]] so other capabilities can react to profile changes. + +## Identity Provider + +[[system|identity-provider]] owns authentication state in [[container|user-directory]]. [[service|oauth-api]] handles [[command|authenticate-customer]] and emits [[event|customer-authenticated]]. + +## Integration guidance + +Services that need customer profile details should use [[query|get-customer]] or consume customer events where a local read model is appropriate. Services that need authentication should integrate with the identity boundary, not the profile database. + +This split prevents order, review and payment systems from accidentally depending on credential storage. diff --git a/examples/default/docs/domains/07-reviews-and-ratings.mdx b/examples/default/docs/domains/07-reviews-and-ratings.mdx new file mode 100644 index 000000000..ae27ac277 --- /dev/null +++ b/examples/default/docs/domains/07-reviews-and-ratings.mdx @@ -0,0 +1,43 @@ +--- +title: Reviews and ratings +summary: How the Reviews domain accepts, moderates, publishes and aggregates product feedback. +owners: + - reviews-platform +badges: + - content: Domain guide + backgroundColor: purple + textColor: purple +--- + +[[domain|reviews]] owns customer feedback on products. Unlike the other domains, it is modelled directly with services and data stores rather than nested systems. + +## Domain map + + + +## Services + +[[service|review-api]] accepts review commands and serves product reviews. It writes to [[container|review-database]] and reads aggregate data from [[container|rating-cache]]. + +[[service|review-moderation-worker]] consumes [[event|review-submitted]] and publishes either [[event|review-published]] or [[event|review-rejected]]. + +[[service|rating-aggregator]] reacts to published reviews and updates the product rating model, publishing [[event|rating-updated]]. + +## Core entity + +[[entity|review]] is the central aggregate. It moves from submitted to moderated to published or rejected. + + + +## Customer interactions + +Customers can submit, flag and vote on reviews: + +- [[command|submit-review]] +- [[command|flag-review]] +- [[command|vote-review-helpful]] +- [[query|get-product-reviews]] + +## Product impact + +Reviews influence product discovery and conversion, but they do not own product data. Product identity remains in [[domain|catalog]], while review state remains in Reviews. diff --git a/examples/default/docs/flows/01-place-an-order.mdx b/examples/default/docs/flows/01-place-an-order.mdx new file mode 100644 index 000000000..1b7396811 --- /dev/null +++ b/examples/default/docs/flows/01-place-an-order.mdx @@ -0,0 +1,43 @@ +--- +title: Place an order +summary: The end-to-end customer purchase journey from cart checkout to confirmed order and downstream fulfilment. +owners: + - ordering-platform +badges: + - content: Flow + backgroundColor: orange + textColor: orange +--- + +[[flow|place-an-order]] is the highest-value customer journey in the catalog. It spans [[domain|shopping]], [[domain|ordering]], [[domain|payments]] and [[domain|fulfilment]]. + +## Flow map + + + +## Happy path + +1. The customer checks out through [[service|cart-api]] using [[command|checkout-cart]]. +2. [[service|cart-api]] publishes [[event|cart-checked-out]]. +3. [[service|checkout-api]] and [[service|checkout-orchestrator]] start checkout orchestration. +4. [[service|checkout-orchestrator]] reserves inventory with [[command|reserve-inventory]]. +5. It authorizes payment with [[command|authorize-payment]]. +6. It creates the order with [[command|create-order]]. +7. [[service|order-service]] publishes [[event|order-created]] and eventually [[event|order-completed]]. +8. Fulfilment consumes completed orders and prepares shipment. + +## Important boundaries + +The flow crosses several ownership boundaries. Shopping owns the cart, Ordering owns the order, Payments owns payment outcome, and Fulfilment owns stock and shipping. Each handoff is a contract boundary. + +## Failure points + +The two most important failure points are inventory and payment. If [[command|reserve-inventory]] or [[command|authorize-payment]] fails, checkout must compensate and avoid creating an order. + + + Review the Acme place-an-order flow and identify the operational impact of a checkout change. Focus on cart checkout, + inventory reservation, payment authorization, order creation, fulfilment handoff, compensation behavior and teams that + need to review the change. Return a concise impact checklist with risks and required catalog updates. + + +Use [[flow|checkout-saga]] for the detailed orchestration view. diff --git a/examples/default/docs/flows/02-checkout-saga.mdx b/examples/default/docs/flows/02-checkout-saga.mdx new file mode 100644 index 000000000..18a2df3c8 --- /dev/null +++ b/examples/default/docs/flows/02-checkout-saga.mdx @@ -0,0 +1,48 @@ +--- +title: Checkout saga +summary: The orchestration pattern used by checkout to coordinate inventory reservation, payment authorization and order creation. +owners: + - ordering-platform +badges: + - content: Flow + backgroundColor: orange + textColor: orange +--- + +[[flow|checkout-saga]] is the detailed orchestration inside [[system|checkout-system]]. It exists because checkout cannot be a single local transaction: inventory, payment and order state are owned by different systems. + +## Flow map + + + +## Saga steps + +| Step | Contract | Owner | +|------|----------|-------| +| Reserve inventory | [[command\|reserve-inventory]] | [[system\|inventory-system]] | +| Authorize payment | [[command\|authorize-payment]] | [[system\|payment-processing-system]] | +| Create order | [[command\|create-order]] | [[system\|order-management-system]] | + +## Success condition + +Checkout succeeds only when inventory is reserved, payment is authorized and an order is created. The durable outcome is an order in [[container|order-database]] and subsequent [[event|order-created]] publication. + +## Compensation + +If inventory is unavailable or payment is declined, checkout should not create an order. The orchestrator should release any previous reservation or void any authorization as needed. + +## Change guidance + +Adding a checkout step means updating: + +- the flow definition, +- the orchestrator implementation, +- the command/event contracts, +- failure and compensation behavior, +- customer-facing status semantics. + + + Review this checkout saga change as a senior distributed systems engineer. Check whether every step has a clear owner, + success condition, failure condition, retry policy, compensation action and customer-visible status. Call out any + missing contracts or catalog pages that should be updated before release. + diff --git a/examples/default/docs/flows/03-product-search-indexing.mdx b/examples/default/docs/flows/03-product-search-indexing.mdx new file mode 100644 index 000000000..850e9d796 --- /dev/null +++ b/examples/default/docs/flows/03-product-search-indexing.mdx @@ -0,0 +1,38 @@ +--- +title: Product search indexing +summary: How product catalog changes are projected into the Search System for customer-facing search. +owners: + - search-platform +badges: + - content: Flow + backgroundColor: orange + textColor: orange +--- + +[[flow|product-search-indexing]] keeps product search aligned with catalog changes. It is intentionally asynchronous: product writes should not wait for search indexing to complete. + +## Flow map + + + +## Producers + +[[system|product-catalog-system]] owns product data. Product changes are represented by: + +- [[event|product-created]] +- [[event|product-updated]] +- [[event|product-deleted]] + +These events are published by services inside the Product Catalog System, including [[service|product-search-publisher]]. + +## Consumers + +[[system|search-system]] consumes product change events and updates [[container|search-index]]. [[service|search-api]] then serves [[query|search-products]] against that index. + +## Consistency model + +The product database is immediately authoritative. The search index is eventually consistent. User interfaces should tolerate a short delay between product update and search result update. + +## Operational signals + +Watch for indexing lag, failed event handling and search query errors. A stale search index affects product discovery even when product writes are healthy. diff --git a/examples/default/docs/flows/04-review-submission.mdx b/examples/default/docs/flows/04-review-submission.mdx new file mode 100644 index 000000000..e9c3f54e5 --- /dev/null +++ b/examples/default/docs/flows/04-review-submission.mdx @@ -0,0 +1,37 @@ +--- +title: Review submission +summary: How product reviews are submitted, moderated, published and aggregated into ratings. +owners: + - reviews-platform +badges: + - content: Flow + backgroundColor: orange + textColor: orange +--- + +[[flow|review-submission]] captures the lifecycle of customer product feedback. + +## Flow map + + + +## Flow stages + +1. A customer submits a review through [[service|review-api]] using [[command|submit-review]]. +2. [[service|review-api]] stores the review in [[container|review-database]] and publishes [[event|review-submitted]]. +3. [[service|review-moderation-worker]] evaluates the review. +4. The worker publishes [[event|review-published]] or [[event|review-rejected]]. +5. [[service|rating-aggregator]] consumes published reviews and updates [[container|rating-cache]]. +6. [[service|review-api]] serves [[query|get-product-reviews]] for storefront reads. + +## Moderation boundary + +Moderation is asynchronous so submission remains responsive and moderation rules can evolve independently. + +## Rating boundary + +Ratings are a read model. [[container|rating-cache]] is optimised for product pages, while [[container|review-database]] remains the durable review store. + +## Follow-up interactions + +After publication, customers can use [[command|vote-review-helpful]] or [[command|flag-review]]. These interactions publish [[event|review-helpful-voted]] and [[event|review-flagged]]. diff --git a/examples/default/docs/guides/creating-new-microservices/01-index.mdx b/examples/default/docs/guides/creating-new-microservices/01-index.mdx deleted file mode 100644 index e06518e50..000000000 --- a/examples/default/docs/guides/creating-new-microservices/01-index.mdx +++ /dev/null @@ -1,95 +0,0 @@ ---- -title: Creating new microservices -summary: A comprehensive guide to creating new microservices at FlowMart following our best practices and standards -sidebar: - label: Introduction - order: 1 -owners: - - dboyne -badges: - - content: 'Guide' - backgroundColor: 'teal' - textColor: 'teal' ---- - -Welcome to the FlowMart microservices creation guide. This documentation will help you understand our microservices architecture and guide you through the process of creating, deploying, and maintaining new services that align with our architectural standards and best practices. - -## FlowMart's Microservices Architecture - -At FlowMart, we've adopted an [Event-**Driven** Architecture (EDA)](/docs/architecture-records/published/02-eda-adoption) for our e-commerce platform. Our architecture consists of domain-oriented microservices that communicate primarily through events, with Apache Kafka serving as our event backbone. - -Key architectural principles we follow: - -- **Domain-Driven Design**: **Services** are organized around business domains with clear bounded contexts -- **Service Autonomy**: Each service owns its data and can be independently deployed -- **Event-First Communication**: Services publish events when state changes and subscribe to events from other domains -- **Eventual Consistency**: We prioritize availability and partition tolerance over immediate consistency -- **API-First Development**: All services expose well-defined APIs using standardized patterns - -## Example service in our architecture (InventoryService) - - -## Example of event stream in our architecture - -If you look at the example service in our architecture, you can see that it has a single event stream. This is a simple example of how we use event streams to communicate between services. - - - - -## Getting Started - -Before creating a new microservice, please familiarize yourself with our architectural decisions: - -1. [Event-Driven Architecture Adoption](/docs/architecture-records/published/02-eda-adoption) -2. [API Gateway Pattern Adoption](/docs/architecture-records/published/01-api-gateway-pattern) -3. [CI/CD and Deployment Strategy](/docs/architecture-records/drafts/02-cicd-deployment-strategy) -4. [API Management and Governance](/docs/architecture-records/drafts/03-api-management-governance) - -## Service Creation Process - -The following steps outline our microservice creation process: - -1. **Domain Analysis**: Identify the business domain and define the bounded context -2. **Service Definition**: Create service specification using our templating tools -3. **Infrastructure Provisioning**: Use our Terraform modules to provision required infrastructure -4. **Service Implementation**: Develop the service following our technological standards -5. **CI/CD Pipeline Setup**: Configure the service pipeline for continuous integration and deployment -6. **Testing**: Implement comprehensive testing (unit, integration, contract, end-to-end) -7. **Documentation**: Document API contracts, event schemas, and service behaviors -8. **Deployment**: Deploy to production using our GitOps workflow - -## Available Service Templates - -We provide several starter templates for new microservices: - -- [Node.js Service Template](/docs/guides/creating-new-microservices/node-service) -- [TypeScript Service Template](/docs/guides/creating-new-microservices/typescript-service) -- [Java Spring Boot Template](/docs/guides/creating-new-microservices/java-spring-boot) -- [Python FastAPI Template](/docs/guides/creating-new-microservices/python-fastapi) - -## Infrastructure as Code - -All FlowMart infrastructure is managed using [Terraform](/docs/guides/creating-new-microservices/terraform-modules). We maintain a set of reusable modules for common infrastructure components, which you should leverage when creating new services. - -## Development Workflow - -1. **Engage with the Platform Team**: Discuss your new service requirements with the platform team -2. **Create Service Repository**: Use our GitLab template to create a new service repository -3. **Setup Local Environment**: Configure your local development environment -4. **Implement Core Functionality**: Develop the service capabilities -5. **Review and Testing**: Submit for architecture and code review -6. **Deploy to Production**: Use our deployment pipeline for production rollout - -## Support - -If you need assistance creating a new microservice, please reach out to: - -- **Platform Engineering Team**: For infrastructure and deployment questions -- **Architecture Team**: For design and architecture guidance -- **DevOps Team**: For CI/CD and operational support - -## Next Steps - -- Continue to [Service Design Principles](/docs/guides/creating-new-microservices/service-design-principles) -- Learn about [Node.js Service Creation](/docs/guides/creating-new-microservices/node-service) -- Explore our [Terraform Modules](/docs/guides/creating-new-microservices/terraform-modules) \ No newline at end of file diff --git a/examples/default/docs/guides/creating-new-microservices/02-service-design-principles.mdx b/examples/default/docs/guides/creating-new-microservices/02-service-design-principles.mdx deleted file mode 100644 index b1346ab84..000000000 --- a/examples/default/docs/guides/creating-new-microservices/02-service-design-principles.mdx +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: Microservice Design Principles -summary: Core design principles and best practices for creating well-structured microservices at FlowMart -sidebar: - label: Design Principles - order: 2 ---- - -At FlowMart, we follow a set of design principles that guide the creation of new microservices. These principles help ensure our services are maintainable, scalable, and aligned with our overall architectural vision. - -## 1. Domain-Driven Design - -Each microservice should be aligned with a specific business domain or subdomain. We follow Domain-Driven Design (DDD) principles to identify service boundaries: - -### Bounded Contexts - -- Define clear bounded contexts for each service -- Maintain a separate ubiquitous language within each context -- Document domain models and context maps - -### Example Domains at FlowMart - -| Domain | Description | Example Services | -|--------|-------------|------------------| -| Order | Order processing and management | order-service, order-history-service | -| Inventory | Product inventory management | inventory-service, stock-management-service | -| Customer | Customer accounts and profiles | customer-service, authentication-service | -| Payment | Payment processing and refunds | payment-service, refund-service | -| Shipping | Shipping and logistics | shipping-service, tracking-service | -| Catalog | Product information management | product-service, search-service | - -## 2. Single Responsibility - -Each microservice should have a single responsibility and a clear purpose: - -- **Focus on one business capability**: Services should do one thing well -- **Right-sized services**: Not too large (mini-monolith) or too small (nano-service) -- **Cohesive functionality**: Related functions should be grouped together - -## 3. Data Ownership - -Microservices should own their data and maintain data autonomy: - -- Each service has its own database or data store -- No direct data sharing between services -- Data is exposed through well-defined APIs -- Services should be the single source of truth for their domain data - -## 4. API Design - -All FlowMart microservices must follow our [API Management and Governance Strategy](/docs/architecture-records/drafts/03-api-management-governance): - -### RESTful APIs - -- Use consistent resource naming conventions -- Follow standard HTTP methods and status codes -- Implement proper error handling and validation -- Design for backward compatibility - -### Event-Driven Interfaces - -- Define clear event schemas using AsyncAPI -- Document event ownership and responsibilities -- Follow event versioning standards -- Implement idempotent event consumers - -## 5. Resilience and Fault Tolerance - -Microservices must be designed to handle failures gracefully: - -- Implement circuit breakers for downstream dependencies -- Use timeouts and retries with exponential backoff -- Design for graceful degradation of functionality -- Implement health checks and readiness probes - -## 6. Observability - -All services must expose monitoring and observability data: - -- Structured logging (using our ELK stack) -- Metrics exposure (Prometheus format) -- Distributed tracing support (Jaeger) -- Health check endpoints - -## 7. Security by Design - -Security must be integrated into every service: - -- Authentication using OAuth 2.0 / OpenID Connect -- Authorization using role-based access control -- TLS encryption for all communications -- Input validation and output encoding -- No sensitive data in logs or traces - -## 8. Testability - -Services should be designed with testing in mind: - -- High unit test coverage (minimum 80%) -- Integration tests for all critical paths -- Contract tests for API interfaces -- Easy local testing setup -- Simulated dependencies for development - -## 9. Configuration Management - -Services should follow our configuration management approach: - -- Environment-specific configuration via Kubernetes ConfigMaps -- Secrets management via HashiCorp Vault -- Feature flags for conditional functionality -- No hardcoded configuration values - -## 10. Independence and Deployability - -Services should be independently deployable: - -- No deployment coupling with other services -- Infrastructure as Code for all resources -- Self-contained CI/CD pipelines -- Blue/green or canary deployment capabilities - -## Microservice Checklist - -Use this checklist when designing a new service: - -- [ ] Service aligns with a specific business domain -- [ ] Clear bounded context defined -- [ ] Service owns its data -- [ ] APIs follow company standards -- [ ] Event schemas are documented -- [ ] Resilience patterns implemented -- [ ] Observability instrumentation added -- [ ] Security controls integrated -- [ ] Comprehensive test suite created -- [ ] Configuration externalized -- [ ] Independent deployment pipeline configured - -## Next Steps - -- Learn how to [create a Node.js microservice](/docs/guides/creating-new-microservices/node-service) -- Explore [TypeScript service implementation](/docs/guides/creating-new-microservices/typescript-service) -- Understand our [Terraform infrastructure modules](/docs/guides/creating-new-microservices/terraform-modules) \ No newline at end of file diff --git a/examples/default/docs/guides/creating-new-microservices/03-database-patterns.mdx b/examples/default/docs/guides/creating-new-microservices/03-database-patterns.mdx deleted file mode 100644 index d6841d246..000000000 --- a/examples/default/docs/guides/creating-new-microservices/03-database-patterns.mdx +++ /dev/null @@ -1,516 +0,0 @@ ---- -title: Database Patterns for Microservices -summary: Best practices and patterns for managing data in FlowMart microservices -sidebar: - label: Database Patterns - order: 5 ---- - -This guide outlines the database patterns and best practices for managing data in FlowMart's microservices architecture. - -## Database Per Service Pattern - -At FlowMart, we follow the **Database Per Service** pattern as our primary data management strategy: - -- **Definition**: Each microservice owns and manages its database exclusively -- **Access Pattern**: Only the service that owns the database can perform direct read/write operations -- **Purpose**: Ensures loose coupling, independent scaling, and domain isolation - -![Database Per Service Diagram](/docs/images/database-per-service.png) - -### Why This Pattern? - -1. **Service Independence**: Services can be developed, deployed, and scaled independently -2. **Technology Freedom**: Teams can choose the most appropriate database technology for their service needs -3. **Resilience**: Database failures are isolated to individual services -4. **Security**: Clear data ownership boundaries limit the blast radius of security incidents -5. **Performance**: Database schemas and indexes can be optimized for specific service requirements - -## Supported Database Technologies - -FlowMart supports the following database technologies for microservices: - -| Database Type | Technology | Best For | Support Level | -|--------------|------------|----------|---------------| -| Document | MongoDB | Flexible schemas, rapid development | Primary | -| Relational | PostgreSQL | Complex transactions, structured data | Primary | -| Key-Value | Redis | Caching, session management, rate limiting | Primary | -| Search | Elasticsearch | Full-text search, analytics | Secondary | -| Graph | Neptune | Relationship-heavy domains (e.g., recommendations) | Secondary | -| Time-Series | InfluxDB | Metrics, monitoring data | Secondary | - -> **Primary**: Fully supported with managed services, infrastructure modules, and dedicated support -> -> **Secondary**: Available but with limited tooling and support - -## Data Modeling Principles - -### 1. Model Around Bounded Contexts - -- Define clear domain boundaries based on business capabilities -- Avoid modeling your database around UI needs -- Focus on the core domain model within each service - -### 2. Design for Access Patterns - -- Consider query patterns when designing the schema -- Optimize for the most frequent queries -- Plan for future data growth and access needs - -### 3. Denormalization When Appropriate - -- Strategic denormalization is often necessary in distributed systems -- Consider composite keys for efficient lookups -- Use materialized views for read-heavy scenarios - -### Example: Product Service Data Model (MongoDB) - -```javascript -// Product Collection -{ - "_id": ObjectId("5f8d0f3e1c9d440000c9a1f5"), - "name": "Ultra HD Smart TV", - "description": "55-inch Ultra HD Smart TV with voice control", - "sku": "TV-55UHD-2023", - "price": { - "amount": 699.99, - "currency": "USD" - }, - "category": "electronics", - "subcategory": "televisions", - "attributes": { - "brand": "TechVision", - "model": "UHD-55X", - "screenSize": "55", - "resolution": "3840x2160", - "smartFeatures": true, - "voiceControl": true, - "energyRating": "A+" - }, - "images": [ - { - "url": "https://storage.flowmart.com/products/tv-55uhd-2023-main.jpg", - "isPrimary": true, - "alt": "TechVision 55-inch Smart TV front view" - }, - { - "url": "https://storage.flowmart.com/products/tv-55uhd-2023-side.jpg", - "isPrimary": false, - "alt": "TechVision 55-inch Smart TV side view" - } - ], - "inventory": { - "inStock": 127, - "reserved": 13, - "available": 114, - "lowStockThreshold": 25, - "lastUpdated": ISODate("2023-09-15T14:23:45.000Z") - }, - "status": "active", - "createdAt": ISODate("2023-05-01T09:30:00.000Z"), - "updatedAt": ISODate("2023-09-15T14:23:45.000Z") -} - -// Review Collection -{ - "_id": ObjectId("5f8d0f3e1c9d440000c9a1f9"), - "productId": ObjectId("5f8d0f3e1c9d440000c9a1f5"), - "customerId": "cust-12345", - "rating": 4.5, - "title": "Great TV for the price", - "content": "The picture quality is excellent and setup was easy...", - "verified": true, - "helpfulVotes": 27, - "createdAt": ISODate("2023-06-15T18:22:10.000Z") -} -``` - -### Example: Order Service Data Model (PostgreSQL) - -```sql --- Orders Table -CREATE TABLE orders ( - id UUID PRIMARY KEY, - customer_id VARCHAR(255) NOT NULL, - order_number VARCHAR(50) NOT NULL UNIQUE, - status VARCHAR(50) NOT NULL, - total_amount DECIMAL(10, 2) NOT NULL, - currency VARCHAR(3) NOT NULL, - shipping_address_id UUID NOT NULL, - billing_address_id UUID NOT NULL, - payment_method_id UUID, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Order Items Table -CREATE TABLE order_items ( - id UUID PRIMARY KEY, - order_id UUID NOT NULL REFERENCES orders(id), - product_id VARCHAR(255) NOT NULL, - product_sku VARCHAR(100) NOT NULL, - product_name VARCHAR(255) NOT NULL, - quantity INTEGER NOT NULL, - unit_price DECIMAL(10, 2) NOT NULL, - subtotal DECIMAL(10, 2) NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Indexes -CREATE INDEX idx_orders_customer_id ON orders(customer_id); -CREATE INDEX idx_orders_status ON orders(status); -CREATE INDEX idx_order_items_order_id ON order_items(order_id); -CREATE INDEX idx_order_items_product_id ON order_items(product_id); -``` - -## Data Consistency Patterns - -### 1. Eventual Consistency with Event-Driven Updates - -At FlowMart, we embrace eventual consistency for most inter-service data coordination: - -```mermaid -sequenceDiagram - participant Order Service - participant Kafka - participant Inventory Service - participant Notification Service - - Order Service->>Order Service: Create Order - Order Service->>Kafka: Publish OrderCreated - Kafka->>Inventory Service: Consume OrderCreated - Inventory Service->>Inventory Service: Update Inventory - Kafka->>Notification Service: Consume OrderCreated - Notification Service->>Notification Service: Send Notification -``` - -### 2. Saga Pattern for Distributed Transactions - -For operations that span multiple services and require transactional semantics: - -```mermaid -sequenceDiagram - participant Order Service - participant Payment Service - participant Inventory Service - participant Shipping Service - - Order Service->>Payment Service: Process Payment - Payment Service-->>Order Service: Payment Processed - Order Service->>Inventory Service: Reserve Inventory - Inventory Service-->>Order Service: Inventory Reserved - Order Service->>Shipping Service: Create Shipment - Shipping Service-->>Order Service: Shipment Created - - alt Failure at any step - Note over Order Service,Shipping Service: Begin compensating transactions - Order Service->>Payment Service: Refund Payment - Order Service->>Inventory Service: Release Inventory - Order Service->>Shipping Service: Cancel Shipment - end -``` - -### 3. CQRS (Command Query Responsibility Segregation) - -For services with complex read patterns or high read-to-write ratios: - -``` -┌───────────────┐ Commands ┌───────────────┐ -│ │ ──────────────▶ │ │ -│ API Layer │ │ Command Model │ -│ │ ◀────────────── │ │ -└───────────────┘ └───────────────┘ - │ │ - │ │ - │ ▼ - │ ┌───────────────┐ - │ │ │ - │ │ Event Store │ - │ │ │ - │ └───────────────┘ - │ │ - │ │ - ▼ ▼ -┌───────────────┐ ┌───────────────┐ -│ │ │ │ -│ Query Model │ ◀────────────── │ Event Handler │ -│ │ │ │ -└───────────────┘ └───────────────┘ -``` - -## Data Migration Patterns - -### 1. Schema Evolution - -For incremental changes to a service's schema: - -- Add new fields with default values -- Make changes backward compatible -- Use database migration tools (e.g., Flyway, Liquibase, MongoDB migrations) - -Example migration script (Flyway): - -```sql --- V2023.09.15.1__Add_Order_Tracking.sql -ALTER TABLE orders ADD COLUMN tracking_number VARCHAR(100); -ALTER TABLE orders ADD COLUMN shipping_carrier VARCHAR(50); -``` - -### 2. Expand-Contract Pattern (Parallel Change) - -For significant schema changes: - -1. **Expand**: Add new fields/tables while maintaining old ones -2. **Migrate**: Copy/transform data from old to new structure -3. **Contract**: Remove old fields/tables after migration is complete - -### 3. Service Decomposition - -When splitting a monolithic database: - -1. Identify bounded contexts -2. Create new service databases -3. Implement dual-write mechanism temporarily -4. Migrate historical data -5. Switch reads to the new database -6. Remove old tables after migration completes - -## Database Access Patterns - -### Repository Pattern - -Our standard approach for database access: - -```typescript -// Product Repository Interface -export interface ProductRepository { - findById(id: string): Promise; - findByCategory(category: string, limit?: number, offset?: number): Promise; - findByQuery(query: ProductQuery): Promise; - save(product: Product): Promise; - update(id: string, updates: Partial): Promise; - delete(id: string): Promise; -} - -// MongoDB Implementation -export class MongoProductRepository implements ProductRepository { - constructor(private readonly db: Db) {} - - async findById(id: string): Promise { - const result = await this.db.collection('products').findOne({ _id: new ObjectId(id) }); - return result ? this.mapToProduct(result) : null; - } - - // Additional implementation methods... -} - -// PostgreSQL Implementation -export class PostgresProductRepository implements ProductRepository { - constructor(private readonly pool: Pool) {} - - async findById(id: string): Promise { - const result = await this.pool.query( - 'SELECT * FROM products WHERE id = $1', - [id] - ); - return result.rows.length ? this.mapToProduct(result.rows[0]) : null; - } - - // Additional implementation methods... -} -``` - -## Connection Management - -### Connection Pooling - -```javascript -// MongoDB Connection Pool (Node.js) -const { MongoClient } = require('mongodb'); - -class MongoDbClient { - constructor(config) { - this.client = new MongoClient(config.uri, { - maxPoolSize: config.maxPoolSize || 10, - minPoolSize: config.minPoolSize || 5, - maxIdleTimeMS: config.maxIdleTimeMS || 30000, - connectTimeoutMS: config.connectTimeoutMS || 5000 - }); - } - - async connect() { - await this.client.connect(); - this.db = this.client.db(config.dbName); - console.log('Connected to MongoDB'); - return this.db; - } - - async disconnect() { - await this.client.close(); - console.log('Disconnected from MongoDB'); - } -} -``` - -### Health Checks - -```typescript -// Database Health Check (TypeScript) -export class DatabaseHealthCheck { - constructor(private readonly dbClient: DbClient) {} - - async check(): Promise { - try { - const startTime = Date.now(); - await this.dbClient.ping(); - const responseTime = Date.now() - startTime; - - return { - status: 'UP', - responseTime, - details: { - database: this.dbClient.getDatabaseName(), - connections: await this.dbClient.getConnectionStats() - } - }; - } catch (error) { - return { - status: 'DOWN', - error: error.message, - details: { - database: this.dbClient.getDatabaseName() - } - }; - } - } -} -``` - -## Database Security Practices - -1. **Use IAM Roles/Service Accounts**: Avoid hardcoded credentials -2. **Encryption**: Enable at-rest and in-transit encryption -3. **No Direct Public Access**: Databases should never be directly exposed to the internet -4. **Least Privilege**: Grant minimal required permissions to service accounts -5. **Data Classification**: Tag data according to sensitivity -6. **Audit Logging**: Enable database auditing for sensitive operations -7. **Regular Backups**: Implement automated backup strategies -8. **Secure Connection Strings**: Store connection information in secure vaults - -## Technology-Specific Guidelines - -### MongoDB Best Practices - -- Use document validation for schema enforcement -- Create indexes for frequent query patterns -- Limit document size (< 16MB) -- Use aggregation pipelines for complex queries -- Implement TTL indexes for expiring data - -### PostgreSQL Best Practices - -- Use connection pooling -- Implement database partitioning for large tables -- Create appropriate indexes based on query patterns -- Use prepared statements to prevent SQL injection -- Implement row-level security for multi-tenant data - -### Redis Best Practices - -- Set appropriate key expiration times -- Use Redis data structures (not just key-value) -- Implement Redis Cluster for high availability -- Monitor memory usage -- Use Redis for its strengths: caching, rate limiting, session storage - -## Infrastructure as Code for Databases - -FlowMart provides Terraform modules for common database setups: - -```hcl -# MongoDB Atlas Cluster -module "mongodb_cluster" { - source = "git::https://gitlab.flowmart.com/platform/terraform-modules//mongodb-atlas" - - project_id = var.atlas_project_id - cluster_name = "${var.service_name}-${var.environment}" - environment = var.environment - instance_size = var.environment == "production" ? "M10" : "M0" - region = var.region - - backup_enabled = var.environment == "production" ? true : false - - teams = { - owner = "team-product", - developer = "team-product-dev" - } - - tags = { - Service = var.service_name - Environment = var.environment - ManagedBy = "terraform" - } -} - -# PostgreSQL RDS Instance -module "postgres_db" { - source = "git::https://gitlab.flowmart.com/platform/terraform-modules//aws-rds-postgres" - - identifier = "${var.service_name}-${var.environment}" - allocated_storage = var.environment == "production" ? 100 : 20 - storage_type = "gp2" - engine_version = "13.4" - instance_class = var.environment == "production" ? "db.m5.large" : "db.t3.small" - database_name = replace(var.service_name, "-", "_") - vpc_id = var.vpc_id - subnet_ids = var.database_subnet_ids - multi_az = var.environment == "production" ? true : false - deletion_protection = var.environment == "production" ? true : false - - backup_retention_period = var.environment == "production" ? 7 : 1 - - tags = { - Service = var.service_name - Environment = var.environment - ManagedBy = "terraform" - } -} -``` - -## Data Governance - -### Practices for Maintaining Data Integrity - -1. **Schema Registry**: Register and validate event schemas -2. **Data Catalogs**: Document available data and owners -3. **Data Lineage**: Track how data flows between services -4. **Data Quality Checks**: Implement validation and integrity checks -5. **Master Data Management**: Establish source-of-truth services - -### GDPR and Compliance - -1. **Data Minimization**: Collect only necessary data -2. **Right to Erasure**: Implement mechanisms to delete customer data -3. **Data Portability**: Support data export in common formats -4. **Consent Management**: Track and honor user consent -5. **Purpose Limitation**: Use data only for its intended purpose - -## Recommended Resources - -- **Books**: - - "Database Internals" by Alex Petrov - - "Designing Data-Intensive Applications" by Martin Kleppmann - -- **Courses**: - - MongoDB University - - PostgreSQL Administration by EnterpriseDB - -- **FlowMart Resources**: - - Internal Database Patterns Playbook - - Monthly Database Office Hours - -## Next Steps - -- Learn about [Event schema design](/docs/guides/creating-new-microservices/event-schemas) -- Explore [API design best practices](/docs/guides/creating-new-microservices/api-design) -- Understand [Observability in microservices](/docs/guides/creating-new-microservices/observability) \ No newline at end of file diff --git a/examples/default/docs/guides/creating-new-microservices/04-event-schemas.mdx b/examples/default/docs/guides/creating-new-microservices/04-event-schemas.mdx deleted file mode 100644 index 9e6807d42..000000000 --- a/examples/default/docs/guides/creating-new-microservices/04-event-schemas.mdx +++ /dev/null @@ -1,894 +0,0 @@ ---- -title: Event Schema Design -summary: Best practices for designing and managing event schemas in FlowMart's event-driven architecture -sidebar: - label: Event Schema Design - order: 6 ---- - -This guide outlines best practices for designing, evolving, and managing event schemas in FlowMart's event-driven architecture. - -## Introduction to Events in Our Architecture - -Events are the backbone of FlowMart's microservices ecosystem. They enable: - -- **Loose coupling** between services -- **Asynchronous communication** -- **Eventual consistency** across service boundaries -- **Event sourcing** for critical business processes -- **Audit trails** of system changes - -## Event Schema Fundamentals - -### What Is an Event Schema? - -An event schema defines the structure and validation rules for events flowing through our system. Properly designed schemas ensure events can be: - -- **Produced** consistently by services -- **Consumed** reliably by other services -- **Evolved** over time without breaking consumers -- **Validated** to prevent invalid data from propagating -- **Documented** for developers to understand and use - -### Event Schema Registry - -FlowMart uses a centralized **Schema Registry** to: - -1. Store all event schemas -2. Validate events at publish time -3. Provide a browsable catalog of events -4. Track schema versions and compatibility -5. Generate client libraries and documentation - -All services must register their event schemas in the central registry before publishing events. - -## Schema Design Principles - -### 1. Design for Evolution - -Events should be designed to evolve over time: - -- **Additive Changes Only**: Add optional fields rather than modifying existing ones -- **Required Minimal Core**: Keep required fields to essential business data -- **Meaningful Defaults**: Provide sensible defaults for optional fields -- **Version Awareness**: Include schema version information - -### 2. Event Ownership - -Each event type has a single owner: - -- The **producing service** owns the event schema -- Only the owner can make changes to the schema -- The owner is responsible for schema compatibility - -### 3. Semantic Versioning - -Follow semantic versioning for event schemas: - -- **Major Version**: Breaking changes (consumers must update) -- **Minor Version**: Backward-compatible feature additions -- **Patch Version**: Backward-compatible bug fixes - -### 4. Business-Oriented Event Naming - -Events should be named using business terminology: - -- Use past tense verbs (e.g., `OrderPlaced`, not `CreateOrder`) -- Follow the pattern: `[Entity][Event]` (e.g., `ProductCreated`, `PaymentProcessed`) -- Use domain-specific terminology consistent with our ubiquitous language - -## Event Schema Format (JSON Schema) - -FlowMart uses JSON Schema as the standard format for defining event schemas: - -```json -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://schemas.flowmart.com/events/product/ProductCreated/1.0.0", - "title": "ProductCreated", - "description": "Represents the creation of a new product in the catalog", - "type": "object", - "required": ["eventId", "eventType", "eventVersion", "timestamp", "data"], - "properties": { - "eventId": { - "type": "string", - "format": "uuid", - "description": "Unique identifier for this event instance" - }, - "eventType": { - "type": "string", - "enum": ["ProductCreated"], - "description": "Type of the event" - }, - "eventVersion": { - "type": "string", - "pattern": "^\\d+\\.\\d+\\.\\d+$", - "description": "Semantic version of the event schema" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "ISO-8601 timestamp when the event was created" - }, - "source": { - "type": "string", - "description": "Service that produced the event" - }, - "data": { - "type": "object", - "required": ["productId", "name", "sku", "price"], - "properties": { - "productId": { - "type": "string", - "format": "uuid", - "description": "Unique identifier for the product" - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "description": "Name of the product" - }, - "description": { - "type": "string", - "maxLength": 2000, - "description": "Description of the product" - }, - "sku": { - "type": "string", - "pattern": "^[A-Z0-9-]{5,20}$", - "description": "Stock keeping unit - unique product identifier" - }, - "price": { - "type": "object", - "required": ["amount", "currency"], - "properties": { - "amount": { - "type": "number", - "exclusiveMinimum": 0, - "description": "Price amount" - }, - "currency": { - "type": "string", - "enum": ["USD", "EUR", "GBP", "CAD"], - "description": "Price currency code" - } - } - }, - "categories": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Categories the product belongs to" - }, - "attributes": { - "type": "object", - "additionalProperties": { - "type": ["string", "number", "boolean"] - }, - "description": "Additional product attributes as key-value pairs" - } - } - }, - "metadata": { - "type": "object", - "additionalProperties": true, - "description": "Additional contextual information about the event" - }, - "correlationId": { - "type": "string", - "format": "uuid", - "description": "ID for correlating related events" - }, - "causationId": { - "type": "string", - "format": "uuid", - "description": "ID of the event that caused this event" - } - }, - "additionalProperties": false -} -``` - -## Standard Event Envelope - -All FlowMart events follow a standard envelope structure: - -```json -{ - "eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "eventType": "ProductCreated", - "eventVersion": "1.0.0", - "timestamp": "2023-09-15T13:25:47.803Z", - "source": "product-service", - "data": { - // Event-specific payload - }, - "metadata": { - // Optional contextual information - }, - "correlationId": "7f8d0e3c-d5f9-42e1-a11b-78ad6c0c380a", - "causationId": "3e4f5d6c-7b8a-9c0d-1e2f-3a4b5c6d7e8f" -} -``` - -### Required Envelope Fields - -| Field | Type | Description | -|-------|------|-------------| -| eventId | UUID | Unique identifier for the event instance | -| eventType | String | Name of the event (e.g., `ProductCreated`) | -| eventVersion | String | Semantic version of the event schema | -| timestamp | ISO-8601 | When the event occurred | -| data | Object | Event-specific payload | - -### Optional Envelope Fields - -| Field | Type | Description | -|-------|------|-------------| -| source | String | Service that produced the event | -| metadata | Object | Additional context about the event | -| correlationId | UUID | ID for correlating related events in a flow | -| causationId | UUID | ID of the event that caused this event | - -## Event Data Types - -### Primitive Types - -- **String**: Use for text data -- **Number**: Use for numeric values (integers or decimals) -- **Boolean**: Use for true/false flags -- **Array**: Use for collections of the same type -- **Object**: Use for nested structures - -### Specialized Formats - -- **UUID**: Use for unique identifiers (format: `uuid`) -- **ISO Date-Time**: Use for timestamps (format: `date-time`) -- **Email**: Use for email addresses (format: `email`) -- **URI**: Use for web addresses (format: `uri`) -- **Decimal**: Use for currency amounts (type: `number`) - -### Complex Types - -For complex or reusable types, create separate schema definitions: - -```json -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://schemas.flowmart.com/common/Address/1.0.0", - "title": "Address", - "type": "object", - "required": ["line1", "city", "postalCode", "country"], - "properties": { - "line1": { - "type": "string", - "maxLength": 100 - }, - "line2": { - "type": "string", - "maxLength": 100 - }, - "city": { - "type": "string", - "maxLength": 100 - }, - "region": { - "type": "string", - "maxLength": 100 - }, - "postalCode": { - "type": "string", - "maxLength": 20 - }, - "country": { - "type": "string", - "maxLength": 2, - "pattern": "^[A-Z]{2}$" - } - } -} -``` - -Then reference them in your event schemas: - -```json -{ - "properties": { - "shippingAddress": { - "$ref": "https://schemas.flowmart.com/common/Address/1.0.0" - } - } -} -``` - -## Common Event Patterns - -### State Change Events - -Represent changes to an entity's state: - -```json -{ - "eventType": "OrderStatusChanged", - "data": { - "orderId": "61fea0a1-2ac4-4e8c-a851-d38f7c8c06f9", - "previousStatus": "PAYMENT_PENDING", - "newStatus": "PAYMENT_COMPLETED", - "reason": "Payment successful", - "changedBy": "payment-service" - } -} -``` - -### Resource Creation Events - -Represent the creation of a new entity: - -```json -{ - "eventType": "CustomerCreated", - "data": { - "customerId": "cust-12345", - "email": "john.doe@example.com", - "firstName": "John", - "lastName": "Doe", - "createdAt": "2023-09-15T10:30:00Z" - } -} -``` - -### Resource Update Events - -Represent updates to an existing entity: - -```json -{ - "eventType": "ProductUpdated", - "data": { - "productId": "b3c631a5-f7c8-4d89-a57f-dd2f069b5730", - "changes": { - "price": { - "amount": 24.99, - "currency": "USD" - }, - "inventory": { - "inStock": 250 - } - }, - "updatedAt": "2023-09-15T14:22:36Z" - } -} -``` - -### Action Events - -Represent business actions that occurred: - -```json -{ - "eventType": "PaymentProcessed", - "data": { - "paymentId": "pay-67890", - "orderId": "ord-12345", - "amount": 99.99, - "currency": "USD", - "status": "SUCCESSFUL", - "paymentMethod": "CREDIT_CARD", - "processedAt": "2023-09-15T13:45:22Z" - } -} -``` - -## Schema Evolution - -### Compatibility Types - -We support the following compatibility modes for schema evolution: - -- **Backward**: New schema can read data produced with previous schema -- **Forward**: Previous schema can read data produced with new schema -- **Full**: Both backward and forward compatibility -- **None**: No compatibility guarantees (use with caution) - -### Backward Compatibility Rules - -1. **Adding optional fields** is safe -2. **Removing optional fields** is safe -3. **Making a required field optional** is safe -4. **Adding new enum values** is safe -5. **Widening numeric ranges** is safe (e.g., int to float) - -### Breaking Changes to Avoid - -1. ❌ **Removing required fields** -2. ❌ **Adding required fields** -3. ❌ **Changing field types** -4. ❌ **Renaming fields** -5. ❌ **Restricting enum values** - -### Handling Breaking Changes - -If you must make a breaking change: - -1. Create a new major version of the schema -2. Maintain both versions for a transition period -3. Implement dual publishing for critical events -4. Help consumers migrate to the new version -5. Deprecate the old version with advance notice - -## Consuming Events - -### Consumer Best Practices - -1. **Be tolerant in what you accept**: - - Ignore unknown fields - - Provide defaults for missing optional fields - - Handle enum values gracefully, including unknown values - -2. **Validate incoming events**: - - Verify events against their schema - - Check required fields - - Validate business rules before processing - -3. **Handle versioning gracefully**: - - Check event version before processing - - Implement version-specific handlers if needed - - Subscribe to schema registry updates - -### Consumer Code Example (TypeScript) - -```typescript -import { KafkaConsumer } from '@flowmart/kafka-client'; -import { SchemaRegistry } from '@flowmart/schema-registry'; -import { OrderProcessingService } from './services'; - -// Initialize schema registry client -const schemaRegistry = new SchemaRegistry({ - baseUrl: 'https://schema-registry.flowmart.com', -}); - -// Define event interface -interface OrderPlacedEvent { - eventId: string; - eventType: 'OrderPlaced'; - eventVersion: string; - timestamp: string; - data: { - orderId: string; - customerId: string; - items: Array<{ - productId: string; - quantity: number; - unitPrice: number; - }>; - totalAmount: number; - currency: string; - shippingAddress: { - // Address fields... - }; - }; - // Other envelope fields... -} - -// Initialize consumer -const orderConsumer = new KafkaConsumer({ - groupId: 'inventory-service', - brokers: ['kafka-1:9092', 'kafka-2:9092', 'kafka-3:9092'], -}); - -// Initialize service -const orderProcessor = new OrderProcessingService(); - -// Start consuming events -async function startConsumer() { - await orderConsumer.subscribe('order-events'); - - orderConsumer.on('message', async (message) => { - try { - // Parse the message - const rawEvent = JSON.parse(message.value.toString()); - - // Skip if not the event we're interested in - if (rawEvent.eventType !== 'OrderPlaced') { - return; - } - - // Validate against schema - const isValid = await schemaRegistry.validate( - rawEvent, - 'OrderPlaced', - rawEvent.eventVersion - ); - - if (!isValid) { - console.error('Invalid event schema', rawEvent); - return; - } - - // Type-safe processing - const event = rawEvent as OrderPlacedEvent; - - // Process the order - await orderProcessor.processNewOrder(event.data); - - // Commit the offset - await orderConsumer.commitOffset(message); - - } catch (error) { - console.error('Error processing order event', error); - // Implement retry/dead-letter logic - } - }); -} - -startConsumer().catch(console.error); -``` - -## Publishing Events - -### Producer Best Practices - -1. **Validate before publishing**: - - Ensure events comply with their schema - - Verify business rules and data integrity - - Set appropriate event headers - -2. **Include essential metadata**: - - Generate a unique event ID - - Set the correct event type and version - - Include accurate timestamp - - Set correlation and causation IDs - -3. **Handle publishing failures**: - - Implement retry mechanisms with backoff - - Store events temporarily if Kafka is unavailable - - Log failed events for troubleshooting - -### Producer Code Example (TypeScript) - -```typescript -import { v4 as uuid } from 'uuid'; -import { KafkaProducer } from '@flowmart/kafka-client'; -import { SchemaRegistry } from '@flowmart/schema-registry'; -import { Product } from './models'; - -// Initialize schema registry client -const schemaRegistry = new SchemaRegistry({ - baseUrl: 'https://schema-registry.flowmart.com', -}); - -// Initialize producer -const producer = new KafkaProducer({ - clientId: 'product-service', - brokers: ['kafka-1:9092', 'kafka-2:9092', 'kafka-3:9092'], -}); - -export class ProductEventService { - async publishProductCreated(product: Product, correlationId?: string): Promise { - const eventId = uuid(); - - const event = { - eventId, - eventType: 'ProductCreated', - eventVersion: '1.0.0', - timestamp: new Date().toISOString(), - source: 'product-service', - data: { - productId: product.id, - name: product.name, - summary: product.description || null, - sku: product.sku, - price: { - amount: product.price, - currency: 'USD' // Default to USD - }, - categories: product.categories || [], - attributes: product.attributes || {} - }, - metadata: { - // Add any additional metadata - }, - correlationId: correlationId || eventId, - causationId: null // No previous event caused this - }; - - // Validate against schema - const isValid = await schemaRegistry.validate( - event, - 'ProductCreated', - '1.0.0' - ); - - if (!isValid) { - const errors = await schemaRegistry.getValidationErrors( - event, - 'ProductCreated', - '1.0.0' - ); - throw new Error(`Invalid event schema: ${JSON.stringify(errors)}`); - } - - // Publish event - await producer.send({ - topic: 'product-events', - messages: [ - { - key: product.id, - value: JSON.stringify(event), - headers: { - 'eventType': 'ProductCreated', - 'contentType': 'application/json' - } - } - ] - }); - - console.log(`Published ProductCreated event: ${eventId}`); - } -} -``` - -## Schema Registry Integration - -### Registering a New Schema - -```bash -# Using CLI tool -flowmart-schema register \ - --file ./schemas/ProductCreated.json \ - --compatibility BACKWARD - -# API endpoint -curl -X POST https://schema-registry.flowmart.com/subjects/ProductCreated/versions \ - -H "Content-Type: application/json" \ - -d @./schemas/ProductCreated.json -``` - -### Retrieving a Schema - -```javascript -// Using JavaScript client -const schema = await schemaRegistry.getSchema('ProductCreated', '1.0.0'); - -// API endpoint -curl https://schema-registry.flowmart.com/subjects/ProductCreated/versions/latest -``` - -### Checking Compatibility - -```javascript -// Using JavaScript client -const isCompatible = await schemaRegistry.checkCompatibility( - newSchema, - 'ProductCreated' -); - -// API endpoint -curl -X POST https://schema-registry.flowmart.com/compatibility/subjects/ProductCreated/versions/latest \ - -H "Content-Type: application/json" \ - -d @./schemas/ProductCreated.v2.json -``` - -## Schema Registry UI - -Our Schema Registry includes a web interface at [https://schema-registry.flowmart.com/ui](https://schema-registry.flowmart.com/ui) that provides: - -- Browsable catalog of all event schemas -- Schema versioning history -- Compatibility information -- Schema validation tools -- Documentation generation - -## Testing Event Schemas - -### Unit Testing Schemas - -```javascript -import { validateAgainstSchema } from '@flowmart/schema-validator'; -import productCreatedSchema from './schemas/ProductCreated.json'; - -describe('ProductCreated schema', () => { - it('validates valid events', () => { - const validEvent = { - eventId: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', - eventType: 'ProductCreated', - eventVersion: '1.0.0', - timestamp: '2023-09-15T13:25:47.803Z', - data: { - productId: 'b3c631a5-f7c8-4d89-a57f-dd2f069b5730', - name: 'Smartphone X Pro', - sku: 'SP-XPRO-2023', - price: { - amount: 999.99, - currency: 'USD' - } - } - }; - - const result = validateAgainstSchema(validEvent, productCreatedSchema); - expect(result.valid).toBe(true); - }); - - it('rejects events with missing required fields', () => { - const invalidEvent = { - eventId: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', - eventType: 'ProductCreated', - eventVersion: '1.0.0', - timestamp: '2023-09-15T13:25:47.803Z', - data: { - // Missing required productId - name: 'Smartphone X Pro', - // Missing required sku - price: { - amount: 999.99, - currency: 'USD' - } - } - }; - - const result = validateAgainstSchema(invalidEvent, productCreatedSchema); - expect(result.valid).toBe(false); - expect(result.errors.length).toBeGreaterThan(0); - }); -}); -``` - -### Integration Testing with Schema Registry - -```javascript -describe('Schema Registry Integration', () => { - it('registers and validates schema', async () => { - // Register test schema - await schemaRegistry.registerSchema( - 'TestEvent', - testEventSchema, - 'BACKWARD' - ); - - // Create test event - const testEvent = { - eventId: uuid(), - eventType: 'TestEvent', - eventVersion: '1.0.0', - timestamp: new Date().toISOString(), - data: { - // Test data... - } - }; - - // Validate against registered schema - const isValid = await schemaRegistry.validate( - testEvent, - 'TestEvent', - '1.0.0' - ); - - expect(isValid).toBe(true); - }); -}); -``` - -## Event Documentation - -### Self-Documenting Schemas - -Use descriptive fields in your JSON Schema to auto-generate documentation: - -```json -{ - "title": "ProductCreated", - "description": "Published when a new product is created in the catalog", - "properties": { - "data": { - "properties": { - "productId": { - "description": "Unique identifier for the product", - "examples": ["p-12345"] - } - } - } - } -} -``` - -### Documentation in Code - -Document event handling with clear comments: - -```typescript -/** - * Handles the ProductCreated event - * This event is triggered when a new product is added to the catalog. - * It updates the inventory service with the new product information. - * - * @param event The ProductCreated event - * @see https://schema-registry.flowmart.com/ui/schemas/ProductCreated/1.0.0 - */ -async function handleProductCreated(event: ProductCreatedEvent): Promise { - // Implementation... -} -``` - -## Event Tracing and Debugging - -### Correlation IDs - -Use correlation IDs to trace requests across services: - -```typescript -// When handling an API request -const correlationId = req.headers['x-correlation-id'] || uuid(); - -// Include in all events -const event = { - // Other event fields... - correlationId, - // If this event was caused by another event - causationId: previousEvent?.eventId -}; -``` - -### Event Logging - -Log event publishing and consumption with consistent format: - -```typescript -// Producer logging -logger.info('Publishing event', { - eventId: event.eventId, - eventType: event.eventType, - correlationId: event.correlationId -}); - -// Consumer logging -logger.info('Consuming event', { - eventId: event.eventId, - eventType: event.eventType, - correlationId: event.correlationId, - consumer: 'inventory-service' -}); -``` - -## Event Monitoring - -Monitor your event streams using our standard observability stack: - -1. **Kafka Metrics**: Lag, throughput, errors -2. **Schema Registry Metrics**: Validation failures, compatibility checks -3. **Service Metrics**: Event processing times, failure rates -4. **Custom Dashboards**: Domain-specific event flows - -Access dashboards at [https://grafana.flowmart.com/d/events](https://grafana.flowmart.com/d/events) - -## Event Schema Governance - -### Change Management - -1. **Proposal**: Document the schema change with rationale -2. **Review**: Domain experts review for business requirements -3. **Compatibility Check**: Verify with schema registry -4. **Approval**: Get sign-off from service team leads -5. **Publication**: Register schema and announce change - -### Schema Review Checklist - -✅ Schema follows naming conventions -✅ Required fields are truly necessary -✅ Field types are appropriate -✅ Enums have complete value lists -✅ Constraints (min, max, etc.) are appropriate -✅ Documentation is complete -✅ Versioning follows semantic versioning -✅ Compatibility type is specified - -## Conclusion - -Well-designed event schemas are foundational to reliable event-driven systems. Following FlowMart's event schema guidelines ensures our services can communicate reliably today and evolve confidently tomorrow. - -## Next Steps - -- Explore [API design best practices](/docs/guides/creating-new-microservices/api-design) -- Understand [Observability in microservices](/docs/guides/creating-new-microservices/observability) -- Learn about [CI/CD for microservices](/docs/guides/creating-new-microservices/cicd-pipeline) \ No newline at end of file diff --git a/examples/default/docs/guides/creating-new-microservices/06-typescript-service.mdx b/examples/default/docs/guides/creating-new-microservices/06-typescript-service.mdx deleted file mode 100644 index 1a7400907..000000000 --- a/examples/default/docs/guides/creating-new-microservices/06-typescript-service.mdx +++ /dev/null @@ -1,959 +0,0 @@ ---- -title: New TypeScript service -summary: Guide to implementing microservices using TypeScript at FlowMart -sidebar: - label: TypeScript Service - order: 4 ---- - -This guide details the recommended patterns, practices, and tools for implementing TypeScript-based microservices at FlowMart. - - -## Why TypeScript? - -At FlowMart, we recommend TypeScript for new microservices because it offers: - -- **Type Safety**: Catch errors during development instead of at runtime -- **Better IDE Support**: Enhanced auto-completion, navigation, and refactoring -- **Self-Documenting Code**: Types serve as documentation for your codebase -- **Enterprise-Ready**: Better maintainability for large codebases and teams -- **Ecosystem Compatibility**: Full access to the Node.js ecosystem - -## Prerequisites - -Before you begin: - -- Install Node.js (v18 or later) -- Familiarize yourself with [our Node.js service guide](/docs/guides/creating-new-microservices/node-service) -- Have basic TypeScript knowledge - -## Scaffolding a TypeScript Service - -Use our service generator to create a TypeScript service: - -```bash -# Install the FlowMart service generator -npm install -g @flowmart/service-generator - -# Create a new TypeScript service -flowmart-create-service my-service --type typescript -``` - -## TypeScript Project Structure - -The generated service follows our standard TypeScript structure: - -``` -my-service/ -├── src/ -│ ├── api/ # API definition and controllers -│ ├── config/ # Configuration management -│ ├── domain/ # Domain models and business logic -│ │ ├── models/ # Domain entities and value objects -│ │ └── services/ # Domain services -│ ├── events/ # Event producers and consumers -│ ├── infrastructure/ # External dependencies and adapters -│ ├── repositories/ # Data access layer -│ ├── types/ # TypeScript type definitions -│ ├── utils/ # Utility functions -│ └── app.ts # Application entry point -├── test/ -│ ├── unit/ # Unit tests -│ ├── integration/ # Integration tests -│ └── contract/ # Contract tests -├── terraform/ # Infrastructure as code -├── .github/ # GitHub Actions workflows -├── Dockerfile # Container definition -├── docker-compose.yml # Local development setup -├── tsconfig.json # TypeScript configuration -├── package.json # Dependencies and scripts -└── README.md # Service documentation -``` - -## TypeScript Configuration - -Our template includes a pre-configured `tsconfig.json`: - -```json -{ - "compilerOptions": { - "target": "ES2020", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2020"], - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "sourceMap": true, - "declaration": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "test"] -} -``` - -## Domain Modeling with TypeScript - -TypeScript enables us to model our domain with strong types: - -### Example Domain Model - -```typescript -// src/domain/models/product.ts -export enum ProductCategory { - ELECTRONICS = 'electronics', - CLOTHING = 'clothing', - GROCERY = 'grocery', - HOME = 'home', - BEAUTY = 'beauty' -} - -export interface ProductAttributes { - [key: string]: string | number | boolean; -} - -export class Product { - constructor( - public readonly id: string, - public name: string, - public summary: string, - public price: number, - public category: ProductCategory, - public sku: string, - public inventoryCount: number, - public attributes: ProductAttributes = {}, - public isActive: boolean = true, - public createdAt: Date = new Date(), - public updatedAt: Date = new Date() - ) {} - - updateInventory(count: number): void { - if (count < 0) { - throw new Error('Inventory count cannot be negative'); - } - this.inventoryCount = count; - this.updatedAt = new Date(); - } - - updatePrice(price: number): void { - if (price < 0) { - throw new Error('Price cannot be negative'); - } - this.price = price; - this.updatedAt = new Date(); - } - - deactivate(): void { - this.isActive = false; - this.updatedAt = new Date(); - } - - activate(): void { - this.isActive = true; - this.updatedAt = new Date(); - } - - isInStock(): boolean { - return this.inventoryCount > 0; - } -} -``` - -## Repository Pattern with TypeScript - -Use interfaces to define repositories: - -```typescript -// src/repositories/product-repository.ts -import { Product } from '../domain/models/product'; - -export interface ProductRepository { - findById(id: string): Promise; - findAll(limit?: number, offset?: number): Promise; - findByCategory(category: string, limit?: number, offset?: number): Promise; - save(product: Product): Promise; - update(product: Product): Promise; - delete(id: string): Promise; -} - -// src/repositories/mongodb-product-repository.ts -import { Collection, MongoClient, ObjectId } from 'mongodb'; -import { Product, ProductCategory } from '../domain/models/product'; -import { ProductRepository } from './product-repository'; -import { logger } from '../infrastructure/observability'; - -export class MongoDBProductRepository implements ProductRepository { - private collection: Collection; - - constructor(client: MongoClient) { - this.collection = client.db('ecommerce').collection('products'); - } - - async findById(id: string): Promise { - try { - const result = await this.collection.findOne({ _id: new ObjectId(id) }); - if (!result) return null; - return this.mapToProduct(result); - } catch (error) { - logger.error('Error finding product by id', { error, id }); - throw error; - } - } - - async findAll(limit = 100, offset = 0): Promise { - try { - const results = await this.collection - .find({}) - .skip(offset) - .limit(limit) - .toArray(); - return results.map(this.mapToProduct); - } catch (error) { - logger.error('Error finding all products', { error }); - throw error; - } - } - - async findByCategory(category: string, limit = 100, offset = 0): Promise { - try { - const results = await this.collection - .find({ category }) - .skip(offset) - .limit(limit) - .toArray(); - return results.map(this.mapToProduct); - } catch (error) { - logger.error('Error finding products by category', { error, category }); - throw error; - } - } - - async save(product: Product): Promise { - try { - const productDoc = { - name: product.name, - summary: product.description, - price: product.price, - category: product.category, - sku: product.sku, - inventoryCount: product.inventoryCount, - attributes: product.attributes, - isActive: product.isActive, - createdAt: product.createdAt, - updatedAt: product.updatedAt - }; - - const result = await this.collection.insertOne(productDoc); - return { - ...product, - id: result.insertedId.toString() - }; - } catch (error) { - logger.error('Error saving product', { error, product }); - throw error; - } - } - - async update(product: Product): Promise { - try { - const result = await this.collection.updateOne( - { _id: new ObjectId(product.id) }, - { - $set: { - name: product.name, - summary: product.description, - price: product.price, - category: product.category, - sku: product.sku, - inventoryCount: product.inventoryCount, - attributes: product.attributes, - isActive: product.isActive, - updatedAt: product.updatedAt - } - } - ); - - if (result.matchedCount === 0) { - throw new Error(`Product with id ${product.id} not found`); - } - - return product; - } catch (error) { - logger.error('Error updating product', { error, productId: product.id }); - throw error; - } - } - - async delete(id: string): Promise { - try { - const result = await this.collection.deleteOne({ _id: new ObjectId(id) }); - return result.deletedCount === 1; - } catch (error) { - logger.error('Error deleting product', { error, id }); - throw error; - } - } - - private mapToProduct(doc: any): Product { - return new Product( - doc._id.toString(), - doc.name, - doc.description, - doc.price, - doc.category as ProductCategory, - doc.sku, - doc.inventoryCount, - doc.attributes, - doc.isActive, - new Date(doc.createdAt), - new Date(doc.updatedAt) - ); - } -} -``` - -## Type-safe API Controllers - -TypeScript enables type-safe API controllers with Express: - -```typescript -// src/api/controllers/product-controller.ts -import { Router, Request, Response, NextFunction } from 'express'; -import { ProductService } from '../../domain/services/product-service'; -import { validateProduct } from '../middleware/product-validator'; -import { tracing } from '../../infrastructure/observability'; -import { Product, ProductCategory } from '../../domain/models/product'; - -export interface CreateProductRequest { - name: string; - summary: string; - price: number; - category: ProductCategory; - sku: string; - inventoryCount: number; - attributes?: { [key: string]: string | number | boolean }; -} - -export interface UpdateProductRequest { - name?: string; - description?: string; - price?: number; - category?: ProductCategory; - sku?: string; - inventoryCount?: number; - attributes?: { [key: string]: string | number | boolean }; - isActive?: boolean; -} - -export class ProductController { - private router: Router; - - constructor(private productService: ProductService) { - this.router = Router(); - this.setupRoutes(); - } - - private setupRoutes(): void { - this.router.get('/', tracing.middleware('get-all-products'), this.getAllProducts.bind(this)); - this.router.get('/:id', tracing.middleware('get-product'), this.getProductById.bind(this)); - this.router.post('/', tracing.middleware('create-product'), validateProduct, this.createProduct.bind(this)); - this.router.put('/:id', tracing.middleware('update-product'), this.updateProduct.bind(this)); - this.router.delete('/:id', tracing.middleware('delete-product'), this.deleteProduct.bind(this)); - } - - getRouter(): Router { - return this.router; - } - - private async getAllProducts(req: Request, res: Response, next: NextFunction): Promise { - try { - const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 100; - const offset = req.query.offset ? parseInt(req.query.offset as string, 10) : 0; - const products = await this.productService.getAllProducts(limit, offset); - res.json(products); - } catch (error) { - next(error); - } - } - - private async getProductById(req: Request, res: Response, next: NextFunction): Promise { - try { - const product = await this.productService.getProductById(req.params.id); - if (!product) { - res.status(404).json({ error: 'Product not found' }); - return; - } - res.json(product); - } catch (error) { - next(error); - } - } - - private async createProduct(req: Request, res: Response, next: NextFunction): Promise { - try { - const productData = req.body as CreateProductRequest; - const product = await this.productService.createProduct(productData); - res.status(201).json(product); - } catch (error) { - next(error); - } - } - - private async updateProduct(req: Request, res: Response, next: NextFunction): Promise { - try { - const productData = req.body as UpdateProductRequest; - const product = await this.productService.updateProduct(req.params.id, productData); - if (!product) { - res.status(404).json({ error: 'Product not found' }); - return; - } - res.json(product); - } catch (error) { - next(error); - } - } - - private async deleteProduct(req: Request, res: Response, next: NextFunction): Promise { - try { - const success = await this.productService.deleteProduct(req.params.id); - if (!success) { - res.status(404).json({ error: 'Product not found' }); - return; - } - res.status(204).send(); - } catch (error) { - next(error); - } - } -} -``` - -## Strongly Typed Event Handling - -Create strongly typed event producers and consumers: - -```typescript -// src/events/types/event-types.ts -export interface EventMetadata { - eventId: string; - timestamp: string; - service: string; - correlationId?: string; - causationId?: string; -} - -export interface Event { - type: string; - data: T; - metadata: EventMetadata; -} - -export interface ProductCreatedEvent extends Event<{ - id: string; - name: string; - price: number; - category: string; - sku: string; - inventoryCount: number; -}> { - type: 'PRODUCT_CREATED'; -} - -export interface ProductUpdatedEvent extends Event<{ - id: string; - changes: { - name?: string; - price?: number; - category?: string; - inventoryCount?: number; - }; -}> { - type: 'PRODUCT_UPDATED'; -} - -export interface InventoryUpdatedEvent extends Event<{ - productId: string; - quantity: number; - warehouseId: string; -}> { - type: 'INVENTORY_UPDATED'; -} - -// src/events/producers/product-event-producer.ts -import { v4 as uuidv4 } from 'uuid'; -import { KafkaClient } from '../../infrastructure/kafka'; -import { Product } from '../../domain/models/product'; -import { ProductCreatedEvent, ProductUpdatedEvent } from '../types/event-types'; - -export class ProductEventProducer { - private readonly topic = 'product-events'; - - constructor(private kafkaClient: KafkaClient) {} - - async productCreated(product: Product): Promise { - const event: ProductCreatedEvent = { - type: 'PRODUCT_CREATED', - data: { - id: product.id, - name: product.name, - price: product.price, - category: product.category, - sku: product.sku, - inventoryCount: product.inventoryCount - }, - metadata: { - eventId: uuidv4(), - timestamp: new Date().toISOString(), - service: 'product-service' - } - }; - - await this.kafkaClient.produce({ - topic: this.topic, - key: product.id, - value: JSON.stringify(event) - }); - } - - async productUpdated(product: Product, changes: Partial): Promise { - const event: ProductUpdatedEvent = { - type: 'PRODUCT_UPDATED', - data: { - id: product.id, - changes: { - name: changes.name, - price: changes.price, - category: changes.category, - inventoryCount: changes.inventoryCount - } - }, - metadata: { - eventId: uuidv4(), - timestamp: new Date().toISOString(), - service: 'product-service' - } - }; - - await this.kafkaClient.produce({ - topic: this.topic, - key: product.id, - value: JSON.stringify(event) - }); - } -} -``` - -## Dependency Injection with TypeScript - -We use the `inversify` library for dependency injection: - -```typescript -// src/infrastructure/ioc/container.ts -import { Container } from 'inversify'; -import { MongoClient } from 'mongodb'; -import { KafkaClient } from '../kafka'; -import { ProductRepository } from '../../repositories/product-repository'; -import { MongoDBProductRepository } from '../../repositories/mongodb-product-repository'; -import { ProductService } from '../../domain/services/product-service'; -import { ProductEventProducer } from '../../events/producers/product-event-producer'; -import { ProductController } from '../../api/controllers/product-controller'; -import { AppConfig } from '../../config/app-config'; -import TYPES from './types'; - -const container = new Container(); - -// Config -container.bind(TYPES.AppConfig).to(AppConfig).inSingletonScope(); - -// Infrastructure -container.bind(TYPES.MongoClient).toDynamicValue((context) => { - const config = context.container.get(TYPES.AppConfig); - return new MongoClient(config.mongoDbUri); -}).inSingletonScope(); - -container.bind(TYPES.KafkaClient).toDynamicValue((context) => { - const config = context.container.get(TYPES.AppConfig); - return new KafkaClient(config.kafkaBrokers); -}).inSingletonScope(); - -// Repositories -container.bind(TYPES.ProductRepository).toDynamicValue((context) => { - const mongoClient = context.container.get(TYPES.MongoClient); - return new MongoDBProductRepository(mongoClient); -}).inSingletonScope(); - -// Event Producers -container.bind(TYPES.ProductEventProducer).toDynamicValue((context) => { - const kafkaClient = context.container.get(TYPES.KafkaClient); - return new ProductEventProducer(kafkaClient); -}).inSingletonScope(); - -// Services -container.bind(TYPES.ProductService).toDynamicValue((context) => { - const repository = context.container.get(TYPES.ProductRepository); - const eventProducer = context.container.get(TYPES.ProductEventProducer); - return new ProductService(repository, eventProducer); -}).inSingletonScope(); - -// Controllers -container.bind(TYPES.ProductController).toDynamicValue((context) => { - const service = context.container.get(TYPES.ProductService); - return new ProductController(service); -}).inSingletonScope(); - -export default container; - -// src/infrastructure/ioc/types.ts -const TYPES = { - AppConfig: Symbol.for('AppConfig'), - MongoClient: Symbol.for('MongoClient'), - KafkaClient: Symbol.for('KafkaClient'), - ProductRepository: Symbol.for('ProductRepository'), - ProductEventProducer: Symbol.for('ProductEventProducer'), - ProductService: Symbol.for('ProductService'), - ProductController: Symbol.for('ProductController') -}; - -export default TYPES; -``` - -## Error Handling with TypeScript - -Use custom error classes: - -```typescript -// src/utils/errors.ts -export class AppError extends Error { - constructor( - public readonly message: string, - public readonly statusCode: number = 500, - public readonly code: string = 'INTERNAL_ERROR', - public readonly details?: any - ) { - super(message); - this.name = this.constructor.name; - Error.captureStackTrace(this, this.constructor); - } -} - -export class NotFoundError extends AppError { - constructor(resource: string, id: string) { - super(`${resource} with id ${id} not found`, 404, 'NOT_FOUND'); - this.name = this.constructor.name; - } -} - -export class ValidationError extends AppError { - constructor(message: string, details?: any) { - super(message, 400, 'VALIDATION_ERROR', details); - this.name = this.constructor.name; - } -} - -export class ConflictError extends AppError { - constructor(message: string) { - super(message, 409, 'CONFLICT_ERROR'); - this.name = this.constructor.name; - } -} - -export class AuthorizationError extends AppError { - constructor(message: string = 'Unauthorized') { - super(message, 401, 'UNAUTHORIZED'); - this.name = this.constructor.name; - } -} - -// src/api/middleware/error-handler.ts -import { Request, Response, NextFunction } from 'express'; -import { AppError } from '../../utils/errors'; -import { logger } from '../../infrastructure/observability'; - -export function errorHandler( - error: Error, - req: Request, - res: Response, - next: NextFunction -): void { - logger.error('Request error', { - error: error.message, - stack: error.stack, - path: req.path, - method: req.method - }); - - if (error instanceof AppError) { - res.status(error.statusCode).json({ - error: { - code: error.code, - message: error.message, - details: error.details - } - }); - return; - } - - res.status(500).json({ - error: { - code: 'INTERNAL_SERVER_ERROR', - message: 'An unexpected error occurred' - } - }); -} -``` - -## Testing TypeScript Services - -We use Jest for testing TypeScript services: - -```typescript -// test/unit/domain/services/product-service.test.ts -import { ProductService } from '../../../../src/domain/services/product-service'; -import { Product, ProductCategory } from '../../../../src/domain/models/product'; -import { NotFoundError } from '../../../../src/utils/errors'; - -describe('ProductService', () => { - const mockProduct = new Product( - '1', - 'Test Product', - 'Test Description', - 9.99, - ProductCategory.ELECTRONICS, - 'TEST-123', - 100 - ); - - const mockRepository = { - findById: jest.fn(), - findAll: jest.fn(), - findByCategory: jest.fn(), - save: jest.fn(), - update: jest.fn(), - delete: jest.fn() - }; - - const mockEventProducer = { - productCreated: jest.fn(), - productUpdated: jest.fn() - }; - - const productService = new ProductService(mockRepository, mockEventProducer); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getProductById', () => { - it('should return a product when found', async () => { - mockRepository.findById.mockResolvedValue(mockProduct); - - const result = await productService.getProductById('1'); - - expect(result).toEqual(mockProduct); - expect(mockRepository.findById).toHaveBeenCalledWith('1'); - }); - - it('should return null when product not found', async () => { - mockRepository.findById.mockResolvedValue(null); - - const result = await productService.getProductById('999'); - - expect(result).toBeNull(); - expect(mockRepository.findById).toHaveBeenCalledWith('999'); - }); - }); - - describe('createProduct', () => { - it('should create and return a new product', async () => { - const productData = { - name: 'New Product', - summary: 'New Description', - price: 19.99, - category: ProductCategory.CLOTHING, - sku: 'NEW-123', - inventoryCount: 50 - }; - - mockRepository.save.mockImplementation(product => Promise.resolve(product)); - - const result = await productService.createProduct(productData); - - expect(result).toMatchObject(productData); - expect(mockRepository.save).toHaveBeenCalledTimes(1); - expect(mockEventProducer.productCreated).toHaveBeenCalledWith(expect.objectContaining(productData)); - }); - }); - - describe('updateProduct', () => { - it('should update and return the product', async () => { - const changes = { price: 29.99, inventoryCount: 75 }; - const updatedProduct = { ...mockProduct, ...changes }; - - mockRepository.findById.mockResolvedValue(mockProduct); - mockRepository.update.mockResolvedValue(updatedProduct); - - const result = await productService.updateProduct('1', changes); - - expect(result).toEqual(updatedProduct); - expect(mockRepository.findById).toHaveBeenCalledWith('1'); - expect(mockRepository.update).toHaveBeenCalledWith(expect.objectContaining(changes)); - expect(mockEventProducer.productUpdated).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining(changes) - ); - }); - - it('should throw NotFoundError when product not found', async () => { - mockRepository.findById.mockResolvedValue(null); - - await expect(productService.updateProduct('999', { price: 29.99 })) - .rejects - .toThrow(NotFoundError); - - expect(mockRepository.update).not.toHaveBeenCalled(); - expect(mockEventProducer.productUpdated).not.toHaveBeenCalled(); - }); - }); -}); -``` - -## Building and Packaging TypeScript Services - -Our template includes optimized build scripts: - -```json -// package.json (excerpt) -{ - "scripts": { - "build": "tsc", - "start": "node dist/app.js", - "dev": "ts-node-dev --respawn --transpile-only src/app.ts", - "lint": "eslint src --ext .ts", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage" - } -} -``` - -## Dockerizing TypeScript Services - -Our Docker setup uses multi-stage builds for optimal container size: - -```dockerfile -# Dockerfile -FROM node:18-alpine AS builder - -WORKDIR /app - -COPY package*.json ./ -RUN npm ci - -COPY tsconfig.json ./ -COPY src/ ./src/ - -RUN npm run build - -FROM node:18-alpine - -WORKDIR /app - -COPY package*.json ./ -RUN npm ci --production - -COPY --from=builder /app/dist ./dist - -ENV NODE_ENV=production -EXPOSE 3000 - -HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ - CMD wget -qO- http://localhost:3000/health || exit 1 - -CMD ["node", "dist/app.js"] -``` - -## CI/CD for TypeScript Services - -Our CI/CD workflow ensures proper TypeScript builds: - -```yaml -# .github/workflows/main.yml (excerpt) -jobs: - test: - name: Test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - cache: 'npm' - - run: npm ci - - run: npm run lint - - run: npm test - - build: - name: Build and Push - needs: test - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: '18' - cache: 'npm' - - run: npm ci - - run: npm run build - # Then Docker build and push steps... -``` - -## TypeScript Best Practices at FlowMart - -1. **Use Interfaces for Public APIs**: Define interfaces for repositories, services, and controllers. - -2. **Embrace Type Inference**: Let TypeScript infer types where it makes sense to reduce verbosity. - -3. **Use Discriminated Unions**: For handling different event types or command patterns. - -4. **Leverage Utility Types**: Use built-in utility types like `Partial`, `Readonly`, `Pick`, etc. - -5. **Strict Null Checks**: Always enable `strictNullChecks` to prevent null/undefined errors. - -6. **Immutability**: Use `readonly` properties and `const` variables to enforce immutability. - -7. **Error Handling**: Use custom error classes with type information. - -8. **Asynchronous Code**: Use `async/await` consistently with proper error handling. - -9. **Use Enums for Constants**: Define related constants as TypeScript enums. - -10. **Module Structure**: Organize code into cohesive modules with clear interfaces. - -## Recommended Libraries - -- **Express.js**: Web framework -- **inversify**: Dependency injection -- **zod**: Runtime validation -- **winston**: Logging -- **prom-client**: Prometheus metrics -- **jaeger-client**: Distributed tracing -- **mongodb**: Database driver -- **kafkajs**: Kafka client -- **jest**: Testing framework -- **supertest**: API testing -- **ts-node-dev**: Development server - -## Next Steps - -- Learn about [Database patterns for microservices](/docs/guides/creating-new-microservices/database-patterns) -- Understand [Event schema design](/docs/guides/creating-new-microservices/event-schemas) -- Explore [API design best practices](/docs/guides/creating-new-microservices/api-design) \ No newline at end of file diff --git a/examples/default/docs/guides/creating-new-microservices/event-stream-example.ecstudio b/examples/default/docs/guides/creating-new-microservices/event-stream-example.ecstudio deleted file mode 100644 index 0b20eba0d..000000000 --- a/examples/default/docs/guides/creating-new-microservices/event-stream-example.ecstudio +++ /dev/null @@ -1,354 +0,0 @@ -{ - "nodes": [ - { - "id": "service-producer1", - "type": "service", - "position": { - "x": 12, - "y": 348.3333333333333 - }, - "data": { - "mode": "full", - "service": { - "id": "order-producer", - "name": "Order Producer", - "owners": [ - "order-team" - ], - "sends": [ - "OrderCreated", - "OrderUpdated" - ], - "receives": [], - "version": "1.0.0", - "summary": "Produces order events to the stream" - } - }, - "measured": { - "width": 262, - "height": 98 - } - }, - { - "id": "service-producer2", - "type": "service", - "position": { - "x": 394, - "y": 526.3333333333333 - }, - "data": { - "mode": "full", - "service": { - "id": "user-producer", - "name": "User Producer", - "owners": [ - "user-team" - ], - "sends": [ - "UserCreated", - "UserUpdated" - ], - "receives": [], - "version": "1.0.0", - "summary": "Produces user events to the stream" - } - }, - "measured": { - "width": 262, - "height": 98 - } - }, - { - "id": "channel-event-stream", - "type": "channel", - "position": { - "x": 776, - "y": 364 - }, - "data": { - "mode": "full", - "channel": { - "id": "event-stream", - "name": "event-stream", - "version": "1.0.0", - "summary": "High-throughput Kafka stream for real-time event processing" - } - }, - "measured": { - "width": 262, - "height": 100 - } - }, - { - "id": "service-stream-processor", - "type": "service", - "position": { - "x": 1158, - "y": 348.3333333333333 - }, - "data": { - "mode": "full", - "service": { - "id": "stream-processor", - "name": "Stream Processor", - "owners": [ - "analytics-team" - ], - "sends": [ - "MetricsCalculated", - "AlertTriggered" - ], - "receives": [ - "OrderCreated", - "UserCreated" - ], - "version": "1.0.0", - "summary": "Real-time stream processing with Kafka Streams" - } - }, - "measured": { - "width": 262, - "height": 98 - } - }, - { - "id": "service-consumer1", - "type": "service", - "position": { - "x": 1158, - "y": 526.3333333333333 - }, - "data": { - "mode": "full", - "service": { - "id": "notification-consumer", - "name": "Notification Consumer", - "owners": [ - "notification-team" - ], - "sends": [ - "NotificationSent" - ], - "receives": [ - "OrderCreated", - "AlertTriggered" - ], - "version": "1.0.0", - "summary": "Consumes events to send real-time notifications" - } - }, - "measured": { - "width": 262, - "height": 98 - } - }, - { - "id": "data-stream-store", - "type": "data", - "position": { - "x": 1540, - "y": 332 - }, - "data": { - "mode": "full", - "data": { - "id": "stream-store", - "name": "Stream State Store", - "owners": [ - "analytics-team" - ], - "version": "1.0.0", - "summary": "Local state store for stream processing aggregations", - "type": "Database" - } - }, - "measured": { - "width": 262, - "height": 98 - } - }, - { - "id": "service-consumer2", - "type": "service", - "position": { - "x": 1540, - "y": 510 - }, - "data": { - "mode": "full", - "service": { - "id": "analytics-consumer", - "name": "Analytics Consumer", - "owners": [ - "analytics-team" - ], - "sends": [], - "receives": [ - "MetricsCalculated" - ], - "version": "1.0.0", - "summary": "Consumes processed metrics for dashboard updates" - } - }, - "measured": { - "width": 262, - "height": 98 - } - }, - { - "id": "data-analytics-db", - "type": "data", - "position": { - "x": 1922, - "y": 510 - }, - "data": { - "mode": "full", - "data": { - "id": "analytics-database", - "name": "Analytics Database", - "owners": [ - "analytics-team" - ], - "version": "1.0.0", - "summary": "Time-series database for storing processed stream data", - "type": "Database" - } - }, - "measured": { - "width": 262, - "height": 98 - } - }, - { - "id": "event-order-created", - "type": "event", - "position": { - "x": 394, - "y": 348.3333333333333 - }, - "data": { - "mode": "full", - "message": { - "id": "order-created-stream", - "name": "OrderCreated", - "version": "1.0.0", - "summary": "Order creation event in the stream" - } - }, - "measured": { - "width": 262, - "height": 98 - } - } - ], - "edges": [ - { - "source": "service-producer1", - "target": "event-order-created", - "label": "Produces", - "markerEnd": { - "type": "arrowclosed", - "color": "#000000" - }, - "animated": true, - "type": "animatedMessage", - "id": "xy-edge__service-producer1-event-order-created" - }, - { - "source": "service-producer2", - "target": "channel-event-stream", - "label": "Produces", - "markerEnd": { - "type": "arrowclosed", - "color": "#000000" - }, - "animated": true, - "type": "animatedMessage", - "id": "xy-edge__service-producer2-channel-event-stream" - }, - { - "source": "event-order-created", - "target": "channel-event-stream", - "label": "Flows to", - "markerEnd": { - "type": "arrowclosed", - "color": "#000000" - }, - "animated": true, - "type": "animatedMessage", - "id": "xy-edge__event-order-created-channel-event-stream" - }, - { - "source": "channel-event-stream", - "target": "service-stream-processor", - "label": "Stream Processing", - "markerEnd": { - "type": "arrowclosed", - "color": "#000000" - }, - "animated": true, - "type": "animatedMessage", - "id": "xy-edge__channel-event-stream-service-stream-processor" - }, - { - "source": "channel-event-stream", - "target": "service-consumer1", - "label": "Consumes", - "markerEnd": { - "type": "arrowclosed", - "color": "#000000" - }, - "animated": true, - "type": "animatedMessage", - "id": "xy-edge__channel-event-stream-service-consumer1" - }, - { - "source": "service-stream-processor", - "target": "data-stream-store", - "label": "Maintains State", - "markerEnd": { - "type": "arrowclosed", - "color": "#000000" - }, - "animated": false, - "type": "default", - "id": "xy-edge__service-stream-processor-data-stream-store" - }, - { - "source": "service-stream-processor", - "target": "service-consumer2", - "label": "Processed Events", - "markerEnd": { - "type": "arrowclosed", - "color": "#000000" - }, - "animated": true, - "type": "animatedMessage", - "id": "xy-edge__service-stream-processor-service-consumer2" - }, - { - "source": "service-consumer2", - "target": "data-analytics-db", - "label": "Stores Metrics", - "markerEnd": { - "type": "arrowclosed", - "color": "#000000" - }, - "animated": false, - "type": "default", - "id": "xy-edge__service-consumer2-data-analytics-db" - } - ], - "viewport": { - "x": 154, - "y": 41, - "zoom": 0.6 - }, - "creationDate": "2025-08-12T09:12:34.680Z", - "name": "Event Streaming Pattern", - "version": "1.0", - "source": "https://app.eventcatalog.dev", - "appState": {}, - "id": "event-streaming-pattern" -} \ No newline at end of file diff --git a/examples/default/docs/guides/event-storming/01-index.mdx b/examples/default/docs/guides/event-storming/01-index.mdx deleted file mode 100644 index 580ed6477..000000000 --- a/examples/default/docs/guides/event-storming/01-index.mdx +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Getting Started with Event Storming -summary: Learn how to use Event Storming to discover and model your business domain effectively -sidebar: - label: Introduction - order: 1 -owners: - - dboyne -badges: - - content: 'Guide' - backgroundColor: 'teal' - textColor: 'teal' ---- - -# Introduction to Event Storming - -Event Storming is a collaborative modeling technique that helps teams explore complex business domains. It was introduced by Alberto Brandolini and has become a valuable tool in Domain-Driven Design (DDD) and Event-Driven Architecture (EDA). - -## Why Event Storming? - -At FlowMart, we use Event Storming because it: - -- Brings together domain experts and technical teams -- Helps identify domain events, commands, and aggregates -- Aligns perfectly with our event-driven architecture -- Facilitates better understanding of business processes -- Helps in defining service boundaries and responsibilities - -## Example Event Storming Session - -Here is an example of an event storming session for the Orders domain. - - - -## Event Storming Example in Lucid Chart - - - -## Git workflow - - - -## Key Concepts - -### Domain Events -- Represent something significant that has happened in the business domain -- Written in past tense (e.g., "Order Placed", "Payment Received") -- Captured on orange sticky notes during the session - -### Commands -- Triggers that cause domain events -- Written in imperative form (e.g., "Place Order", "Process Payment") -- Captured on blue sticky notes - -### Aggregates -- Business entities that ensure consistency -- Group related events and commands -- Captured on yellow sticky notes - -### Policies -- Business rules that react to events -- Automated processes or human workflows -- Captured on purple sticky notes - -## Event Storming Levels - -1. **Big Picture Event Storming** - - High-level view of the entire business domain - - Focuses on major events and workflows - - Helps identify bounded contexts - -2. **Process Level Event Storming** - - Detailed view of specific processes - - Includes commands, policies, and external systems - - Helps design individual services - -3. **Software Design Level Event Storming** - - Technical perspective of the domain - - Includes aggregates, entities, and value objects - - Leads to implementation decisions - -## Benefits for FlowMart - -Event Storming has helped us: -- Design our microservices architecture -- Identify service boundaries -- Define event contracts between services -- Improve collaboration between teams -- Reduce misunderstandings and rework - -## Prerequisites for Event Storming - -To conduct an effective Event Storming session, you'll need: - -- A large modeling space (physical or virtual) -- Sticky notes in different colors -- Domain experts and technical team members -- A skilled facilitator -- 2-4 hours of uninterrupted time - -## Next Steps - -Continue reading to learn: -- [How to Facilitate an Event Storming Session](/docs/guides/event-storming/02-facilitation) -- [From Event Storming to Implementation](/docs/guides/event-storming/03-implementation) \ No newline at end of file diff --git a/examples/default/docs/guides/event-storming/02-facilitation.mdx b/examples/default/docs/guides/event-storming/02-facilitation.mdx deleted file mode 100644 index 13f9be89d..000000000 --- a/examples/default/docs/guides/event-storming/02-facilitation.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Facilitating an Event Storming Session -summary: A comprehensive guide on how to run effective Event Storming workshops at FlowMart -sidebar: - label: Facilitation Guide - order: 2 -owners: - - dboyne -badges: - - content: 'Guide' - backgroundColor: 'teal' - textColor: 'teal' ---- - -# Facilitating an Event Storming Session - -This guide will help you run effective Event Storming sessions at FlowMart, ensuring you get the most value from this collaborative modeling technique. - -## Pre-Session Preparation - -### 1. Define the Scope -- Identify the business domain or process to explore -- Set clear objectives for the session -- Determine the appropriate level (Big Picture, Process, or Design) - -### 2. Invite the Right People -- Domain experts who understand the business processes -- Technical team members who will implement the solution -- Product owners and stakeholders -- Limit to 8-12 participants for optimal interaction - -### 3. Prepare the Space -- Large continuous wall space or virtual whiteboard -- Sticky notes in different colors: - - Orange: Domain Events - - Blue: Commands - - Yellow: Aggregates - - Purple: Policies - - Pink: External Systems - - Red: Problems/Questions - -## Running the Session - -### 1. Introduction (15 minutes) -- Explain Event Storming concepts and notation -- Set ground rules: - - No laptops/phones unless necessary - - Everyone participates - - No wrong answers - - Focus on the business process - -### 2. Event Discovery (45-60 minutes) -- Start with "What happens in this domain?" -- Let participants write domain events on orange stickies -- Place events on the timeline (left to right) -- Don't worry about order initially - -### 3. Timeline Organization (30 minutes) -- Review all events as a group -- Organize events chronologically -- Identify missing events -- Group related events together - -### 4. Adding Detail (60-90 minutes) -- Add commands (blue) that trigger events -- Identify external systems (pink) -- Mark problem areas (red) -- Add policies and reactions (purple) - -### 5. Identifying Boundaries (45 minutes) -- Group related concepts -- Look for natural service boundaries -- Discuss integration points -- Identify aggregates (yellow) - -## Common Challenges and Solutions - -### Challenge: Dominant Participants -- Actively engage quieter participants -- Use round-robin techniques -- Split into smaller groups temporarily - -### Challenge: Too Much Detail -- Keep focus on relevant abstraction level -- Park detailed discussions for later -- Use "parking lot" for important but off-topic items - -### Challenge: Losing Focus -- Take regular breaks (10 minutes every hour) -- Use timeboxing for each activity -- Keep referring back to session goals - -## Remote Facilitation Tips - -When running remote Event Storming sessions: - -- Use tools like Miro or Mural -- Pre-create templates and sticky note colors -- Use breakout rooms for small group discussions -- Schedule more frequent but shorter sessions -- Use video to maintain engagement - -## Post-Session Activities - -1. **Documentation** - - Photograph or export the board - - Capture key insights and decisions - - Document identified bounded contexts - -2. **Follow-up** - - Schedule deep-dive sessions for specific areas - - Create action items and assign owners - - Plan next steps for implementation - -3. **Review and Refine** - - Review findings with stakeholders - - Validate assumptions - - Plan additional sessions if needed - -## Measuring Success - -A successful Event Storming session should: -- Create shared understanding -- Identify key domain events and processes -- Highlight potential problems and solutions -- Generate actionable next steps -- Engage all participants effectively - -## Next Steps - -Continue to [From Event Storming to Implementation](/docs/guides/event-storming/03-implementation) to learn how to turn your Event Storming insights into working software. \ No newline at end of file diff --git a/examples/default/docs/guides/event-storming/03-implementation.mdx b/examples/default/docs/guides/event-storming/03-implementation.mdx deleted file mode 100644 index 83e93197f..000000000 --- a/examples/default/docs/guides/event-storming/03-implementation.mdx +++ /dev/null @@ -1,207 +0,0 @@ ---- -title: From Event Storming to Implementation -summary: Learn how to transform Event Storming outcomes into concrete microservice implementations at FlowMart -sidebar: - label: Implementation - order: 3 -owners: - - dboyne -badges: - - content: 'Guide' - backgroundColor: 'teal' - textColor: 'teal' ---- - -# From Event Storming to Implementation - -This guide will help you transform the insights gained from Event Storming sessions into concrete microservice implementations at FlowMart. - -## Translating Event Storming Artifacts - -### Domain Events to Event Schema -Convert orange sticky notes (domain events) into event schemas: - -```typescript -// Example Event Schema for "OrderPlaced" -interface OrderPlacedEvent { - eventType: 'OrderPlaced'; - orderId: string; - customerId: string; - orderItems: OrderItem[]; - totalAmount: number; - timestamp: string; -} -``` - -### Commands to API Endpoints -Transform blue sticky notes (commands) into API endpoints: - -```typescript -// Example API endpoint for "PlaceOrder" -@POST -@Path("/orders") -async placeOrder(orderRequest: OrderRequest): Promise { - // Implementation -} -``` - -### Aggregates to Service Boundaries -Convert yellow sticky notes (aggregates) into service definitions: - -```typescript -// Example Order Service -@Service -class OrderService { - private readonly orderRepository: OrderRepository; - private readonly eventPublisher: EventPublisher; - - async createOrder(order: Order): Promise { - // Implementation - } -} -``` - -## Implementation Steps - -### 1. Define Service Boundaries -- Use bounded contexts identified during Event Storming -- Create new service repositories using our [templates](/docs/guides/creating-new-microservices) -- Define service interfaces and contracts - -### 2. Design Event Schemas -- Create event schemas for all domain events -- Use our [schema registry](/docs/technical-architecture-design/schema-registry) -- Version schemas appropriately -- Document event ownership and consumers - -### 3. Implement Commands -- Create API endpoints for commands -- Implement command handlers -- Add validation and error handling -- Document API contracts using OpenAPI - -### 4. Set Up Event Publishing -- Implement event publishers -- Configure Kafka topics -- Set up dead letter queues -- Implement retry mechanisms - -### 5. Create Event Subscribers -- Implement event handlers -- Set up consumer groups -- Handle failure scenarios -- Implement idempotency - -## Example Implementation Flow - -Here's a typical flow from Event Storming to implementation: - -1. **Event Storming Identifies**: - - Domain Event: "OrderPlaced" - - Command: "PlaceOrder" - - Aggregate: "Order" - -2. **Create Service Structure**: -```typescript -// Service structure based on Event Storming -src/ - ├── domain/ - │ ├── Order.ts - │ └── OrderItem.ts - ├── commands/ - │ └── PlaceOrderCommand.ts - ├── events/ - │ └── OrderPlacedEvent.ts - └── api/ - └── OrderController.ts -``` - -3. **Implement Domain Model**: -```typescript -class Order { - private readonly items: OrderItem[]; - private status: OrderStatus; - - placeOrder(): OrderPlacedEvent { - // Implementation - return new OrderPlacedEvent(this); - } -} -``` - -## Best Practices - -### Event Design -- Use past tense for event names -- Include all necessary context -- Follow our [event naming conventions](/docs/technical-architecture-design/event-naming) -- Version events appropriately - -### Service Design -- Keep services focused on single bounded context -- Implement proper error handling -- Add comprehensive logging -- Include monitoring and metrics - -### Testing Strategy -- Unit test domain logic -- Integration test event flows -- Contract test event schemas -- End-to-end test critical paths - -## Common Pitfalls - -1. **Too Many Events** - - Solution: Focus on meaningful state changes - - Combine related events when appropriate - -2. **Tight Coupling** - - Solution: Use event-driven communication - - Avoid direct service-to-service calls - -3. **Missing Context** - - Solution: Include necessary data in events - - Document event purpose and usage - -## Monitoring and Observability - -Implement proper monitoring for: -- Event processing metrics -- Command execution times -- Error rates and types -- Service health indicators - -## Example: Order Processing Flow - -Here's a complete example of implementing an order processing flow: - -1. **Event Storming Identified**: - - Command: "PlaceOrder" - - Event: "OrderPlaced" - - Event: "PaymentProcessed" - - Policy: "Send Order Confirmation" - -2. **Implementation**: -```typescript -// Command Handler -async function handlePlaceOrder(command: PlaceOrderCommand) { - const order = new Order(command.orderDetails); - const event = order.placeOrder(); - await eventPublisher.publish('order-events', event); - return order; -} - -// Event Handler -async function handleOrderPlaced(event: OrderPlacedEvent) { - await paymentService.processPayment(event.orderId); - await notificationService.sendConfirmation(event.orderId); -} -``` - -## Next Steps - -1. Review our [service templates](/docs/guides/creating-new-microservices) -2. Study our [event schema guidelines](/docs/technical-architecture-design/event-schemas) -3. Explore our [monitoring setup](/docs/operations-and-support/monitoring) - -Remember: Event Storming is an iterative process. As you implement and learn more about the domain, you may need to revisit and refine your model. Keep the dialogue open between domain experts and developers throughout the implementation phase. \ No newline at end of file diff --git a/examples/default/docs/operations-and-support/runbooks/inventory-service-runbook.mdx b/examples/default/docs/operations-and-support/runbooks/inventory-service-runbook.mdx deleted file mode 100644 index fe86f1d58..000000000 --- a/examples/default/docs/operations-and-support/runbooks/inventory-service-runbook.mdx +++ /dev/null @@ -1,349 +0,0 @@ ---- -title: Inventory Service - Runbook -summary: Operational runbook for troubleshooting and maintaining the InventoryService -sidebar: - label: InventoryService - order: 2 -owners: - - order-management -badges: - - content: 🚀 Operational Procedures - textColor: white - backgroundColor: blue - - content: 🚨 Troubleshooting Guide - textColor: white - backgroundColor: red - - content: 🔄 Maintenance Schedule - textColor: white - backgroundColor: yellow ---- - -This runbook provides operational procedures for the InventoryService, which is responsible for managing product inventory and stock levels across the FlowMart e-commerce platform. - -## Architecture - -The InventoryService is responsible for: -- Managing product inventory and stock levels -- Reserving inventory for pending orders -- Tracking inventory across warehouses and locations -- Providing real-time availability information -- Triggering restock notifications - -### Service Dependencies - -```mermaid -flowchart TD - InventoryService --> PostgresDB[(PostgreSQL Database)] - InventoryService --> Redis[(Redis Cache)] - InventoryService --> EventBus[Event Bus] - InventoryService --> WarehouseSystem[Warehouse Management System] -``` - -## Monitoring and Alerting - -### Key Metrics - -| Metric | Description | Warning Threshold | Critical Threshold | -|--------|-------------|-------------------|-------------------| -| `inventory_check_rate` | Inventory availability checks per minute | > 1000 | > 5000 | -| `inventory_check_latency` | Time to check inventory availability | > 100ms | > 500ms | -| `inventory_update_latency` | Time to update inventory levels | > 200ms | > 1s | -| `low_stock_items` | Number of items with low stock | > 50 | > 100 | -| `connection_pool_usage` | Database connection pool utilization | > 70% | > 90% | -| `redis_hit_rate` | Cache hit rate | < 80% | < 60% | - -### Dashboards - -- [InventoryService Overview](https://grafana.flowmart.com/d/inventory-overview) -- [Stock Level Alerts](https://grafana.flowmart.com/d/inventory-stock-alerts) -- [Database Performance](https://grafana.flowmart.com/d/inventory-database) - -### Common Alerts - -| Alert | Description | Troubleshooting Steps | -|-------|-------------|----------------------| -| `InventoryServiceHighLatency` | API latency exceeds thresholds | See [High Latency](#high-latency) | -| `InventoryServiceDatabaseIssues` | Database connection or performance issues | See [Database Issues](#database-issues) | -| `InventoryServiceCacheFailure` | Redis cache unavailable or performance degraded | See [Cache Issues](#cache-issues) | -| `InventoryServiceOutOfStock` | Critical products out of stock | See [Stock Management](#stock-management) | - -## Troubleshooting Guides - -### High Latency - -If the service is experiencing high latency: - -1. **Check system resource usage**: - ```bash - kubectl top pods -n inventory - ``` - -2. **Check database connection pool**: - ```bash - kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl localhost:8080/actuator/metrics/hikaricp.connections.usage - ``` - -3. **Check cache hit rate**: - ```bash - kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl localhost:8080/actuator/metrics/cache.gets | grep "hit_ratio" - ``` - -4. **Check for slow queries** in the database: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- psql -U postgres -c "SELECT query, calls, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;" - ``` - -5. **Scale the service** if needed: - ```bash - kubectl scale deployment inventory-service -n inventory --replicas=5 - ``` - -### Database Issues - -If there are database connection or performance issues: - -1. **Check PostgreSQL status**: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- pg_isready -U postgres - ``` - -2. **Check for long-running transactions**: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- psql -U postgres -c "SELECT pid, now() - xact_start AS duration, state, query FROM pg_stat_activity WHERE state != 'idle' ORDER BY duration DESC;" - ``` - -3. **Check for table bloat**: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- psql -U postgres -c "SELECT schemaname, relname, n_live_tup, n_dead_tup, (n_dead_tup::float / n_live_tup::float) AS dead_ratio FROM pg_stat_user_tables WHERE n_live_tup > 1000 ORDER BY dead_ratio DESC;" - ``` - -4. **Restart database connections** in the application if needed: - ```bash - kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl -X POST localhost:8080/actuator/restart-db-connections - ``` - -### Cache Issues - -If there are Redis cache issues: - -1. **Check Redis status**: - ```bash - kubectl exec -it $(kubectl get pods -l app=redis -n data -o jsonpath='{.items[0].metadata.name}') -n data -- redis-cli ping - ``` - -2. **Check Redis memory usage**: - ```bash - kubectl exec -it $(kubectl get pods -l app=redis -n data -o jsonpath='{.items[0].metadata.name}') -n data -- redis-cli info memory - ``` - -3. **Check cache hit rate**: - ```bash - kubectl exec -it $(kubectl get pods -l app=redis -n data -o jsonpath='{.items[0].metadata.name}') -n data -- redis-cli info stats | grep hit_rate - ``` - -4. **Clear cache** if necessary: - ```bash - kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl -X POST localhost:8080/actuator/caches/clearAll - ``` - -### Stock Management - -For critical stock issues: - -1. **Identify products with low or no stock**: - ```bash - kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl localhost:8080/internal/api/inventory/low-stock - ``` - -2. **Check for stuck inventory reservations**: - ```bash - kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl localhost:8080/internal/api/inventory/stuck-reservations - ``` - -3. **Release expired reservations** if necessary: - ```bash - kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl -X POST localhost:8080/internal/api/inventory/release-expired-reservations - ``` - -4. **Manually update inventory levels** for emergency corrections: - ```bash - curl -X PUT https://api.internal.flowmart.com/inventory/products/{productId}/stock \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"warehouseId": "WAREHOUSE_ID", "quantity": 100, "reason": "Manual correction"}' - ``` - -## Common Operational Tasks - -### Scaling the Service - -To scale the service horizontally: - -```bash -kubectl scale deployment inventory-service -n inventory --replicas= -``` - -### Restarting the Service - -To restart all pods: - -```bash -kubectl rollout restart deployment inventory-service -n inventory -``` - -### Database Maintenance - -For routine database maintenance: - -1. **Run VACUUM ANALYZE to optimize tables**: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- psql -U postgres -c "VACUUM ANALYZE inventory_items;" - ``` - -2. **Update database statistics**: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- psql -U postgres -c "ANALYZE;" - ``` - -### Reconcile Inventory - -To reconcile inventory with the warehouse management system: - -```bash -kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl -X POST localhost:8080/internal/api/inventory/reconcile -``` - -### Manually Trigger Restock Notifications - -To trigger restock notifications for low stock items: - -```bash -kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl -X POST localhost:8080/internal/api/inventory/trigger-restock-notifications -``` - -## Recovery Procedures - -### Database Failure Recovery - -If the PostgreSQL database becomes unavailable: - -1. Verify the status of the PostgreSQL cluster: - ```bash - kubectl get pods -l app=postgresql -n data - ``` - -2. If the primary instance is down, check if automatic failover has occurred: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql-patroni -n data -o jsonpath='{.items[0].metadata.name}') -n data -- patronictl list - ``` - -3. If automatic failover has not occurred, initiate manual failover: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql-patroni -n data -o jsonpath='{.items[0].metadata.name}') -n data -- patronictl failover - ``` - -4. Once database availability is restored, validate the InventoryService functionality: - ```bash - curl -X GET https://api.internal.flowmart.com/inventory/health - ``` - -### Cache Failure Recovery - -If the Redis cache becomes unavailable: - -1. Verify Redis cluster status: - ```bash - kubectl get pods -l app=redis -n data - ``` - -2. If needed, restart the Redis cluster: - ```bash - kubectl rollout restart statefulset redis -n data - ``` - -3. The InventoryService will fall back to database queries when the cache is unavailable. - -4. When the cache is restored, you can warm it up: - ```bash - kubectl exec -it $(kubectl get pods -l app=inventory-service -n inventory -o jsonpath='{.items[0].metadata.name}') -n inventory -- curl -X POST localhost:8080/internal/api/inventory/warm-cache - ``` - -## Disaster Recovery - -### Complete Service Failure - -In case of a complete service failure: - -1. Initiate incident response by notifying the on-call team through PagerDuty. - -2. Verify the deployment status: - ```bash - kubectl describe deployment inventory-service -n inventory - ``` - -3. If necessary, restore from a previous version: - ```bash - kubectl rollout undo deployment inventory-service -n inventory - ``` - -4. If the primary region is experiencing issues, fail over to the secondary region: - ```bash - ./scripts/dr-failover.sh inventory-service - ``` - -5. Verify the service is functioning in the secondary region: - ```bash - curl -X GET https://api-dr.internal.flowmart.com/inventory/health - ``` - -## Maintenance Tasks - -### Deploying New Versions - -```bash -kubectl set image deployment/inventory-service -n inventory inventory-service=ecr.aws/flowmart/inventory-service:$VERSION -``` - -### Database Schema Updates - -For database schema updates: - -1. Notify stakeholders through the #maintenance Slack channel. - -2. Set InventoryService to maintenance mode: - ```bash - curl -X POST https://api.internal.flowmart.com/inventory/admin/maintenance -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"maintenanceMode": true, "message": "Database schema update"}' - ``` - -3. Apply the database migrations: - ```bash - kubectl apply -f inventory-flyway-job.yaml - ``` - -4. Verify migration completion: - ```bash - kubectl logs -l job-name=inventory-flyway-migration -n inventory - ``` - -5. Turn off maintenance mode: - ```bash - curl -X POST https://api.internal.flowmart.com/inventory/admin/maintenance -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"maintenanceMode": false}' - ``` - -## Contact Information - -**Primary On-Call:** Inventory Team (rotating schedule) -**Secondary On-Call:** Platform Team -**Escalation Path:** Inventory Team Lead > Engineering Manager > CTO - -**Slack Channels:** -- #inventory-support (primary support channel) -- #inventory-alerts (automated alerts) -- #incident-response (for major incidents) - -## Reference Information - -- [InventoryService API Documentation](https://docs.internal.flowmart.com/inventory/api) -- [Architecture Diagram](https://docs.internal.flowmart.com/architecture/inventory) -- [Service Level Objectives (SLOs)](https://docs.internal.flowmart.com/slo/inventory) -- [Database Schema](https://docs.internal.flowmart.com/inventory/database-schema) \ No newline at end of file diff --git a/examples/default/docs/operations-and-support/runbooks/orders-service-runbook.mdx b/examples/default/docs/operations-and-support/runbooks/orders-service-runbook.mdx deleted file mode 100644 index 4799ddca9..000000000 --- a/examples/default/docs/operations-and-support/runbooks/orders-service-runbook.mdx +++ /dev/null @@ -1,279 +0,0 @@ ---- -title: OrdersService Runbook -summary: Operational runbook for troubleshooting and maintaining the OrdersService -sidebar: - label: OrdersService - order: 1 ---- - -This runbook provides operational procedures for the OrdersService, which is responsible for managing the entire lifecycle of customer orders in the FlowMart e-commerce platform. - - -## Architecture - -The OrdersService is responsible for: -- Creating and processing customer orders -- Tracking order status throughout fulfillment -- Coordinating with other services (Inventory, Payment, Shipping) -- Managing order history and amendments - -### Service Dependencies - -```mermaid -flowchart TD - OrdersService --> InventoryService[Inventory Service] - OrdersService --> PaymentService[Payment Service] - OrdersService --> ShippingService[Shipping Service] - OrdersService --> NotificationService[Notification Service] - OrdersService --> OrdersDB[(Orders Database)] - OrdersService --> EventBus[Event Bus] -``` - -## Monitoring and Alerting - -### Key Metrics - -| Metric | Description | Warning Threshold | Critical Threshold | -|--------|-------------|-------------------|-------------------| -| `order_creation_rate` | Orders created per minute | < 5 | < 1 | -| `order_creation_latency` | Time to create an order | > 2s | > 5s | -| `order_error_rate` | Percentage of failed orders | > 1% | > 5% | -| `database_connection_pool` | Database connection pool utilization | > 70% | > 90% | -| `memory_usage` | Container memory usage | > 80% | > 90% | -| `cpu_usage` | Container CPU usage | > 70% | > 85% | - -### Dashboards - -- [OrdersService Overview](https://grafana.flowmart.com/d/orders-overview) -- [OrdersService API Metrics](https://grafana.flowmart.com/d/orders-api) -- [OrdersService Error Tracking](https://grafana.flowmart.com/d/orders-errors) - -### Common Alerts - -| Alert | Description | Troubleshooting Steps | -|-------|-------------|----------------------| -| `OrdersServiceHighLatency` | API latency exceeds thresholds | See [High Latency](#high-latency) | -| `OrdersServiceHighErrorRate` | Error rate exceeds thresholds | See [High Error Rate](#high-error-rate) | -| `OrdersServiceDatabaseConnectionIssues` | Database connection issues | See [Database Issues](#database-issues) | - -## Troubleshooting Guides - -### High Latency - -If the service is experiencing high latency: - -1. **Check system metrics**: - ```bash - kubectl top pods -n orders - ``` - -2. **Check database metrics** in the MongoDB dashboard to identify slow queries. - -3. **Check dependent services** to see if delays are caused by downstream systems: - ```bash - curl -X GET https://api.internal.flowmart.com/inventory/health - curl -X GET https://api.internal.flowmart.com/payment/health - ``` - -4. **Analyze recent changes** that might have impacted performance. - -5. **Scale the service** if needed: - ```bash - kubectl scale deployment orders-service -n orders --replicas=5 - ``` - -### High Error Rate - -If the service is experiencing a high error rate: - -1. **Check application logs**: - ```bash - kubectl logs -l app=orders-service -n orders --tail=100 - ``` - -2. **Check for recent deployments** that might have introduced issues: - ```bash - kubectl rollout history deployment/orders-service -n orders - ``` - -3. **Verify database connectivity**: - ```bash - kubectl exec -it $(kubectl get pods -l app=orders-service -n orders -o jsonpath='{.items[0].metadata.name}') -n orders -- node -e "const mongoose = require('mongoose'); mongoose.connect(process.env.MONGODB_URI).then(() => console.log('Connected!')).catch(err => console.error('Connection error', err));" - ``` - -4. **Check dependent services** for failures: - ```bash - curl -X GET https://api.internal.flowmart.com/inventory/health - curl -X GET https://api.internal.flowmart.com/payment/health - ``` - -5. **Consider rolling back** if issues persist: - ```bash - kubectl rollout undo deployment/orders-service -n orders - ``` - -### Database Issues - -If there are database connection issues: - -1. **Check MongoDB status**: - ```bash - kubectl exec -it $(kubectl get pods -l app=mongodb -n data -o jsonpath='{.items[0].metadata.name}') -n data -- mongo admin -u admin -p $MONGODB_PASSWORD --eval "db.serverStatus()" - ``` - -2. **Verify network connectivity**: - ```bash - kubectl exec -it $(kubectl get pods -l app=orders-service -n orders -o jsonpath='{.items[0].metadata.name}') -n orders -- ping mongodb.data.svc.cluster.local - ``` - -3. **Check MongoDB resource usage**: - ```bash - kubectl top pods -l app=mongodb -n data - ``` - -4. **Review MongoDB logs**: - ```bash - kubectl logs -l app=mongodb -n data --tail=100 - ``` - -## Common Operational Tasks - -### Scaling the Service - -To scale the service horizontally: - -```bash -kubectl scale deployment orders-service -n orders --replicas= -``` - -### Restarting the Service - -To restart all pods: - -```bash -kubectl rollout restart deployment orders-service -n orders -``` - -### Viewing Recent Orders - -To view recent orders in the database: - -```bash -kubectl exec -it $(kubectl get pods -l app=orders-service -n orders -o jsonpath='{.items[0].metadata.name}') -n orders -- node -e "const mongoose = require('mongoose'); const Order = require('./models/order'); mongoose.connect(process.env.MONGODB_URI).then(async () => { const orders = await Order.find().sort({createdAt: -1}).limit(10); console.log(JSON.stringify(orders, null, 2)); process.exit(0); });" -``` - -### Manually Processing Stuck Orders - -If orders are stuck in a particular state: - -1. Identify stuck orders: - ```bash - kubectl exec -it $(kubectl get pods -l app=orders-service -n orders -o jsonpath='{.items[0].metadata.name}') -n orders -- node -e "const mongoose = require('mongoose'); const Order = require('./models/order'); mongoose.connect(process.env.MONGODB_URI).then(async () => { const stuckOrders = await Order.find({status: 'PROCESSING', updatedAt: {$lt: new Date(Date.now() - 30*60*1000)}}); console.log(JSON.stringify(stuckOrders, null, 2)); process.exit(0); });" - ``` - -2. Manually trigger processing for a specific order: - ```bash - curl -X POST https://api.internal.flowmart.com/orders/process -H "Content-Type: application/json" -d '{"orderId": "ORDER_ID", "force": true}' - ``` - -## Recovery Procedures - -### Database Failure Recovery - -If the MongoDB database becomes unavailable: - -1. Verify the status of the MongoDB cluster: - ```bash - kubectl get pods -l app=mongodb -n data - ``` - -2. If the primary node is down, initiate a manual failover if necessary: - ```bash - kubectl exec -it mongodb-0 -n data -- mongo admin -u admin -p $MONGODB_PASSWORD --eval "rs.stepDown()" - ``` - -3. If the entire cluster is unavailable, create an incident and notify the Database Team. - -4. Once database availability is restored, validate the OrdersService functionality: - ```bash - curl -X GET https://api.internal.flowmart.com/orders/health - ``` - -### Event Bus Failure Recovery - -If the Event Bus is unavailable: - -1. The OrdersService implements the Circuit Breaker pattern and will queue messages locally. - -2. When the Event Bus is restored, check the backlog of events: - ```bash - kubectl exec -it $(kubectl get pods -l app=orders-service -n orders -o jsonpath='{.items[0].metadata.name}') -n orders -- curl localhost:9090/metrics | grep event_queue - ``` - -3. Manually trigger event processing if necessary: - ```bash - curl -X POST https://api.internal.flowmart.com/orders/admin/process-event-queue -H "Authorization: Bearer $ADMIN_TOKEN" - ``` - -## Disaster Recovery - -### Complete Service Failure - -In case of a complete service failure: - -1. Initiate incident response by notifying the on-call team through PagerDuty. - -2. Check for region-wide AWS issues on the AWS Status page. - -3. If necessary, trigger the DR plan to fail over to the secondary region: - ```bash - ./scripts/dr-failover.sh orders-service - ``` - -4. Update Route53 DNS to point to the secondary region if global failover is needed: - ```bash - aws route53 change-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID --change-batch file://dr-dns-change.json - ``` - -## Maintenance Tasks - -### Deploying New Versions - -```bash -kubectl set image deployment/orders-service -n orders orders-service=ecr.aws/flowmart/orders-service:$VERSION -``` - -### Database Maintenance - -Scheduled database maintenance should be performed during off-peak hours: - -1. Notify stakeholders through the #maintenance Slack channel. - -2. Set OrdersService to maintenance mode: - ```bash - curl -X POST https://api.internal.flowmart.com/orders/admin/maintenance -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"maintenanceMode": true, "message": "Scheduled maintenance"}' - ``` - -3. Perform database maintenance operations. - -4. Turn off maintenance mode: - ```bash - curl -X POST https://api.internal.flowmart.com/orders/admin/maintenance -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"maintenanceMode": false}' - ``` - -## Contact Information - -**Primary On-Call:** Orders Team (rotating schedule) -**Secondary On-Call:** Platform Team -**Escalation Path:** Orders Team Lead > Engineering Manager > CTO - -**Slack Channels:** -- #orders-support (primary support channel) -- #orders-alerts (automated alerts) -- #incident-response (for major incidents) - -## Reference Information - -- [OrdersService API Documentation](https://docs.internal.flowmart.com/orders/api) -- [Architecture Diagram](https://docs.internal.flowmart.com/architecture/orders) -- [Service Level Objectives (SLOs)](https://docs.internal.flowmart.com/slo/orders) \ No newline at end of file diff --git a/examples/default/docs/operations-and-support/runbooks/payment-service-runbook.mdx b/examples/default/docs/operations-and-support/runbooks/payment-service-runbook.mdx deleted file mode 100644 index 1bc27f5cf..000000000 --- a/examples/default/docs/operations-and-support/runbooks/payment-service-runbook.mdx +++ /dev/null @@ -1,376 +0,0 @@ ---- -title: PaymentService Runbook -summary: Operational runbook for troubleshooting and maintaining the PaymentService -sidebar: - label: PaymentService - order: 3 ---- - -This runbook provides operational procedures for the PaymentService, which is responsible for processing payments, refunds, and managing financial transactions in the FlowMart e-commerce platform. - -## Architecture - -The PaymentService is responsible for: -- Processing customer payments -- Managing refunds and chargebacks -- Integrating with external payment gateways -- Storing payment transactions -- Handling subscription billing - -### Service Dependencies - -```mermaid -flowchart TD - PaymentService --> PostgresDB[(PostgreSQL Database)] - PaymentService --> EventBus[Event Bus] - PaymentService --> StripeGateway[Stripe Payment Gateway] - PaymentService --> PayPalGateway[PayPal Gateway] - PaymentService --> VaultService[Vault - Secret Management] -``` - -## Monitoring and Alerting - -### Key Metrics - -| Metric | Description | Warning Threshold | Critical Threshold | -|--------|-------------|-------------------|-------------------| -| `payment_processing_rate` | Payments processed per minute | < 5 | < 1 | -| `payment_success_rate` | Percentage of successful payments | < 95% | < 90% | -| `payment_processing_latency` | Time to process a payment | > 3s | > 8s | -| `refund_processing_latency` | Time to process a refund | > 5s | > 15s | -| `gateway_error_rate` | Payment gateway errors | > 2% | > 5% | -| `fraud_detection_latency` | Time for fraud checks | > 1s | > 3s | - -### Dashboards - -- [PaymentService Overview](https://grafana.flowmart.com/d/payment-overview) -- [Payment Gateway Status](https://grafana.flowmart.com/d/payment-gateways) -- [Transaction Success Rates](https://grafana.flowmart.com/d/payment-success-rates) - -### Common Alerts - -| Alert | Description | Troubleshooting Steps | -|-------|-------------|----------------------| -| `PaymentServiceHighErrorRate` | Payment failure rate above threshold | See [High Error Rate](#high-error-rate) | -| `PaymentServiceGatewayFailure` | Payment gateway connection issues | See [Gateway Issues](#payment-gateway-issues) | -| `PaymentServiceHighLatency` | Payment processing latency issues | See [High Latency](#high-latency) | -| `PaymentServiceDatabaseIssues` | Database connection issues | See [Database Issues](#database-issues) | - -## Troubleshooting Guides - -### High Error Rate - -If the service is experiencing a high payment error rate: - -1. **Check application logs** for error patterns: - ```bash - kubectl logs -l app=payment-service -n payment --tail=100 - ``` - -2. **Check payment gateway status** on their status pages: - - [Stripe Status](https://status.stripe.com/) - - [PayPal Status](https://status.paypal.com/) - -3. **Check for patterns in failed transactions**: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/query-failed-transactions.js --last-hour - ``` - -4. **Check for recent deployments** that might have introduced issues: - ```bash - kubectl rollout history deployment/payment-service -n payment - ``` - -5. **Verify if the issue is specific to a payment method** (credit card, PayPal, etc.): - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/payment-method-success-rates.js - ``` - -### Payment Gateway Issues - -If there are issues with payment gateways: - -1. **Check gateway connectivity**: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- curl -o /dev/null -s -w "%{http_code}\n" https://api.stripe.com/v1/charges -H "Authorization: Bearer $STRIPE_TEST_KEY" - ``` - -2. **Check payment gateway API keys** rotation status: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/check-api-key-rotation.js - ``` - -3. **Check gateway timeouts** in application logs: - ```bash - kubectl logs -l app=payment-service -n payment | grep "gateway timeout" - ``` - -4. **Verify if the issue is isolated to a specific gateway**: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/gateway-health-check.js - ``` - -5. **Switch to backup payment gateway** if primary is down: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- curl -X POST localhost:3000/internal/api/payment/switch-gateway -H "Content-Type: application/json" -d '{"primaryGateway": "paypal", "reason": "Stripe outage"}' - ``` - -### High Latency - -If the service is experiencing high latency: - -1. **Check system metrics**: - ```bash - kubectl top pods -n payment - ``` - -2. **Check database connection pool**: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/db-pool-stats.js - ``` - -3. **Check slow queries** in the payment database: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- psql -U postgres -d payments -c "SELECT query, calls, mean_exec_time, max_exec_time FROM pg_stat_statements WHERE mean_exec_time > 100 ORDER BY mean_exec_time DESC LIMIT 10;" - ``` - -4. **Check payment gateway response times**: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/gateway-latency-check.js - ``` - -5. **Scale the service** if needed: - ```bash - kubectl scale deployment payment-service -n payment --replicas=5 - ``` - -### Database Issues - -If there are database issues: - -1. **Check PostgreSQL status**: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- pg_isready -U postgres -d payments - ``` - -2. **Check for long-running transactions**: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- psql -U postgres -d payments -c "SELECT pid, now() - xact_start AS duration, state, query FROM pg_stat_activity WHERE state != 'idle' ORDER BY duration DESC LIMIT 10;" - ``` - -3. **Check for database locks**: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- psql -U postgres -d payments -c "SELECT relation::regclass, mode, pid, granted FROM pg_locks l JOIN pg_stat_activity a ON l.pid = a.pid WHERE relation = 'payments.transactions'::regclass;" - ``` - -4. **Restart database connections** if needed: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- curl -X POST localhost:3000/internal/api/system/refresh-db-connections - ``` - -## Common Operational Tasks - -### Managing API Keys - -#### Rotating Payment Gateway API Keys - -1. **Generate new API keys** in the payment gateway admin portal. - -2. **Store the new keys** in AWS Secrets Manager: - ```bash - aws secretsmanager update-secret --secret-id flowmart/payment/stripe-api-key --secret-string '{"api_key": "sk_live_NEW_KEY", "webhook_secret": "whsec_NEW_SECRET"}' - ``` - -3. **Trigger key rotation** in the service: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- curl -X POST localhost:3000/internal/api/system/reload-api-keys - ``` - -4. **Verify the new keys are active**: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/verify-api-keys.js - ``` - -### Managing Refunds - -#### Processing Manual Refunds - -For special cases requiring manual intervention: - -```bash -curl -X POST https://api.internal.flowmart.com/payment/transactions/{transactionId}/refund \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"amount": 1999, "reason": "Customer service request", "refundToOriginalMethod": true}' -``` - -#### Finding Failed Refunds - -To identify and retry failed refunds: - -```bash -kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/list-failed-refunds.js --last-24h -``` - -### Handling Chargebacks - -To record and process a new chargeback: - -```bash -curl -X POST https://api.internal.flowmart.com/payment/transactions/{transactionId}/chargeback \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"chargebackReference": "CB12345", "amount": 1999, "reason": "Unauthorized transaction"}' -``` - -### Payment Reconciliation - -To trigger payment reconciliation with payment gateway: - -```bash -kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/reconcile-payments.js --gateway=stripe --date=2023-05-15 -``` - -## Recovery Procedures - -### Failed Transactions Recovery - -If transactions are stuck or failed: - -1. **Identify stuck transactions**: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/find-stuck-transactions.js - ``` - -2. **Check transaction status** with the payment gateway: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/check-gateway-transaction.js --transaction-id=TXN123456 - ``` - -3. **Resolve transactions** that completed at gateway but failed in our system: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/resolve-stuck-transaction.js --transaction-id=TXN123456 --status=completed - ``` - -### Payment Gateway Failure Recovery - -If a payment gateway is unavailable: - -1. **Enable fallback gateway** mode: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- curl -X POST localhost:3000/internal/api/system/enable-fallback-gateway - ``` - -2. **Monitor gateway status** for recovery: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/monitor-gateway-health.js --gateway=stripe - ``` - -3. **Disable fallback mode** once the primary gateway is restored: - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- curl -X POST localhost:3000/internal/api/system/disable-fallback-gateway - ``` - -### Database Failure Recovery - -If the PostgreSQL database becomes unavailable: - -1. Verify the status of the PostgreSQL cluster: - ```bash - kubectl get pods -l app=postgresql -n data - ``` - -2. Check if automatic failover has occurred: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql-patroni -n data -o jsonpath='{.items[0].metadata.name}') -n data -- patronictl list - ``` - -3. Once database availability is restored, validate the PaymentService functionality: - ```bash - curl -X GET https://api.internal.flowmart.com/payment/health - ``` - -## Disaster Recovery - -### Complete Service Failure - -In case of a complete service failure: - -1. Initiate incident response by notifying the on-call team through PagerDuty. - -2. If necessary, deploy to the disaster recovery environment: - ```bash - ./scripts/dr-failover.sh payment-service - ``` - -3. Update DNS records to point to the DR environment: - ```bash - aws route53 change-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID --change-batch file://dr-dns-change.json - ``` - -4. Enable simplified payment flow (if necessary): - ```bash - kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- curl -X POST localhost:3000/internal/api/system/enable-simplified-flow - ``` - -5. Regularly check primary environment recovery status. - -## Maintenance Tasks - -### Deploying New Versions - -```bash -kubectl set image deployment/payment-service -n payment payment-service=ecr.aws/flowmart/payment-service:$VERSION -``` - -### Database Migrations - -For database schema updates: - -1. Notify stakeholders through the #maintenance Slack channel. - -2. Create a migration plan and backup the database: - ```bash - kubectl exec -it $(kubectl get pods -l app=postgresql -n data -o jsonpath='{.items[0].metadata.name}') -n data -- pg_dump -U postgres -d payments > payments_backup_$(date +%Y%m%d).sql - ``` - -3. Apply database migrations: - ```bash - kubectl apply -f payment-migration-job.yaml - ``` - -4. Verify migration completion: - ```bash - kubectl logs -l job-name=payment-db-migration -n payment - ``` - -### Compliance and Auditing - -To generate PCI compliance reports: - -```bash -kubectl exec -it $(kubectl get pods -l app=payment-service -n payment -o jsonpath='{.items[0].metadata.name}') -n payment -- node scripts/generate-pci-audit-report.js --month=2023-05 -``` - -## Contact Information - -**Primary On-Call:** Payments Team (rotating schedule) -**Secondary On-Call:** Platform Team -**Escalation Path:** Payments Team Lead > Engineering Manager > CTO - -**Slack Channels:** -- #payments-support (primary support channel) -- #payments-alerts (automated alerts) -- #incident-response (for major incidents) - -**External Contacts:** -- Stripe Support: support@stripe.com, 1-888-555-1234 -- PayPal Support: merchant-support@paypal.com, 1-888-555-5678 - -## Reference Information - -- [PaymentService API Documentation](https://docs.internal.flowmart.com/payment/api) -- [Architecture Diagram](https://docs.internal.flowmart.com/architecture/payment) -- [Service Level Objectives (SLOs)](https://docs.internal.flowmart.com/slo/payment) -- [PCI Compliance Documentation](https://docs.internal.flowmart.com/security/payment-pci) -- [Payment Gateway Integration Guides](https://docs.internal.flowmart.com/payment/gateway-integration) \ No newline at end of file diff --git a/examples/default/docs/operations-and-support/runbooks/shipping-service-runbook.mdx b/examples/default/docs/operations-and-support/runbooks/shipping-service-runbook.mdx deleted file mode 100644 index 95a79438c..000000000 --- a/examples/default/docs/operations-and-support/runbooks/shipping-service-runbook.mdx +++ /dev/null @@ -1,434 +0,0 @@ ---- -title: ShippingService Runbook -summary: Operational runbook for troubleshooting and maintaining the ShippingService -sidebar: - label: ShippingService - order: 4 ---- - -This runbook provides operational procedures for the ShippingService, which is responsible for managing shipping options, carrier integration, and delivery tracking in the FlowMart e-commerce platform. - - -## Architecture - -The ShippingService is responsible for: -- Calculating shipping costs and delivery estimates -- Managing shipping carriers and integration -- Generating shipping labels -- Tracking shipments -- Handling delivery exceptions and returns - -### Service Dependencies - -```mermaid -flowchart TD - ShippingService --> MongoDB[(MongoDB Database)] - ShippingService --> EventBus[Event Bus] - ShippingService --> Redis[(Redis Cache)] - ShippingService --> FedExAPI[FedEx API] - ShippingService --> UPSApi[UPS API] - ShippingService --> USPSApi[USPS API] - ShippingService --> DHLApi[DHL API] - ShippingService --> VaultService[Vault - Secret Management] -``` - -## Monitoring and Alerting - -### Key Metrics - -| Metric | Description | Warning Threshold | Critical Threshold | -|--------|-------------|-------------------|-------------------| -| `shipping_rate_calculation_rate` | Rate calculations per minute | < 10 | < 2 | -| `shipping_label_generation_success` | Label generation success % | < 98% | < 95% | -| `carrier_api_response_time` | Carrier API response time | > 2s | > 5s | -| `carrier_api_error_rate` | Carrier API errors % | > 2% | > 5% | -| `tracking_update_processing_rate` | Tracking updates processed per minute | < 50 | < 10 | -| `shipment_tracking_lag` | Delay in tracking information | > 15m | > 1h | - -### Dashboards - -- [Shipping Service Overview](https://grafana.flowmart.com/d/shipping-overview) -- [Carrier API Status](https://grafana.flowmart.com/d/shipping-carriers) -- [Delivery Performance](https://grafana.flowmart.com/d/shipping-delivery-performance) - -### Common Alerts - -| Alert | Description | Troubleshooting Steps | -|-------|-------------|----------------------| -| `ShippingServiceHighErrorRate` | Shipping API error rate above threshold | See [High Error Rate](#high-error-rate) | -| `ShippingCarrierAPIDown` | Carrier API connection issues | See [Carrier API Issues](#carrier-api-issues) | -| `ShippingServiceHighLatency` | Shipping service latency issues | See [High Latency](#high-latency) | -| `ShippingServiceDatabaseIssues` | Database connection issues | See [Database Issues](#database-issues) | - -## Troubleshooting Guides - -### High Error Rate - -If the service is experiencing a high error rate: - -1. **Check application logs** for error patterns: - ```bash - kubectl logs -l app=shipping-service -n shipping --tail=100 - ``` - -2. **Check specific error types**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/error-analyzer.jar --last-hour - ``` - -3. **Check for patterns in failed shipments**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/failed-shipments-analyzer.jar - ``` - -4. **Check for recent deployments** that might have introduced issues: - ```bash - kubectl rollout history deployment/shipping-service -n shipping - ``` - -5. **Verify if the issue is specific to a carrier** (FedEx, UPS, etc.): - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/carrier-success-rates.jar - ``` - -### Carrier API Issues - -If there are issues with carrier APIs: - -1. **Check carrier API connectivity**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/carrier-health-check.jar - ``` - -2. **Check carrier API credentials** and rotation status: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/check-carrier-credentials.jar - ``` - -3. **Check carrier status pages** for announced outages: - - [FedEx Status](https://www.fedex.com/en-us/service-alerts.html) - - [UPS Status](https://www.ups.com/service-alerts) - - [USPS Status](https://about.usps.com/newsroom/service-alerts/) - -4. **Check carrier timeouts** in application logs: - ```bash - kubectl logs -l app=shipping-service -n shipping | grep "carrier timeout" - ``` - -5. **Enable fallback shipping carrier**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- curl -X POST localhost:8080/internal/api/shipping/enable-fallback-carrier -H "Content-Type: application/json" -d '{"primaryCarrier": "fedex", "fallbackCarrier": "ups", "reason": "FedEx API outage"}' - ``` - -### High Latency - -If the service is experiencing high latency: - -1. **Check system metrics**: - ```bash - kubectl top pods -n shipping - ``` - -2. **Check JVM memory and GC metrics**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/jvm-metrics.jar - ``` - -3. **Check MongoDB performance**: - ```bash - kubectl exec -it $(kubectl get pods -l app=mongodb -n data -o jsonpath='{.items[0].metadata.name}') -n data -- mongo --eval "db.currentOp()" - ``` - -4. **Check carrier API response times**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/carrier-response-times.jar - ``` - -5. **Scale the service** if needed: - ```bash - kubectl scale deployment shipping-service -n shipping --replicas=5 - ``` - -### Database Issues - -If there are MongoDB issues: - -1. **Check MongoDB status**: - ```bash - kubectl exec -it $(kubectl get pods -l app=mongodb -n data -o jsonpath='{.items[0].metadata.name}') -n data -- mongo --eval "rs.status()" - ``` - -2. **Check for slow queries**: - ```bash - kubectl exec -it $(kubectl get pods -l app=mongodb -n data -o jsonpath='{.items[0].metadata.name}') -n data -- mongo --eval "db.currentOp({ 'active': true, 'secs_running': { '$gt': 5 } })" - ``` - -3. **Check database connection pool**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/db-pool-stats.jar - ``` - -4. **Restart database connections** if needed: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- curl -X POST localhost:8080/internal/api/system/refresh-db-connections - ``` - -## Common Operational Tasks - -### Managing Carrier API Credentials - -#### Rotating Carrier API Keys - -1. **Generate new API keys** in the carrier portal: - - FedEx Developer Portal: [https://developer.fedex.com](https://developer.fedex.com) - - UPS Developer Portal: [https://developer.ups.com](https://developer.ups.com) - - USPS Web Tools: [https://www.usps.com/business/web-tools-apis](https://www.usps.com/business/web-tools-apis) - -2. **Store the new keys** in AWS Secrets Manager: - ```bash - aws secretsmanager update-secret --secret-id flowmart/shipping/fedex-api-key --secret-string '{"api_key": "NEW_KEY", "password": "NEW_PASSWORD", "account_number": "ACCOUNT_NUMBER"}' - ``` - -3. **Trigger key rotation** in the service: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- curl -X POST localhost:8080/internal/api/system/reload-carrier-credentials - ``` - -4. **Verify the new keys are working** by testing label generation: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/test-label-generation.jar --carrier=fedex - ``` - -### Managing Shipping Rates - -#### Updating Shipping Rate Tables - -When carrier rates change: - -1. **Prepare the new rate table** in the required JSON format. - -2. **Upload the rate table** to S3: - ```bash - aws s3 cp new-fedex-rates.json s3://flowmart-configs/shipping/rates/ - ``` - -3. **Trigger rate table reload**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- curl -X POST localhost:8080/internal/api/shipping/reload-rate-tables -H "Content-Type: application/json" -d '{"carrier": "fedex"}' - ``` - -4. **Verify rate calculations** with test scenarios: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/test-rate-calculation.jar - ``` - -### Shipping Label Generation Troubleshooting - -#### Debugging Failed Label Generation - -If labels are failing to generate: - -```bash -# Find recent failed label generation attempts -kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/find-failed-labels.jar --hours=2 - -# Get detailed error for a specific shipment -kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/label-error-details.jar --shipment-id=SHIP123456 -``` - -#### Manual Label Generation - -For special cases requiring manual intervention: - -```bash -curl -X POST https://api.internal.flowmart.com/shipping/shipments/{shipmentId}/generate-label \ - -H "Authorization: Bearer $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"carrier": "fedex", "service": "PRIORITY_OVERNIGHT", "forceGeneration": true}' -``` - -### Tracking Updates - -#### Triggering Manual Tracking Updates - -To manually trigger tracking updates: - -```bash -kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/sync-tracking.jar --shipment-id=SHIP123456 - -# For bulk tracking updates -kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/sync-tracking.jar --status=in_transit --hours=24 -``` - -#### Tracking Webhook Troubleshooting - -If tracking webhooks from carriers are failing: - -```bash -# Check recent webhook failures -kubectl logs -l app=shipping-webhook-service -n shipping | grep "Webhook failure" - -# Replay failed webhooks -kubectl exec -it $(kubectl get pods -l app=shipping-webhook-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/replay-webhooks.jar --hours=2 -``` - -## Recovery Procedures - -### Failed Shipment Recovery - -If shipments are stuck or failed: - -1. **Identify stuck shipments**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/find-stuck-shipments.jar - ``` - -2. **Check shipment status** with the carrier: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/check-carrier-shipment.jar --shipment-id=SHIP123456 - ``` - -3. **Resolve shipments** that completed at carrier but failed in our system: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/resolve-shipment.jar --shipment-id=SHIP123456 --tracking-number=1Z999AA10123456784 --status=label_created - ``` - -### Carrier API Failure Recovery - -If a carrier API is unavailable: - -1. **Enable automatic carrier fallback**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- curl -X POST localhost:8080/internal/api/system/enable-carrier-fallback - ``` - -2. **Monitor carrier API status** for recovery: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/monitor-carrier-health.jar --carrier=fedex - ``` - -3. **Switch back to primary carrier** once it's restored: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- curl -X POST localhost:8080/internal/api/system/disable-carrier-fallback - ``` - -### Database Failure Recovery - -If the MongoDB database becomes unavailable: - -1. **Verify the status of the MongoDB cluster**: - ```bash - kubectl get pods -l app=mongodb -n data - ``` - -2. **Check if automatic failover has occurred**: - ```bash - kubectl exec -it $(kubectl get pods -l app=mongodb -n data -o jsonpath='{.items[0].metadata.name}') -n data -- mongo --eval "rs.status()" - ``` - -3. **Once database availability is restored, validate ShippingService functionality**: - ```bash - curl -X GET https://api.internal.flowmart.com/shipping/health - ``` - -## Disaster Recovery - -### Complete Service Failure - -In case of a complete service failure: - -1. **Initiate incident response** by notifying the on-call team through PagerDuty. - -2. **Deploy to the disaster recovery environment** if necessary: - ```bash - ./scripts/dr-failover.sh shipping-service - ``` - -3. **Update DNS records** to point to the DR environment: - ```bash - aws route53 change-resource-record-sets --hosted-zone-id $HOSTED_ZONE_ID --change-batch file://dr-dns-change.json - ``` - -4. **Enable simplified shipping flow** (if necessary): - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- curl -X POST localhost:8080/internal/api/system/enable-simplified-flow - ``` - -5. **Regularly check primary environment recovery status**. - -## Maintenance Tasks - -### Deploying New Versions - -```bash -kubectl set image deployment/shipping-service -n shipping shipping-service=ecr.aws/flowmart/shipping-service:$VERSION -``` - -### Database Maintenance - -#### MongoDB Index Maintenance - -Periodically verify and optimize MongoDB indexes: - -```bash -# Check current indexes -kubectl exec -it $(kubectl get pods -l app=mongodb -n data -o jsonpath='{.items[0].metadata.name}') -n data -- mongo --eval "db.shipments.getIndexes()" - -# Add new index (example) -kubectl exec -it $(kubectl get pods -l app=mongodb -n data -o jsonpath='{.items[0].metadata.name}') -n data -- mongo --eval "db.shipments.createIndex({carrier: 1, status: 1, createdAt: -1})" -``` - -#### Database Backups - -Verify scheduled MongoDB backups: - -```bash -# Check recent backups -aws s3 ls s3://flowmart-mongodb-backups/shipping/ --human-readable - -# Trigger manual backup if needed -kubectl apply -f shipping-db-backup-job.yaml -``` - -### Carrier Integration Updates - -When a carrier updates their API: - -1. **Test the API changes** in the staging environment: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service-staging -n shipping-staging -o jsonpath='{.items[0].metadata.name}') -n shipping-staging -- java -jar /app/tools/test-carrier-integration.jar --carrier=fedex --mode=new - ``` - -2. **Update integration configuration** if needed: - ```bash - kubectl apply -f updated-fedex-integration-config.yaml - ``` - -3. **Validate the updated integration**: - ```bash - kubectl exec -it $(kubectl get pods -l app=shipping-service -n shipping -o jsonpath='{.items[0].metadata.name}') -n shipping -- java -jar /app/tools/validate-carrier-integration.jar --carrier=fedex - ``` - -## Contact Information - -**Primary On-Call:** Logistics Team (rotating schedule) -**Secondary On-Call:** Platform Team -**Escalation Path:** Logistics Team Lead > Engineering Manager > CTO - -**Slack Channels:** -- #shipping-support (primary support channel) -- #shipping-alerts (automated alerts) -- #incident-response (for major incidents) - -**External Contacts:** -- FedEx API Support: apisupport@fedex.com, 1-800-555-1234 -- UPS Developer Support: developer@ups.com, 1-800-555-5678 -- USPS Web Tools Support: uspstechsupport@usps.gov, 1-800-555-9012 - -## Reference Information - -- [ShippingService API Documentation](https://docs.internal.flowmart.com/shipping/api) -- [Architecture Diagram](https://docs.internal.flowmart.com/architecture/shipping) -- [Service Level Objectives (SLOs)](https://docs.internal.flowmart.com/slo/shipping) -- [Carrier Integration Guides](https://docs.internal.flowmart.com/shipping/carrier-integration) -- [Rate Calculation Documentation](https://docs.internal.flowmart.com/shipping/rate-calculation) \ No newline at end of file diff --git a/examples/default/docs/operations/01-team-ownership.mdx b/examples/default/docs/operations/01-team-ownership.mdx new file mode 100644 index 000000000..d261d9ae6 --- /dev/null +++ b/examples/default/docs/operations/01-team-ownership.mdx @@ -0,0 +1,35 @@ +--- +title: Team ownership +summary: Which platform teams own the major Acme Inc domains and systems documented in the default catalog. +owners: + - dboyne +badges: + - content: Operations + backgroundColor: gray + textColor: gray +--- + +Ownership in the default catalog is modelled with team resources. The owning team is responsible for contract changes, service reliability and documentation freshness for its resources. + +## Teams by capability + +| Team | Owns | +|------|------| +| [[team\|product-platform]] | [[domain\|catalog]], [[system\|product-catalog-system]] and product APIs/workers. | +| [[team\|search-platform]] | [[system\|search-system]], search indexing and search query contracts. | +| [[team\|shopping-platform]] | [[domain\|shopping]], [[system\|cart-system]] and [[system\|promotion-system]]. | +| [[team\|ordering-platform]] | [[domain\|ordering]], [[system\|checkout-system]] and [[system\|order-management-system]]. | +| [[team\|payments-platform]] | [[domain\|payments]] and [[system\|payment-processing-system]]. | +| [[team\|fulfilment-platform]] | [[domain\|fulfilment]], inventory, warehouse and shipping systems. | +| [[team\|customer-platform]] | [[domain\|customer]], customer profile and identity systems. | +| [[team\|reviews-platform]] | [[domain\|reviews]], review moderation and rating aggregation. | + +## Ownership rules + +The owner listed on the producing resource owns the contract. Consumers can request changes, but producers decide versioning and rollout. + +When a flow spans multiple teams, the team owning the orchestrator is responsible for keeping the flow page current. For checkout, that is [[team|ordering-platform]]. + +## Documentation expectations + +Every resource page should explain responsibility, owners and major contracts. These custom docs should explain cross-resource narratives that do not belong to one resource. diff --git a/examples/default/docs/operations/02-change-management.mdx b/examples/default/docs/operations/02-change-management.mdx new file mode 100644 index 000000000..8fd032958 --- /dev/null +++ b/examples/default/docs/operations/02-change-management.mdx @@ -0,0 +1,36 @@ +--- +title: Change management +summary: How to safely change contracts, schemas, systems and cross-domain flows in the Acme Inc catalog. +owners: + - dboyne +badges: + - content: Operations + backgroundColor: gray + textColor: gray +--- + +Changes should start with ownership and blast radius. A small service implementation change may not affect the catalog. A schema or flow change usually does. + +## Contract changes + +Before changing a command, event or query: + +1. Identify the producer and all consumers. +2. Check whether the change is backward compatible. +3. Update the schema and resource summary. +4. Update affected flow pages. +5. Coordinate rollout with consumer owners. + +High-risk contracts include [[event|cart-checked-out]], [[command|reserve-inventory]], [[command|authorize-payment]], [[command|create-order]], [[event|order-created]] and [[event|review-published]]. + +## System changes + +When a system adds a service or data store, update the system page and any domain page that lists that system. If the change alters ownership of data, update the data ownership page too. + +## Flow changes + +For flows such as [[flow|place-an-order]], [[flow|checkout-saga]], [[flow|product-search-indexing]] and [[flow|review-submission]], update the diagram and the narrative together. A diagram without the operating rules is not enough for on-call or onboarding. + +## Decision records + +Use ADRs when a decision changes a long-lived pattern. The Product Catalog System already documents key decisions such as [[adr|adr-001-use-transactional-outbox]], [[adr|adr-002-postgres-as-system-of-record]] and [[adr|adr-003-offload-async-work-to-worker]]. diff --git a/examples/default/docs/operations/03-governance-model.mdx b/examples/default/docs/operations/03-governance-model.mdx new file mode 100644 index 000000000..d057c399b --- /dev/null +++ b/examples/default/docs/operations/03-governance-model.mdx @@ -0,0 +1,47 @@ +--- +title: Governance model +summary: The operating model Acme teams use to keep domain ownership, catalog quality and cross-team decisions clear. +owners: + - platform-governance +badges: + - content: Standard + backgroundColor: gray + textColor: gray +--- + +Governance at Acme is lightweight but explicit. Teams own the catalog entries for their domains, systems, services, messages and flows, while the Platform Governance group maintains the standards that keep those entries consistent. + +## Ownership rules + +Every production resource must have: + +- a clear owning team, +- a business capability or system boundary, +- documented upstream and downstream dependencies, +- a current version where the resource has versioned contracts, +- enough context for another team to assess impact without asking in chat first. + +Domain teams own business meaning. Platform teams own shared tooling and catalog hygiene. Integration decisions are shared when a change crosses domain boundaries. + +## Governance forums + +| Forum | Cadence | Purpose | +|-------|---------|---------| +| Architecture review | Weekly | Review cross-domain changes, new systems and integration risks. | +| Contract review | On demand | Review breaking command, query and event changes before implementation. | +| Operations review | Weekly | Review incidents, SLO misses and catalog gaps found during support. | +| Catalog health review | Monthly | Remove stale resources and improve ownership metadata. | + +## Decision records + +Use ADRs when the decision changes a system boundary, persistence model, integration pattern or team operating model. The Catalog domain shows the expected level of detail with [[adr|adr-001-use-transactional-outbox]], [[adr|adr-002-postgres-as-system-of-record]] and [[adr|adr-003-offload-async-work-to-worker]]. + + + Review this catalog area for governance gaps. Check for missing owners, unclear domain boundaries, undocumented + dependencies, stale decision records, unreviewed cross-domain contracts and missing operational notes. Return the gaps as + a prioritized checklist with suggested updates to EventCatalog pages. + + +## Escalation path + +If two teams disagree on a contract or boundary, the owning domain proposes the default path, affected consuming teams document the impact, and the architecture review makes the final call. diff --git a/examples/default/docs/operations/04-contract-standards.mdx b/examples/default/docs/operations/04-contract-standards.mdx new file mode 100644 index 000000000..84e1e383d --- /dev/null +++ b/examples/default/docs/operations/04-contract-standards.mdx @@ -0,0 +1,48 @@ +--- +title: Contract standards +summary: The rules Acme teams follow when defining, versioning and changing commands, events and queries. +owners: + - platform-governance +badges: + - content: Standard + backgroundColor: orange + textColor: orange +--- + +Commands, events and queries are the public language between Acme systems. A contract is not ready until it is understandable by a team that does not own the implementation. + +## Contract types + +| Type | Meaning | Example | +|------|---------|---------| +| Command | A request for an owner to do work. | [[command\|reserve-inventory]], [[command\|authorize-payment]], [[command\|create-order]] | +| Event | A fact that already happened. | [[event\|order-created]], [[event\|payment-succeeded]], [[event\|inventory-reserved]] | +| Query | A read contract owned by one capability. | [[query\|get-order]], [[query\|search-products]], [[query\|get-product-reviews]] | + +## Required fields + +Every contract page should document: + +- business purpose, +- producer or handler, +- consumers where known, +- schema or payload reference, +- version and compatibility policy, +- failure semantics for commands and queries, +- replay and ordering assumptions for events. + +## Versioning rules + +Additive fields are allowed when consumers can ignore them. Removing fields, changing meaning, tightening validation or changing delivery semantics is breaking and requires a new version or a migration plan. + +Use deprecation before removal. Keep the old contract documented until all consumers have moved. + +## Review checklist + +Before changing a contract, teams must identify all known consumers, confirm whether the change is additive or breaking, update flow pages that depend on the contract, and add an ADR when the change introduces a new integration pattern. + + + Review a proposed command, event or query contract change for Acme Inc. Decide whether it is additive or breaking, list + known consumer risks, identify missing schema or payload details, and suggest a migration plan if the change is not + backward compatible. Include the catalog pages that need updates. + diff --git a/examples/default/docs/operations/05-event-design-standards.mdx b/examples/default/docs/operations/05-event-design-standards.mdx new file mode 100644 index 000000000..a6232ddaa --- /dev/null +++ b/examples/default/docs/operations/05-event-design-standards.mdx @@ -0,0 +1,47 @@ +--- +title: Event design standards +summary: How Acme teams name, publish, consume and operate business events across the commerce platform. +owners: + - platform-governance +badges: + - content: Standard + backgroundColor: blue + textColor: blue +--- + +Events represent business facts, not instructions. The publishing team owns the meaning of the event and the state transition that produced it. + +## Naming + +Event names should be past tense and business-readable: + +- [[event|cart-checked-out]] +- [[event|order-created]] +- [[event|payment-succeeded]] +- [[event|review-published]] + +Avoid names that describe infrastructure actions, implementation classes or subscriber intent. + +## Publishing + +Publish events from the system that owns the state change. For product changes, [[system|product-catalog-system]] publishes product events because [[container|product-database]] is authoritative. For order lifecycle changes, [[system|order-management-system]] publishes order events because [[container|order-database]] is authoritative. + +Use an outbox or equivalent durable handoff for events that represent committed business state. + +## Consumption + +Consumers must be idempotent. Delivery can be retried, delayed or observed out of order across independent streams. Consumers should store processed event identifiers when duplicate handling would otherwise create side effects. + +## Event payloads + +Payloads should carry enough information for consumers to decide whether they care, but not a full copy of another domain's private data model. Include stable identifiers, timestamps and business status fields. Keep sensitive customer and payment data out of broad event payloads. + + + Review this event design against Acme's event standards. Check that the event name is a past-tense business fact, the + publisher owns the state change, the payload avoids private data models, consumers can process it idempotently, and the + related flow documentation is updated. Return specific recommendations. + + +## Flow impact + +When an event is added to a critical journey, update the related flow page. Checkout-related event changes should be reflected in [[flow|place-an-order]] or [[flow|checkout-saga]]. diff --git a/examples/default/docs/operations/06-operational-readiness.mdx b/examples/default/docs/operations/06-operational-readiness.mdx new file mode 100644 index 000000000..c246a290e --- /dev/null +++ b/examples/default/docs/operations/06-operational-readiness.mdx @@ -0,0 +1,50 @@ +--- +title: Operational readiness +summary: The launch and support standards Acme applies before a system or contract is considered production-ready. +owners: + - platform-governance +badges: + - content: Standard + backgroundColor: green + textColor: green +--- + +A system is production-ready when another team can understand ownership, dependencies and failure behavior from the catalog before an incident starts. + +## Required catalog coverage + +Before production launch, each system must document: + +- owning team and escalation path, +- services and containers, +- commands, queries and events, +- critical flows that include the system, +- external dependencies, +- operational dashboards or runbooks where available, +- known failure modes and recovery expectations. + +## Critical system map + +Checkout is the reference standard for operational coverage because it crosses Shopping, Ordering, Payments and Fulfilment. + + + +## Launch gates + +| Gate | Required evidence | +|------|-------------------| +| Ownership | Team and escalation path are present in the catalog. | +| Contracts | Public commands, events and queries have schemas or payload notes. | +| Observability | Error, latency and throughput signals exist for customer-facing paths. | +| Recovery | Retry, compensation or manual recovery behavior is documented. | +| Dependencies | Upstream and downstream systems are linked in the catalog. | + + + Review this system for production readiness at Acme Inc. Check ownership, escalation, contracts, observability, + dependency mapping, failure modes, recovery behavior and customer impact. Return launch blockers, follow-up tasks and + catalog pages that should be updated before release. + + +## Incident updates + +After an incident, update the catalog when the incident reveals a missing dependency, stale owner, unclear contract, undocumented failure mode or misleading flow. diff --git a/examples/default/docs/operations/07-review-and-change-workflow.mdx b/examples/default/docs/operations/07-review-and-change-workflow.mdx new file mode 100644 index 000000000..3c480af77 --- /dev/null +++ b/examples/default/docs/operations/07-review-and-change-workflow.mdx @@ -0,0 +1,56 @@ +--- +title: Review and change workflow +summary: The standard workflow Acme teams use to make catalog-backed architecture and integration changes. +owners: + - platform-governance +badges: + - content: Standard + backgroundColor: purple + textColor: purple +--- + +Catalog updates should land with the system change they describe. The catalog is part of the delivery artifact, not an after-the-fact diagram store. + +## Change categories + +| Change | Required review | +|--------|-----------------| +| Local service implementation only | Owning team review. | +| New command, event or query | Owning team plus known consumers. | +| Breaking contract change | Contract review and migration plan. | +| New system or external dependency | Architecture review. | +| Critical flow behavior change | Owning teams for every domain in the flow. | + +## Standard workflow + + + + Update the affected domain, system or flow page with the business reason for the change. + + + Add or revise command, event and query pages before consumers integrate with them. + + + Check system maps and flow pages for upstream and downstream dependencies. + + + Add an ADR when the change affects ownership, persistence, integration style or operational recovery. + + + After release, confirm the catalog matches the deployed behavior and remove stale notes. + + + + + Create a review checklist for a proposed Acme architecture change. Include affected domains, systems, services, + commands, events, queries, flows, owners, operational risks, decision records and release follow-up. Keep the output + practical enough to paste into a pull request description. + + +## Flow review + +For checkout changes, review both the customer journey and the internal saga before implementation. + + + + diff --git a/examples/default/docs/platform/01-architecture-overview.mdx b/examples/default/docs/platform/01-architecture-overview.mdx new file mode 100644 index 000000000..4ce014361 --- /dev/null +++ b/examples/default/docs/platform/01-architecture-overview.mdx @@ -0,0 +1,50 @@ +--- +title: Architecture overview +summary: How Acme Inc's commerce platform is organised across domains, systems, services, messages, data stores and teams. +owners: + - dboyne +badges: + - content: Overview + backgroundColor: blue + textColor: blue +--- + +Acme Inc runs a commerce platform that takes a customer from product discovery through cart, checkout, payment, fulfilment, delivery and post-purchase feedback. The catalog is organised around business capabilities rather than deployment topology. + +The main business domains are [[domain|catalog]], [[domain|shopping]], [[domain|ordering]], [[domain|payments]], [[domain|fulfilment]], [[domain|customer]] and [[domain|reviews]]. Each domain owns the services, messages and data stores that support its business language. + +## Core checkout map + +The checkout capability is the best compact view of how Acme's platform is connected. It shows the orchestration boundary between cart checkout, inventory reservation, payment authorization and durable order creation. + + + +## Platform shape + +| Area | Primary capability | Core systems | +|------|--------------------|--------------| +| [[domain\|catalog]] | Product data and search | [[system\|product-catalog-system]], [[system\|search-system]] | +| [[domain\|shopping]] | Cart and promotions | [[system\|cart-system]], [[system\|promotion-system]] | +| [[domain\|ordering]] | Checkout and order lifecycle | [[system\|checkout-system]], [[system\|order-management-system]] | +| [[domain\|payments]] | Payment authorization, capture and refunds | [[system\|payment-processing-system]], [[system\|stripe]], [[system\|fraud-detection]] | +| [[domain\|fulfilment]] | Stock, warehouse and shipment | [[system\|inventory-system]], [[system\|warehouse-system]], [[system\|shipping-system]], [[system\|carrier]] | +| [[domain\|customer]] | Customer profile and authentication | [[system\|customer-management-system]], [[system\|identity-provider]] | +| [[domain\|reviews]] | Product feedback and ratings | [[service\|review-api]], [[service\|review-moderation-worker]], [[service\|rating-aggregator]] | + +## Architectural style + +The platform is event-driven where state changes matter to another capability. Commands are used to request work from an owning capability, queries are used for explicit read access, and events announce facts that have already happened. + +The most important chain is the order journey: + +1. [[service|cart-api]] publishes [[event|cart-checked-out]]. +2. [[service|checkout-orchestrator]] coordinates [[command|reserve-inventory]], [[command|authorize-payment]] and [[command|create-order]]. +3. [[service|order-service]] publishes [[event|order-created]] and [[event|order-completed]]. +4. [[service|warehouse-service]] reacts to completed orders and publishes [[event|order-ready-for-shipping]]. +5. Shipment and carrier events track delivery progress. + +## Reading the catalog + +Use resource pages when you need the authoritative contract for a single component. Use these custom docs when you need the operating narrative: why a capability exists, how systems collaborate, and where ownership boundaries sit. + +Start with the domain pages when orienting around business capability. Start with flow pages when diagnosing an end-to-end customer journey. diff --git a/examples/default/docs/platform/02-domain-map.mdx b/examples/default/docs/platform/02-domain-map.mdx new file mode 100644 index 000000000..99fe1dabf --- /dev/null +++ b/examples/default/docs/platform/02-domain-map.mdx @@ -0,0 +1,37 @@ +--- +title: Domain map +summary: The bounded contexts in the Acme Inc catalog and how their responsibilities connect across the commerce journey. +owners: + - dboyne +badges: + - content: Architecture + backgroundColor: purple + textColor: purple +--- + +The domain map shows where business language changes. A cart, an order, a payment and a shipment are different concepts with different owners, lifecycles and consistency rules. + +## Core domains + +[[domain|shopping]], [[domain|ordering]], [[domain|payments]] and [[domain|fulfilment]] form the core purchase path. A failure in any of these domains can block revenue or customer delivery, so their contracts should be reviewed together when changing checkout behavior. + +| Domain | Owns | Publishes or handles | +|--------|------|----------------------| +| [[domain\|shopping]] | Cart contents, checkout intent and discounts | [[command\|checkout-cart]], [[event\|cart-checked-out]], [[command\|calculate-discount]] | +| [[domain\|ordering]] | Checkout orchestration and order state | [[command\|reserve-inventory]], [[command\|authorize-payment]], [[command\|create-order]], [[event\|order-created]] | +| [[domain\|payments]] | Payment intent, payment outcome and refunds | [[event\|payment-requested]], [[event\|payment-succeeded]], [[event\|payment-failed]], [[event\|refund-requested]] | +| [[domain\|fulfilment]] | Stock reservation, warehouse packing and shipping | [[event\|inventory-reserved]], [[event\|order-ready-for-shipping]], [[command\|create-shipment]] | + +## Supporting domains + +[[domain|catalog]], [[domain|customer]] and [[domain|reviews]] support the buying experience. They are still product-critical, but their change cadence and runtime failure modes are different from checkout orchestration. + +Catalog and Search keep product data discoverable. Customer and Identity keep profiles and authentication separate from order state. Reviews and Ratings feed product trust signals back into the storefront. + +## Integration boundaries + +External systems are modelled explicitly so ownership and failure handling are visible. [[system|stripe]], [[system|fraud-detection]] and [[system|carrier]] are outside Acme's control. Internal services should treat these integrations as unreliable boundaries and persist enough state to retry safely. + +## Change guidance + +When a change crosses a domain boundary, update both sides of the contract. For example, changing checkout payloads requires reviewing [[event|cart-checked-out]], the [[flow|checkout-saga]], and any commands sent by [[service|checkout-orchestrator]]. diff --git a/examples/default/docs/platform/03-systems-map.mdx b/examples/default/docs/platform/03-systems-map.mdx new file mode 100644 index 000000000..e5ebcf48e --- /dev/null +++ b/examples/default/docs/platform/03-systems-map.mdx @@ -0,0 +1,51 @@ +--- +title: Systems map +summary: The system-level view of Acme Inc's commerce platform and the responsibilities of each major system. +owners: + - dboyne +badges: + - content: Systems + backgroundColor: gray + textColor: gray +--- + +Systems group services and data stores around a stable business capability. They are the best unit for understanding runtime ownership, operational dashboards and integration boundaries. + +## Checkout and payment maps + +The checkout and payment systems are the highest-coupling parts of the platform. Use these maps when assessing checkout changes, payment incident impact or ownership questions across Ordering and Payments. + + + + + +## Internal systems + +| System | Domain | Role | +|--------|--------|------| +| [[system\|product-catalog-system]] | [[domain\|catalog]] | Source of truth for product data and product change events. | +| [[system\|search-system]] | [[domain\|catalog]] | Maintains a search index and serves product search queries. | +| [[system\|cart-system]] | [[domain\|shopping]] | Owns carts and emits checkout intent. | +| [[system\|promotion-system]] | [[domain\|shopping]] | Calculates discounts for carts. | +| [[system\|checkout-system]] | [[domain\|ordering]] | Coordinates checkout as a saga. | +| [[system\|order-management-system]] | [[domain\|ordering]] | Owns order state and order lifecycle events. | +| [[system\|payment-processing-system]] | [[domain\|payments]] | Records payment intent and drives payment/refund processing. | +| [[system\|inventory-system]] | [[domain\|fulfilment]] | Owns stock and reservations. | +| [[system\|warehouse-system]] | [[domain\|fulfilment]] | Picks and packs completed orders. | +| [[system\|shipping-system]] | [[domain\|fulfilment]] | Creates shipments and tracks delivery handoff. | +| [[system\|customer-management-system]] | [[domain\|customer]] | Owns customer profile data. | +| [[system\|identity-provider]] | [[domain\|customer]] | Authenticates customers and manages user credentials. | + +## External systems + +The catalog also documents external dependencies: + +- [[system|stripe]] processes charges and refunds. +- [[system|fraud-detection]] screens payments. +- [[system|carrier]] creates and tracks shipments. + +## Why systems matter + +Services can change implementation technology, but system responsibilities should stay stable. For example, [[service|payment-api]] and [[service|payment-worker]] can evolve independently, but both remain inside [[system|payment-processing-system]] and share the same payment data ownership boundary. + +Use system pages for impact analysis when a change affects multiple services or a shared data store. diff --git a/examples/default/docs/platform/04-data-ownership.mdx b/examples/default/docs/platform/04-data-ownership.mdx new file mode 100644 index 000000000..aa3c42b87 --- /dev/null +++ b/examples/default/docs/platform/04-data-ownership.mdx @@ -0,0 +1,40 @@ +--- +title: Data ownership +summary: The source-of-truth data stores in the default catalog and the rules for reading and writing business state. +owners: + - dboyne +badges: + - content: Data + backgroundColor: green + textColor: green +--- + +Every durable business concept has an owning system and a source-of-truth store. Other systems should depend on published contracts instead of reaching into another system's data store. + +## Source-of-truth stores + +| Data store | Owner | Business data | +|------------|-------|---------------| +| [[container\|product-database]] | [[system\|product-catalog-system]] | Products, product attributes and catalog change outbox. | +| [[container\|search-index]] | [[system\|search-system]] | Search-optimised product documents. | +| [[container\|cart-database]] | [[system\|cart-system]] | Active shopping carts and checkout state. | +| [[container\|promotion-database]] | [[system\|promotion-system]] | Promotion rules and discount configuration. | +| [[container\|order-database]] | [[system\|order-management-system]] | Orders and order lifecycle state. | +| [[container\|payment-database]] | [[system\|payment-processing-system]] | Payment intents, charge outcomes and refunds. | +| [[container\|inventory-database]] | [[system\|inventory-system]] | Stock levels and reservations. | +| [[container\|warehouse-database]] | [[system\|warehouse-system]] | Picking and packing work. | +| [[container\|customer-database]] | [[system\|customer-management-system]] | Customer profiles. | +| [[container\|review-database]] | [[domain\|reviews]] | Reviews and moderation state. | +| [[container\|rating-cache]] | [[domain\|reviews]] | Product rating read model. | + +## Read model pattern + +[[system|search-system]] and [[container|rating-cache]] are read models. They do not own the original facts. They rebuild their state from events such as [[event|product-created]], [[event|product-updated]], [[event|review-published]] and [[event|rating-updated]]. + +## Write rule + +Only the owning system writes its source-of-truth store. Cross-system updates should be commands or events. For example, checkout asks fulfilment to reserve stock with [[command|reserve-inventory]] rather than writing [[container|inventory-database]] directly. + +## Consistency rule + +User-facing flows should make the consistency model explicit. Checkout needs coordinated confirmation across inventory, payment and order creation, so it uses the [[flow|checkout-saga]]. Search indexing can lag product writes, so it uses asynchronous product events. diff --git a/examples/default/docs/platform/05-eventing-model.mdx b/examples/default/docs/platform/05-eventing-model.mdx new file mode 100644 index 000000000..858dc3c79 --- /dev/null +++ b/examples/default/docs/platform/05-eventing-model.mdx @@ -0,0 +1,48 @@ +--- +title: Eventing model +summary: How commands, events and queries are used across Acme Inc's event-driven commerce platform. +owners: + - dboyne +badges: + - content: Contracts + backgroundColor: orange + textColor: orange +--- + +The catalog separates messages by intent. + +- **Commands** ask another capability to do something. +- **Events** announce that something has already happened. +- **Queries** request information without changing state. + +## Commands + +Commands are named in the imperative and should have one clear handler. Examples: + +- [[command|checkout-cart]] asks [[service|cart-api]] to check out a cart. +- [[command|reserve-inventory]] asks [[service|inventory-service]] to hold stock. +- [[command|authorize-payment]] asks [[service|payment-api]] to authorize payment. +- [[command|create-order]] asks [[service|order-service]] to create an order. +- [[command|submit-review]] asks [[service|review-api]] to accept a product review. + +## Events + +Events are facts and should be safe for multiple consumers. Examples: + +- [[event|cart-checked-out]] starts the checkout process. +- [[event|order-created]] signals that an order exists. +- [[event|payment-succeeded]] and [[event|payment-failed]] report external processor outcomes. +- [[event|order-ready-for-shipping]] hands fulfilment to shipping. +- [[event|review-published]] updates review read models and product trust signals. + +## Queries + +Queries are explicit read contracts. Examples include [[query|get-product]], [[query|search-products]], [[query|get-order]], [[query|get-customer]], [[query|get-stock-level]] and [[query|get-product-reviews]]. + +## Versioning expectations + +Backward-compatible additions are preferred. Removing fields or changing semantics requires a versioned contract and a migration plan. For customer-critical flows, review the flow page and all participating messages before changing a schema. + +## Failure expectations + +Consumers should be idempotent. Producers should publish facts after durable state changes. Long-running processes should persist their progress, especially [[flow|checkout-saga]], payment processing, search indexing and review moderation. diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/01-architecture-desicion-record.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/01-architecture-desicion-record.mdx deleted file mode 100644 index 5d2b3b6ec..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/01-architecture-desicion-record.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: Architecture Decision Records (ADR) -summary: A document that captures important architectural decisions and their context -sidebar: - label: ADR Template - order: 1 ---- - - - -## What is an Architecture Decision Record (ADR)? - - - - - - -Architecture Decision Records (ADRs) are documents that capture important architectural decisions made during the development of a software system. Each ADR describes a choice the team has made, the context in which it was made, and the consequences of that choice. - -## Why ADRs are Important - -:::tip -Use this template to create your own ADRs. Once you read this page you can submit new ADRs to the [Architecture Decision Records](https://github.com/eventcatalog/eventcatalog/issues/new?assignees=&labels=architecture&template=architecture-decision-record.mdx&title=ADR%3A+) repository. -::: - -ADRs help teams: -- Document decisions for future reference -- Communicate architectural choices across the organization -- Onboard new team members by providing insight into past decisions -- Track the evolution of the system architecture over time -- Establish a process for making and documenting significant technical decisions - -## ADR Format - -Our ADRs follow this structure: -- **Title**: A descriptive name for the decision -- **Status**: Current status (Proposed, Accepted, Superseded, etc.) -- **Context**: The factors that influenced the decision -- **Decision**: The choice that was made -- **Consequences**: The resulting outcomes, both positive and negative -- **Compliance Requirements**: Any regulatory or policy requirements that must be met -- **Implementation Details**: How and when the decision will be implemented -- **Alternatives**: Other options that were considered -- **References**: Resources that support or provide more information -- **Decision History**: The record of changes to the ADR - -```mermaid -flowchart TD - A[Identify Architectural Decision Point] --> B[Discuss Options] - B --> C[Draft ADR] - C --> D[Review with Stakeholders] - D --> E{Approved?} - E -->|Yes| F[Update Status to Approved] - E -->|No| G[Revise ADR] - G --> D - F --> H[Implement Decision] - H --> I[Monitor & Evaluate] - I --> J{Changes Needed?} - J -->|Yes| K[Create New or Update ADR] - K --> B - J -->|No| L[Document Learnings] -``` diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/drafts/01-cloud-infrastructure-strategy.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/drafts/01-cloud-infrastructure-strategy.mdx deleted file mode 100644 index 264533d6a..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/drafts/01-cloud-infrastructure-strategy.mdx +++ /dev/null @@ -1,441 +0,0 @@ ---- -title: Cloud Infrastructure Strategy -summary: Architectural decision record for cloud infrastructure approach for the FlowMart e-commerce platform -sidebar: - label: Cloud Infrastructure Strategy - order: 1 ---- - -# DRAFT - NOT YET APPROVED - -:::warning -This is a draft ADR. It is not yet approved and should not be used as a reference. -::: - -## ADR-007: Cloud Infrastructure Strategy for FlowMart E-commerce Platform - -### Status - -Draft (Last Updated: 2024-09-25) - - - - - -### Context - -As part of our transition to a microservices architecture, we need to define our cloud infrastructure strategy to support the new FlowMart e-commerce platform. Our current infrastructure consists primarily of on-premises data centers with some workloads in AWS, creating operational and scaling challenges: - -1. **Infrastructure Provisioning**: Manual processes for provisioning infrastructure lead to long lead times for new environments and services. - -2. **Scaling Limitations**: Physical hardware constraints prevent rapid scaling during peak shopping periods. - -3. **High Operational Overhead**: Significant effort required for hardware maintenance, patching, and capacity management. - -4. **Disaster Recovery Challenges**: Limited geographic redundancy and complex DR procedures. - -5. **Cloud Fragmentation**: Ad-hoc adoption of cloud services has created inconsistent practices and tooling. - -6. **Developer Experience**: Complex local development setup and environment inconsistencies slow down development cycles. - -7. **Cost Management**: Difficult to attribute infrastructure costs to specific business capabilities or teams. - -8. **Security Compliance**: Maintaining compliance across hybrid infrastructure requires duplicate controls and auditing. - -We need a cohesive infrastructure strategy that enables rapid delivery, scalability, and operational efficiency for our new e-commerce platform. - -### Decision - -We will adopt a **cloud-native infrastructure strategy** with a **multi-cloud capability** but **AWS-primary approach**. Key aspects of this strategy include: - -1. **Primary Cloud Platform**: - - AWS will be our primary cloud provider for all new workloads - - Azure will be maintained as a secondary provider for specific use cases (e.g., Microsoft-ecosystem services) - - GCP may be selectively used for specialized AI/ML workloads - -2. **Infrastructure as Code (IaC)**: - - Terraform as primary IaC tool for provisioning cloud resources - - AWS CDK for complex, AWS-specific resource configurations - - GitOps-based deployment workflows with infrastructure CI/CD pipelines - -3. **Containerization Strategy**: - - Containerize all new microservices using Docker - - Amazon EKS (Kubernetes) as primary container orchestration platform - - Amazon ECR for container registry with cross-region replication - -4. **Platform Services Over Custom Infrastructure**: - - Prefer managed services over self-managed infrastructure where possible - - AWS RDS, DocumentDB, ElastiCache for database needs - - AWS MSK (Managed Kafka) for event streaming - - CloudFront for CDN and edge caching - -5. **Multi-Region Architecture**: - - Primary operations in AWS US-East-1 and US-West-2 - - Active-active configuration for critical services - - Region-specific deployments for EU and APAC markets to address data residency - -6. **Landing Zone and Account Structure**: - - Hub-and-spoke model with centralized security and governance - - Separate AWS accounts for production, staging, development, and sandbox environments - - Service-oriented account structure within each environment category - - Centralized logging, monitoring, and security controls - -7. **Network Architecture**: - - Transit Gateway for interconnecting VPCs and on-premises - - VPC design aligned with microservice domains - - AWS PrivateLink for service-to-service connectivity - - AWS Shield and WAF for DDoS protection and application security - -8. **Cost Optimization**: - - Tagging strategy for cost allocation and tracking - - Automated cost monitoring and anomaly detection - - Leverage Savings Plans and Reserved Instances strategically - - Implemented auto-scaling for elastic workloads - -### Infrastructure Architecture by Domain - -| Domain | Primary Services | Scaling Strategy | Special Requirements | -|--------|------------------|------------------|----------------------| -| Product Catalog | EKS, ElastiCache, DocumentDB | Horizontal pod scaling, Read replicas | High read throughput, Global availability | -| Order Processing | EKS, RDS (PostgreSQL), MSK | Horizontal pod scaling, DB connection pooling | Strong consistency, Transaction support | -| Payment | EKS, RDS (PostgreSQL), KMS | Fixed scaling with headroom | PCI-DSS compliance, Encryption requirements | -| Inventory | EKS, DocumentDB, Lambda | Horizontal auto-scaling | Event-sourcing patterns, Eventual consistency | -| User Authentication | Cognito, Lambda, DynamoDB | Regional deployments | Multi-factor auth, Token management | -| Content Delivery | S3, CloudFront, Lambda@Edge | Edge caching, Regional replication | Image optimization, Low latency delivery | -| Search | OpenSearch, Lambda, EKS | Search domain scaling, Query throttling | High cardinality, Complex query support | -| Analytics | Redshift, Kinesis, EMR | Workload-based scaling | Batch processing, Data lake integration | - -### Consequences - -#### Positive - -1. **Improved Scalability**: Elastic infrastructure that scales with demand without manual intervention. - -2. **Faster Time-to-Market**: Automated provisioning and deployment enable rapid delivery of new features. - -3. **Enhanced Resilience**: Multi-region architecture improves availability and disaster recovery capabilities. - -4. **Cost Efficiency**: Pay-for-use model and automated scaling optimize infrastructure costs. - -5. **Developer Productivity**: Consistent environments and self-service capabilities improve developer experience. - -6. **Security Improvements**: Standardized security controls and automated compliance checks. - -7. **Operational Efficiency**: Reduced operational overhead through managed services and automation. - -8. **Global Reach**: Ability to deploy services closer to customers in different geographic regions. - -#### Negative - -1. **Cloud Vendor Dependency**: While designed for multi-cloud, primary workloads will have AWS dependencies. - -2. **Cost Management Complexity**: Cloud costs can escalate without proper governance and monitoring. - -3. **Skill Set Transition**: Team needs to develop new skills in cloud-native technologies and practices. - -4. **Increased Architectural Complexity**: Distributed cloud architecture is more complex to design and troubleshoot. - -5. **Security Model Changes**: Cloud security requires different approaches and tools than on-premises. - -6. **Data Transfer Costs**: Cross-region and internet data transfer can become a significant cost factor. - -7. **Service Maturity Variations**: Some AWS services are more mature and reliable than others. - -### Mitigation Strategies - -1. **Cloud Platform Team**: - - Create a dedicated platform engineering team for cloud infrastructure - - Develop reusable infrastructure modules and patterns - - Provide internal consultation and support to service teams - -2. **Cloud Center of Excellence (CCoE)**: - - Establish cloud best practices and governance - - Regular architecture reviews and guidance - - Develop certification and training program for engineering teams - -3. **Cloud Financial Management**: - - Implement FinOps practices and tooling - - Regular cost reviews and optimization cycles - - Showback/chargeback mechanisms to drive accountability - -4. **Abstraction Layers**: - - Create service abstractions to minimize direct cloud provider coupling - - Develop infrastructure interfaces that could support multiple providers - - Use cloud-agnostic tools and practices where practical - -5. **Hybrid Transition Strategy**: - - Phased migration from on-premises to cloud - - Maintain hybrid capabilities during transition period - - Clear exit criteria for legacy infrastructure decommissioning - -### Implementation Details - -#### Phase 1: Foundation (Q4 2024) - -1. Establish AWS Landing Zone and account structure -2. Implement core networking and security controls -3. Set up CI/CD pipelines for infrastructure -4. Deploy EKS clusters for development and staging -5. Migrate first non-critical workloads - -#### Phase 2: Production Migration (Q1-Q2 2025) - -1. Deploy production EKS clusters and platform services -2. Migrate core services to cloud infrastructure -3. Implement multi-region capabilities for critical services -4. Establish cloud cost management and optimization -5. Complete developer tooling and self-service capabilities - -#### Phase 3: Advanced Capabilities (Q3-Q4 2025) - -1. Implement advanced security and compliance controls -2. Deploy cross-region data synchronization -3. Optimize for global performance and availability -4. Enhance disaster recovery capabilities -5. Begin decommissioning legacy infrastructure - -### Considered Alternatives - -#### 1. Maintain and Expand On-Premises Infrastructure - -**Pros**: Full control, potentially lower ongoing costs for stable workloads, no data sovereignty concerns -**Cons**: Limited agility, high capital expenditure, scaling limitations, operational overhead - -This approach would not provide the agility and scalability needed for our strategic growth and would perpetuate our current limitations. - -#### 2. Single Cloud Provider (AWS Only) - -**Pros**: Simplified operations, deeper integration between services, volume discounts, focused expertise -**Cons**: Vendor lock-in, limited negotiating leverage, regional provider limitations - -While simpler operationally, this approach would increase our dependency on a single provider and limit flexibility. - -#### 3. Multi-Cloud with Equal Workload Distribution - -**Pros**: Maximize negotiating leverage, no single provider dependency, best-of-breed services -**Cons**: Significantly increased complexity, higher operations costs, fragmented expertise, integration challenges - -The operational complexity and cost of maintaining equal capabilities across multiple cloud providers outweighs the benefits for our current needs. - -#### 4. Cloud Service Provider (CSP) Abstraction Layer - -**Pros**: Provider independence, standardized interfaces, easier migration between providers -**Cons**: Significant development overhead, lowest common denominator functionality, performance impacts - -Building comprehensive abstractions across cloud providers would create substantial engineering overhead and limit access to valuable provider-specific capabilities. - -### References - -1. AWS Well-Architected Framework ([AWS Documentation](https://aws.amazon.com/architecture/well-architected/)) -2. "Cloud Strategy Leadership" (Gartner) -3. "Architecting for the Cloud: AWS Best Practices" ([AWS Whitepaper](https://d1.awsstatic.com/whitepapers/AWS_Cloud_Best_Practices.pdf)) -4. "Multi-cloud: The good, the bad and the ugly" (ThoughtWorks Technology Radar) -5. Terraform Documentation ([Terraform.io](https://www.terraform.io/docs/)) -6. "Cloud Native Infrastructure" by Justin Garrison and Kris Nova - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2024-09-10 | 0.1 | Initial draft | Marcus Johnson | -| 2024-09-18 | 0.2 | Added implementation phases and domain details | Patricia Lopez | -| 2024-09-25 | 0.3 | Incorporated feedback from architecture review | David Boyne | -| TBD | 1.0 | Pending approval | Architecture Board | - -## Appendix A: Cloud Infrastructure Architecture - -```mermaid -flowchart TB - subgraph "AWS Global" - subgraph "AWS Organizations" - org[Management Account] - security[Security Account] - shared[Shared Services Account] - log[Logging Account] - net[Network Account] - - subgraph "Production" - prod1[Product Domain] - prod2[Order Domain] - prod3[Customer Domain] - prod4[Payment Domain] - end - - subgraph "Non-Production" - dev[Development] - stage[Staging] - test[Testing] - sandbox[Sandbox] - end - end - end - - subgraph "On-Premises" - dc[Data Center] - legacy[Legacy Systems] - end - - subgraph "Network Fabric" - dx[Direct Connect] - tgw[Transit Gateway] - vpn[VPN] - cf[CloudFront] - end - - org --> security - org --> shared - org --> log - org --> net - org --> prod1 - org --> prod2 - org --> prod3 - org --> prod4 - org --> dev - org --> stage - org --> test - org --> sandbox - - dc --> dx - dc --> vpn - legacy --> dx - - dx --> tgw - vpn --> tgw - - tgw --> net - net --> prod1 - net --> prod2 - net --> prod3 - net --> prod4 - net --> dev - net --> stage - - cf --> prod1 - cf --> prod2 - cf --> prod3 - cf --> prod4 -``` - -## Appendix B: Deployment Architecture - -```mermaid -flowchart LR - subgraph "Developer Experience" - ide[IDE Plugins] - cli[CLI Tools] - local[Local Dev Environment] - end - - subgraph "CI/CD Pipeline" - git[Git Repository] - build[Build System] - test[Automated Tests] - scan[Security Scans] - artifact[Artifact Repository] - end - - subgraph "GitOps Deployment" - config[Config Repository] - argocd[ArgoCD] - flux[Flux] - end - - subgraph "AWS EKS Clusters" - subgraph "Development" - devNS1[Team 1 Namespace] - devNS2[Team 2 Namespace] - end - - subgraph "Staging" - stageNS1[Team 1 Namespace] - stageNS2[Team 2 Namespace] - end - - subgraph "Production" - prodNS1[Team 1 Namespace] - prodNS2[Team 2 Namespace] - end - end - - ide --> git - cli --> git - local --> git - - git --> build - build --> test - test --> scan - scan --> artifact - - artifact --> config - config --> argocd - config --> flux - - argocd --> devNS1 - argocd --> devNS2 - argocd --> stageNS1 - argocd --> stageNS2 - argocd --> prodNS1 - argocd --> prodNS2 - - flux --> devNS1 - flux --> devNS2 - flux --> stageNS1 - flux --> stageNS2 - flux --> prodNS1 - flux --> prodNS2 -``` - -## Appendix C: AWS Account Structure - -```mermaid -flowchart TB - subgraph "Management & Governance" - mgmt[Management Account] - style mgmt fill:#f9f,stroke:#333,stroke-width:2px - - security[Security Account] - audit[Audit Account] - shared[Shared Services] - network[Network Account] - - mgmt --> security - mgmt --> audit - mgmt --> shared - mgmt --> network - end - - subgraph "Production OU" - prodOU[Production] - - payment[Payment Services] - order[Order Services] - product[Product Catalog] - identity[Identity Services] - content[Content Services] - analytics[Analytics] - - prodOU --> payment - prodOU --> order - prodOU --> product - prodOU --> identity - prodOU --> content - prodOU --> analytics - end - - subgraph "Non-Production OU" - nonProdOU[Non-Production] - - dev[Development] - stage[Staging] - test[Testing] - sandbox[Sandbox] - - nonProdOU --> dev - nonProdOU --> stage - nonProdOU --> test - nonProdOU --> sandbox - end - - mgmt --> prodOU - mgmt --> nonProdOU -``` \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/drafts/02-cicd-deployment-strategy.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/drafts/02-cicd-deployment-strategy.mdx deleted file mode 100644 index a3a1a283d..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/drafts/02-cicd-deployment-strategy.mdx +++ /dev/null @@ -1,471 +0,0 @@ ---- -title: CI/CD and Deployment Strategy -summary: Architectural decision record for continuous integration, delivery and deployment approach for the FlowMart e-commerce platform -sidebar: - label: CI/CD Strategy - order: 2 ---- - -# DRAFT - NOT YET APPROVED - -:::warning -This is a draft ADR. It is not yet approved and should not be used as a reference. -::: - -## ADR-008: CI/CD and Deployment Strategy for FlowMart E-commerce Platform - -### Status - -Draft (Last Updated: 2024-10-05) - -### Context - -As we transition to a microservices architecture with dozens of independently deployable services, our current deployment approach presents several challenges: - -1. **Manual Deployment Processes**: Deployments are largely manual, requiring significant coordination and causing deployment anxiety. - -2. **Environment Inconsistency**: Configuration differences between environments lead to environment-specific bugs and "works on my machine" issues. - -3. **Long Lead Times**: The process from code commit to production deployment takes days or weeks due to manual testing and approval gates. - -4. **Deployment Coupling**: Services must be deployed together in coordinated releases, slowing down the delivery of all features. - -5. **Limited Testing Automation**: Insufficient automated testing leads to quality issues discovered late in the delivery process. - -6. **Configuration Management**: Configuration is managed inconsistently across environments and services. - -7. **Deployment Visibility**: Limited visibility into deployment status, history, and metrics. - -8. **Rollback Challenges**: Rolling back problematic deployments is difficult and error-prone. - -Our current approach does not support the rapid, independent delivery of microservices that is essential for our new architecture. We need a comprehensive CI/CD and deployment strategy that enables teams to deliver high-quality services with velocity and confidence. - -### Decision - -We will implement a **GitOps-based CI/CD and deployment strategy** with **continuous deployment to production** for our microservices architecture. Key aspects of this strategy include: - -1. **Trunk-Based Development Model**: - - Short-lived feature branches merged frequently to main/trunk - - Feature toggles for in-progress work - - Automated code quality checks and linting on pull requests - - Main branch always deployable - -2. **Continuous Integration Pipeline**: - - Automated builds triggered on every commit - - Comprehensive automated testing suite - - Security scanning (SAST, SCA, secrets scanning) - - Container image building and signing - - Test environments provisioned on demand for PR validation - -3. **GitOps Deployment Approach**: - - Declarative infrastructure and application configuration in Git - - ArgoCD as primary GitOps operator - - Environment-specific configuration via Kustomize overlays - - Git as single source of truth for deployed state - - Automatic drift detection and remediation - -4. **Deployment Progression Strategy**: - - Automated deployments through dev and test environments - - Production deployments with optional approval (human in the loop) - - Environment promotion rather than rebuilding artifacts - - Canary deployments for high-risk services - - Blue/green deployments for critical components - -5. **Configuration Management**: - - Externalized configuration in Git repositories - - Kubernetes ConfigMaps and Secrets for application configuration - - Sealed Secrets for sensitive information - - HashiCorp Vault for secrets rotation and dynamic credentials - - Environment-specific configuration via layered overlays - -6. **Deployment Safety Mechanisms**: - - Progressive delivery with canary deployments - - Automated pre-deployment validation - - Automated post-deployment testing - - Automated rollback on failure - - Circuit breakers for dependent services - -7. **Release Coordination**: - - API versioning and backwards compatibility requirements - - Service-level dependency management - - Deployment sequencing for interdependent services - - Deployment windows for critical services - -8. **Deployment Metrics and Observability**: - - Deployment frequency tracking - - Change lead time measurement - - Mean time to recovery monitoring - - Change failure rate tracking - - Deployment health dashboards - -### Technology Stack - -| Component | Primary Technology | Alternative/Backup | Purpose | -|-----------|-------------------|-------------------|---------| -| Source Control | GitHub | GitLab | Version control and collaboration | -| CI Pipeline | GitHub Actions | Jenkins | Build, test, and validation | -| Artifact Registry | AWS ECR | GitHub Packages | Container image storage | -| GitOps Operator | ArgoCD | Flux | Kubernetes-based deployment automation | -| Secrets Management | Sealed Secrets + Vault | AWS Secrets Manager | Secure configuration management | -| Deployment Orchestration | ArgoCD + Argo Rollouts | Spinnaker | Controlled deployment progression | -| Feature Flags | LaunchDarkly | Flagsmith | Runtime feature enablement/disablement | -| Testing Framework | Jest, Cypress, k6 | Various | Automated testing across layers | -| Deployment Monitoring | Prometheus + Grafana | Datadog | Deployment metrics and alerting | - -### Consequences - -#### Positive - -1. **Accelerated Delivery**: Reduced lead time from commit to production deployment. - -2. **Improved Quality**: Comprehensive automated testing and validation. - -3. **Increased Deployment Frequency**: Teams can deploy independently at their own pace. - -4. **Enhanced Reliability**: Consistent, repeatable deployment processes with automated rollbacks. - -5. **Better Visibility**: Clear audit trail and status of all deployments. - -6. **Reduced Coordination Overhead**: Less need for cross-team deployment coordination. - -7. **Improved Developer Experience**: Self-service deployments with rapid feedback. - -8. **Environment Consistency**: Reproducible environments with minimal drift. - -#### Negative - -1. **Learning Curve**: Teams need to adapt to new tools and processes. - -2. **Initial Setup Complexity**: Significant effort to establish the complete CI/CD pipeline. - -3. **Infrastructure Requirements**: Additional infrastructure to support the CI/CD toolchain. - -4. **Potential Deployment Sprawl**: Multiple services deploying independently can create coordination challenges. - -5. **Testing Complexity**: Comprehensive testing across distributed services is challenging. - -6. **Feature Flag Management**: Complexity of managing feature flags across services. - -7. **Observability Requirements**: Need for sophisticated monitoring to detect deployment issues. - -### Mitigation Strategies - -1. **Platform Team Support**: - - Create a dedicated platform engineering team focused on CI/CD - - Provide standardized pipeline templates and documentation - - Enable self-service capabilities with guardrails - -2. **Phased Implementation**: - - Start with less critical services - - Gradually increase automation and reduce manual gates - - Measure and demonstrate improved outcomes - -3. **Developer Enablement**: - - Comprehensive documentation and examples - - Regular training sessions and office hours - - Inner-source model for pipeline improvements - -4. **Testing Strategy**: - - Standard test libraries and frameworks - - Service virtualization for dependencies - - Comprehensive end-to-end testing strategy - -5. **Change Management**: - - Clear communication about process changes - - Regular retrospectives and continuous improvement - - Celebrate success stories and share lessons learned - -### Implementation Details - -#### Phase 1: Foundation (Q4 2024) - -1. Establish CI pipeline standardization -2. Implement container build and security scanning -3. Set up artifact repositories and image signing -4. Deploy ArgoCD and initial GitOps workflows -5. Implement trunk-based development practices - -#### Phase 2: Advanced Delivery (Q1 2025) - -1. Enable canary and blue/green deployments -2. Implement comprehensive automated testing -3. Set up feature flag management -4. Deploy secrets management solution -5. Create deployment metrics dashboards - -#### Phase 3: Continuous Deployment (Q2 2025) - -1. Implement continuous deployment to production -2. Enable automated rollbacks and circuit breakers -3. Set up deployment SLOs and monitoring -4. Implement sophisticated deployment strategies -5. Optimize deployment performance and efficiency - -### Deployment Process Flow - -The following outlines our target deployment process flow from code commit to production: - -1. **Code Commit & PR**: - - Developer creates branch and commits changes - - Pull request created with automated linting and checks - - CI pipeline validates build, tests, and security - -2. **CI Verification**: - - Automated unit and integration tests - - Security scanning (SAST, SCA, container scanning) - - Code quality metrics and coverage checks - - On-demand test environment provisioning - -3. **Artifact Creation**: - - Container images built and tagged - - Images signed and pushed to registry - - Deployment manifests generated - - Configuration updates prepared - -4. **Development Deployment**: - - Automatic deployment to development environment - - Post-deployment testing and validation - - Integration testing with other services - - Performance and security validation - -5. **Staging Deployment**: - - Promotion of verified artifacts to staging - - Environment-specific configuration applied - - System-level testing and validation - - Performance testing against production-like load - -6. **Production Deployment**: - - Optional approval gate for high-risk services - - Canary or blue/green deployment strategy - - Incremental traffic shifting - - Health check verification at each step - -7. **Post-Deployment Validation**: - - Automated smoke tests - - Synthetic transaction monitoring - - Key metric monitoring and alerting - - Automated rollback if metrics deviate - -### Considered Alternatives - -#### 1. Traditional Release-Based Deployment Model - -**Pros**: Familiar approach, coordinated releases, comprehensive testing cycles -**Cons**: Slow delivery, limited independence, large batch sizes increasing risk - -This approach would not provide the delivery velocity required for our business needs and would limit the benefits of our microservices architecture. - -#### 2. Pure Environment Promotion Model - -**Pros**: Artifact consistency, simplified promotion process, reduced build time -**Cons**: Limited environment-specific customization, potential configuration complexity - -While we adopt aspects of this approach, we need the flexibility of environment-specific configuration that a pure promotion model limits. - -#### 3. Central Deployment Team - -**Pros**: Standardized processes, specialized expertise, controlled deployments -**Cons**: Potential bottleneck, reduced team autonomy, slower feedback loops - -This approach would create a deployment bottleneck and reduce the ownership and autonomy of our product teams. - -#### 4. Fully Automated No-Approval Deployments - -**Pros**: Maximum velocity, reduced human intervention, forced quality automation -**Cons**: Increased risk for critical systems, cultural resistance, advanced testing requirements - -While this is our long-term goal, we need to balance velocity with appropriate controls, especially for critical payment and order processing systems. - -### References - -1. Forsgren, N., Humble, J., & Kim, G. "Accelerate: The Science of Lean Software and DevOps" (IT Revolution Press) -2. Humble, J. & Farley, D. "Continuous Delivery" (Addison-Wesley) -3. [GitOps Working Group](https://github.com/gitops-working-group/gitops-working-group) -4. [Argo CD Documentation](https://argo-cd.readthedocs.io/) -5. [Kubernetes Deployment Strategies](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy) -6. [Trunk Based Development](https://trunkbaseddevelopment.com/) - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2024-09-28 | 0.1 | Initial draft | Jason Miller | -| 2024-10-03 | 0.2 | Added implementation phases and deployment flow | Thomas Wong | -| 2024-10-05 | 0.3 | Incorporated feedback from DevOps and development teams | David Boyne | -| TBD | 1.0 | Pending approval | Architecture Board | - -## Appendix A: CI/CD Pipeline Architecture - -```mermaid -flowchart TB - subgraph "Developer Workflow" - dev[Developer] - ide[IDE] - local[Local Testing] - - dev --> ide - ide --> local - end - - subgraph "Source Control" - branch[Feature Branch] - pr[Pull Request] - trunk[Main Branch] - - local --> branch - branch --> pr - pr --> trunk - end - - subgraph "Continuous Integration" - build[Build & Test] - security[Security Scan] - quality[Quality Gates] - artifact[Create Artifacts] - - trunk --> build - build --> security - security --> quality - quality --> artifact - end - - subgraph "Artifact Management" - registry[Container Registry] - config[Config Repository] - - artifact --> registry - artifact --> config - end - - subgraph "GitOps Deployment" - argo[ArgoCD] - dev_env[Development] - stage_env[Staging] - prod_env[Production] - - config --> argo - argo --> dev_env - argo --> stage_env - argo --> prod_env - end - - subgraph "Deployment Strategies" - canary[Canary] - blue_green[Blue/Green] - - prod_env --> canary - prod_env --> blue_green - end - - subgraph "Observability" - metrics[Deployment Metrics] - alerts[Deployment Alerts] - dash[Deployment Dashboard] - - canary --> metrics - blue_green --> metrics - metrics --> alerts - metrics --> dash - end -``` - -## Appendix B: Deployment Pipeline Flow - -```mermaid -sequenceDiagram - participant Dev as Developer - participant Git as GitHub - participant CI as CI Pipeline - participant Reg as Container Registry - participant Config as Config Repository - participant ArgoCD as ArgoCD - participant Env as Environments - participant Monitor as Monitoring - - Dev->>Git: Commit Code - Git->>CI: Trigger Pipeline - - CI->>CI: Build & Test - CI->>CI: Security Scan - CI->>CI: Quality Gates - - CI->>Reg: Push Image - CI->>Config: Update Manifests - - Config->>ArgoCD: Detected Changes - ArgoCD->>Env: Deploy to Dev - - ArgoCD->>Env: Post-Deployment Tests - Env->>Monitor: Collect Metrics - - alt Tests Pass - ArgoCD->>Env: Deploy to Staging - Env->>Monitor: Collect Metrics - - alt Staging Healthy - ArgoCD->>Env: Deploy Canary to Production - Env->>Monitor: Collect Metrics - - alt Canary Healthy - ArgoCD->>Env: Complete Production Rollout - else Canary Unhealthy - ArgoCD->>Env: Rollback Canary - end - else Staging Unhealthy - ArgoCD->>Env: Rollback Staging - end - else Tests Fail - ArgoCD->>Env: Rollback Dev - end - - Monitor->>Dev: Deployment Status & Metrics -``` - -## Appendix C: Environment Configuration Strategy - -```mermaid -flowchart LR - subgraph "Git Repositories" - app[Application Code] - infra[Infrastructure Code] - config[Configuration] - end - - subgraph "Configuration Layers" - base[Base Configuration] - env[Environment Overlays] - service[Service-Specific Config] - end - - subgraph "Secret Management" - sealed[Sealed Secrets] - vault[HashiCorp Vault] - esm[External Secrets Manager] - end - - subgraph "Kubernetes Resources" - cm[ConfigMaps] - secrets[Secrets] - pods[Deployments] - end - - app --> build{Build Process} - infra --> terraform{Terraform} - - config --> base - base --> kustomize{Kustomize} - env --> kustomize - service --> kustomize - - sealed --> kustomize - vault --> esm - esm --> secrets - - kustomize --> cm - kustomize --> secrets - - build --> pods - terraform --> platform[Platform Resources] - cm --> pods - secrets --> pods -``` \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/drafts/03-api-management-governance.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/drafts/03-api-management-governance.mdx deleted file mode 100644 index 5f56ff38a..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/drafts/03-api-management-governance.mdx +++ /dev/null @@ -1,480 +0,0 @@ ---- -title: API Management and Governance -summary: Architectural decision record for our API management and governance approach for the FlowMart e-commerce platform -sidebar: - label: API Management - order: 3 ---- - -# DRAFT - NOT YET APPROVED - -## ADR-009: API Management and Governance Strategy - -### Status - -Draft (Last Updated: 2024-10-07) - -### Context - -As we expand our microservices architecture for the FlowMart e-commerce platform, we need a comprehensive approach to API management and governance. We are facing several challenges with our current approach to APIs: - -1. **Proliferation of APIs**: With our transition to microservices, we anticipate having 50+ internal APIs and multiple external-facing APIs within the next year. - -2. **Inconsistent Design Patterns**: Teams are implementing APIs with inconsistent patterns, naming conventions, error handling, and response formats. - -3. **Documentation Gaps**: API documentation is inconsistent, often outdated, and frequently exists only in code comments or team wikis. - -4. **Discoverability Issues**: Developers struggle to find existing APIs, leading to duplication of functionality. - -5. **Security Concerns**: APIs lack consistent security controls, authentication mechanisms, and authorization models. - -6. **Version Management**: No clear strategy for versioning APIs, handling breaking changes, or maintaining backward compatibility. - -7. **Performance Visibility**: Limited visibility into API performance, usage patterns, and availability. - -8. **Cross-Cutting Concerns**: Features like rate limiting, caching, and observability are implemented inconsistently across services. - -9. **Developer Experience**: Onboarding to consume APIs is complex and time-consuming, both for internal teams and external partners. - -FlowMart is transitioning from a monolithic architecture to microservices, with a growing ecosystem of APIs. We need a cohesive approach to ensure these APIs are secure, discoverable, consistent, and manageable at scale. - -### Decision - -We will implement a comprehensive **API Management and Governance Strategy** with the following key components: - -1. **API Gateway Architecture**: - - Implement Kong as our primary API gateway for both internal and external APIs - - Deploy separate gateway instances for external, internal, and partner traffic - - Utilize a mesh pattern for internal service-to-service communication - - Implement a Backend for Frontend (BFF) pattern for frontend-specific aggregation - -2. **API Design Standards**: - - Adopt OpenAPI (formerly Swagger) as our API specification standard - - Establish comprehensive REST API design guidelines - - Implement a design-first approach for all new APIs - - Define standard patterns for resource naming, query parameters, pagination, filtering, and error responses - - Standardize on JSON:API specification for response formatting - -3. **API Lifecycle Management**: - - Define clear stages: Design → Review → Development → Testing → Deployment → Deprecation → Retirement - - Implement automated API contract testing in CI/CD pipelines - - Require specification update for any API changes - - Establish deprecation and sunsetting policies with clear timelines - -4. **API Governance Model**: - - Create an API Guild with representatives from each team - - Establish an API review process for all new APIs and significant changes - - Implement automated linting and compliance checking against standards - - Define metrics for API quality and compliance - - Regular review of API portfolio for duplication and consolidation opportunities - -5. **API Documentation and Discovery**: - - Deploy a central API developer portal using Backstage - - Ensure all APIs have OpenAPI specifications - - Implement automated documentation generation from OpenAPI specs - - Create a searchable API catalog with metadata, ownership, and usage examples - - Develop interactive API playground environments - -6. **API Security Framework**: - - Standardize on OAuth 2.0 and OpenID Connect for authentication - - Implement role-based and attribute-based access control - - Deploy centralized API key management - - Establish security scanning and penetration testing for APIs - - Implement API request validation based on schemas - -7. **API Monitoring and Analytics**: - - Deploy comprehensive API metrics collection - - Create dashboards for performance, availability, and usage - - Implement distributed tracing for end-to-end request flows - - Set up alerting on API SLOs and error rates - - Establish usage analytics for product and developer experience improvement - -8. **API Versioning Strategy**: - - Adopt semantic versioning (major.minor.patch) for all APIs - - Support at least one previous major version for backward compatibility - - Use URI versioning for major versions (/v1/, /v2/) - - Implement feature flags for progressive rollout of API changes - - Establish clear communication channels for API changes - -### Technology Stack - -| Component | Primary Technology | Alternative/Backup | Purpose | -|-----------|-------------------|-------------------|---------| -| API Gateway | Kong | Apigee, Amazon API Gateway | Traffic management, routing, policies | -| API Specification | OpenAPI 3.0 | AsyncAPI (for event-driven) | API contract and documentation | -| Developer Portal | Backstage | SwaggerHub, Readme.io | Discovery, documentation, onboarding | -| Identity Provider | Auth0 | Keycloak, AWS Cognito | Authentication and authorization | -| API Testing | Postman + Newman | SoapUI, Karate | Automated API testing | -| API Monitoring | Datadog | New Relic, Prometheus | Observability and analytics | -| Contract Testing | Pact | Spring Cloud Contract | Consumer-driven contract testing | -| GraphQL Federation | Apollo Federation | Netflix DGS | GraphQL implementation | -| API Design Tooling | Stoplight Studio | SwaggerHub, Insomnia | Design-first workflow | - -### Consequences - -#### Positive - -1. **Improved Developer Experience**: Consistent, well-documented APIs accelerate development. - -2. **Enhanced Security**: Standardized authentication and authorization patterns. - -3. **Better Visibility**: Comprehensive monitoring and analytics of API usage and performance. - -4. **Reduced Duplication**: Central catalog prevents redundant API implementations. - -5. **Faster Integration**: Partners and internal teams can onboard more quickly. - -6. **Higher Quality**: Standardized design patterns and automated testing improve quality. - -7. **Future-Proofing**: Versioning strategy ensures backward compatibility. - -8. **Operational Efficiency**: Centralized management of cross-cutting concerns. - -#### Negative - -1. **Initial Overhead**: Additional processes and governance may slow initial development. - -2. **Learning Curve**: Teams need to adapt to new standards and practices. - -3. **Migration Effort**: Existing APIs need to be refactored to meet new standards. - -4. **Tooling Investment**: Significant investment in API management infrastructure. - -5. **Governance Challenges**: Maintaining compliance across teams requires persistent effort. - -6. **Potential Bottlenecks**: API review processes could become a bottleneck if not well-designed. - -7. **Operational Complexity**: Additional infrastructure components to maintain. - -### Mitigation Strategies - -1. **Phased Implementation**: - - Start with high-priority, externally facing APIs - - Gradually implement standards for internal APIs - - Provide flexible transition periods for existing APIs - -2. **Developer Enablement**: - - Create comprehensive training materials and workshops - - Develop starter templates and code generators - - Provide API design consultation services - - Create self-service tools for standards compliance - -3. **Governance Optimization**: - - Implement automated compliance checking - - Create lightweight review processes for low-risk changes - - Empower teams with self-service validation tools - - Focus governance on enablement rather than control - -4. **Migration Support**: - - Develop clear migration guidelines and timelines - - Provide tooling to assist with API migrations - - Allow grandfathering of legacy APIs with clear deprecation plans - - Prioritize high-value, high-impact APIs for migration - -5. **Platform Team Support**: - - Create a dedicated API platform team - - Offer consulting services to teams implementing or consuming APIs - - Develop reusable components for common API patterns - - Continuously evolve standards based on feedback - -### Implementation Details - -#### Phase 1: Foundation (Q4 2024) - -1. Establish API design standards and guidelines -2. Deploy API gateway for external-facing services -3. Implement initial developer portal -4. Create API governance process and guild -5. Define API security standards - -#### Phase 2: Expansion (Q1 2025) - -1. Extend API gateway to internal services -2. Implement comprehensive monitoring and analytics -3. Deploy automated compliance checking -4. Develop API versioning framework -5. Create self-service API documentation tooling - -#### Phase 3: Optimization (Q2 2025) - -1. Implement advanced BFF patterns -2. Deploy GraphQL federation for complex client needs -3. Establish API performance optimization framework -4. Create advanced analytics and business insights -5. Develop partner API developer experiences - -### API Design Principles - -Our API design will adhere to the following principles: - -1. **Resource-Oriented Design**: Model APIs around business resources rather than operations. - -2. **Consistency**: APIs should behave consistently across the platform. - -3. **Least Privilege**: APIs should request only the permissions they need. - -4. **Robustness**: APIs should handle error cases gracefully and provide clear error messages. - -5. **Forward Compatibility**: Design APIs to be extended without breaking existing clients. - -6. **Discoverability**: APIs should be self-documenting and easily discoverable. - -7. **Performance**: APIs should be designed with performance considerations in mind. - -8. **Security by Design**: Security should be considered at every stage of the API lifecycle. - -### API Governance Model - -Our API governance model follows these principles: - -1. **Federated Ownership**: Teams own their APIs but follow central standards. - -2. **Standards over Rules**: Prefer guidance and patterns over strict enforcement. - -3. **Automated Compliance**: Automate standards checking wherever possible. - -4. **Continuous Improvement**: Regularly review and refine standards based on feedback. - -5. **Value-Driven**: Focus governance efforts on high-value, high-risk APIs. - -6. **Developer Experience**: Prioritize developer experience in governance decisions. - -7. **Transparency**: Make governance processes and decisions transparent to all teams. - -### Considered Alternatives - -#### 1. Decentralized API Management - -**Pros**: Maximum team autonomy, reduced coordination overhead -**Cons**: Inconsistent developer experience, potential security gaps, duplicated effort - -This approach would give teams complete freedom but would result in an inconsistent and potentially insecure API landscape that would be difficult to navigate and maintain. - -#### 2. Centralized API Development Team - -**Pros**: Maximum consistency, specialized expertise, controlled quality -**Cons**: Development bottleneck, reduced team ownership, slower delivery - -While this would ensure consistency, it would create bottlenecks and reduce the autonomy and ownership of our product teams, contradicting our microservices philosophy. - -#### 3. Multiple API Gateways by Domain - -**Pros**: Domain isolation, reduced blast radius, team autonomy -**Cons**: Management complexity, potential inconsistencies, higher operational overhead - -This approach offers benefits for large organizations but introduces unnecessary complexity for our current scale. We may revisit this approach as we grow. - -#### 4. GraphQL-First Approach - -**Pros**: Flexible client queries, reduced over-fetching, schema registry -**Cons**: Learning curve, performance concerns for some use cases, security complexity - -GraphQL offers benefits for certain use cases, but we believe a REST-first approach with GraphQL for specific complex client needs provides a better balance for our organization. - -### References - -1. "Web API Design: The Missing Link" by API Academy -2. "REST API Design Rulebook" by Mark Masse -3. [Google API Design Guide](https://cloud.google.com/apis/design) -4. [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md) -5. [JSON:API Specification](https://jsonapi.org/) -6. [API Governance: What, Why, and How](https://swagger.io/resources/articles/api-governance-what-why-how/) - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2024-09-30 | 0.1 | Initial draft | Sarah Johnson | -| 2024-10-04 | 0.2 | Added governance model and phasing | Michael Chen | -| 2024-10-07 | 0.3 | Incorporated feedback from API Guild | David Boyne | -| TBD | 1.0 | Pending approval | Architecture Board | - -## Appendix A: API Governance Process - -```mermaid -flowchart TB - subgraph "API Lifecycle" - design[Design Phase] - review[Review Phase] - develop[Development Phase] - test[Testing Phase] - deploy[Deployment Phase] - monitor[Monitoring Phase] - deprecate[Deprecation Phase] - - design --> review - review --> develop - develop --> test - test --> deploy - deploy --> monitor - monitor --> deprecate - end - - subgraph "Governance Touchpoints" - standards[Standards & Guidelines] - templates[API Templates] - linting[Automated Linting] - reviews[Peer Reviews] - testing[Contract Testing] - security[Security Scanning] - analytics[Usage Analytics] - - standards --> design - templates --> design - linting --> review - reviews --> review - testing --> test - security --> test - analytics --> monitor - end - - subgraph "Key Roles" - guild[API Guild] - owners[API Owners] - platform[Platform Team] - consumers[API Consumers] - - guild --> standards - owners --> design - platform --> templates - consumers --> analytics - end - - subgraph "Artifacts" - spec[OpenAPI Specification] - docs[Documentation] - portal[Developer Portal] - metrics[Metrics Dashboard] - - design --> spec - spec --> docs - docs --> portal - monitor --> metrics - end -``` - -## Appendix B: API Management Architecture - -```mermaid -flowchart TB - subgraph "Clients" - web[Web Clients] - mobile[Mobile Clients] - partners[Partner Systems] - internal[Internal Services] - end - - subgraph "API Gateways" - public[Public API Gateway] - partner[Partner API Gateway] - internal_gw[Internal API Gateway] - end - - subgraph "BFF Layer" - web_bff[Web BFF] - mobile_bff[Mobile BFF] - end - - subgraph "API Services" - products[Product APIs] - orders[Order APIs] - customers[Customer APIs] - payments[Payment APIs] - inventory[Inventory APIs] - end - - subgraph "Cross-Cutting Concerns" - auth[Authentication] - rate[Rate Limiting] - cache[Caching] - logging[Logging] - analytics[Analytics] - end - - web --> web_bff - mobile --> mobile_bff - partners --> partner - internal --> internal_gw - - web_bff --> public - mobile_bff --> public - - public --> products - public --> orders - public --> customers - partner --> products - partner --> orders - internal_gw --> products - internal_gw --> orders - internal_gw --> customers - internal_gw --> payments - internal_gw --> inventory - - auth --> public - auth --> partner - auth --> internal_gw - rate --> public - rate --> partner - cache --> public - cache --> partner - cache --> internal_gw - logging --> public - logging --> partner - logging --> internal_gw - analytics --> public - analytics --> partner - analytics --> internal_gw -``` - -## Appendix C: API Developer Portal Architecture - -```mermaid -flowchart LR - subgraph "API Developer Portal" - catalog[API Catalog] - docs[API Documentation] - playground[API Playground] - analytics[Usage Analytics] - sandbox[Sandbox Environment] - support[Developer Support] - end - - subgraph "API Sources" - openapi[OpenAPI Specifications] - asyncapi[AsyncAPI Specifications] - graphql[GraphQL Schemas] - code[Source Code] - end - - subgraph "Developer Tools" - cli[CLI Tools] - sdks[Client SDKs] - ci[CI/CD Integration] - codegen[Code Generators] - end - - subgraph "Governance Tools" - standards[Standards Library] - linting[Linting Tools] - testing[Testing Framework] - workflow[Approval Workflows] - end - - openapi --> catalog - asyncapi --> catalog - graphql --> catalog - code --> docs - - catalog --> playground - catalog --> analytics - catalog --> sdks - catalog --> codegen - - playground --> sandbox - sandbox --> support - - standards --> linting - linting --> ci - testing --> ci - workflow --> ci -``` \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/examples/01-microservice-mesh-adoption.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/examples/01-microservice-mesh-adoption.mdx deleted file mode 100644 index 4e5df73f0..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/examples/01-microservice-mesh-adoption.mdx +++ /dev/null @@ -1,313 +0,0 @@ ---- -title: Service Mesh Adoption -summary: Architectural decision record for adopting a service mesh for TechNova's cloud platform -sidebar: - label: Service Mesh Adoption - order: 1 ---- - -## ADR-2023-05: Service Mesh Adoption for TechNova Cloud Platform - -### Status - -Approved (2023-08-15) - -### Context - -TechNova's cloud platform has grown to include over 75 microservices developed and maintained by 12 different teams. As our platform has scaled, we've encountered several challenges: - -1. **Service-to-Service Communication Complexity**: Managing service-to-service communication has become increasingly complex, with inconsistent implementation of patterns like circuit breaking, retries, and timeout handling. - -2. **Security Concerns**: Service-to-service authentication and authorization is implemented inconsistently, creating potential security vulnerabilities. - -3. **Observability Gaps**: Tracking request flows across services is difficult, making troubleshooting and performance optimization challenging. - -4. **Traffic Management Challenges**: We lack sophisticated traffic control capabilities such as traffic splitting, canary deployments, and blue/green releases. - -5. **Certificate Management**: Managing and rotating TLS certificates for service-to-service communication has become an operational burden. - -6. **Network Policy Enforcement**: Network access controls are difficult to implement and maintain consistently across services. - -Our engineering teams currently spend approximately 25% of their development effort on these cross-cutting concerns, diverting resources from building business functionality. - -### Decision - -We will adopt **Istio** as our service mesh solution for the TechNova Cloud Platform. Specifically: - -1. **Sidecar Deployment Model**: We will deploy Istio using the sidecar proxy architecture to minimize disruption to existing services. - -2. **Gradual Implementation**: We will introduce the service mesh incrementally, starting with non-critical services and gradually expanding to include all services. - -3. **Standardized Patterns**: We will establish company-wide standards for: - - Circuit breaking configurations - - Retry policies - - Timeout configurations - - Mutual TLS implementation - - Traffic management patterns - - Observability integrations - -4. **Platform Team Ownership**: A dedicated platform team will own the service mesh infrastructure, including: - - Istio version management and upgrades - - Performance monitoring and optimization - - Security policy definition and enforcement - - Training and documentation - -5. **Integration with Existing Tooling**: - - Prometheus and Grafana for metrics and dashboarding - - Jaeger for distributed tracing - - Kiali for service mesh visualization - - Cert-Manager for certificate management - -6. **Control Plane Configuration**: We will deploy Istio with: - - High availability control plane (3 replicas) - - Split along environment boundaries (separate control planes for production and non-production) - - Regular automated backups of control plane configuration - -### Consequences - -#### Positive - -1. **Simplified Service Code**: Application developers can focus on business logic rather than communication concerns. - -2. **Enhanced Security**: Consistent mTLS implementation will improve our security posture. - -3. **Improved Observability**: End-to-end request tracing and detailed service metrics will enhance troubleshooting. - -4. **Advanced Traffic Management**: Capabilities for canary deployments and traffic steering will reduce deployment risk. - -5. **Certificate Automation**: Automated certificate management will reduce operational overhead. - -6. **Standardized Networking Policies**: Centralized network policy enforcement will improve security and compliance. - -#### Negative - -1. **Increased Complexity**: Service mesh adds architectural complexity and new failure modes. - -2. **Resource Overhead**: Sidecar proxies consume additional CPU and memory resources (~10-15% based on POC testing). - -3. **Learning Curve**: Teams will need to learn new concepts, tooling, and debugging approaches. - -4. **Deployment Changes**: CI/CD pipelines will need updates to accommodate service mesh configuration. - -5. **Potential Performance Impact**: Additional network hops may increase latency (observed ~5-10ms per hop in testing). - -6. **Operational Changes**: Incident response and troubleshooting procedures will need updates. - -### Mitigation Strategies - -1. **Comprehensive Education Program**: - - Create internal training curriculum for all engineering teams - - Schedule hands-on workshops focusing on practical use cases - - Develop troubleshooting playbooks and runbooks - -2. **Gradual Rollout Plan**: - - Phase 1 (Q3 2023): Deploy to non-critical services and gather metrics - - Phase 2 (Q4 2023): Expand to backend services with low customer impact - - Phase 3 (Q1 2024): Roll out to critical customer-facing services - -3. **Resource Optimization**: - - Tune proxy resource requests based on actual usage patterns - - Implement horizontal pod autoscaling for proxies - - Monitor and optimize control plane resource utilization - -4. **Performance Monitoring**: - - Establish baseline performance metrics before service mesh adoption - - Implement detailed performance monitoring dashboards - - Set alerting on latency increases beyond acceptable thresholds - -5. **Escape Hatch Mechanisms**: - - Document procedures for temporarily bypassing the service mesh if issues arise - - Create fast rollback capabilities for service mesh configurations - -### Implementation Details - -#### Phase 1: Foundation (Q3 2023) - -1. Deploy Istio control plane in non-production environments -2. Implement mTLS for a subset of non-critical services -3. Set up integration with observability stack -4. Develop initial training materials and documentation -5. Create standard Istio configuration templates - -#### Phase 2: Expansion (Q4 2023) - -1. Deploy production control plane -2. Expand to backend services -3. Implement traffic management capabilities -4. Develop automated testing for service mesh configurations -5. Integrate service mesh configurations into CI/CD pipelines - -#### Phase 3: Completion (Q1 2024) - -1. Roll out to remaining services -2. Implement advanced security policies -3. Complete comprehensive monitoring and alerting -4. Finalize operational procedures and runbooks -5. Document lessons learned and best practices - -### Considered Alternatives - -#### 1. Linkerd - -**Pros**: Lighter weight, simpler operations, lower resource overhead -**Cons**: Fewer features, smaller community, less integration with enterprise tools - -While Linkerd offers a more lightweight approach, we found that its limited feature set would not address all our requirements, particularly around sophisticated traffic management and extensibility. - -#### 2. AWS App Mesh - -**Pros**: Native AWS integration, managed control plane, simplified operations -**Cons**: AWS-specific, limited feature set compared to Istio, less community support - -AWS App Mesh would align well with our AWS infrastructure but lacks some advanced features we require and would limit our multi-cloud flexibility. - -#### 3. Custom Solution with Envoy - -**Pros**: Tailored to our exact needs, potentially lower overhead -**Cons**: High development and maintenance cost, lack of community support, reinventing the wheel - -Building our own solution would require significant engineering resources and ongoing maintenance, which would outweigh the benefits of customization. - -#### 4. No Service Mesh (Status Quo) - -**Pros**: No additional complexity or overhead, familiar patterns -**Cons**: Continued inconsistency across services, growing technical debt, security risks - -Maintaining the status quo would fail to address our current challenges and would further increase technical debt as our platform continues to grow. - -### References - -1. [Istio Documentation](https://istio.io/latest/docs/) -2. "The Service Mesh Era" - Cloud Native Computing Foundation Whitepaper -3. Internal POC Report: "Service Mesh Performance and Compatibility Testing" (TechNova Engineering, June 2023) -4. [CNCF Service Mesh Comparison](https://servicemesh.es/) -5. [Envoy Proxy Documentation](https://www.envoyproxy.io/docs/envoy/latest/) -6. "Production Istio" - O'Reilly Media, 2022 - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2023-05-10 | 0.1 | Initial draft | Alexander Chen, Principal Architect | -| 2023-06-22 | 0.2 | Updated based on POC results | Sophia Rodriguez, Platform Engineering | -| 2023-07-15 | 0.3 | Added phased implementation plan | Marcus Johnson, Engineering Director | -| 2023-08-15 | 1.0 | Approved by Architecture Review Board | TechNova ARB | - -## Appendix A: Service Mesh Architecture - -```mermaid -flowchart TB - subgraph "Control Plane" - istiod[Istiod] - config[Configuration API] - certs[Certificate Authority] - end - - subgraph "Ingress/Egress" - ingress[Ingress Gateway] - egress[Egress Gateway] - end - - subgraph "Observability" - prom[Prometheus] - grafana[Grafana] - jaeger[Jaeger] - kiali[Kiali] - end - - subgraph "Kubernetes Cluster" - subgraph "Service A" - svcA[Service A] - proxyA[Envoy Proxy] - end - - subgraph "Service B" - svcB[Service B] - proxyB[Envoy Proxy] - end - - subgraph "Service C" - svcC[Service C] - proxyC[Envoy Proxy] - end - end - - client[Client] --> ingress - ingress --> proxyA - proxyA --> svcA - proxyA --> proxyB - proxyB --> svcB - proxyB --> proxyC - proxyC --> svcC - proxyC --> egress - egress --> external[External Services] - - istiod --> proxyA - istiod --> proxyB - istiod --> proxyC - istiod --> ingress - istiod --> egress - - config --> istiod - certs --> istiod - - proxyA --> prom - proxyB --> prom - proxyC --> prom - prom --> grafana - proxyA --> jaeger - proxyB --> jaeger - proxyC --> jaeger - prom --> kiali - jaeger --> kiali - config --> kiali -``` - -## Appendix B: Service Mesh Implementation Timeline - -```mermaid -gantt - title TechNova Service Mesh Implementation - dateFormat YYYY-MM-DD - - section Planning - Initial Research :done, 2023-03-01, 2023-04-15 - Proof of Concept :done, 2023-04-16, 2023-06-15 - Architecture Decision :done, 2023-06-16, 2023-08-15 - - section Phase 1: Foundation - Control Plane Setup :active, 2023-08-16, 2023-09-15 - Observability Integration :active, 2023-09-01, 2023-09-30 - Initial Service Onboarding :2023-09-15, 2023-10-15 - Training & Documentation :2023-09-01, 2023-10-31 - - section Phase 2: Expansion - Production Control Plane :2023-11-01, 2023-11-30 - Backend Service Onboarding :2023-11-15, 2024-01-15 - CI/CD Integration :2023-12-01, 2024-01-31 - Advanced Traffic Management:2024-01-01, 2024-02-15 - - section Phase 3: Completion - Remaining Service Onboarding :2024-02-01, 2024-03-31 - Security Policy Implementation:2024-02-15, 2024-03-31 - Finalize Operations Procedures:2024-03-01, 2024-04-15 - Performance Optimization :2024-03-15, 2024-04-30 - Project Completion :milestone, 2024-04-30 -``` - -## Appendix C: Estimated Resource Requirements - -| Component | CPU Request | Memory Request | Replicas | Environment | Notes | -|-----------|-------------|---------------|----------|-------------|-------| -| Istiod | 500m | 2Gi | 3 | Production | HA configuration | -| Istiod | 500m | 2Gi | 1 | Non-Production | Single replica | -| Ingress Gateway | 500m | 1Gi | 3 | Production | External traffic entry | -| Ingress Gateway | 200m | 512Mi | 1 | Non-Production | Development testing | -| Egress Gateway | 500m | 1Gi | 2 | Production | External service access | -| Envoy Proxy | 100m | 256Mi | 1 per pod | All | Sidecar containers | -| Prometheus | 1000m | 8Gi | 2 | Production | Metric collection | -| Grafana | 200m | 1Gi | 2 | Production | Dashboarding | -| Jaeger | 500m | 4Gi | 2 | Production | Distributed tracing | -| Kiali | 200m | 1Gi | 2 | Production | Mesh visualization | - -*Note: These values are initial estimates based on our proof of concept and may be adjusted based on actual usage patterns.* \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/examples/02-data-platform-strategy.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/examples/02-data-platform-strategy.mdx deleted file mode 100644 index 81632af66..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/examples/02-data-platform-strategy.mdx +++ /dev/null @@ -1,415 +0,0 @@ ---- -title: Data Platform Strategy -summary: Architectural decision record for adopting a modern data platform at FinSecure -sidebar: - label: Data Platform Strategy - order: 2 ---- - -## ADR-2023-12: Modern Data Platform Strategy for FinSecure - -### Status - -Approved (2023-12-18) - -### Context - -FinSecure is experiencing significant challenges with our current data architecture: - -1. **Data Silos**: Customer, transaction, and risk data are siloed across multiple legacy systems with inconsistent data models. - -2. **Limited Analytics Capabilities**: Rigid data warehousing solutions limit our ability to perform advanced analytics and machine learning. - -3. **Scalability Constraints**: Current data processing infrastructure is struggling to handle increasing data volumes (now exceeding 5TB daily). - -4. **Compliance Complexity**: Meeting GDPR, CCPA, and financial regulatory requirements across fragmented data systems is increasingly difficult. - -5. **Slow Time-to-Insight**: Business teams wait 2-3 weeks for new analytics dashboards or data models to be developed. - -6. **Technical Debt**: Legacy ETL processes are complex, brittle, and expensive to maintain. - -7. **Limited Real-time Capabilities**: Current architecture is primarily batch-oriented with limited ability to process streaming data for fraud detection and real-time decisioning. - -8. **Data Quality Issues**: Inconsistent data quality across systems impacts business decisions and customer experience. - -These challenges are limiting our ability to leverage data as a strategic asset and inhibiting our digital transformation initiatives aimed at enhancing customer experiences and operational efficiency. - -### Decision - -We will implement a **modern, cloud-based data platform** with a **lakehouse architecture**. Key components include: - -1. **Data Lake Foundation**: - - Azure Data Lake Storage Gen2 as the foundation for our data lake - - Databricks Delta Lake for ACID transactions and data reliability - - Structured organization with bronze (raw), silver (refined), and gold (business) layers - -2. **Data Ingestion and Processing**: - - Azure Data Factory for orchestration and batch data movement - - Kafka and Azure Event Hubs for real-time data ingestion - - Databricks for large-scale data processing - - Stream processing with Spark Structured Streaming - -3. **Semantic Layer and Data Serving**: - - Databricks SQL Warehouses for analytics workloads - - Azure Synapse Analytics for enterprise data warehousing needs - - Power BI as primary business intelligence tool - - REST APIs for serving data to applications - -4. **Data Governance and Security**: - - Azure Purview for data catalog and lineage - - Column-level encryption for sensitive data - - Role-based access control aligned with data classification - - Automated data retention and purging based on policies - -5. **Machine Learning Platform**: - - MLflow for experiment tracking and model registry - - Databricks ML for model development and deployment - - Model monitoring and retraining pipelines - - Feature store for reusable feature engineering - -6. **DataOps and Automation**: - - Infrastructure as Code using Terraform - - CI/CD pipelines for data pipelines and transformations - - Automated testing for data quality and pipeline integrity - - Comprehensive monitoring and alerting - -### Platform Architecture by Domain - -| Domain | Data Types | Primary Tools | Access Patterns | Special Requirements | -|--------|------------|--------------|-----------------|----------------------| -| Customer 360 | Customer profiles, interactions, preferences | Delta Lake, Databricks SQL | Batch analytics, Real-time lookups | GDPR compliance, Entity resolution | -| Transaction Processing | Payment transactions, transfers, statements | Kafka, Delta Lake, Azure Synapse | Real-time streaming, Batch reporting | PCI-DSS compliance, 7-year retention | -| Risk Management | Credit scores, market data, exposure calculations | Databricks, Delta Lake, ML models | Batch processing, Model inference | Auditability, Model governance | -| Fraud Detection | Transaction patterns, behavioral signals | Kafka, Spark Streaming, ML models | Real-time streaming, Low-latency scoring | Sub-second latency, High availability | -| Regulatory Reporting | Aggregated financial data, compliance metrics | Azure Synapse, Power BI | Scheduled batch, Ad-hoc analysis | Immutability, Approval workflows | -| Marketing Analytics | Campaign data, customer segments, attribution | Databricks, Delta Lake, Power BI | Interactive queries, ML-based segmentation | Identity resolution, Attribution models | - -### Consequences - -#### Positive - -1. **Unified Data Access**: Single platform for accessing enterprise data with consistent governance. - -2. **Enhanced Analytical Capabilities**: Support for advanced analytics, machine learning, and AI initiatives. - -3. **Improved Scalability**: Cloud-native architecture can scale to handle growing data volumes. - -4. **Reduced Time-to-Insight**: Self-service capabilities and streamlined data pipelines reduce time to deliver insights. - -5. **Better Data Governance**: Centralized data catalog, lineage tracking, and security controls. - -6. **Real-time Capabilities**: Support for both batch and real-time processing using the same platform. - -7. **Cost Optimization**: Pay-for-use cloud model with ability to scale resources as needed. - -8. **Regulatory Compliance**: Improved ability to implement and demonstrate regulatory compliance. - -#### Negative - -1. **Implementation Complexity**: Significant effort required to migrate from legacy systems. - -2. **Skills Gap**: New technologies require reskilling of existing teams. - -3. **Initial Cost Increase**: Short-term investment in new technology and parallel running of systems. - -4. **Data Migration Challenges**: Data quality and mapping issues during migration. - -5. **Operational Changes**: New operational procedures and support models needed. - -6. **Integration Complexity**: Connecting legacy systems to new platform requires careful planning. - -7. **Organization Change Management**: New workflows and responsibilities across business and technical teams. - -### Mitigation Strategies - -1. **Phased Implementation Approach**: - - Start with highest-value, least-critical data domains - - Implement foundational capabilities before complex use cases - - Run legacy and new systems in parallel during transition - - Create clear success criteria for each phase - -2. **Talent and Skill Development**: - - Develop comprehensive training program for existing staff - - Strategic hiring for key specialized roles - - Partner with platform vendors for enablement - - Create centers of excellence for key technologies - -3. **Modern Data Governance**: - - Establish data governance council with cross-functional representation - - Define clear data ownership and stewardship model - - Implement automated data quality monitoring - - Create comprehensive data classification framework - -4. **Financial Management**: - - Detailed cloud cost monitoring and optimization - - Business-aligned chargeback model - - Clear ROI tracking for data initiatives - - Regular cost optimization reviews - -5. **Change Management Program**: - - Executive sponsorship and visible leadership - - Regular communication and success stories - - Early involvement of business stakeholders - - Incentives aligned with adoption goals - -### Implementation Details - -#### Phase 1: Foundation (Q1-Q2 2024) - -1. Establish cloud environment and core infrastructure -2. Implement data lake foundation with initial data domains -3. Deploy data catalog and basic governance tools -4. Migrate first non-critical data workloads -5. Establish DataOps practices and pipelines - -#### Phase 2: Expansion (Q3-Q4 2024) - -1. Migrate core analytical workloads to the platform -2. Implement real-time data processing capabilities -3. Deploy self-service analytics for business users -4. Enhance data quality frameworks and monitoring -5. Develop initial ML use cases on the platform - -#### Phase 3: Advanced Capabilities (Q1-Q2 2025) - -1. Full enterprise adoption across all data domains -2. Advanced ML capabilities and feature store -3. Comprehensive data governance implementation -4. Legacy system decommissioning -5. Advanced real-time analytics and decisioning - -### Considered Alternatives - -#### 1. Modernize Existing Data Warehouse - -**Pros**: Lower initial disruption, familiar technology, focused scope -**Cons**: Limited flexibility, higher long-term costs, limited real-time capabilities - -This approach would not address our fundamental needs for real-time processing, advanced analytics, and managing unstructured data. - -#### 2. Traditional Data Lake Architecture - -**Pros**: Lower cost storage, support for varied data types, scalability -**Cons**: Complexity in ensuring data quality, limited transactional support, governance challenges - -A traditional data lake without the lakehouse capabilities would create significant challenges for data reliability, performance, and governance. - -#### 3. Multiple Purpose-Built Systems - -**Pros**: Optimized solutions for specific use cases, potentially best-in-class capabilities -**Cons**: Increased integration complexity, data duplication, inconsistent governance - -This approach would perpetuate our data silo issues and create ongoing integration and consistency challenges. - -#### 4. Maintain and Incrementally Improve Current Systems - -**Pros**: Minimal disruption, lower initial investment, familiar technology -**Cons**: Perpetuates technical debt, limited capability improvement, increasing maintenance costs - -This would fail to address our fundamental challenges and put us at a competitive disadvantage as data volumes and complexity increase. - -### References - -1. "Designing Data-Intensive Applications" by Martin Kleppmann -2. [Databricks Lakehouse Platform Documentation](https://docs.databricks.com/lakehouse/index.html) -3. [Azure Data Factory Documentation](https://docs.microsoft.com/en-us/azure/data-factory/) -4. "Data Mesh: Delivering Data-Driven Value at Scale" by Zhamak Dehghani -5. FinSecure Internal Report: "Data Platform Requirements Analysis" (October 2023) -6. [DAMA Data Management Body of Knowledge](https://www.dama.org/cpages/body-of-knowledge) - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2023-10-15 | 0.1 | Initial draft | Jennifer Wu, Chief Data Officer | -| 2023-11-08 | 0.2 | Updated based on technical review | Raj Patel, Data Engineering Lead | -| 2023-12-02 | 0.3 | Added implementation phases and cost estimates | Michael Torres, Enterprise Architect | -| 2023-12-18 | 1.0 | Approved by Executive Technology Committee | FinSecure ETC | - -## Appendix A: Data Platform Architecture - -```mermaid -flowchart TB - subgraph "Data Sources" - core[Core Banking Systems] - crm[CRM Systems] - channels[Digital Channels] - market[Market Data Feeds] - apps[Applications] - ext[External Partners] - end - - subgraph "Data Ingestion" - batch[Batch Processing] - stream[Stream Processing] - cdc[Change Data Capture] - api[API Integration] - end - - subgraph "Data Storage" - subgraph "Data Lake" - bronze[Bronze Layer
    Raw Data] - silver[Silver Layer
    Cleansed & Conformed] - gold[Gold Layer
    Business Aggregates] - end - - dw[Enterprise Data Warehouse] - fs[Feature Store] - end - - subgraph "Processing & Analytics" - etl[ETL/ELT Processes] - bi[Business Intelligence] - ml[Machine Learning] - ad[Advanced Analytics] - realtime[Real-time Analytics] - end - - subgraph "Data Consumption" - reports[Reports & Dashboards] - apps2[Applications] - apis[APIs & Services] - exports[Exports & Feeds] - ml_svc[ML Services] - end - - subgraph "Governance & Operations" - catalog[Data Catalog] - quality[Data Quality] - lineage[Data Lineage] - security[Security & Privacy] - lifecycle[Data Lifecycle] - end - - core --> batch - crm --> batch - channels --> stream - market --> stream - apps --> cdc - ext --> api - - batch --> bronze - stream --> bronze - cdc --> bronze - api --> bronze - - bronze --> etl - etl --> silver - silver --> etl - etl --> gold - etl --> dw - gold --> dw - silver --> fs - - dw --> bi - gold --> bi - silver --> ml - gold --> ml - fs --> ml - silver --> ad - bronze --> realtime - silver --> realtime - - bi --> reports - ml --> ml_svc - ad --> apis - realtime --> apps2 - dw --> exports - - catalog --> bronze - catalog --> silver - catalog --> gold - catalog --> dw - - quality --> silver - lineage --> etl - security --> dw - lifecycle --> bronze -``` - -## Appendix B: Data Platform Implementation Timeline - -```mermaid -gantt - title FinSecure Data Platform Implementation - dateFormat YYYY-MM-DD - - section Strategy & Planning - Requirements Analysis :done, 2023-08-01, 2023-10-15 - Vendor Evaluation :done, 2023-09-15, 2023-11-15 - Architecture Design :done, 2023-10-01, 2023-12-15 - - section Phase 1: Foundation - Core Infrastructure Setup :active, 2024-01-15, 2024-02-28 - Data Lake Implementation :2024-02-01, 2024-03-31 - Governance Framework :2024-02-15, 2024-04-15 - Initial Data Migration :2024-03-01, 2024-05-31 - DataOps Implementation :2024-04-01, 2024-06-30 - - section Phase 2: Expansion - Advanced Analytics Platform :2024-07-01, 2024-09-30 - Streaming Data Processing :2024-08-01, 2024-10-31 - Self-service Analytics :2024-09-01, 2024-11-30 - Data Quality Framework :2024-10-01, 2024-12-15 - ML Platform Deployment :2024-10-15, 2025-01-31 - - section Phase 3: Advanced - Enterprise-wide Adoption :2025-02-01, 2025-04-30 - Legacy System Decomm :2025-03-01, 2025-06-30 - Advanced Governance :2025-04-01, 2025-06-30 - Real-time Decisioning :2025-05-01, 2025-07-31 - Program Completion :milestone, 2025-07-31 -``` - -## Appendix C: Target State Data Flow - Customer 360 Example - -```mermaid -sequenceDiagram - participant Source as Source Systems - participant Ingest as Data Ingestion - participant Bronze as Bronze Layer - participant Process as Data Processing - participant Silver as Silver Layer - participant Gold as Gold Layer - participant Consume as Data Consumption - - Source->>Ingest: Customer Data
    (Core Banking, CRM, Channels) - Ingest->>Bronze: Raw Data Ingestion
    (Batch & Streaming) - - Bronze->>Process: Validation & Cleansing - Process->>Process: Data Quality Checks - Process->>Process: Deduplication - Process->>Process: Standardization - Process->>Silver: Refined Customer Records - - Silver->>Process: Entity Resolution - Process->>Process: Customer Attribute Generation - Process->>Process: Relationship Mapping - Process->>Process: History Processing - Process->>Gold: Unified Customer Profiles - - Gold->>Consume: Customer 360 Views - Gold->>Consume: Segmentation Models - Gold->>Consume: Marketing Analytics - Gold->>Consume: Risk Assessment - Gold->>Consume: Real-time Personalization - - Note over Bronze,Gold: Data Governance
    - Lineage Tracking
    - Privacy Controls
    - Data Classification
    - Quality Monitoring -``` - -## Appendix D: Key Performance Indicators - -| KPI | Current State | Target (2025) | Measurement Method | -|-----|---------------|---------------|-------------------| -| Data Integration Cycle Time | 7-14 days | <24 hours | Average time from source change to data availability | -| Self-service BI Adoption | 15% of business users | >60% of business users | Monthly active users in self-service tools | -| Data Quality Score | ~75% | >95% | Composite score from automated quality checks | -| Cost per TB of Analytics Storage | $2,500/TB | <$500/TB | Total cost of ownership / storage volume | -| Time to New Analytics | 2-3 weeks | <3 days | Time from request to dashboard availability | -| Data Platform Availability | 99.5% | 99.95% | Measured service uptime | -| Regulatory Report Production Time | 10-15 days | 1-2 days | Time to produce monthly regulatory reports | -| Real-time Decision Latency | Not available | <250ms | Response time for real-time decision APIs | -| ML Model Deployment Time | 4-6 weeks | <1 week | Time from model approval to production deployment | -| Data Engineer Productivity | ~30% on new features | >70% on new features | Time allocation analysis | - -*Note: Metrics will be tracked quarterly and reported to the Data Governance Council.* \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/01-api-gateway-pattern.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/published/01-api-gateway-pattern.mdx deleted file mode 100644 index bfa97bf35..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/01-api-gateway-pattern.mdx +++ /dev/null @@ -1,250 +0,0 @@ ---- -title: API Gateway Pattern Adoption -summary: Architectural decision record for implementing an API Gateway for the FlowMart e-commerce platform -sidebar: - label: API Gateway Pattern - order: 1 ---- - -## ADR-001: Adoption of API Gateway Pattern for FlowMart E-commerce Platform - -### Status - -Approved (2024-06-10) - -### Context - -As we migrate to a microservices-based architecture for our e-commerce platform, we face several challenges in managing API endpoints: - -1. **API Proliferation**: Each microservice exposes its own APIs, leading to a large number of endpoints that clients need to interact with. -2. **Cross-cutting Concerns**: Common functionality like authentication, logging, and rate limiting needs to be implemented consistently across all services. -3. **Client Complexity**: Mobile apps, web frontends, and third-party integrations would need to understand the topology of our microservices ecosystem. -4. **Protocol Diversity**: Some internal services may use protocols (gRPC, AMQP) that aren't suitable for direct client consumption. -5. **Network Performance**: Multiple direct client-to-service calls increase network overhead, especially on mobile networks. -6. **Security Exposure**: Direct exposure of microservices increases the attack surface of our platform. - -We need a solution that simplifies our API landscape while maintaining the benefits of our microservices architecture. - -### Decision - -We will implement an **API Gateway Pattern** as the primary entry point for all client applications interacting with the FlowMart e-commerce platform. Specifically: - -1. **Gateway Implementation**: We will use **Kong API Gateway** as our primary implementation due to its robust plugin ecosystem, performance characteristics, and open-source foundation. - -2. **Gateway Responsibilities**: - - **Request Routing**: Directing requests to appropriate backend services - - **Authentication & Authorization**: Validating user identity and permissions - - **Rate Limiting**: Protecting services from excessive load - - **Request/Response Transformation**: Adapting data formats between clients and services - - **Caching**: Reducing backend load for frequently requested data - - **Logging & Monitoring**: Centralized request tracking - - **Circuit Breaking**: Preventing cascading failures - - **Protocol Translation**: Supporting REST, GraphQL, and gRPC as needed - -3. **Gateway Topology**: - - **Edge Gateway**: A public-facing gateway for all external clients - - **Internal Gateway**: For service-to-service communication within our private network - -4. **API Composition**: The gateway will aggregate responses from multiple services when needed to reduce client-side complexity. - -5. **Backend-for-Frontend (BFF) Pattern**: We will implement specific gateway configurations for different client platforms (web, mobile iOS, mobile Android) to optimize payloads and calls. - -6. **API Versioning**: The gateway will support multiple API versions to enable graceful evolution of our services. - -### Consequences - -#### Positive - -1. **Simplified Client Development**: Clients only need to know about a single entry point rather than multiple service endpoints. - -2. **Consistent Security Enforcement**: Authentication, authorization, and other security controls can be implemented uniformly. - -3. **Operational Visibility**: Centralized logging and monitoring of all API traffic. - -4. **Performance Optimization**: Opportunity for response caching, request collapsing, and other optimizations. - -5. **Flexible Evolution**: Backend services can be refactored, replaced, or decomposed without changing client implementations. - -6. **Traffic Control**: Ability to throttle, prioritize, or redirect traffic based on various conditions. - -#### Negative - -1. **Potential Single Point of Failure**: The gateway becomes critical infrastructure that must be highly available. - -2. **Increased Latency**: Adding another network hop introduces some additional latency. - -3. **Gateway Complexity**: The gateway configuration grows in complexity as the number of services increases. - -4. **Operational Overhead**: Additional infrastructure to monitor, maintain, and scale. - -5. **Deployment Coupling**: Gateway configuration changes may need coordination with service deployments. - -6. **Performance Bottleneck Risk**: Without proper scaling, the gateway could become a bottleneck under high load. - -### Mitigation Strategies - -1. **High Availability**: Deploy the API Gateway in a redundant, auto-scaling configuration across multiple availability zones. - -2. **Performance Optimization**: Implement efficient caching strategies and regular performance testing. - -3. **Circuit Breakers**: Implement circuit breakers to prevent cascading failures if backend services are unavailable. - -4. **Automated Configuration**: Use infrastructure as code to manage gateway configuration, with automated testing. - -5. **API Governance**: Establish clear ownership and review processes for gateway configuration changes. - -6. **Observability**: Implement comprehensive monitoring and alerting specifically for the gateway. - -### Implementation Details - -#### Phase 1: Core Infrastructure (Q2 2024) - -1. Deploy Kong API Gateway in development and staging environments -2. Implement basic routing for 2-3 core services -3. Set up authentication and authorization -4. Establish monitoring and logging -5. Create CI/CD pipeline for gateway configuration - -#### Phase 2: Feature Expansion (Q3 2024) - -1. Migrate all existing APIs to the gateway -2. Implement rate limiting and circuit breaking -3. Add response caching for appropriate endpoints -4. Develop BFF configurations for web and mobile -5. Implement comprehensive end-to-end testing - -#### Phase 3: Advanced Capabilities (Q4 2024) - -1. Implement GraphQL support for specific use cases -2. Add advanced analytics and API usage dashboards -3. Integrate with service mesh for internal service communication -4. Implement advanced security features (WAF, etc.) -5. Optimize for global performance - -### Considered Alternatives - -#### 1. Direct Client-to-Microservice Communication - -**Pros**: Simplicity, no additional network hop, no central bottleneck -**Cons**: Client complexity, inconsistent policy enforcement, security challenges - -This approach would expose our internal service architecture directly to clients, creating significant complexity and security concerns. - -#### 2. Service Mesh Only (no API Gateway) - -**Pros**: Great for service-to-service communication, built-in resilience -**Cons**: Not designed for edge traffic, less focus on client-specific concerns - -While we plan to use a service mesh for internal communication, it doesn't address the client-facing concerns that an API Gateway solves. - -#### 3. Bespoke API Gateway - -**Pros**: Custom-built for our exact needs, no license costs -**Cons**: Development and maintenance overhead, feature gaps, opportunity cost - -Building our own gateway would divert significant resources from our core business and likely result in a less robust solution than established products. - -#### 4. Alternative Gateway Products (Apigee, AWS API Gateway) - -**Pros**: Managed services (reduced operational burden), rich feature sets -**Cons**: Higher costs, potential vendor lock-in, less customization - -These were viable alternatives, but Kong offered the best balance of features, flexibility, and cost for our requirements. - -### References - -1. Richardson, Chris. "Pattern: API Gateway / Backends for Frontends." [Microservices.io](https://microservices.io/patterns/apigateway.html) -2. Fowler, Martin. "BFF: Backend for Frontend." [martinfowler.com](https://martinfowler.com/articles/gateway-pattern.html) -3. [Kong API Gateway Documentation](https://docs.konghq.com/) -4. Newman, Sam. Building Microservices (O'Reilly) -5. API Gateway Pattern, Microsoft Azure Architecture Center. [Microsoft Docs](https://docs.microsoft.com/en-us/azure/architecture/microservices/design/gateway) - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2024-05-15 | 0.1 | Initial draft | James Wilson | -| 2024-05-25 | 0.2 | Added implementation phases and alternatives | Sarah Chen | -| 2024-06-05 | 0.3 | Incorporated feedback from architecture review | David Boyne | -| 2024-06-10 | 1.0 | Approved by Architecture Board | Architecture Board | - -## Appendix A: API Gateway Architecture - -```mermaid -flowchart TB - subgraph "External Clients" - web[Web Application] - mobile[Mobile Apps] - partners[Partner Systems] - end - - subgraph "API Layer" - gateway[Kong API Gateway] - style gateway fill:#b9e0a5,stroke:#333,stroke-width:2px - auth[Auth Service] - gateway <--> auth - end - - subgraph "Backend Services" - products[Product Service] - orders[Order Service] - inventory[Inventory Service] - pricing[Pricing Service] - users[User Service] - recommendations[Recommendation Service] - end - - web --> gateway - mobile --> gateway - partners --> gateway - - gateway --> products - gateway --> orders - gateway --> inventory - gateway --> pricing - gateway --> users - gateway --> recommendations -``` - -## Appendix B: Gateway Policy Application Flow - -```mermaid -sequenceDiagram - participant Client - participant Gateway as API Gateway - participant Auth as Authentication Service - participant Service as Backend Service - - Client->>Gateway: API Request - - Gateway->>Gateway: Apply Rate Limiting - Note over Gateway: Check quota and rate limits - - Gateway->>Auth: Validate Token - Auth-->>Gateway: Token Valid/Invalid - - alt Token Valid - Gateway->>Gateway: Apply Request Transformation - Gateway->>Service: Forward Request - Service-->>Gateway: Service Response - Gateway->>Gateway: Apply Response Transformation - Gateway->>Gateway: Apply Caching Headers - Gateway-->>Client: Transformed Response - else Token Invalid - Gateway-->>Client: 401 Unauthorized - end -``` - -## Appendix C: Gateway Feature Rollout - -| Feature | Target Date | Priority | Dependencies | Owner | -|---------|-------------|----------|--------------|-------| -| Basic Routing | Q2 2024 | Critical | Gateway Infrastructure | DevOps Team | -| Authentication | Q2 2024 | Critical | Auth Service | Security Team | -| Monitoring & Logging | Q2 2024 | High | Observability Platform | DevOps Team | -| Rate Limiting | Q3 2024 | High | None | Platform Team | -| Request/Response Transformation | Q3 2024 | Medium | API Standards | API Team | -| Circuit Breaking | Q3 2024 | Medium | Service Health Metrics | Platform Team | -| Caching | Q3 2024 | Medium | Cache Invalidation Strategy | Performance Team | -| GraphQL Support | Q4 2024 | Low | GraphQL Schema | API Team | -| Advanced Analytics | Q4 2024 | Low | Data Platform | Data Team | \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/02-eda-adoption.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/published/02-eda-adoption.mdx deleted file mode 100644 index 187a322e7..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/02-eda-adoption.mdx +++ /dev/null @@ -1,290 +0,0 @@ ---- -title: Event-driven architecture adoption -summary: A document that captures important architectural decisions and their context -sidebar: - label: EDA Adoption - order: 2 ---- - -## ADR-001: Adoption of Event-Driven Architecture for FlowMart E-commerce Platform - -### Status - -Approved (2024-07-15) - -### Context - -FlowMart is building a new e-commerce platform to replace our legacy monolithic application. The current system faces several challenges: - -1. **Scalability Issues**: During peak shopping periods (e.g., Black Friday, holiday season), the system struggles to handle increased traffic, resulting in degraded performance and occasional outages. -2. **Maintenance Complexity**: Adding new features or modifying existing ones requires extensive regression testing and often leads to unexpected side effects. -3. **Technology Constraints**: The monolithic architecture limits our ability to adopt new technologies or update components independently. -4. **Data Consistency**: Ensuring data consistency across different parts of the application has become increasingly difficult. -5. **Team Independence**: Multiple development teams working on different aspects of the application frequently block each other. - -We need an architecture that addresses these challenges while enabling rapid innovation and scaling to meet our projected growth over the next 3-5 years. - -### Decision - -We will adopt an **Event-Driven Architecture (EDA)** using a microservices approach for the new FlowMart e-commerce platform. Specifically: - -1. **Domain-Driven Design (DDD)**: We will organize our services around business domains (Orders, Inventory, Payment, Shipping, etc.) with clearly defined bounded contexts. - -2. **Event Sourcing**: Critical business transactions will be stored as a sequence of immutable events that can be used to reconstruct the system state at any point in time. - -3. **Command Query Responsibility Segregation (CQRS)**: We will separate read and write operations where appropriate to optimize for different performance and scaling requirements. - -4. **Apache Kafka** will serve as our primary event streaming platform for asynchronous communication between services. - -5. **Eventual Consistency Model**: We acknowledge that the system will prioritize availability and partition tolerance over immediate consistency (following the CAP theorem), with mechanisms to ensure eventual consistency. - -6. **Service Autonomy**: Each service will: - - Have its own database - - Be independently deployable - - Have well-defined APIs and event contracts - - Be responsible for publishing domain events when state changes occur - -7. **Choreography Over Orchestration**: Services will primarily react to events rather than being orchestrated by a central coordinator, though we will use orchestration for complex workflows when necessary. - -### Consequences - -#### Positive - -1. **Improved Scalability**: Individual services can scale independently based on demand, allowing us to allocate resources more efficiently. - -2. **Better Fault Isolation**: Failures in one service are less likely to cascade across the entire system, improving overall reliability. - -3. **Technology Flexibility**: Teams can choose the most appropriate technologies for their specific domains, allowing for incremental adoption of new technologies. - -4. **Team Autonomy**: Domain-aligned teams can develop, test, and deploy their services independently, reducing cross-team dependencies. - -5. **Enhanced Auditability**: Event sourcing provides a complete audit trail of all system changes, which is valuable for debugging, compliance, and business analytics. - -6. **Improved Extensibility**: New capabilities can be added by creating new consumers of existing events without modifying the original producers. - -#### Negative - -1. **Increased Complexity**: Distributed systems are inherently more complex to develop, test, debug, and operate compared to monolithic applications. - -2. **Learning Curve**: The team will need to learn new patterns, technologies, and operational practices, which may slow initial development. - -3. **Eventual Consistency Challenges**: Business operations and UI design must account for data that might not be immediately consistent across services. - -4. **Operational Overhead**: Managing multiple services, event streams, and databases requires more sophisticated monitoring, deployment, and operational tools. - -5. **Transaction Management**: Ensuring transactional integrity across service boundaries requires careful design and implementation of compensation patterns. - -6. **Testing Complexity**: End-to-end testing becomes more challenging, requiring new testing strategies and tools. - -### Compliance Requirements - -Our implementation must adhere to the following requirements: - -1. **Data Privacy**: Personal customer data must be handled in compliance with GDPR, CCPA, and other applicable regulations. - -2. **PCI DSS**: Payment processing components must comply with Payment Card Industry Data Security Standards. - -3. **Audit Trail**: All critical business transactions must be traceable and auditable for a minimum of 7 years. - -4. **Security**: Authentication, authorization, and data encryption standards must be consistently applied across all services. - -### Implementation Details - -#### Phase 1: Core Domain Decomposition (Q3 2024) - -1. Identify and define core domain boundaries -2. Establish event schemas and contracts -3. Implement Kafka infrastructure and operational tooling -4. Migrate the first domain (Orders) to the new architecture -5. Set up CI/CD pipelines and monitoring - -#### Phase 2: Domain Expansion (Q4 2024) - -1. Migrate Inventory and Payment domains -2. Implement event sourcing for critical domains -3. Develop read models for reporting and analytics -4. Establish cross-domain consistency patterns - -#### Phase 3: Legacy Decommissioning (Q1-Q2 2025) - -1. Migrate remaining domains -2. Implement advanced monitoring and alerting -3. Gradually decommission legacy system components -4. Complete performance tuning and optimization - -### Considered Alternatives - -#### 1. Modular Monolith - -**Pros**: Simpler development model, transactional integrity, easier testing -**Cons**: Limited independent scaling, technology constraints, deployment coupling - -This approach would address some concerns (maintainability, modularity) but would not solve our scalability and team independence challenges. - -#### 2. Microservices with REST-only Communication - -**Pros**: Well-understood patterns, synchronous communication simplicity -**Cons**: Tighter coupling, limited resilience, cascading failures - -This approach would improve modularity but would not adequately address resilience and scalability concerns. - -#### 3. Serverless Architecture - -**Pros**: Minimal infrastructure management, high elasticity, pay-per-use model -**Cons**: Vendor lock-in, cold start latency, limited control over infrastructure - -While appealing for certain scenarios, this approach would not provide the control and predictability needed for our core business functions. - -### References - -1. Building Event-Driven Microservices (Adam Bellemare, O'Reilly) -2. Domain-Driven Design (Eric Evans, Addison-Wesley) -3. Enterprise Integration Patterns (Gregor Hohpe, Bobby Woolf, Addison-Wesley) -4. [Kafka Documentation](https://kafka.apache.org/documentation/) -5. [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html) (Martin Fowler) -6. [Event Sourcing Pattern](https://martinfowler.com/eaaDev/EventSourcing.html) (Martin Fowler) - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2024-06-22 | 0.1 | Initial draft | David Boyne | -| 2024-06-30 | 0.2 | Incorporate feedback from architecture review | Amy Smith | -| 2024-07-10 | 0.3 | Added implementation phasing and compliance requirements | Kiran Patel | -| 2024-07-15 | 1.0 | Approved by Architecture Board | Architecture Board | - -## Appendix A: High-Level Architecture Diagram - -```mermaid -flowchart TB - subgraph "API Gateway" - gw[API Gateway] - end - - subgraph "Customer Domain" - cust[Customer Service] - auth[Authentication Service] - end - - subgraph "Order Domain" - order[Order Service] - orderhist[Order History Service] - end - - subgraph "Inventory Domain" - inv[Inventory Service] - stock[Stock Management] - end - - subgraph "Payment Domain" - payment[Payment Service] - refund[Refund Service] - end - - subgraph "Shipping Domain" - ship[Shipping Service] - track[Tracking Service] - end - - subgraph "Event Backbone" - kafka[Apache Kafka] - end - - gw --> order - gw --> cust - gw --> auth - gw --> inv - gw --> payment - gw --> ship - gw --> track - - order --> kafka - cust --> kafka - inv --> kafka - payment --> kafka - ship --> kafka - track --> kafka - - kafka --> orderhist - kafka --> stock - kafka --> refund -``` - -## Appendix B: Key Event Flows - -### Order Placement Flow - -```mermaid -sequenceDiagram - participant C as Customer - participant OS as Order Service - participant IS as Inventory Service - participant PS as Payment Service - participant SS as Shipping Service - participant NS as Notification Service - - C->>OS: Place Order - OS->>IS: Check Inventory - IS-->>OS: Inventory Available - OS->>PS: Process Payment - PS-->>OS: Payment Approved - OS->>OS: Create Order - OS->>+kafka: Publish OrderCreated - kafka->>IS: OrderCreated - IS->>IS: Reserve Inventory - IS->>kafka: Publish InventoryReserved - kafka->>SS: InventoryReserved - SS->>SS: Create Shipment - SS->>kafka: Publish ShipmentCreated - kafka->>NS: ShipmentCreated - NS->>C: Send Order Confirmation -``` - -### Out-of-Stock Handling Flow - -```mermaid -sequenceDiagram - participant IS as Inventory Service - participant K as Kafka - participant NS as Notification Service - participant RS as Replenishment Service - participant OS as Order Service - participant CS as Customer - - IS->>IS: Stock level check - IS->>+K: Publish OutOfStockEvent - K->>NS: OutOfStockEvent - K->>RS: OutOfStockEvent - K->>OS: OutOfStockEvent - - NS->>CS: Send out-of-stock notification - RS->>RS: Create replenishment order - RS->>+K: Publish ReplenishmentOrderedEvent - OS->>OS: Update product availability - OS->>CS: Show "Notify when available" option - - note over RS,IS: After stock is replenished - RS->>IS: Restock inventory - IS->>+K: Publish InventoryRestockedEvent - K->>NS: InventoryRestockedEvent - K->>OS: InventoryRestockedEvent - - NS->>CS: Send back-in-stock notification - OS->>OS: Update product availability -``` - -## Appendix C: Service Ownership - -| Domain | Service | Team | -|--------|---------|------| -| Customer | Customer Service | Full Stack Team | -| Customer | Authentication Service | Security Team | -| Order | Order Service | Order Management Team | -| Order | Order History Service | Order Management Team | -| Inventory | Inventory Service | Full Stack Team | -| Inventory | Stock Management | Full Stack Team | -| Payment | Payment Service | Payment Team | -| Payment | Refund Service | Payment Team | -| Shipping | Shipping Service | Logistics Team | -| Shipping | Tracking Service | Logistics Team | - diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/03-auth-strategy.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/published/03-auth-strategy.mdx deleted file mode 100644 index 20c916065..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/03-auth-strategy.mdx +++ /dev/null @@ -1,296 +0,0 @@ ---- -title: Authentication and Authorization Strategy -summary: Architectural decision record for implementing authentication and authorization for the FlowMart e-commerce platform -sidebar: - label: Auth Strategy - order: 3 ---- - -## ADR-003: Authentication and Authorization Strategy for FlowMart E-commerce Platform - -### Status - -Approved (2024-08-01) - -### Context - -As we move to a microservices architecture for our e-commerce platform, we need a robust, scalable, and secure approach to authentication and authorization. The current monolithic application uses a custom auth solution with session-based authentication, which presents several challenges in a distributed environment: - -1. **Session Stickiness**: Requires load balancer configuration to route users to the same server. -2. **Scalability Constraints**: Session state storage becomes challenging as we scale horizontally. -3. **Cross-Service Authentication**: No standardized way for services to validate user identity and permissions. -4. **Partner and Third-Party Integration**: Difficult to securely expose APIs to external systems. -5. **Multiple Authentication Factors**: Need to support various authentication methods (passwords, social logins, biometrics). -6. **Varying Authorization Requirements**: Different resources require different permission models. -7. **Compliance Requirements**: GDPR, PCI-DSS, and industry regulations impose strict requirements on handling authentication data. - -We need an authentication and authorization strategy that works effectively in a distributed microservices environment while maintaining high security standards. - -### Decision - -We will implement a token-based authentication and authorization strategy using **OAuth 2.0** and **OpenID Connect (OIDC)** standards with **Auth0** as our identity provider. Specifically: - -1. **Authentication Pattern**: - - **JWT (JSON Web Tokens)** for representing claims between parties - - **Stateless authentication** where possible to improve scalability - - **OAuth 2.0 Authorization Code Flow with PKCE** for web applications - - **OAuth 2.0 Resource Owner Password Grant** (limited to legacy/internal applications) - - **OAuth 2.0 Client Credentials Grant** for service-to-service communication - -2. **Authorization Pattern**: - - **Role-Based Access Control (RBAC)** as the primary mechanism for coarse-grained permissions - - **Attribute-Based Access Control (ABAC)** for fine-grained access decisions - - **Policy Enforcement Points (PEP)** implemented at the API Gateway level for common policies - - **Service-level authorization** for domain-specific access control - -3. **User Management**: - - Centralized user directory in Auth0 - - Self-service user registration and profile management - - Admin-managed role assignments and permissions - - Progressive profiling to collect user information gradually - -4. **Multi-Factor Authentication (MFA)**: - - Required for administrative accounts and sensitive operations - - Optional but encouraged for standard user accounts - - Risk-based authentication triggers (unusual location, device, behavior) - -5. **Service-to-Service Authentication**: - - Mutual TLS (mTLS) for service mesh communication - - Client credentials OAuth flow with short-lived tokens for cross-boundary requests - -6. **API Security**: - - Token validation at the API Gateway - - Scoped access tokens for limiting API permissions - - Token introspection for high-security operations - -### Consequences - -#### Positive - -1. **Improved Scalability**: Stateless authentication allows for better horizontal scaling without session replication. - -2. **Standardized Security**: Using established security protocols (OAuth 2.0/OIDC) provides well-tested security patterns. - -3. **Reduced Development Burden**: Auth0 handles many security concerns (token issuance, validation, revocation, etc.). - -4. **Flexible Integration**: Easier to integrate with third-party systems and identity providers. - -5. **Centralized Policy Management**: Authorization policies can be managed centrally and applied consistently. - -6. **Enhanced User Experience**: Support for modern authentication patterns (social login, passwordless, etc.). - -7. **Compliance Support**: Built-in features for consent management, audit logging, and other compliance requirements. - -#### Negative - -1. **Increased Complexity**: More moving parts in the authentication flow compared to simple session-based auth. - -2. **Token Management Overhead**: Need to handle token lifecycle, refresh, and invalidation carefully. - -3. **External Dependency**: Reliance on Auth0 as a critical service and potential single point of failure. - -4. **Performance Considerations**: Token validation and policy evaluation add some latency to requests. - -5. **Cost**: Subscription fees for Auth0 based on active users and features used. - -6. **Learning Curve**: Team needs to understand OAuth 2.0 flows and token-based authentication patterns. - -### Mitigation Strategies - -1. **Caching and Performance Optimization**: - - Implement efficient token validation with local caching - - Use token introspection only when necessary - - Optimize policy evaluation paths - -2. **Resilience Planning**: - - Implement graceful degradation if Auth0 is temporarily unavailable - - Consider a multi-region Auth0 deployment for critical workloads - - Regular disaster recovery testing - -3. **Security Hardening**: - - Set appropriate token lifetimes (shorter for sensitive operations) - - Implement proper token storage in clients (secure cookies, encrypted storage) - - Regular security reviews and penetration testing - -4. **Developer Experience**: - - Create SDKs and libraries to abstract authentication complexity - - Comprehensive documentation and training - - Authentication dev sandbox environment - -### Implementation Details - -#### Phase 1: Core Authentication Infrastructure (Q3 2024) - -1. Set up Auth0 tenant and configure core settings -2. Implement login, registration, and password reset flows -3. Migrate existing user accounts (passwords securely hashed) -4. Integrate with API Gateway for token validation -5. Set up basic roles and permissions - -#### Phase 2: Advanced Authorization (Q4 2024) - -1. Implement fine-grained ABAC policies -2. Develop centralized policy management tools -3. Set up delegated administration capabilities -4. Implement audit logging and monitoring -5. Configure MFA for administrative accounts - -#### Phase 3: Partner and Integration Capabilities (Q1 2025) - -1. Implement client credentials for service-to-service -2. Set up partner authentication portal -3. Configure rate limiting and throttling -4. Implement token exchange capabilities -5. Set up public developer documentation - -### Considered Alternatives - -#### 1. Custom Authentication Solution - -**Pros**: Complete control, no external dependencies, potentially lower direct costs -**Cons**: Development time, security risks, maintenance burden, limited features - -Building our own solution would require significant security expertise and ongoing maintenance, with higher risk of vulnerabilities. - -#### 2. Keycloak (Open Source Identity Provider) - -**Pros**: Open source, flexible, no subscription fees, self-hosted control -**Cons**: Operational overhead, scalability challenges, fewer integrations - -Keycloak would reduce subscription costs but increase operational complexity and require more internal expertise. - -#### 3. AWS Cognito - -**Pros**: Tight AWS integration, managed service, good scalability -**Cons**: Less flexible than Auth0, fewer enterprise features, AWS lock-in - -Cognito would be suitable if we were fully committed to AWS, but we needed more flexibility for our multi-cloud strategy. - -#### 4. Session-Based Authentication with Distributed Cache - -**Pros**: Simpler architecture, familiar pattern, less paradigm shift -**Cons**: Scalability challenges, higher operational complexity, less standardized - -This would maintain our current approach but with scalability improvements, though it wouldn't address many of our requirements. - -### Standards Compliance - -Our implementation will comply with the following standards and regulations: - -1. **OAuth 2.0 (RFC 6749)**: The authorization framework -2. **OpenID Connect 1.0**: Identity layer on top of OAuth 2.0 -3. **JWT (RFC 7519)**: Compact token format -4. **GDPR**: For EU user data protection -5. **PCI-DSS**: For payment-related authentication -6. **NIST 800-63B**: Digital Identity Guidelines - -### References - -1. [OAuth 2.0 RFC 6749](https://tools.ietf.org/html/rfc6749) -2. [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) -3. [Auth0 Architecture Scenarios](https://auth0.com/docs/architecture-scenarios) -4. [JWT Introduction](https://jwt.io/introduction) -5. Nate Barbettini, "OAuth 2.0 and OpenID Connect in Plain English" ([YouTube](https://www.youtube.com/watch?v=996OiexHze0)) -6. "API Security in Action" by Neil Madden (Manning Publications) - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2024-07-01 | 0.1 | Initial draft | Elena Rodriguez | -| 2024-07-15 | 0.2 | Added implementation phases and alternatives | Michael Chang | -| 2024-07-25 | 0.3 | Incorporated security team feedback | Sarah Chen | -| 2024-08-01 | 1.0 | Approved by Architecture and Security Boards | Architecture Board | - -## Appendix A: Authentication Flows - -```mermaid -sequenceDiagram - participant User - participant Client as Web/Mobile App - participant API as API Gateway - participant Auth0 as Auth0 - participant Service as Microservice - - %% Login Flow - User->>Client: Initiates Login - Client->>Auth0: Redirect to Auth0 (/authorize) - Auth0->>User: Present Login UI - User->>Auth0: Enter Credentials - opt MFA Triggered - Auth0->>User: Request Second Factor - User->>Auth0: Provide Second Factor - end - Auth0->>Client: Authorization Code - Client->>Auth0: Exchange Code for Tokens - Auth0->>Client: Access Token, ID Token, Refresh Token - - %% API Access Flow - Client->>API: Request with Access Token - API->>API: Validate Token - API->>Service: Forward Request + Claims - Service->>Service: Check Authorization - Service->>API: Response - API->>Client: Response - - %% Token Refresh - Note over Client: When Access Token Expires - Client->>Auth0: Request with Refresh Token - Auth0->>Client: New Access Token -``` - -## Appendix B: Authorization Model - -```mermaid -flowchart TB - subgraph "Authorization Layers" - direction TB - - subgraph "Layer 1: Authentication" - auth[User Authentication] - token[Token Issuance] - end - - subgraph "Layer 2: Coarse Authorization" - rbac[Role-Based Access Control] - scopes[OAuth Scopes] - end - - subgraph "Layer 3: Fine Authorization" - abac[Attribute-Based Policies] - biz[Business Rules] - end - end - - subgraph "Enforcement Points" - gateway[API Gateway] - services[Microservices] - end - - auth --> token - token --> rbac - token --> scopes - rbac --> abac - scopes --> abac - abac --> biz - - rbac --> gateway - scopes --> gateway - abac --> gateway - biz --> services - - gateway --> services -``` - -## Appendix C: Role and Permission Mapping - -| Role | Description | Permissions | MFA Required | -|------|-------------|------------|--------------| -| Anonymous | Unauthenticated user | Browse catalog, View public content | No | -| Customer | Registered shopper | Place orders, Manage profile, Write reviews | Optional | -| Premium Customer | Paid tier customer | Customer + Early access, Special discounts | Optional | -| Customer Service | Support staff | View orders, Issue refunds, Update customer info | Yes | -| Store Manager | Retail location manager | Customer Service + Inventory management, Staff management | Yes | -| Admin | System administrator | All permissions | Yes | -| API Partner | External system | Specific API access based on agreement | No (mTLS) | -| Service Account | Internal service | Specific service-to-service communication | No (mTLS) | \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/04-database-strategy.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/published/04-database-strategy.mdx deleted file mode 100644 index 4dc39480c..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/04-database-strategy.mdx +++ /dev/null @@ -1,363 +0,0 @@ ---- -title: Database Strategy for Microservices -summary: Architectural decision record for database selection and data management in the FlowMart e-commerce platform -sidebar: - label: Database Strategy - order: 4 ---- - -## ADR-004: Database Strategy for Microservices in FlowMart E-commerce Platform - -### Status - -Approved (2024-08-20) - -### Context - -Our transition from a monolithic architecture to microservices necessitates a reassessment of our database strategy. The existing monolithic application uses a centralized Oracle database with hundreds of tables spanning multiple business domains. This approach has created several challenges: - -1. **Schema Coupling**: Changes to database schemas require careful coordination across teams to avoid breaking dependent services. -2. **Scalability Limitations**: The relational database becomes a bottleneck during high-traffic periods, affecting all application features. -3. **One-Size-Fits-All Approach**: Different data access patterns are all forced into the same relational model, leading to suboptimal performance. -4. **Operational Overhead**: Database administration is complex due to competing requirements from different application components. -5. **Development Bottlenecks**: Teams must coordinate database changes, slowing down development velocity. -6. **Cost Efficiency**: The enterprise database licensing model is expensive and doesn't scale cost-effectively with our varying workloads. - -As we decompose our application into microservices, we need a database strategy that promotes service autonomy while ensuring data consistency, performance, and operational efficiency. - -### Decision - -We will adopt a **polyglot persistence strategy** for our microservices architecture, following the principle of "right database for the right job." Key aspects of this strategy include: - -1. **Database-per-Service Pattern**: - - Each microservice will own and exclusively manage its database - - No direct database access from other services - - Services interact via well-defined APIs, not shared databases - -2. **Primary Database Technologies**: - - **PostgreSQL**: Primary relational database for services with complex transactional requirements - - **MongoDB**: Document database for services with flexible schema requirements and read-heavy workloads - - **Redis**: In-memory data store for caching, session management, and real-time features - - **Elasticsearch**: Search engine for product catalog and content search - - **Apache Cassandra**: Wide-column store for high-volume time-series data and analytics - - **Amazon DynamoDB**: Managed NoSQL for highly scalable microservices with predictable access patterns - -3. **Data Consistency Patterns**: - - **Saga Pattern**: For coordinating transactions across multiple services - - **Event Sourcing**: For critical business domains requiring complete audit trails - - **CQRS**: For services with asymmetric read/write loads - - **Outbox Pattern**: For reliable event publishing during state changes - -4. **Data Migration and Evolution**: - - Versioned database schemas - - Backward-compatible schema changes - - Blue/green deployment for database changes - - Incremental data migration approach - -5. **Data Governance**: - - Centralized data catalog and lineage tracking - - Common data models for shared business entities - - Standard approach to master data management - - Automated data quality monitoring - -6. **Operational Excellence**: - - Infrastructure as Code (IaC) for all database resources - - Automated backup and recovery procedures - - Standardized monitoring and alerting - - Database reliability engineering practices - -### Database Selection Criteria by Domain - -| Domain | Database Type | Primary Factors | Secondary Considerations | -|--------|---------------|-----------------|--------------------------| -| Product Catalog | MongoDB + Elasticsearch | Schema flexibility, Search capabilities | High read-to-write ratio, Caching | -| Order Management | PostgreSQL | ACID transactions, Complex queries | Event sourcing for audit trail | -| Customer Profile | PostgreSQL | Data consistency, Relational integrity | Privacy compliance, Scalable reads | -| Inventory | MongoDB | Frequent schema evolution, High write throughput | Eventual consistency model | -| Cart & Checkout | Redis + PostgreSQL | Low latency, High availability | Transaction support for checkout | -| Payment Processing | PostgreSQL | Strong consistency, Transaction support | Compliance requirements, Security | -| Analytics | Cassandra | High write throughput, Time-series data | Analytical query patterns | -| Recommendations | Redis + MongoDB | Low latency reads, Flexible data model | Machine learning feature support | -| User Sessions | Redis | Ultra-low latency, Expiration support | High availability, Cross-region replication | -| Content Management | MongoDB | Flexible schema, Document structure | Full-text search capability | - -### Consequences - -#### Positive - -1. **Service Autonomy**: Teams can independently select, optimize, and evolve their databases without cross-team coordination. - -2. **Optimized Performance**: Each service can use a database technology optimized for its specific data access patterns. - -3. **Independent Scaling**: Database resources can be scaled according to the specific needs of each service. - -4. **Improved Resilience**: Database failures are isolated to specific services rather than affecting the entire system. - -5. **Fit-for-Purpose Solutions**: Different data models (relational, document, key-value, etc.) can be applied where most appropriate. - -6. **Cost Optimization**: Resources can be allocated based on the specific needs of each service. - -7. **Incremental Adoption**: New database technologies can be introduced for new services without migrating legacy data. - -#### Negative - -1. **Increased Operational Complexity**: Managing multiple database technologies requires broader expertise and more sophisticated tooling. - -2. **Data Consistency Challenges**: Maintaining consistency across service boundaries requires careful design and implementation. - -3. **Learning Curve**: Development teams need to learn multiple database technologies and data access patterns. - -4. **Distributed Transactions**: Business processes spanning multiple services require more complex transaction management. - -5. **Increased Infrastructure Costs**: Running and maintaining multiple database systems may increase overall infrastructure costs. - -6. **Data Duplication**: Some data may need to be duplicated across services, creating synchronization challenges. - -7. **Monitoring and Troubleshooting Complexity**: Different databases require different monitoring approaches and expertise. - -### Mitigation Strategies - -1. **Platform Database Service**: - - Create an internal platform team that provides database-as-a-service capabilities - - Standardize on a limited set of supported database technologies - - Provide automated provisioning, backup, and monitoring - -2. **Data Access Layer Pattern**: - - Create standardized libraries for common database access patterns - - Abstract database-specific details behind consistent interfaces - - Implement retry logic, circuit breakers, and observability - -3. **Data Synchronization Framework**: - - Implement a standardized approach to CDC (Change Data Capture) - - Create reusable components for the Outbox Pattern - - Develop standard data synchronization workflows - -4. **Database Reliability Engineering**: - - Establish dedicated database reliability engineers - - Create runbooks for common database operations - - Implement chaos engineering practices for database failure testing - -5. **Developer Training and Support**: - - Comprehensive training program for supported database technologies - - Internal knowledge base and best practice documentation - - Database design review process for new services - -### Implementation Details - -#### Phase 1: Foundation (Q3-Q4 2024) - -1. Establish database platform team and core services -2. Set up standard PostgreSQL and MongoDB offerings -3. Implement database CI/CD pipelines -4. Develop initial monitoring and alerting -5. Create database provisioning automation - -#### Phase 2: Expansion (Q1-Q2 2025) - -1. Add Redis and Elasticsearch to supported offerings -2. Implement data synchronization framework -3. Develop CQRS and Event Sourcing patterns -4. Create data governance tooling -5. Expand monitoring capabilities - -#### Phase 3: Advanced Capabilities (Q3-Q4 2025) - -1. Add Cassandra and specialized databases -2. Implement advanced high availability features -3. Develop cross-region replication capabilities -4. Create advanced analytics integration -5. Implement predictive scaling and optimization - -### Considered Alternatives - -#### 1. Shared Database Approach - -**Pros**: Simplicity, familiar model, transactional integrity, easier reporting -**Cons**: Tight coupling, scalability limitations, schema coordination challenges - -This approach would minimize short-term changes but would recreate many of the current issues in our new architecture. - -#### 2. Single Database Technology (PostgreSQL Only) - -**Pros**: Operational simplicity, consistent expertise, familiar tooling -**Cons**: Not optimal for all workloads, potential performance compromises - -While PostgreSQL is versatile, it isn't the best choice for all our diverse data requirements. - -#### 3. Database-as-a-Service Only (e.g., AWS RDS/DynamoDB) - -**Pros**: Reduced operational overhead, managed scaling, built-in high availability -**Cons**: Vendor lock-in, potential cost concerns, less flexibility - -While we'll use managed services where appropriate, we need the flexibility to run certain databases on our own infrastructure. - -#### 4. Fully Centralized Data Lake Approach - -**Pros**: Centralized analytics, simplified data governance, consistent data model -**Cons**: Complex real-time synchronization, potential performance issues, development overhead - -This approach works well for analytics but isn't suitable for operational data needs. - -### References - -1. Kleppmann, Martin. "Designing Data-Intensive Applications" (O'Reilly Media) -2. Vernon, Vaughn. "Implementing Domain-Driven Design" (Addison-Wesley) -3. Fowler, Martin. "PolyglotPersistence" [martinfowler.com](https://martinfowler.com/bliki/PolyglotPersistence.html) -4. Richardson, Chris. "Microservices Patterns" (Manning Publications) -5. [Database per Service Pattern](https://microservices.io/patterns/data/database-per-service.html) -6. [Saga Pattern](https://microservices.io/patterns/data/saga.html) - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2024-07-20 | 0.1 | Initial draft | Robert Kim | -| 2024-08-02 | 0.2 | Added domain-specific database recommendations | Maria Garcia | -| 2024-08-15 | 0.3 | Incorporated feedback from architecture review | David Boyne | -| 2024-08-20 | 1.0 | Approved by Architecture Board | Architecture Board | - -## Appendix A: Database Architecture Overview - -```mermaid -flowchart TB - subgraph "Service Domains" - subgraph "Product Domain" - pc[Product Catalog] - ps[Product Search] - pc --- MongoDB - ps --- Elasticsearch - end - - subgraph "Order Domain" - om[Order Management] - oh[Order History] - om --- PostgreSQL - oh --- MongoDB - end - - subgraph "Customer Domain" - cp[Customer Profile] - auth[Authentication] - sess[Sessions] - cp --- PostgreSQL - auth --- PostgreSQL - sess --- Redis - end - - subgraph "Inventory Domain" - inv[Inventory Management] - stk[Stock Tracking] - inv --- MongoDB - stk --- MongoDB - end - - subgraph "Payment Domain" - pay[Payment Processing] - tx[Transaction Ledger] - pay --- PostgreSQL - tx --- PostgreSQL - end - - subgraph "Analytics Domain" - metrics[Metrics Collection] - reports[Reporting] - metrics --- Cassandra - reports --- Elasticsearch - end - end - - subgraph "Data Integration" - ksql[KSQL] - cdc[CDC Connectors] - etl[ETL Processes] - - Kafka --> ksql - cdc --> Kafka - ksql --> etl - end - - MongoDB --> cdc - PostgreSQL --> cdc - Cassandra --> cdc - - etl --> DataLake -``` - -## Appendix B: Data Consistency Patterns by Service Type - -```mermaid -flowchart TB - subgraph "Data Consistency Patterns" - direction TB - - subgraph "Transactional Services" - saga[Saga Pattern] - tcc[Try-Confirm/Cancel Pattern] - 2pc[Two-Phase Commit] - end - - subgraph "Event-Driven Services" - es[Event Sourcing] - cqrs[CQRS] - outbox[Outbox Pattern] - end - - subgraph "Caching Patterns" - cache[Cache-Aside] - writeBehind[Write-Behind] - readThrough[Read-Through] - end - end - - subgraph "Service Examples" - checkout[Checkout Service] - inventory[Inventory Service] - catalog[Product Catalog] - recommendations[Recommendations] - end - - checkout --> saga - checkout --> 2pc - inventory --> es - inventory --> outbox - catalog --> cqrs - catalog --> cache - recommendations --> readThrough -``` - -## Appendix C: Database Migration Strategy - -```mermaid -sequenceDiagram - participant Legacy as Legacy Database - participant Middleware as Migration Middleware - participant New as New Service Databases - participant CDC as Change Data Capture - participant Events as Event Bus - - Note over Legacy, Events: Phase 1: Shadow Reading - - Legacy->>Middleware: Original Data Access - Middleware->>New: Shadow Writes - - Note over Legacy, Events: Phase 2: Dual Write - - Legacy->>Middleware: Original Data Access - Middleware->>Legacy: Write to Legacy - Middleware->>New: Write to New System - - Note over Legacy, Events: Phase 3: CDC Migration - - Legacy->>CDC: Capture Changes - CDC->>Events: Publish Change Events - Events->>New: Update New System - - Note over Legacy, Events: Phase 4: Reverse Flow - - New->>Middleware: Primary Data Access - Middleware->>Legacy: Synchronize for Legacy Systems - - Note over Legacy, Events: Phase 5: Legacy Retirement - - New->>New: Fully Migrated Operations -``` \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/05-frontend-architecture.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/published/05-frontend-architecture.mdx deleted file mode 100644 index 89d36d343..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/05-frontend-architecture.mdx +++ /dev/null @@ -1,373 +0,0 @@ ---- -title: Frontend Architecture -summary: Architectural decision record for frontend architecture of the FlowMart e-commerce platform -sidebar: - label: Frontend Architecture - order: 5 ---- - -## ADR-005: Frontend Architecture for FlowMart E-commerce Platform - -### Status - -Approved (2024-09-05) - -### Context - -The frontend of our current monolithic e-commerce application faces numerous challenges: - -1. **Performance Issues**: The current server-rendered application has slow page loads and poor mobile performance. -2. **Developer Productivity**: Shared frontend codebase creates development bottlenecks and team dependencies. -3. **Inconsistent User Experience**: Different parts of the application have divergent design patterns and interaction models. -4. **Limited Reusability**: Components are tightly coupled to specific pages, making code reuse difficult. -5. **Testing Challenges**: The current codebase has limited test coverage and is difficult to test effectively. -6. **Technology Constraints**: Outdated technology stack limits our ability to leverage modern frontend capabilities. -7. **Scalability Concerns**: Our current approach doesn't scale well with increasing development team size. -8. **Mobile Experience**: The responsive web approach doesn't deliver optimal mobile experiences. - -As we transition to a microservices backend architecture, we need a complementary frontend strategy that addresses these challenges while supporting our business goals of improved customer experience, faster time-to-market, and technical agility. - -### Decision - -We will adopt a **modern, component-based frontend architecture** with the following key characteristics: - -1. **Micro-Frontend Approach**: - - Decompose the frontend into domain-aligned micro-frontends - - Enable independent development and deployment of frontend components - - Provide clear ownership boundaries aligned with backend microservices - -2. **Core Technology Stack**: - - **React**: Primary UI library for component development - - **Next.js**: Framework for server-rendering and static generation - - **TypeScript**: For type safety and improved developer experience - - **Styled Components**: For component-scoped styling - - **React Query**: For data fetching and state management - - **Cypress & React Testing Library**: For testing - -3. **Design System**: - - Create a comprehensive design system with reusable UI components - - Implement a living style guide and component documentation - - Establish design tokens for consistent theming - - Support multiple brands and white-labeling capabilities - -4. **Architecture Patterns**: - - **Module Federation**: For sharing components between micro-frontends - - **Composition Layer**: Shell application for integrating micro-frontends - - **BFF Pattern**: Backend-for-Frontend APIs for optimized data access - - **State Management**: Local state when possible, shared state when necessary - - **Feature Flags**: For controlled feature rollout and A/B testing - -5. **Performance Optimization**: - - Server-side rendering for initial page load performance - - Client-side rendering for rich interactive experiences - - Code splitting and lazy loading for optimized bundle sizes - - Aggressive caching strategies for static assets - - Optimized media delivery with responsive images and lazy loading - -6. **Mobile Strategy**: - - Progressive Web App (PWA) capabilities for mobile web - - Responsive design with mobile-first approach - - Native app shell with React Native for iOS/Android applications - - Shared business logic between web and native through abstraction layers - -### Frontend Domain Decomposition - -| Domain | Micro-Frontend | Primary Responsibilities | Team | -|--------|---------------|--------------------------|------| -| Product Discovery | product-browser | Product listing, search, filtering, recommendations | Catalog Team | -| Product Details | product-details | Product information, options, reviews, related items | Catalog Team | -| Shopping Cart | cart-experience | Cart management, saved items, quick checkout | Checkout Team | -| Checkout | checkout-flow | Multi-step checkout, address management, payment | Checkout Team | -| User Account | account-portal | Profile management, preferences, order history | Customer Team | -| Content | content-pages | CMS-managed content, landing pages, promotional content | Marketing Team | -| Store Locator | store-finder | Store search, maps integration, store details | Location Team | -| Order Management | order-tracker | Order status, tracking, returns management | Order Team | - -### Consequences - -#### Positive - -1. **Improved Development Velocity**: Teams can work independently on their domains without blocking each other. - -2. **Better Performance**: Optimized loading strategies and modern frontend practices will improve user experience. - -3. **Enhanced Reusability**: Shared component library and design system enable consistent, reusable UI elements. - -4. **Independent Deployments**: Micro-frontends can be deployed independently, reducing release coordination. - -5. **Technology Flexibility**: Different domains can adopt new technologies at their own pace. - -6. **Better Testing**: Smaller, more focused codebases are easier to test thoroughly. - -7. **Improved User Experience**: Consistent design language and optimized interactions improve customer satisfaction. - -8. **Team Autonomy**: Clear ownership boundaries enable teams to take full responsibility for their domains. - -#### Negative - -1. **Increased Complexity**: Micro-frontend architectures add operational and integration complexity. - -2. **Learning Curve**: Teams need to adapt to new patterns and technologies. - -3. **Potential Duplication**: Without careful governance, similar solutions may be implemented multiple times. - -4. **Integration Challenges**: Ensuring consistent behavior across micro-frontends requires careful coordination. - -5. **Performance Overhead**: Micro-frontend composition can introduce additional runtime overhead if not carefully managed. - -6. **Increased Infrastructure Needs**: More sophisticated build, deployment, and monitoring infrastructure required. - -7. **Governance Challenges**: Balancing team autonomy with architectural consistency requires active governance. - -### Mitigation Strategies - -1. **Frontend Platform Team**: - - Create a dedicated platform team to provide shared infrastructure and tools - - Develop reusable patterns and documentation for micro-frontend implementation - - Provide developer tooling and simplified local development experience - -2. **Comprehensive Design System**: - - Invest in a robust design system with clear guidelines and components - - Create automated tools for design compliance checking - - Regular design system sessions to ensure alignment across teams - -3. **Performance Budgeting**: - - Establish clear performance metrics and budgets for each micro-frontend - - Automated performance testing in CI/CD pipeline - - Regular performance reviews and optimization workshops - -4. **Developer Experience**: - - Create standardized templates and generators for new micro-frontends - - Provide comprehensive documentation and internal training - - Establish frontend community of practice for knowledge sharing - -5. **Governance Model**: - - Create a frontend architecture council with representatives from each team - - Regular architecture reviews and pattern sharing - - Clear guidelines for when to share vs. create new components - -### Implementation Details - -#### Phase 1: Foundation (Q4 2024) - -1. Create core design system and component library -2. Establish micro-frontend shell architecture -3. Develop initial build and deployment pipeline -4. Implement authentication and session management -5. Create developer documentation and examples - -#### Phase 2: Domain Migration (Q1-Q2 2025) - -1. Migrate high-priority domains to micro-frontend architecture -2. Implement analytics and monitoring strategy -3. Develop advanced patterns for cross-domain interaction -4. Enhance performance optimization capabilities -5. Create specialized mobile experiences - -#### Phase 3: Advanced Capabilities (Q3-Q4 2025) - -1. Implement personalization framework -2. Develop advanced A/B testing capabilities -3. Enhance internationalization and localization -4. Create specialized native experiences -5. Implement advanced analytics and behavior tracking - -### Considered Alternatives - -#### 1. Monolithic Single-Page Application (SPA) - -**Pros**: Simpler architecture, unified codebase, shared state management -**Cons**: Development bottlenecks, scaling challenges, larger bundle sizes - -This approach would be simpler initially but would recreate many of our current scaling challenges. - -#### 2. Server-Side Rendering Only - -**Pros**: Simpler technology stack, better SEO by default, reduced client-side JavaScript -**Cons**: Limited interactivity, slower subsequent navigation, poorer offline capabilities - -While this would improve initial load performance, it would limit our ability to create rich interactive experiences. - -#### 3. Native Mobile Apps Only for Mobile - -**Pros**: Optimal mobile experience, full native capabilities, offline functionality -**Cons**: Development cost, platform duplication, release friction - -This would deliver better mobile experiences but at significantly higher development and maintenance costs. - -#### 4. Framework-Agnostic Approach - -**Pros**: Maximum team autonomy, best-tool-for-job flexibility -**Cons**: Duplication of efforts, inconsistent experiences, integration challenges - -While offering maximum flexibility, this would lead to significant inconsistency and integration challenges. - -### References - -1. [Micro Frontends](https://martinfowler.com/articles/micro-frontends.html) (Martin Fowler) -2. [Module Federation](https://webpack.js.org/concepts/module-federation/) (Webpack Documentation) -3. ["Building Micro-Frontends" by Luca Mezzalira](https://www.oreilly.com/library/view/building-micro-frontends/9781492082989/) -4. [Frontend Design Systems](https://designsystemsrepo.com/design-systems) -5. [Atomic Design](https://atomicdesign.bradfrost.com/) by Brad Frost -6. [React Documentation](https://reactjs.org/docs/getting-started.html) -7. [Next.js Documentation](https://nextjs.org/docs) - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2024-08-10 | 0.1 | Initial draft | Jennifer Lee | -| 2024-08-20 | 0.2 | Added implementation phases and domain decomposition | Alex Johnson | -| 2024-08-30 | 0.3 | Incorporated feedback from UX and frontend teams | Sarah Chen | -| 2024-09-05 | 1.0 | Approved by Architecture and UX Boards | Architecture Board | - -## Appendix A: Frontend Architecture Overview - -```mermaid -flowchart TB - subgraph "Client Applications" - web[Web Browser] - ios[iOS App] - android[Android App] - end - - subgraph "Micro-Frontend Shell" - shell[Shell Application] - router[Routing Layer] - auth[Auth Module] - end - - subgraph "Micro-Frontends" - product[Product Experience] - cart[Cart & Checkout] - account[User Account] - content[Content Pages] - order[Order Management] - end - - subgraph "Shared Foundation" - design[Design System] - core[Core Components] - utils[Utility Functions] - hooks[React Hooks] - end - - subgraph "Backend Integration" - bff[BFF Layer] - api[API Gateway] - end - - web --> shell - ios --> shell - android --> shell - - shell --> router - shell --> auth - - router --> product - router --> cart - router --> account - router --> content - router --> order - - product --> design - cart --> design - account --> design - content --> design - order --> design - - product --> core - cart --> core - account --> core - content --> core - order --> core - - product --> bff - cart --> bff - account --> bff - content --> bff - order --> bff - - bff --> api -``` - -## Appendix B: Component Development Workflow - -```mermaid -sequenceDiagram - participant Designer - participant Dev as Developer - participant PR as Pull Request - participant CI as CI/CD Pipeline - participant DS as Design System - participant App as Application - - Designer->>Designer: Create Component Design - Designer->>Dev: Handoff Design Specs - - Dev->>Dev: Develop Component - Dev->>Dev: Write Component Tests - Dev->>Dev: Document Component - - Dev->>PR: Submit Pull Request - PR->>CI: Trigger CI Pipeline - - CI->>CI: Lint Check - CI->>CI: Type Check - CI->>CI: Unit Tests - CI->>CI: Visual Regression Tests - CI->>CI: Bundle Size Analysis - CI->>CI: Accessibility Tests - - CI->>PR: Report Results - PR->>DS: Merge to Design System - - DS->>App: Component Available for Use - App->>App: Integrate Component -``` - -## Appendix C: Micro-Frontend Integration Patterns - -```mermaid -flowchart TB - subgraph "Integration Approaches" - direction TB - - subgraph "Build-Time Integration" - npm[NPM Dependencies] - mono[Monorepo Packages] - end - - subgraph "Run-Time Integration" - fed[Module Federation] - iframes[IFrames] - web[Web Components] - end - - subgraph "Edge-Side Integration" - esi[Edge Side Includes] - ssi[Server Side Includes] - end - end - - subgraph "Implementation Patterns" - direction TB - - subgraph "Horizontal Split" - domains[By Domain/Business Function] - end - - subgraph "Vertical Split" - pages[Page-Based Composition] - end - - subgraph "Hybrid Approach" - shell[Shell + Domain Modules] - end - end - - fed --> shell - domains --> fed - pages --> iframes - shell --> web -``` \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/06-observability-strategy.mdx b/examples/default/docs/technical-architecture-design/architecture-decision-records/published/06-observability-strategy.mdx deleted file mode 100644 index 419ebdeea..000000000 --- a/examples/default/docs/technical-architecture-design/architecture-decision-records/published/06-observability-strategy.mdx +++ /dev/null @@ -1,439 +0,0 @@ ---- -title: Observability Strategy -summary: Architectural decision record for implementing observability in the FlowMart e-commerce platform -sidebar: - label: Observability Strategy - order: 6 ---- - -## ADR-006: Observability Strategy for FlowMart E-commerce Platform - -### Status - -Approved (2024-09-15) - -### Context - -As we transition from a monolithic architecture to a distributed microservices-based e-commerce platform, traditional monitoring approaches are no longer sufficient. The increased complexity of our architecture introduces several challenges: - -1. **Distributed Systems Complexity**: With dozens of microservices communicating asynchronously, understanding system behavior becomes significantly more difficult. - -2. **Increased Failure Points**: A distributed architecture introduces more potential failure points and complex failure modes. - -3. **Service Interdependencies**: Issues in one service can cascade to others, making root cause analysis challenging. - -4. **Multiple Technologies**: Different services use different languages, frameworks, and datastores, requiring diverse instrumentation approaches. - -5. **Deployment Frequency**: With continuous deployment across multiple services, correlating issues with specific changes becomes more complex. - -6. **Performance Bottlenecks**: Identifying performance bottlenecks in a distributed system requires end-to-end visibility. - -7. **Cross-Team Collaboration**: Multiple teams own different services, requiring a common observability approach and shared understanding. - -8. **Business Impact Correlation**: Need to connect technical metrics with business outcomes to prioritize improvements. - -Our current monitoring strategy is primarily focused on infrastructure metrics and basic application health checks, which is insufficient for effectively operating our new architecture. - -### Decision - -We will implement a comprehensive **observability strategy** based on the "three pillars" approach (metrics, logs, and traces) with distributed tracing as a foundational element. Key components of this strategy include: - -1. **Observability Stack**: - - **Metrics**: Prometheus for metrics collection and alerting - - **Logs**: Elasticsearch, Logstash, and Kibana (ELK) for log aggregation and analysis - - **Traces**: Jaeger for distributed tracing - - **Dashboard**: Grafana for unified visualization and dashboarding - - **Alerting**: Prometheus Alertmanager with PagerDuty integration - -2. **Instrumentation Standards**: - - **Distributed Tracing**: OpenTelemetry as the standard instrumentation framework - - **Structured Logging**: JSON-formatted logs with standardized fields across all services - - **Metrics Naming**: Consistent metrics naming convention following Prometheus best practices - - **Service Level Objectives (SLOs)**: Defined for all critical user journeys - - **Error Budgets**: Established for each service and user journey - -3. **Core Observability Capabilities**: - - **Request Tracing**: End-to-end tracing for all user-initiated actions - - **Dependency Monitoring**: Monitoring of all external dependencies and services - - **Business Metrics**: Tracking of key business metrics alongside technical metrics - - **Synthetic Monitoring**: Regular testing of critical user journeys - - **Real User Monitoring (RUM)**: Frontend performance and error tracking - - **Anomaly Detection**: Automated detection of abnormal system behavior - - **Correlation Engine**: Tools to correlate metrics, logs, and traces during investigation - -4. **Data Retention and Sampling**: - - Critical business transaction traces retained for 30 days - - High-cardinality metrics sampled at appropriate rates - - Error logs retained for 90 days - - Regular logs retained for 30 days - - Aggregated metrics retained for 13 months for year-over-year analysis - -5. **Implementation Approach**: - - Platform team creates and maintains observability infrastructure - - Standardized libraries and SDKs for each supported language/framework - - Observability as code, with instrumentation verified in CI/CD pipelines - - Service templates with pre-configured observability components - - Progressive enhancement of observability capabilities - -### Observability Requirements by Domain - -| Domain | Key Metrics | Special Requirements | SLO Targets | -|--------|------------|----------------------|-------------| -| Product Catalog | Search latency, Cache hit rate | High cardinality data handling | 99.9% availability, p95 < 300ms | -| Order Processing | Order volume, Processing time, Error rate | Comprehensive transaction tracing | 99.95% availability, p95 < 500ms | -| Payment | Transaction volume, Success rate, Fraud detection rate | PCI compliance in logging | 99.99% availability, p95 < 800ms | -| Inventory | Stock level changes, Reservation rate, Stockout events | Event-sourcing visibility | 99.9% availability, p95 < 400ms | -| User Authentication | Login volume, Success rate, MFA usage | Security-focused monitoring | 99.99% availability, p95 < 250ms | -| Checkout | Cart conversion rate, Abandonment points, Session duration | User journey analysis | 99.95% availability, p95 < 600ms | -| Shipping | Fulfillment time, Carrier performance, Tracking accuracy | Third-party integration monitoring | 99.9% availability, p95 < 350ms | -| Content Delivery | Cache hit ratio, Origin fetch time, Asset size | CDN performance visibility | 99.9% availability, p95 < 200ms | - -### Consequences - -#### Positive - -1. **Improved Troubleshooting**: Faster identification and resolution of issues through correlated observability data. - -2. **Proactive Detection**: Ability to detect potential issues before they impact users through anomaly detection and trend analysis. - -3. **Enhanced Understanding**: Better understanding of system behavior, dependencies, and performance characteristics. - -4. **Data-Driven Optimization**: Ability to make targeted performance improvements based on actual usage patterns. - -5. **Cross-Team Collaboration**: Common observability platform enables better collaboration during incident response. - -6. **Business Alignment**: Correlation between technical metrics and business outcomes helps prioritize technical work. - -7. **Resilience Verification**: Ability to verify that resilience mechanisms (circuit breakers, retries, etc.) function properly. - -8. **Capacity Planning**: Better data for capacity planning and scaling decisions. - -#### Negative - -1. **Implementation Overhead**: Adding comprehensive instrumentation requires additional development effort. - -2. **Data Volume Challenges**: Managing the volume of observability data requires careful planning and potential sampling. - -3. **Performance Impact**: Instrumentation adds some overhead to application performance, which must be managed. - -4. **Complexity**: A sophisticated observability stack adds operational complexity and maintenance requirements. - -5. **Learning Curve**: Teams need to learn new tools, concepts, and practices for effective use of observability data. - -6. **Cost Considerations**: Storage and processing of observability data has significant cost implications at scale. - -7. **Privacy and Security**: Observability data may contain sensitive information requiring appropriate controls. - -### Mitigation Strategies - -1. **Automated Instrumentation**: - - Use auto-instrumentation agents where possible - - Create starter templates with instrumentation pre-configured - - Build instrumentation verification into CI/CD pipelines - -2. **Data Management**: - - Implement appropriate sampling strategies for high-volume data - - Utilize data compression and aggregation techniques - - Define appropriate retention policies based on data criticality - -3. **Operating Model**: - - Create an observability platform team to manage the core infrastructure - - Establish observability champions within each service team - - Regular observability review and enhancement sessions - -4. **Knowledge Sharing**: - - Comprehensive documentation and training on observability tools - - Regular workshops on effective use of observability data - - Shared dashboards and runbooks for common scenarios - -5. **Security and Privacy**: - - Automated PII detection and redaction in logs and traces - - Role-based access control for observability data - - Regular audits of observability data for sensitive information - -### Implementation Details - -#### Phase 1: Foundation (Q4 2024) - -1. Deploy core observability infrastructure (Prometheus, ELK, Jaeger, Grafana) -2. Implement standardized logging format and collection pipeline -3. Create initial service dashboards and alerting -4. Develop instrumentation libraries for primary service frameworks -5. Establish basic SLOs for critical services - -#### Phase 2: Enhanced Capabilities (Q1 2025) - -1. Implement distributed tracing across all critical user journeys -2. Create business metrics dashboards correlated with technical metrics -3. Develop anomaly detection for key system behaviors -4. Implement synthetic monitoring for critical paths -5. Create runbooks integrated with observability tools - -#### Phase 3: Advanced Observability (Q2-Q3 2025) - -1. Implement ML-based anomaly detection and prediction -2. Create self-service observability platform capabilities -3. Develop advanced correlation between metrics, logs, and traces -4. Implement automated performance testing with observability verification -5. Develop capacity planning and forecasting based on observability data - -### Considered Alternatives - -#### 1. Commercial APM Solution Only (e.g., Dynatrace, New Relic) - -**Pros**: Comprehensive out-of-the-box capabilities, reduced implementation effort, integrated platform -**Cons**: High cost at scale, reduced flexibility, potential vendor lock-in - -While commercial APM tools provide excellent capabilities, we chose an open-source approach for cost flexibility and customization ability. We will reevaluate this decision as our needs evolve. - -#### 2. Minimal Custom Instrumentation - -**Pros**: Reduced development overhead, simplicity, lower initial investment -**Cons**: Limited visibility, reactive troubleshooting, challenges scaling observability with system growth - -This approach would not provide the depth of insight needed for effective operation of our distributed system. - -#### 3. Service Mesh-Based Observability - -**Pros**: Reduced application instrumentation, consistent approach, network-level visibility -**Cons**: Limited application-level context, additional infrastructure complexity, potential performance impact - -While we will leverage service mesh observability capabilities, we need application-level instrumentation for complete visibility. - -#### 4. Multiple Independent Monitoring Systems - -**Pros**: Specialized tools for each domain, team autonomy in tooling decisions -**Cons**: Fragmented visibility, integration challenges, inconsistent practices - -This approach would create silos and make cross-service troubleshooting significantly more difficult. - -### References - -1. Charity Majors, Liz Fong-Jones, George Miranda, "Observability Engineering" (O'Reilly) -2. Cindy Sridharan, "Distributed Systems Observability" (O'Reilly) -3. [OpenTelemetry Documentation](https://opentelemetry.io/docs/) -4. [Google SRE Book - Monitoring Distributed Systems](https://sre.google/sre-book/monitoring-distributed-systems/) -5. [Prometheus Best Practices](https://prometheus.io/docs/practices/naming/) -6. [Grafana Observability Strategy](https://grafana.com/blog/2019/10/21/whats-next-for-observability/) - -### Decision Record History - -| Date | Version | Description | Author | -|------|---------|-------------|--------| -| 2024-08-20 | 0.1 | Initial draft | Kevin Zhang | -| 2024-09-01 | 0.2 | Added implementation phases and domain details | Rachel Williams | -| 2024-09-10 | 0.3 | Incorporated feedback from SRE and platform teams | David Boyne | -| 2024-09-15 | 1.0 | Approved by Architecture and Operations Boards | Architecture Board | - -## Appendix A: Observability Architecture - -```mermaid -flowchart TB - subgraph "Data Sources" - apps[Applications] - infra[Infrastructure] - db[Databases] - net[Network] - fe[Frontend] - end - - subgraph "Collection Layer" - prom[Prometheus] - fluent[FluentBit] - otel[OpenTelemetry Collector] - beats[Beats] - end - - subgraph "Storage Layer" - tsdb[Prometheus TSDB] - es[Elasticsearch] - jaeger_db[Jaeger Storage] - loki[Loki] - end - - subgraph "Processing Layer" - alerts[Alertmanager] - anom[Anomaly Detection] - corr[Correlation Engine] - agg[Log Aggregation] - end - - subgraph "Visualization Layer" - grafana[Grafana] - kibana[Kibana] - jaeger_ui[Jaeger UI] - dashboards[Custom Dashboards] - end - - subgraph "Notification Layer" - pd[PagerDuty] - slack[Slack] - email[Email] - webhook[Webhooks] - end - - apps --> otel - apps --> fluent - apps --> prom - infra --> beats - infra --> prom - db --> prom - db --> beats - net --> prom - net --> beats - fe --> otel - - otel --> jaeger_db - otel --> loki - otel --> tsdb - fluent --> es - beats --> es - prom --> tsdb - - tsdb --> alerts - tsdb --> anom - es --> agg - es --> corr - jaeger_db --> corr - loki --> corr - - tsdb --> grafana - es --> kibana - es --> grafana - jaeger_db --> jaeger_ui - jaeger_db --> grafana - corr --> dashboards - agg --> dashboards - - alerts --> pd - alerts --> slack - alerts --> email - alerts --> webhook -``` - -## Appendix B: Observability Data Flow - -```mermaid -sequenceDiagram - participant User - participant Service1 as Service A - participant Service2 as Service B - participant Service3 as Service C - participant OTel as OpenTelemetry - participant Logs as Log System - participant Metrics as Metrics System - participant Traces as Tracing System - participant Dashboard as Dashboard - participant Alerts as Alert System - - User->>Service1: Request - - Service1->>OTel: Generate Span 1 - Service1->>Logs: Log Request Details - Service1->>Metrics: Update Request Counter - - Service1->>Service2: Internal Request - Service2->>OTel: Generate Span 2 (child of Span 1) - Service2->>Logs: Log Processing Details - Service2->>Metrics: Update Processing Metrics - - Service2->>Service3: Database Query - Service3->>OTel: Generate Span 3 (child of Span 2) - Service3->>Logs: Log Query Details - Service3->>Metrics: Update Query Metrics - - Service3-->>Service2: Query Result - Service2-->>Service1: Internal Response - Service1-->>User: Response - - Service1->>OTel: Complete Span 1 - Service1->>Metrics: Update Response Time - - OTel->>Traces: Store Complete Trace - - Logs->>Dashboard: Visualize Logs - Metrics->>Dashboard: Visualize Metrics - Traces->>Dashboard: Visualize Traces - - Metrics->>Alerts: Trigger Alerts (if threshold exceeded) - Alerts->>Dashboard: Display Alert Status -``` - -## Appendix C: Service Level Objectives (SLOs) Framework - -```mermaid -flowchart TB - subgraph "SLO Definition Process" - direction TB - - subgraph "1. Identify Critical User Journeys" - journey1[Product Search] - journey2[Add to Cart] - journey3[Checkout] - journey4[Order Status] - end - - subgraph "2. Define Service Level Indicators (SLIs)" - availability[Availability %] - latency[Latency (p50, p95, p99)] - errors[Error Rate %] - saturation[Resource Saturation] - end - - subgraph "3. Set Target Objectives" - slo1[99.9% Availability] - slo2[p95 < 300ms] - slo3[Error Rate < 0.1%] - end - - subgraph "4. Establish Error Budgets" - monthly[Monthly Budget] - quarterly[Quarterly Budget] - policy[Error Budget Policy] - end - end - - subgraph "SLO Monitoring & Reporting" - direction TB - - subgraph "Real-time Dashboards" - current[Current Status] - trends[Burn Rate] - history[Historical Performance] - end - - subgraph "Alerting Strategy" - warning[Budget Warning (50%)] - critical[Budget Critical (75%)] - depleted[Budget Depleted (90%)] - end - - subgraph "Continuous Improvement" - retro[SLO Reviews] - adjust[Target Adjustments] - prioritize[Reliability Work] - end - end - - journey1 --> availability - journey1 --> latency - journey2 --> availability - journey2 --> latency - journey3 --> errors - journey3 --> availability - journey4 --> latency - - availability --> slo1 - latency --> slo2 - errors --> slo3 - - slo1 --> monthly - slo2 --> monthly - slo3 --> monthly - - monthly --> current - monthly --> warning - - current --> retro - warning --> prioritize -``` \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/infrastructure-as-code/01-iac-overview.mdx b/examples/default/docs/technical-architecture-design/infrastructure-as-code/01-iac-overview.mdx deleted file mode 100644 index ed1f79350..000000000 --- a/examples/default/docs/technical-architecture-design/infrastructure-as-code/01-iac-overview.mdx +++ /dev/null @@ -1,175 +0,0 @@ ---- -title: Infrastructure as Code (IaC) Overview -summary: An overview of the infrastructure-as-code approach used in the FlowMart e-commerce platform -sidebar: - label: 01 - IaC Overview - order: 1 ---- - -# Infrastructure as Code (IaC) Overview - -This document provides an overview of the Infrastructure as Code (IaC) approach used to manage and provision the infrastructure for the FlowMart e-commerce platform. - -## What is Infrastructure as Code? - -Infrastructure as Code (IaC) is an approach to infrastructure management where infrastructure resources are defined and provisioned through machine-readable definition files, rather than through manual processes or interactive configuration tools. This approach allows us to: - -- **Version control** our infrastructure definitions alongside our application code -- **Automate** the provisioning and management of infrastructure -- **Standardize** configurations across different environments -- **Document** our infrastructure setup as living code rather than static documentation -- **Test** infrastructure changes before deploying to production - -## Our IaC Tech Stack - -For the FlowMart e-commerce platform, we use the following technologies for our infrastructure management: - -### Primary Tools - -| Tool | Purpose | -|------|---------| -| **Terraform** | Infrastructure provisioning across cloud providers (primary tool) | -| **Kubernetes (K8s)** | Container orchestration | -| **Helm Charts** | Kubernetes application deployment packaging | -| **GitHub Actions** | CI/CD pipeline automation | -| **AWS CloudFormation** | Specific AWS infrastructure components | - -### Additional Supporting Tools - -| Tool | Purpose | -|------|---------| -| **Terragrunt** | Terraform code organization and management | -| **Packer** | Virtual machine image building | -| **Ansible** | Configuration management | -| **Prometheus & Grafana** | Monitoring and alerting | -| **ELK Stack** | Logging | - -## Infrastructure Architecture - -Our infrastructure is organized into the following logical components: - -```mermaid -flowchart TD - classDef networkInfra fill:#e1d5e7,stroke:#9673a6,stroke-width:2px - classDef computeInfra fill:#d5e8d4,stroke:#82b366,stroke-width:2px - classDef datastoreInfra fill:#dae8fc,stroke:#6c8ebf,stroke-width:2px - classDef securityInfra fill:#f8cecc,stroke:#b85450,stroke-width:2px - classDef monitoringInfra fill:#fff2cc,stroke:#d6b656,stroke-width:2px - - subgraph "Networking Infrastructure" - VPC[VPC & Subnets] - IGW[Internet Gateway] - NLB[Network Load Balancer] - Route53[DNS - Route53] - end - - subgraph "Compute Infrastructure" - EKS[EKS Cluster] - EC2[EC2 Instances] - Lambda[Lambda Functions] - end - - subgraph "Data Storage" - RDS[RDS Databases] - DynamoDB[DynamoDB Tables] - S3[S3 Buckets] - ElastiCache[ElastiCache Redis] - end - - subgraph "Security" - IAM[IAM Roles & Policies] - SG[Security Groups] - WAF[WAF & Shield] - Secrets[Secrets Manager] - end - - subgraph "Monitoring & Logging" - CloudWatch[CloudWatch] - Prometheus[Prometheus] - Grafana[Grafana Dashboards] - ELK[ELK Stack] - end - - %% Connections - VPC --> IGW - IGW --> NLB - NLB --> EKS - Route53 --> NLB - - EKS --> SG - EC2 --> SG - Lambda --> SG - - EKS --> RDS - EKS --> DynamoDB - EKS --> S3 - EKS --> ElastiCache - - IAM --> EKS - IAM --> Lambda - IAM --> RDS - - CloudWatch --> EKS - CloudWatch --> RDS - CloudWatch --> Lambda - Prometheus --> EKS - Grafana --> Prometheus - ELK --> EKS - - %% Apply styles - class VPC,IGW,NLB,Route53 networkInfra - class EKS,EC2,Lambda computeInfra - class RDS,DynamoDB,S3,ElastiCache datastoreInfra - class IAM,SG,WAF,Secrets securityInfra - class CloudWatch,Prometheus,Grafana,ELK monitoringInfra -``` - -## Repository Structure - -Our infrastructure code is organized as follows: - -``` -infrastructure/ -│ -├── terraform/ # Terraform configuration -│ ├── environments/ # Environment-specific configurations -│ │ ├── dev/ -│ │ ├── staging/ -│ │ └── production/ -│ ├── modules/ # Reusable Terraform modules -│ │ ├── networking/ -│ │ ├── compute/ -│ │ ├── database/ -│ │ └── monitoring/ -│ └── global/ # Global resources (e.g., Route53) -│ -├── kubernetes/ # Kubernetes manifests -│ ├── base/ # Base configurations -│ └── overlays/ # Environment-specific overlays (Kustomize) -│ -├── helm-charts/ # Helm charts for application deployment -│ -├── scripts/ # Utility scripts -│ -└── packer/ # Packer templates for image building -``` - -## Deployment Principles - -1. **Infrastructure Changes via Pull Requests**: All infrastructure changes must go through a pull request process, with automated testing and reviews. - -2. **Environment Promotion**: Changes are first deployed to development, then staging, and finally production, with appropriate testing at each stage. - -3. **Immutable Infrastructure**: We prefer to replace rather than modify infrastructure components. - -4. **Least Privilege**: We follow the principle of least privilege for all IAM roles and security groups. - -5. **Automated Rollbacks**: Our CI/CD pipelines include automated rollback capabilities if deployments fail. - -## Next Steps - -For more detailed information about our infrastructure as code setup, please refer to the following documents: - -- [Terraform Implementation](./02-terraform-implementation.mdx) -- [Environment Setups](./03-environment-setups.mdx) -- [CI/CD Pipelines](./04-cicd-pipelines.mdx) \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/infrastructure-as-code/02-terraform-implementation.mdx b/examples/default/docs/technical-architecture-design/infrastructure-as-code/02-terraform-implementation.mdx deleted file mode 100644 index e20441bba..000000000 --- a/examples/default/docs/technical-architecture-design/infrastructure-as-code/02-terraform-implementation.mdx +++ /dev/null @@ -1,332 +0,0 @@ ---- -title: Terraform Implementation -summary: Details of how Terraform is used to provision and manage the FlowMart infrastructure -sidebar: - label: 02 - Terraform Implementation - order: 2 ---- - -# Terraform Implementation - -This document describes how we use Terraform to manage and provision the infrastructure for the FlowMart e-commerce platform. - -## Why Terraform? - -We chose Terraform as our primary IaC tool for the following reasons: - -- **Cloud-agnostic**: While we primarily use AWS, Terraform gives us the flexibility to work with multiple cloud providers if needed -- **Declarative syntax**: Define what you want, not how to get there -- **State management**: Tracks the current state of infrastructure to plan changes -- **Modular approach**: Supports reusable components through modules -- **Strong community support**: Wide adoption means better documentation and resources -- **Extensible**: Can be extended through providers and modules - -## Terraform Structure - -Our Terraform code follows a structured approach: - -### Directory Structure - -``` -terraform/ -│ -├── environments/ # Environment-specific configurations -│ ├── dev/ -│ │ ├── main.tf # Main configuration file -│ │ ├── variables.tf # Input variables -│ │ ├── outputs.tf # Output variables -│ │ └── terraform.tfvars # Variable values -│ ├── staging/ -│ │ └── ... -│ └── production/ -│ └── ... -│ -├── modules/ # Reusable Terraform modules -│ ├── networking/ # VPC, subnets, routing -│ │ ├── main.tf -│ │ ├── variables.tf -│ │ └── outputs.tf -│ ├── eks/ # EKS cluster configuration -│ │ └── ... -│ ├── rds/ # Database configurations -│ │ └── ... -│ ├── lambda/ # Serverless functions -│ │ └── ... -│ └── monitoring/ # Monitoring resources -│ └── ... -│ -└── global/ # Global resources - ├── iam/ # IAM roles and policies - │ └── ... - └── route53/ # DNS configurations - └── ... -``` - -### Module Design - -Each module follows a consistent pattern: - -- **Inputs**: Defined in `variables.tf` -- **Resources**: Defined in `main.tf` -- **Outputs**: Defined in `outputs.tf` - -## Core Infrastructure Components - -Here's an overview of our main Terraform modules and what they provision: - -### Networking Module - -The networking module provisions our VPC infrastructure: - -```hcl -module "vpc" { - source = "../../modules/networking" - - name = "flowmart-${var.environment}" - cidr = "10.0.0.0/16" - azs = ["us-west-2a", "us-west-2b", "us-west-2c"] - private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] - public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] - database_subnets = ["10.0.201.0/24", "10.0.202.0/24", "10.0.203.0/24"] - - enable_nat_gateway = true - single_nat_gateway = var.environment != "production" - - tags = { - Environment = var.environment - Project = "FlowMart" - ManagedBy = "Terraform" - } -} -``` - -### EKS Module - -The EKS module provisions our Kubernetes cluster: - -```hcl -module "eks" { - source = "../../modules/eks" - - cluster_name = "flowmart-${var.environment}" - cluster_version = "1.23" - - vpc_id = module.vpc.vpc_id - subnet_ids = module.vpc.private_subnets - - node_groups = { - application = { - desired_capacity = 3 - max_capacity = 10 - min_capacity = 2 - instance_types = ["t3.large"] - disk_size = 50 - } - - system = { - desired_capacity = 2 - max_capacity = 4 - min_capacity = 2 - instance_types = ["t3.medium"] - disk_size = 20 - } - } - - tags = { - Environment = var.environment - Project = "FlowMart" - ManagedBy = "Terraform" - } -} -``` - -### Database Module - -The database module provisions our RDS instances: - -```hcl -module "database" { - source = "../../modules/rds" - - identifier = "flowmart-${var.environment}" - engine = "postgres" - engine_version = "13.4" - instance_class = var.environment == "production" ? "db.r5.large" : "db.t3.medium" - allocated_storage = 100 - - name = "flowmart" - username = "flowmart_admin" - password = var.db_password - - vpc_security_group_ids = [module.security_groups.database_sg_id] - subnet_ids = module.vpc.database_subnets - - backup_retention_period = var.environment == "production" ? 30 : 7 - - tags = { - Environment = var.environment - Project = "FlowMart" - ManagedBy = "Terraform" - } -} -``` - -### DynamoDB Module - -For NoSQL database needs, we use DynamoDB: - -```hcl -module "dynamodb" { - source = "../../modules/dynamodb" - - tables = { - inventory = { - name = "inventory-${var.environment}" - billing_mode = "PROVISIONED" - read_capacity = var.environment == "production" ? 20 : 5 - write_capacity = var.environment == "production" ? 20 : 5 - hash_key = "productId" - attributes = [ - { - name = "productId" - type = "S" - } - ] - }, - - shopping_cart = { - name = "shopping-cart-${var.environment}" - billing_mode = "PROVISIONED" - read_capacity = var.environment == "production" ? 20 : 5 - write_capacity = var.environment == "production" ? 20 : 5 - hash_key = "sessionId" - attributes = [ - { - name = "sessionId" - type = "S" - } - ] - } - } - - tags = { - Environment = var.environment - Project = "FlowMart" - ManagedBy = "Terraform" - } -} -``` - -## Terraform Workflows - -We follow these workflows when making infrastructure changes: - -### Development Workflow - -```mermaid -flowchart LR - classDef highlightStep fill:#d5e8d4,stroke:#82b366,stroke-width:2px - - Init[terraform init] --> Plan[terraform plan] - Plan --> Review[Review plan] - Review --> Apply[terraform apply] - Apply --> Validate[Validate changes] - - class Plan,Review highlightStep -``` - -### Terraform in CI/CD Pipeline - -```mermaid -flowchart TD - Code[Code changes pushed] --> PR[Pull request created] - PR --> Init[terraform init] - Init --> Plan[terraform plan] - Plan --> AutoReview[Automated plan review] - AutoReview --> HumanReview[Human review] - HumanReview --> Approved{Approved?} - Approved -->|Yes| Merge[Merge PR] - Approved -->|No| Revise[Revise changes] - Revise --> PR - Merge --> CI[CI/CD pipeline] - CI --> Apply[terraform apply] - Apply --> PostCheck[Post-apply validation] -``` - -## Terraform State Management - -We manage Terraform state using a remote backend with the following characteristics: - -- **S3 Bucket**: For state storage -- **DynamoDB Table**: For state locking -- **IAM Roles**: For secure access to state files -- **State Encryption**: For security of sensitive data - -Example remote backend configuration: - -```hcl -terraform { - backend "s3" { - bucket = "flowmart-terraform-state" - key = "environments/${var.environment}/terraform.tfstate" - region = "us-west-2" - dynamodb_table = "terraform-lock" - encrypt = true - role_arn = "arn:aws:iam::ACCOUNT_ID:role/TerraformStateManager" - } -} -``` - -## Terraform Best Practices - -We follow these best practices for our Terraform codebase: - -1. **Version Pinning**: Lock provider and module versions to ensure reproducibility - -```hcl -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 4.16.0" - } - } - required_version = ">= 1.2.0" -} -``` - -2. **Resource Tagging**: All resources are tagged for billing and management purposes - -```hcl -tags = { - Environment = var.environment - Project = "FlowMart" - ManagedBy = "Terraform" - Service = "Orders" -} -``` - -3. **Variable Validation**: Validate inputs to prevent errors - -```hcl -variable "environment" { - description = "The deployment environment (e.g., dev, staging, production)" - type = string - - validation { - condition = contains(["dev", "staging", "production"], var.environment) - error_message = "Environment must be one of: dev, staging, production." - } -} -``` - -4. **Modular Design**: Use modules for reusable components - -5. **Minimal Permissions**: Follow the principle of least privilege for IAM roles - -## Next Steps - -For more information about our IaC implementation, please refer to: - -- [Environment Setups](./03-environment-setups.mdx) -- [CI/CD Pipelines](./04-cicd-pipelines.mdx) \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/infrastructure-as-code/03-environment-setups.mdx b/examples/default/docs/technical-architecture-design/infrastructure-as-code/03-environment-setups.mdx deleted file mode 100644 index 9eb0bf558..000000000 --- a/examples/default/docs/technical-architecture-design/infrastructure-as-code/03-environment-setups.mdx +++ /dev/null @@ -1,264 +0,0 @@ ---- -title: Environment Setups -summary: Detailed information about the different environments for the FlowMart e-commerce platform -sidebar: - label: 03 - Environment Setups - order: 3 ---- - -# Environment Setups - -This document describes the different environments used in the FlowMart e-commerce platform, their purposes, configurations, and the promotion process between them. - -## Environment Strategy - -We follow a multi-environment strategy with clear separation and purposes: - -```mermaid -flowchart TD - classDef devEnv fill:#dae8fc,stroke:#6c8ebf,stroke-width:2px - classDef stagingEnv fill:#d5e8d4,stroke:#82b366,stroke-width:2px - classDef prodEnv fill:#f8cecc,stroke:#b85450,stroke-width:2px - classDef dataEnv fill:#fff2cc,stroke:#d6b656,stroke-width:2px - - Dev[Development Environment] --> Staging[Staging Environment] - Staging --> Production[Production Environment] - - Dev --- Sandbox[Sandbox Environments] - Production --- DR[Disaster Recovery] - - class Dev,Sandbox devEnv - class Staging stagingEnv - class Production,DR prodEnv - class Data dataEnv -``` - -## Environment Descriptions - -### Development Environment - -The development environment is used by developers to test changes and new features. - -- **Purpose**: Development, testing, and integration -- **Access**: Development team -- **Data**: Subset of anonymized production data or synthetic data -- **Infrastructure Scale**: Minimal, cost-optimized -- **Deployment Frequency**: Multiple times per day -- **Automated Testing**: Unit tests, API tests - -**Key Characteristics**: -- Shared development environment -- Reduced redundancy (e.g., single NAT gateway, smaller instances) -- Feature flags enabled for work-in-progress features -- Debug and verbose logging enabled -- Daily database refresh from anonymized production data - -### Staging Environment - -The staging environment is a pre-production environment that closely mirrors production. - -- **Purpose**: System testing, performance testing, UAT -- **Access**: Development team, QA, selected stakeholders -- **Data**: Full anonymized copy of production data -- **Infrastructure Scale**: Nearly identical to production, but smaller scale -- **Deployment Frequency**: Once per release (multiple times per week) -- **Automated Testing**: Integration tests, performance tests, security scans - -**Key Characteristics**: -- Configuration as close to production as possible -- Full feature set enabled -- Production-level logging -- Staged rollout of new features -- Regular (weekly) database refresh from anonymized production data - -### Production Environment - -The production environment serves real customers and processes real transactions. - -- **Purpose**: Serving end-users -- **Access**: Limited access via break-glass procedures -- **Data**: Real customer data -- **Infrastructure Scale**: Full scale, highly available -- **Deployment Frequency**: Multiple times per week, during designated windows -- **Automated Testing**: Smoke tests, canary tests - -**Key Characteristics**: -- High availability across multiple availability zones -- Auto-scaling based on demand -- Enhanced security controls -- Full monitoring and alerting -- Regular backups -- Blue/green deployment strategy - -### Sandbox Environments - -Ephemeral environments for developers to test specific features or changes. - -- **Purpose**: Feature development, experimentation -- **Access**: Individual developers or teams -- **Data**: Synthetic data -- **Infrastructure Scale**: Minimal -- **Deployment Frequency**: On-demand -- **Lifetime**: Temporary (hours to days) - -### Disaster Recovery Environment - -A standby environment that can be activated in case of a major outage in the production environment. - -- **Purpose**: Business continuity -- **State**: Warm standby -- **Data**: Regular replication from production -- **Region**: Different from primary production region -- **Activation**: Automated with manual approval - -## Environment Configuration - -We manage environment-specific configurations through: - -1. **Terraform Variables**: Each environment has its own `terraform.tfvars` file -2. **Kubernetes ConfigMaps**: Environment-specific Kubernetes configurations -3. **Environment Variables**: Set at the pod or container level -4. **Feature Flags**: Application-level feature toggles - -Example Terraform Variable Differences: - -| Variable | Dev | Staging | Production | -|----------|-----|---------|------------| -| `vpc_cidr` | 10.0.0.0/16 | 10.1.0.0/16 | 10.2.0.0/16 | -| `eks_node_count` | 2 | 3 | 5-10 (auto-scaling) | -| `rds_instance_type` | db.t3.medium | db.r5.large | db.r5.2xlarge | -| `rds_multi_az` | false | true | true | -| `enable_waf` | false | true | true | - -## Environment Promotion Process - -We follow a structured promotion process for changes moving through environments: - -```mermaid -sequenceDiagram - participant Dev as Development - participant Staging as Staging - participant Prod as Production - - Note over Dev: Feature development complete - Dev->>Staging: Promote code - Note over Staging: Run integration tests - Note over Staging: Performance testing - Note over Staging: Security scanning - Note over Staging: UAT approval - Staging->>Prod: Promote code - Note over Prod: Canary deployment - Note over Prod: Monitoring - Note over Prod: Full rollout -``` - -### Promotion Guidelines - -1. **Development to Staging**: - - All unit tests pass - - Code review completed - - Feature implementation verified in development - - Feature documentation completed - -2. **Staging to Production**: - - All integration tests pass - - Performance tests meet SLAs - - Security scans show no critical or high vulnerabilities - - UAT completed and signed off - - Release notes prepared - -## Environment Variables Management - -We manage environment variables securely using: - -1. **AWS Parameter Store**: For non-secret configuration -2. **AWS Secrets Manager**: For sensitive values -3. **Kubernetes Secrets**: Mounted into containers at runtime - -Example parameter naming convention: -``` -/flowmart/{environment}/{service}/{parameter-name} -``` - -Example secret access in application code: -```javascript -const AWS = require('aws-sdk'); -const ssm = new AWS.SSM(); - -async function getDatabaseConfig() { - const params = { - Name: `/flowmart/${process.env.ENVIRONMENT}/orders-service/db-connection-string`, - WithDecryption: true - }; - - const result = await ssm.getParameter(params).promise(); - return result.Parameter.Value; -} -``` - -## Network Isolation - -Our environments are isolated from each other: - -```mermaid -flowchart TB - subgraph "AWS Account: Production" - ProdVPC[Production VPC] - end - - subgraph "AWS Account: Non-Production" - StagingVPC[Staging VPC] - DevVPC[Development VPC] - end - - Internet[Internet] --> PVPN[Production VPN] - Internet --> NPVPN[Non-Production VPN] - - PVPN --> ProdVPC - NPVPN --> StagingVPC - NPVPN --> DevVPC -``` - -Key security controls: -- Separate AWS accounts for production and non-production -- VPC isolation for each environment -- Separate VPN access for production and non-production -- Restricted traffic between environments -- Different IAM roles for each environment - -## Data Management Across Environments - -We handle data carefully across environments: - -1. **Production**: Real customer data with full security controls -2. **Staging**: Anonymized production data, refreshed weekly -3. **Development**: Subset of anonymized data or synthetic data -4. **Sandbox**: Synthetic data only - -Data anonymization process: -```mermaid -flowchart LR - ProdDB[(Production Database)] --> Extract[Extract Data] - Extract --> Anonymize[Anonymize Sensitive Data] - Anonymize --> Load[Load to Non-Prod Environments] - Load --> StageDB[(Staging Database)] - Load --> DevDB[(Development Database)] -``` - -## Monitoring and Observability - -Each environment has appropriate monitoring: - -| Monitoring Aspect | Development | Staging | Production | -|-------------------|-------------|---------|------------| -| Metrics Collection | Basic | Full | Full | -| Logs Retention | 7 days | 14 days | 90 days | -| Alerting | Critical only | High and Critical | All severities | -| Dashboards | Basic | Full | Full with extended | -| Tracing | Sampled (50%) | Sampled (75%) | Sampled (10%) | - -## Next Steps - -For more information about our environment management and deployment processes, please refer to: - -- [CI/CD Pipelines](./04-cicd-pipelines.mdx) \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/infrastructure-as-code/04-cicd-pipelines.mdx b/examples/default/docs/technical-architecture-design/infrastructure-as-code/04-cicd-pipelines.mdx deleted file mode 100644 index 5105ca406..000000000 --- a/examples/default/docs/technical-architecture-design/infrastructure-as-code/04-cicd-pipelines.mdx +++ /dev/null @@ -1,364 +0,0 @@ ---- -title: CI/CD Pipelines -summary: Detailed overview of the CI/CD pipelines used to deploy and maintain the FlowMart e-commerce platform -sidebar: - label: 04 - CI/CD Pipelines - order: 4 ---- - -# CI/CD Pipelines - -This document provides an overview of the Continuous Integration (CI) and Continuous Deployment (CD) pipelines used to build, test, and deploy the FlowMart e-commerce platform. - -## CI/CD Philosophy - -Our CI/CD approach follows these key principles: - -1. **Automation First**: Automate everything that can be automated -2. **Fast Feedback**: Provide developers with quick feedback on their changes -3. **Consistency**: Ensure consistent builds and deployments across all environments -4. **Security**: Integrate security testing throughout the pipeline -5. **Observability**: Monitor and track all deployments and their impacts -6. **Self-service**: Enable teams to deploy independently, but safely - -## CI/CD Technology Stack - -Our CI/CD pipeline utilizes the following key technologies: - -| Technology | Purpose | -|------------|---------| -| GitHub Actions | Main CI/CD orchestration platform | -| ArgoCD | Kubernetes GitOps deployment tool | -| Helm | Kubernetes package management | -| Docker | Container building and registry | -| Terraform | Infrastructure as Code deployment | -| SonarQube | Code quality and security analysis | -| Jest, JUnit, pytest | Unit testing frameworks | -| Playwright | End-to-end testing | -| K6 | Performance testing | -| OWASP ZAP | Security scanning | -| Snyk | Dependency vulnerability scanning | -| AWS ECR | Container registry | - -## CI/CD Pipeline Overview - -Our CI/CD pipeline consists of multiple stages with specific responsibilities: - -```mermaid -flowchart TD - classDef build fill:#dae8fc,stroke:#6c8ebf,stroke-width:2px - classDef test fill:#d5e8d4,stroke:#82b366,stroke-width:2px - classDef deploy fill:#ffe6cc,stroke:#d79b00,stroke-width:2px - classDef release fill:#f8cecc,stroke:#b85450,stroke-width:2px - - CodePush[Code Push] --> Build[Build & Package] - Build --> UnitTest[Unit Tests] - UnitTest --> CodeQuality[Code Quality Analysis] - CodeQuality --> SecurityScan[Security Scanning] - SecurityScan --> DockerBuild[Docker Build] - DockerBuild --> PushRegistry[Push to Registry] - PushRegistry --> DeployDev[Deploy to Dev] - DeployDev --> IntegrationTest[Integration Tests] - IntegrationTest --> DeployStaging[Deploy to Staging] - DeployStaging --> PerformanceTest[Performance Tests] - DeployStaging --> E2ETest[End-to-End Tests] - PerformanceTest --> Approval{Approval} - E2ETest --> Approval - Approval -->|Approved| DeployProd[Deploy to Production] - Approval -->|Rejected| Feedback[Feedback Loop] - DeployProd --> SmokeTest[Smoke Tests] - SmokeTest --> Monitoring[Monitoring & Observability] - - class Build,DockerBuild build - class UnitTest,CodeQuality,SecurityScan,IntegrationTest,PerformanceTest,E2ETest,SmokeTest test - class DeployDev,DeployStaging,DeployProd deploy - class Approval,Monitoring release -``` - -## Pipeline Stages in Detail - -### 1. Build & Package - -- Triggered by code push or pull request -- Compiles application code -- Installs dependencies using package managers (npm, Maven, pip) -- Generates build artifacts -- Built in isolated environments with cached dependencies - -```yaml -# Example GitHub Actions code snippet -build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: '16' - cache: 'npm' - - name: Install dependencies - run: npm ci - - name: Build - run: npm run build - - name: Upload build artifacts - uses: actions/upload-artifact@v3 - with: - name: build-artifacts - path: build/ -``` - -### 2. Test - -Multiple types of tests run in parallel to provide rapid feedback: - -- **Unit Tests**: Test individual components in isolation -- **Integration Tests**: Test component interactions -- **End-to-End Tests**: Test complete user flows -- **Performance Tests**: Test application performance under load -- **Security Tests**: Scan for vulnerabilities in code and dependencies - -```mermaid -flowchart LR - subgraph "Test Phase" - direction TB - Unit[Unit Tests] - Integration[Integration Tests] - E2E[End-to-End Tests] - Performance[Performance Tests] - Security[Security Tests] - end - - CodeBase[Code Base] --> Unit - CodeBase --> Integration - CodeBase --> E2E - CodeBase --> Performance - CodeBase --> Security - - Unit --> Results[Test Results] - Integration --> Results - E2E --> Results - Performance --> Results - Security --> Results -``` - -### 3. Docker Build & Registry Push - -- Builds Docker images for all services -- Tags images with git commit SHA and environment -- Pushes images to AWS ECR -- Scans images for vulnerabilities before pushing - -```yaml -# Example GitHub Actions code snippet -docker-build: - runs-on: ubuntu-latest - needs: [build, test] - steps: - - uses: actions/checkout@v3 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-west-2 - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: . - push: true - tags: ${{ steps.login-ecr.outputs.registry }}/flowmart-orders-service:${{ github.sha }} -``` - -### 4. Deployment - -We use GitOps with ArgoCD for managing deployments: - -- **Development**: Automatic deployment on successful build -- **Staging**: Automatic deployment after integration tests pass -- **Production**: Manual approval required, then automated deployment - -```mermaid -sequenceDiagram - participant Dev as Developer - participant GH as GitHub Actions - participant Reg as Container Registry - participant Git as Git Repo (Manifests) - participant Argo as ArgoCD - participant K8s as Kubernetes Cluster - - Dev->>GH: Push Code - GH->>GH: Build & Test - GH->>Reg: Push Container Image - GH->>Git: Update Image Tag in Manifests - Git->>Argo: Sync (Automatic or Manual) - Argo->>K8s: Apply Manifests - K8s->>K8s: Deploy Application - K8s->>Argo: Report Status - Argo->>Git: Update Deployment Status -``` - -### 5. Post-Deployment Verification - -- **Smoke Tests**: Quick tests to verify basic functionality -- **Canary Deployment**: Rolling deployment with traffic shifting -- **Monitoring**: Performance and error tracking during and after deployment - -## Infrastructure Pipeline - -For infrastructure changes, we have a separate pipeline: - -```mermaid -flowchart TD - InfraChange[Infrastructure Change] --> TerraformPlan[Terraform Plan] - TerraformPlan --> AutoReview[Automated Review] - AutoReview --> HumanReview[Human Review] - HumanReview --> Approval{Approved?} - Approval -->|Yes| TerraformApply[Terraform Apply] - Approval -->|No| Feedback[Feedback Loop] - TerraformApply --> Verification[Infrastructure Verification] - Verification --> Documentation[Update Documentation] -``` - -## Feature Branch Workflow - -We follow a feature branch workflow for development: - -```mermaid -gitGraph - commit - commit - branch feature/order-tracking - checkout feature/order-tracking - commit - commit - commit - checkout main - merge feature/order-tracking - branch feature/payment-gateway - checkout feature/payment-gateway - commit - commit - checkout main - merge feature/payment-gateway - commit -``` - -## Deployment to Multiple Environments - -Our pipeline handles deployments to multiple environments: - -```mermaid -flowchart TD - Build[Build & Test] --> DevDeploy[Deploy to Dev] - - DevDeploy --> IntegrationTest[Run Integration Tests] - IntegrationTest -->|Pass| StagingDeploy[Deploy to Staging] - IntegrationTest -->|Fail| FixIssues[Fix Issues] - FixIssues --> Build - - StagingDeploy --> StagingTests[Run E2E & Performance Tests] - StagingTests -->|Pass| ApprovalGate{Approval Gate} - StagingTests -->|Fail| FixIssues - - ApprovalGate -->|Approved| ProdDeploy[Deploy to Production] - ApprovalGate -->|Rejected| FixIssues - - ProdDeploy --> CanaryDeploy[Canary Deployment] - CanaryDeploy --> Monitor[Monitor] - Monitor -->|Healthy| FullRollout[Full Rollout] - Monitor -->|Issues| Rollback[Rollback] -``` - -## Rollback Strategy - -In case of deployment issues, we have an automated rollback strategy: - -1. **Immediate Automated Rollback**: Triggered by health checks or error rate spikes -2. **One-Click Manual Rollback**: Available through the deployment dashboard -3. **Previous Version Restoration**: Reverts to the last known good state - -```mermaid -sequenceDiagram - participant Metrics as Metrics System - participant CD as CD Pipeline - participant Git as Git Repository - participant K8s as Kubernetes - - Note over Metrics,K8s: New deployment shows issues - Metrics->>CD: Alert on error threshold exceeded - CD->>CD: Trigger rollback process - CD->>Git: Revert to previous manifest version - Git->>K8s: Apply previous manifests - K8s->>K8s: Restore previous deployment - K8s->>CD: Report successful rollback - CD->>Metrics: Verify metrics returning to normal -``` - -## Pipeline Security - -Security is integrated throughout our pipeline: - -- **Secrets Management**: Secrets stored in AWS Secrets Manager and injected at runtime -- **SAST**: Static Application Security Testing integrated in build phase -- **DAST**: Dynamic Application Security Testing during staging deployment -- **Dependency Scanning**: Checks for vulnerable dependencies -- **Container Scanning**: Scans container images for vulnerabilities -- **Infrastructure Scanning**: Checks IaC for security misconfigurations - -## Observability - -Our pipeline provides comprehensive observability: - -- **Deployment Tracking**: Each deployment is traced from commit to production -- **Metrics Collection**: Performance metrics before and after deployment -- **Log Aggregation**: Centralized logging for all pipeline stages -- **Alerting**: Automated alerts for pipeline failures or anomalies -- **Dashboards**: Visual representation of pipeline health and history - -Example deployment dashboard: - -```mermaid -gantt - title Recent Deployments Timeline - dateFormat YYYY-MM-DD HH:mm - axisFormat %H:%M - - section Orders Service - Build #452 :a1, 2023-05-10 09:00, 5m - Deploy to Dev :a2, after a1, 10m - Integration Tests :a3, after a2, 15m - Deploy to Staging :a4, after a3, 10m - E2E Tests :a5, after a4, 30m - Deploy to Prod :a6, after a5, 15m - - section Inventory Service - Build #389 :b1, 2023-05-10 10:00, 5m - Deploy to Dev :b2, after b1, 10m - Integration Tests :b3, after b2, 15m - Deploy to Staging :b4, after b3, 10m - E2E Tests :b5, after b4, 30m - - section Payment Service - Build #421 :c1, 2023-05-10 08:30, 5m - Deploy to Dev :c2, after c1, 10m - Integration Tests :c3, after c2, 15m - Deploy to Staging :c4, after c3, 10m - E2E Tests :c5, after c4, 30m - Deploy to Prod :c6, after c5, 15m -``` - -## Continuous Improvement - -We continuously improve our pipeline through: - -1. **Pipeline Metrics**: Track build times, success rates, and deployment frequency -2. **Postmortems**: Document and learn from deployment failures -3. **Automation Improvements**: Regularly identify manual steps for automation -4. **Cross-team Learning**: Share best practices across development teams - -## Conclusion - -Our CI/CD pipeline provides a robust, secure, and efficient process for deploying changes to the FlowMart platform. By automating the build, test, and deployment processes, we can deliver new features and bug fixes to users quickly and reliably while maintaining high quality standards. \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/system-architecture-diagrams/01-high-level-system-overview.mdx b/examples/default/docs/technical-architecture-design/system-architecture-diagrams/01-high-level-system-overview.mdx deleted file mode 100644 index ebca398a1..000000000 --- a/examples/default/docs/technical-architecture-design/system-architecture-diagrams/01-high-level-system-overview.mdx +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: High-Level FlowMart System Architecture Overview -summary: A high-level overview of the FlowMart e-commerce system architecture -sidebar: - label: 01 - High-Level Overview - order: 1 ---- - -# FlowMart System Architecture: High-Level Overview - -This diagram provides a high-level overview of the FlowMart e-commerce platform architecture. It illustrates the main components and how they interact to deliver our online shopping experience. - -## Business Context - -FlowMart is an e-commerce platform that enables customers to browse products, place orders, make payments, and track shipments. The architecture is designed to be scalable, resilient, and maintainable, following domain-driven design and event-driven architecture principles. - -## High-Level Architecture Diagram - -```mermaid -flowchart TB - subgraph "Customer Channels" - Web["Web Application"] - Mobile["Mobile Apps"] - ThirdParty["Third-Party Integrations"] - end - - subgraph "API Gateway Layer" - APIGateway["API Gateway / BFF"] - end - - subgraph "Core Business Domains" - Orders["Orders Domain"] - Inventory["Inventory Domain"] - Payment["Payment Domain"] - Shipping["Shipping Domain"] - Subscription["Subscription Domain"] - Notification["Notification Domain"] - end - - subgraph "Data Stores" - OrdersDB[(Orders Database)] - InventoryDB[(Inventory Database)] - PaymentDB[(Payment Database)] - ShippingDB[(Shipping Database)] - SubscriptionDB[(Subscription Database)] - end - - subgraph "External Systems" - PaymentGateways["Payment Gateways"] - LogisticsProviders["Logistics Providers"] - EmailSMS["Email/SMS Providers"] - end - - Web --> APIGateway - Mobile --> APIGateway - ThirdParty --> APIGateway - - APIGateway --> Orders - APIGateway --> Inventory - APIGateway --> Payment - APIGateway --> Shipping - APIGateway --> Subscription - - Orders <--> OrdersDB - Inventory <--> InventoryDB - Payment <--> PaymentDB - Shipping <--> ShippingDB - Subscription <--> SubscriptionDB - - Orders <--> Inventory - Orders <--> Payment - Orders <--> Shipping - Orders <--> Notification - Payment <--> Subscription - - Payment <--> PaymentGateways - Shipping <--> LogisticsProviders - Notification <--> EmailSMS -``` - -## Component Descriptions - -### Customer Channels -- **Web Application**: A responsive web interface for customers to browse products and place orders -- **Mobile Apps**: Native iOS and Android applications for mobile shopping -- **Third-Party Integrations**: External platforms that integrate with our services - -### API Gateway Layer -- **API Gateway / BFF**: Backend for Frontend that routes requests, handles authentication, and optimizes responses for different clients - -### Core Business Domains -- **Orders Domain**: Manages the order lifecycle from creation to fulfillment -- **Inventory Domain**: Tracks product availability and stock levels -- **Payment Domain**: Processes payments and manages financial transactions -- **Shipping Domain**: Handles order delivery and shipment tracking -- **Subscription Domain**: Manages recurring subscriptions and memberships -- **Notification Domain**: Delivers notifications to customers across different channels - -### Data Stores -- Each domain has its dedicated database to ensure domain isolation and independent scalability - -### External Systems -- **Payment Gateways**: Third-party services for processing credit card payments -- **Logistics Providers**: External shipping and delivery services -- **Email/SMS Providers**: Services for sending notifications to customers - -## Key Architectural Principles - -1. **Domain-Driven Design**: Our system is organized around business domains and their bounded contexts -2. **Event-Driven Architecture**: Services communicate primarily through events, enhancing decoupling and scalability -3. **Microservices**: Each domain is implemented as one or more microservices with clear boundaries -4. **API-First Approach**: All functionality is exposed through well-defined APIs -5. **Polyglot Persistence**: Each domain can choose the most appropriate database technology - -## Next Steps - -For a more detailed view of our architecture, refer to the following documents: -- [Domain-Level Architecture](./02-domain-level-architecture.mdx) -- [Service-Level Architecture](./03-service-level-architecture.mdx) -- [Data Flow Architecture](./04-data-flow-architecture.mdx) \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/system-architecture-diagrams/02-domain-level-architecture.mdx b/examples/default/docs/technical-architecture-design/system-architecture-diagrams/02-domain-level-architecture.mdx deleted file mode 100644 index 4fd27b0ed..000000000 --- a/examples/default/docs/technical-architecture-design/system-architecture-diagrams/02-domain-level-architecture.mdx +++ /dev/null @@ -1,253 +0,0 @@ ---- -title: FlowMart Domain-Level Architecture -summary: A detailed view of FlowMart's domain architecture showing bounded contexts and domain interactions -sidebar: - label: 02 - Domain Architecture - order: 2 ---- - -# FlowMart Domain-Level Architecture - -This document describes the domain-level architecture of the FlowMart e-commerce platform, showing how different domains interact with each other through well-defined interfaces and events. - -## Domain Architecture Diagram - -The following diagram illustrates the bounded contexts of each domain and how they communicate with each other: - -```mermaid -flowchart TD - classDef domainStyle fill:#f9f9f9,stroke:#333,stroke-width:2px - classDef eventStyle fill:#e1f5fe,stroke:#0288d1,stroke-width:1px,color:#01579b - classDef commandStyle fill:#f3e5f5,stroke:#7b1fa2,stroke-width:1px,color:#4a148c - classDef queryStyle fill:#e8f5e9,stroke:#388e3c,stroke-width:1px,color:#1b5e20 - - subgraph Orders["Orders Domain"] - class Orders domainStyle - OrdersService["Orders Service"] - - subgraph OrderEvents[" "] - OrderConfirmed["OrderConfirmed Event"] - OrderCancelled["OrderCancelled Event"] - OrderAmended["OrderAmended Event"] - end - - subgraph OrderCommands[" "] - PlaceOrder["PlaceOrder Command"] - end - - subgraph OrderQueries[" "] - GetOrder["GetOrder Query"] - end - end - - subgraph Inventory["Inventory Domain"] - class Inventory domainStyle - InventoryService["Inventory Service"] - - subgraph InventoryEvents[" "] - InventoryAdjusted["InventoryAdjusted Event"] - OutOfStock["OutOfStock Event"] - end - - subgraph InventoryCommands[" "] - AddInventory["AddInventory Command"] - UpdateInventory["UpdateInventory Command"] - DeleteInventory["DeleteInventory Command"] - end - - subgraph InventoryQueries[" "] - GetInventoryStatus["GetInventoryStatus Query"] - GetInventoryList["GetInventoryList Query"] - end - end - - subgraph Payment["Payment Domain"] - class Payment domainStyle - PaymentService["Payment Service"] - - subgraph PaymentEvents[" "] - PaymentProcessed["PaymentProcessed Event"] - end - - subgraph PaymentCommands[" "] - PaymentInitiated["PaymentInitiated Command"] - end - - subgraph PaymentQueries[" "] - GetPaymentStatus["GetPaymentStatus Query"] - end - end - - subgraph Shipping["Shipping Domain"] - class Shipping domainStyle - ShippingService["Shipping Service"] - - subgraph ShippingEvents[" "] - ShipmentCreated["ShipmentCreated Event"] - ShipmentDispatched["ShipmentDispatched Event"] - ShipmentInTransit["ShipmentInTransit Event"] - ShipmentDelivered["ShipmentDelivered Event"] - DeliveryFailed["DeliveryFailed Event"] - ReturnInitiated["ReturnInitiated Event"] - end - - subgraph ShippingCommands[" "] - CreateShipment["CreateShipment Command"] - UpdateShipmentStatus["UpdateShipmentStatus Command"] - CancelShipment["CancelShipment Command"] - CreateReturnLabel["CreateReturnLabel Command"] - end - end - - subgraph Subscription["Subscription Domain"] - class Subscription domainStyle - SubscriptionService["Subscription Service"] - - subgraph SubscriptionEvents[" "] - UserSubscriptionStarted["UserSubscriptionStarted Event"] - UserSubscriptionCancelled["UserSubscriptionCancelled Event"] - end - - subgraph SubscriptionCommands[" "] - SubscribeUser["SubscribeUser Command"] - CancelSubscription["CancelSubscription Command"] - end - - subgraph SubscriptionQueries[" "] - GetSubscriptionStatus["GetSubscriptionStatus Query"] - end - end - - subgraph Notification["Notification Domain"] - class Notification domainStyle - NotificationService["Notification Service"] - - subgraph NotificationQueries[" "] - GetUserNotifications["GetUserNotifications Query"] - GetNotificationDetails["GetNotificationDetails Query"] - end - end - - %% Domain Interactions through Events - OrderConfirmed -->|Consumed by| InventoryService - OrderConfirmed -->|Consumed by| ShippingService - OrderCancelled -->|Consumed by| InventoryService - OrderAmended -->|Consumed by| InventoryService - - InventoryAdjusted -->|Consumed by| OrdersService - InventoryAdjusted -->|Consumed by| NotificationService - OutOfStock -->|Consumed by| NotificationService - - PaymentProcessed -->|Consumed by| OrdersService - PaymentProcessed -->|Consumed by| ShippingService - PaymentProcessed -->|Consumed by| SubscriptionService - - UserSubscriptionCancelled -->|Consumed by| OrdersService - - %% Apply styles to events, commands and queries - class OrderConfirmed,OrderCancelled,OrderAmended,InventoryAdjusted,OutOfStock,PaymentProcessed,ShipmentCreated,ShipmentDispatched,ShipmentInTransit,ShipmentDelivered,DeliveryFailed,ReturnInitiated,UserSubscriptionStarted,UserSubscriptionCancelled eventStyle - - class PlaceOrder,AddInventory,UpdateInventory,DeleteInventory,PaymentInitiated,CreateShipment,UpdateShipmentStatus,CancelShipment,CreateReturnLabel,SubscribeUser,CancelSubscription commandStyle - - class GetOrder,GetInventoryStatus,GetInventoryList,GetPaymentStatus,GetSubscriptionStatus,GetUserNotifications,GetNotificationDetails queryStyle -``` - -## Domain Descriptions - -### Orders Domain -The Orders Domain is the central domain of our e-commerce platform. It manages the entire lifecycle of customer orders, from creation to fulfillment. It communicates with other domains to check inventory availability, process payments, and arrange shipping. - -**Key Events Published:** -- OrderConfirmed -- OrderCancelled -- OrderAmended - -**Commands:** -- PlaceOrder - -**Queries:** -- GetOrder - -### Inventory Domain -The Inventory Domain manages product stock levels across all warehouses. It ensures accurate inventory tracking and provides real-time stock information to other domains. - -**Key Events Published:** -- InventoryAdjusted -- OutOfStock - -**Commands:** -- AddInventory -- UpdateInventory -- DeleteInventory - -**Queries:** -- GetInventoryStatus -- GetInventoryList - -### Payment Domain -The Payment Domain handles all financial transactions within the platform. It processes customer payments, manages refunds, and communicates with external payment gateways. - -**Key Events Published:** -- PaymentProcessed - -**Commands:** -- PaymentInitiated - -**Queries:** -- GetPaymentStatus - -### Shipping Domain -The Shipping Domain manages order delivery to customers. It tracks shipment status, handles returns, and integrates with external logistics providers. - -**Key Events Published:** -- ShipmentCreated -- ShipmentDispatched -- ShipmentInTransit -- ShipmentDelivered -- DeliveryFailed -- ReturnInitiated - -**Commands:** -- CreateShipment -- UpdateShipmentStatus -- CancelShipment -- CreateReturnLabel - -### Subscription Domain -The Subscription Domain manages recurring customer subscriptions. It handles subscription lifecycle, billing cycles, and renewal processes. - -**Key Events Published:** -- UserSubscriptionStarted -- UserSubscriptionCancelled - -**Commands:** -- SubscribeUser -- CancelSubscription - -**Queries:** -- GetSubscriptionStatus - -### Notification Domain -The Notification Domain is responsible for sending notifications to customers through various channels like email, SMS, and push notifications. - -**Queries:** -- GetUserNotifications -- GetNotificationDetails - -## Domain Integration Patterns - -The domains in FlowMart's architecture interact using several integration patterns: - -1. **Event-Driven Communication**: Most domain interactions occur through asynchronous events published to a message broker, promoting loose coupling. - -2. **Synchronous API Calls**: For operations requiring immediate responses, domains expose REST APIs that can be called synchronously. - -3. **Saga Pattern**: For complex workflows spanning multiple domains, we use choreography-based sagas where services respond to events to maintain data consistency. - -4. **CQRS**: We separate command (write) and query (read) operations, allowing for optimization of each path. - -## Next Steps - -For more detailed architectural information, see: -- [Service-Level Architecture](./03-service-level-architecture.mdx) -- [Data Flow Architecture](./04-data-flow-architecture.mdx) \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/system-architecture-diagrams/03-service-level-architecture.mdx b/examples/default/docs/technical-architecture-design/system-architecture-diagrams/03-service-level-architecture.mdx deleted file mode 100644 index 0adc493f6..000000000 --- a/examples/default/docs/technical-architecture-design/system-architecture-diagrams/03-service-level-architecture.mdx +++ /dev/null @@ -1,253 +0,0 @@ ---- -title: FlowMart Service-Level Architecture -summary: A detailed view of the services within each domain and their interactions -sidebar: - label: 03 - Service Architecture - order: 3 ---- - -# FlowMart Service-Level Architecture - -This document provides a detailed view of the individual services within FlowMart's e-commerce platform, their responsibilities, and how they interact. - -## Service Architecture Overview - -The following diagram illustrates the services that make up our platform and how they communicate: - -```mermaid -flowchart TD - classDef apiGateway fill:#f8cecc,stroke:#b85450,stroke-width:2px - classDef orderService fill:#dae8fc,stroke:#6c8ebf,stroke-width:2px - classDef inventoryService fill:#d5e8d4,stroke:#82b366,stroke-width:2px - classDef paymentService fill:#ffe6cc,stroke:#d79b00,stroke-width:2px - classDef shippingService fill:#e1d5e7,stroke:#9673a6,stroke-width:2px - classDef notificationService fill:#fff2cc,stroke:#d6b656,stroke-width:2px - classDef subscriptionService fill:#f8cecc,stroke:#b85450,stroke-width:2px - classDef datastore fill:#f5f5f5,stroke:#666666,stroke-width:1px - classDef messagebroker fill:#e6ffcc,stroke:#36b37e,stroke-width:2px - classDef externalSystem fill:#f5f5f5,stroke:#666666,stroke-width:1px,stroke-dasharray: 5 5 - - APIGateway[API Gateway] - EventBus[Event Bus / Message Broker] - - subgraph OrdersDomain["Orders Domain"] - OrdersService[Orders Service] - OrdersDB[(Orders Database)] - end - - subgraph InventoryDomain["Inventory Domain"] - InventoryService[Inventory Service] - InventoryDB[(Inventory Database)] - end - - subgraph PaymentDomain["Payment Domain"] - PaymentService[Payment Service] - PaymentDB[(Payment Database)] - end - - subgraph ShippingDomain["Shipping Domain"] - ShippingService[Shipping Service] - ShippingDB[(Shipping Database)] - end - - subgraph NotificationDomain["Notification Domain"] - NotificationService[Notification Service] - end - - subgraph SubscriptionDomain["Subscription Domain"] - SubscriptionService[Subscription Service] - SubscriptionDB[(Subscription Database)] - end - - subgraph ExternalSystems["External Systems"] - PaymentGateway[Payment Gateway] - EmailProvider[Email Provider] - SMSProvider[SMS Provider] - LogisticsPartner[Logistics Partner] - end - - %% API Gateway connections - APIGateway --> OrdersService - APIGateway --> InventoryService - APIGateway --> PaymentService - APIGateway --> ShippingService - APIGateway --> SubscriptionService - - %% Database connections - OrdersService <--> OrdersDB - InventoryService <--> InventoryDB - PaymentService <--> PaymentDB - ShippingService <--> ShippingDB - SubscriptionService <--> SubscriptionDB - - %% Event bus connections - OrdersService <--> EventBus - InventoryService <--> EventBus - PaymentService <--> EventBus - ShippingService <--> EventBus - NotificationService <--> EventBus - SubscriptionService <--> EventBus - - %% External system connections - PaymentService --> PaymentGateway - NotificationService --> EmailProvider - NotificationService --> SMSProvider - ShippingService --> LogisticsPartner - - %% Apply styles - class APIGateway apiGateway - class OrdersService orderService - class InventoryService inventoryService - class PaymentService paymentService - class ShippingService shippingService - class NotificationService notificationService - class SubscriptionService subscriptionService - class OrdersDB,InventoryDB,PaymentDB,ShippingDB,SubscriptionDB datastore - class EventBus messagebroker - class PaymentGateway,EmailProvider,SMSProvider,LogisticsPartner externalSystem -``` - -## Service Component Details - -### API Gateway -- **Description**: Entry point for all client requests, handles routing, authentication, and load balancing -- **Technologies**: AWS API Gateway, Kong, or Nginx -- **Key Responsibilities**: - - Route requests to appropriate services - - Handle authentication and authorization - - API rate limiting and throttling - - Request validation - - Response caching - -### Orders Service -- **Description**: Manages the entire lifecycle of customer orders -- **Technologies**: Node.js, Express, MongoDB -- **Key Responsibilities**: - - Create and manage orders - - Process order amendments and cancellations - - Coordinate with other services for order fulfillment - - Maintain order history and status -- **Key Events**: - - Publishes: OrderConfirmed, OrderCancelled, OrderAmended - - Consumes: InventoryAdjusted, PaymentProcessed, UserSubscriptionCancelled - -### Inventory Service -- **Description**: Tracks and manages product inventory across warehouses -- **Technologies**: Java, Spring Boot, PostgreSQL -- **Key Responsibilities**: - - Maintain accurate inventory levels - - Process inventory adjustments - - Monitor stock levels and trigger alerts - - Support inventory queries -- **Key Events**: - - Publishes: InventoryAdjusted, OutOfStock - - Consumes: OrderConfirmed, OrderCancelled, OrderAmended - -### Payment Service -- **Description**: Handles all payment processing and financial transactions -- **Technologies**: Node.js, Express, PostgreSQL -- **Key Responsibilities**: - - Process customer payments - - Manage refunds and chargebacks - - Integrate with payment gateways - - Track payment status -- **Key Events**: - - Publishes: PaymentProcessed - - Consumes: PaymentInitiated, UserSubscriptionStarted, InventoryAdjusted - -### Shipping Service -- **Description**: Manages logistics and shipment of orders -- **Technologies**: Python, Flask, MongoDB -- **Key Responsibilities**: - - Create and track shipments - - Integrate with logistics providers - - Process returns and exchanges - - Calculate shipping costs -- **Key Events**: - - Publishes: ShipmentCreated, ShipmentDispatched, ShipmentInTransit, ShipmentDelivered, DeliveryFailed, ReturnInitiated - - Consumes: OrderConfirmed, PaymentProcessed - -### Notification Service -- **Description**: Delivers notifications to customers through various channels -- **Technologies**: Node.js, Express, Redis -- **Key Responsibilities**: - - Send transactional emails - - Deliver SMS notifications - - Push mobile app notifications - - Store notification history -- **Key Events**: - - Consumes: InventoryAdjusted, OutOfStock, PaymentProcessed - -### Subscription Service -- **Description**: Manages recurring subscriptions and memberships -- **Technologies**: Java, Spring Boot, MySQL -- **Key Responsibilities**: - - Handle subscription lifecycle - - Process recurring billing - - Manage subscription plans and tiers - - Track subscription status -- **Key Events**: - - Publishes: UserSubscriptionStarted, UserSubscriptionCancelled - - Consumes: PaymentProcessed - -### Event Bus / Message Broker -- **Description**: Central messaging system that enables asynchronous communication between services -- **Technologies**: Apache Kafka, RabbitMQ, or AWS SNS/SQS -- **Key Responsibilities**: - - Reliable event delivery - - Support for event persistence - - Message routing - - Scalable message throughput - -## Service Interaction Patterns - -### Synchronous Communication -Services communicate directly with each other through REST APIs for operations that require immediate responses. - -```mermaid -sequenceDiagram - participant Client - participant OrdersService - participant InventoryService - participant PaymentService - - Client->>OrdersService: Place Order - OrdersService->>InventoryService: Check Inventory Availability - InventoryService-->>OrdersService: Inventory Available - OrdersService->>PaymentService: Process Payment - PaymentService-->>OrdersService: Payment Successful - OrdersService-->>Client: Order Confirmation -``` - -### Asynchronous Communication -Services communicate through events published to the event bus, allowing for loose coupling. - -```mermaid -sequenceDiagram - participant OrdersService - participant EventBus - participant InventoryService - participant ShippingService - participant NotificationService - - OrdersService->>EventBus: Publish OrderConfirmed Event - EventBus->>InventoryService: OrderConfirmed Event - InventoryService->>EventBus: Publish InventoryAdjusted Event - EventBus->>ShippingService: OrderConfirmed Event - ShippingService->>EventBus: Publish ShipmentCreated Event - EventBus->>NotificationService: ShipmentCreated Event - NotificationService->>Customer: Send Shipment Notification -``` - -## Infrastructure Considerations - -- All services are containerized using Docker and orchestrated with Kubernetes -- Services are deployed across multiple availability zones for high availability -- Auto-scaling is configured based on CPU and memory metrics -- Database replication and backups are implemented for data durability -- Centralized logging and monitoring using Prometheus and Grafana - -## Next Steps - -For more information about data flows within the system, refer to: -- [Data Flow Architecture](./04-data-flow-architecture.mdx) \ No newline at end of file diff --git a/examples/default/docs/technical-architecture-design/system-architecture-diagrams/04-data-flow-architecture.mdx b/examples/default/docs/technical-architecture-design/system-architecture-diagrams/04-data-flow-architecture.mdx deleted file mode 100644 index 8e26fd5d2..000000000 --- a/examples/default/docs/technical-architecture-design/system-architecture-diagrams/04-data-flow-architecture.mdx +++ /dev/null @@ -1,460 +0,0 @@ ---- -title: FlowMart Data Flow Architecture -summary: A detailed view of key data flows and business processes within the FlowMart e-commerce platform -sidebar: - label: 04 - Data Flow Architecture - order: 4 ---- - -# FlowMart Data Flow Architecture - -This document illustrates the key data flows within the FlowMart e-commerce platform, focusing on the most important business processes and how data moves through the system. - -## Key Business Process Flows - -### Order Placement and Fulfillment Flow - -This diagram shows the complete flow from order placement to order fulfillment: - -```mermaid -flowchart TD - classDef customerAction fill:#ffcccc,stroke:#ff0000 - classDef systemProcess fill:#ccffcc,stroke:#00aa00 - classDef decisionPoint fill:#ffffcc,stroke:#ffcc00 - classDef externalSystem fill:#ccccff,stroke:#0000ff - classDef dataStore fill:#f5f5f5,stroke:#333333 - - Start([Customer starts checkout]) --> ValidateCart[Validate Shopping Cart] - class Start customerAction - - ValidateCart --> CartValid{Is cart valid?} - class ValidateCart systemProcess - class CartValid decisionPoint - - CartValid -->|No| UpdateCart[Customer updates cart] - class UpdateCart customerAction - - UpdateCart --> ValidateCart - - CartValid -->|Yes| CheckInventory[Check Inventory Availability] - class CheckInventory systemProcess - - CheckInventory --> InventoryAvailable{Inventory available?} - class InventoryAvailable decisionPoint - - InventoryAvailable -->|No| NotifyCustomer[Notify Customer of Unavailability] - class NotifyCustomer systemProcess - - NotifyCustomer --> UpdateCart - - InventoryAvailable -->|Yes| ProcessPayment[Process Payment] - class ProcessPayment systemProcess - - ProcessPayment --> PaymentGateway[Payment Gateway] - class PaymentGateway externalSystem - - PaymentGateway --> PaymentSuccessful{Payment successful?} - class PaymentSuccessful decisionPoint - - PaymentSuccessful -->|No| NotifyPaymentFailure[Notify Payment Failure] - class NotifyPaymentFailure systemProcess - - NotifyPaymentFailure --> UpdatePayment[Customer updates payment] - class UpdatePayment customerAction - - UpdatePayment --> ProcessPayment - - PaymentSuccessful -->|Yes| CreateOrder[Create Order] - class CreateOrder systemProcess - - CreateOrder --> OrderDB[(Orders Database)] - class OrderDB dataStore - - CreateOrder --> ReserveInventory[Reserve Inventory] - class ReserveInventory systemProcess - - ReserveInventory --> InventoryDB[(Inventory Database)] - class InventoryDB dataStore - - ReserveInventory --> CreateShipment[Create Shipment] - class CreateShipment systemProcess - - CreateShipment --> ShipmentDB[(Shipment Database)] - class ShipmentDB dataStore - - CreateShipment --> NotifyCustomerOrder[Send Order Confirmation] - class NotifyCustomerOrder systemProcess - - NotifyCustomerOrder --> End([Order Placement Complete]) - class End customerAction -``` - -### Payment Processing Flow - -This diagram details the payment processing flow: - -```mermaid -sequenceDiagram - participant Customer - participant OrdersService - participant PaymentService - participant PaymentGateway - participant PaymentDB - participant EventBus - participant NotificationService - - Customer->>OrdersService: Place Order - OrdersService->>PaymentService: Request Payment - PaymentService->>PaymentGateway: Process Payment - - alt Payment Successful - PaymentGateway->>PaymentService: Confirm Payment - PaymentService->>PaymentDB: Store Payment Information - PaymentService->>EventBus: Publish PaymentProcessed Event - EventBus->>OrdersService: PaymentProcessed Event - EventBus->>NotificationService: PaymentProcessed Event - NotificationService->>Customer: Send Payment Confirmation - OrdersService->>Customer: Complete Order - else Payment Failed - PaymentGateway->>PaymentService: Payment Failure - PaymentService->>PaymentDB: Log Failed Transaction - PaymentService->>OrdersService: Payment Failed Response - OrdersService->>Customer: Request Different Payment Method - end -``` - -### Inventory Management Flow - -This diagram shows how inventory is managed across the system: - -```mermaid -stateDiagram-v2 - [*] --> Available: Initial Stock - - Available --> Reserved: Customer Places Order - Reserved --> Allocated: Order Confirmed - Reserved --> Available: Order Cancelled - - Allocated --> Shipped: Order Shipped - Shipped --> Delivered: Order Delivered - Delivered --> [*] - - Available --> Replenished: Inventory Low - Replenished --> Available: Stock Received - - Available --> OutOfStock: All Units Reserved - OutOfStock --> Available: Replenishment -``` - -### Subscription Processing Flow - -This diagram illustrates the subscription management flow: - -```mermaid -flowchart TD - classDef process fill:#d5e8d4,stroke:#82b366 - classDef event fill:#dae8fc,stroke:#6c8ebf - classDef externalSystem fill:#f5f5f5,stroke:#666666,stroke-width:1px - classDef decision fill:#fff2cc,stroke:#d6b656 - - Start([Start Subscription Process]) --> NewSubscription[Create Subscription] - class NewSubscription process - - NewSubscription --> InitialPayment[Process Initial Payment] - class InitialPayment process - - InitialPayment --> PaymentSystem[Payment Gateway] - class PaymentSystem externalSystem - - PaymentSystem --> PaymentSuccessful{Payment Successful?} - class PaymentSuccessful decision - - PaymentSuccessful -->|Yes| ActivateSubscription[Activate Subscription] - class ActivateSubscription process - - PaymentSuccessful -->|No| FailedSubscription[Failed Subscription] - class FailedSubscription process - - FailedSubscription --> NotifyFailure[Notify Customer of Failure] - class NotifyFailure process - - ActivateSubscription --> UserSubscriptionStarted[Publish UserSubscriptionStarted Event] - class UserSubscriptionStarted event - - UserSubscriptionStarted --> ScheduleRenewal[Schedule Next Renewal] - class ScheduleRenewal process - - ScheduleRenewal --> TimeForRenewal{Time for Renewal?} - class TimeForRenewal decision - - TimeForRenewal -->|Yes| ProcessRenewalPayment[Process Renewal Payment] - class ProcessRenewalPayment process - - TimeForRenewal -->|No| Wait[Wait for Renewal Date] - class Wait process - - Wait --> TimeForRenewal - - ProcessRenewalPayment --> PaymentSystem - - PaymentSystem --> RenewalSuccessful{Renewal Successful?} - class RenewalSuccessful decision - - RenewalSuccessful -->|Yes| ExtendSubscription[Extend Subscription] - class ExtendSubscription process - - RenewalSuccessful -->|No, after retries| CancelSubscription[Cancel Subscription] - class CancelSubscription process - - ExtendSubscription --> ScheduleRenewal - - CancelSubscription --> UserSubscriptionCancelled[Publish UserSubscriptionCancelled Event] - class UserSubscriptionCancelled event - - UserSubscriptionCancelled --> End([End Subscription Process]) -``` - -## Detailed Data Flow Examples - -### Customer Order Flow - -The following diagram shows the data flow when a customer places an order: - -```mermaid -sequenceDiagram - participant Customer - participant Web/Mobile App - participant API Gateway - participant OrdersService - participant InventoryService - participant PaymentService - participant ShippingService - participant NotificationService - participant EventBus - - Customer->>Web/Mobile App: Add items to cart - Customer->>Web/Mobile App: Proceed to checkout - Web/Mobile App->>API Gateway: POST /orders - API Gateway->>OrdersService: Create order request - - OrdersService->>InventoryService: Check inventory availability - InventoryService-->>OrdersService: Inventory status - - OrdersService->>PaymentService: Process payment - PaymentService-->>OrdersService: Payment result - - alt Order Successful - OrdersService->>EventBus: Publish OrderConfirmed - EventBus->>InventoryService: OrderConfirmed - InventoryService->>EventBus: Publish InventoryAdjusted - EventBus->>ShippingService: OrderConfirmed - ShippingService->>EventBus: Publish ShipmentCreated - EventBus->>NotificationService: OrderConfirmed - NotificationService->>Customer: Send order confirmation - else Order Failed - OrdersService->>EventBus: Publish OrderFailed - EventBus->>NotificationService: OrderFailed - NotificationService->>Customer: Send failure notification - end -``` - -### Product Return Flow - -The following diagram shows the data flow when a customer returns a product: - -```mermaid -sequenceDiagram - participant Customer - participant Web/Mobile App - participant API Gateway - participant OrdersService - participant ShippingService - participant InventoryService - participant PaymentService - participant NotificationService - participant EventBus - - Customer->>Web/Mobile App: Request return - Web/Mobile App->>API Gateway: POST /returns - API Gateway->>OrdersService: Create return request - OrdersService->>ShippingService: Generate return label - - ShippingService->>EventBus: Publish ReturnInitiated - EventBus->>NotificationService: ReturnInitiated - NotificationService->>Customer: Send return instructions - - Note over Customer,ShippingService: Customer ships the item back - - ShippingService->>EventBus: Publish ReturnReceived - EventBus->>InventoryService: ReturnReceived - InventoryService->>EventBus: Publish InventoryAdjusted - - EventBus->>PaymentService: ReturnReceived - PaymentService->>EventBus: Publish RefundIssued - - EventBus->>NotificationService: RefundIssued - NotificationService->>Customer: Send refund confirmation -``` - -## Data Storage Overview - -The following diagram provides a high-level overview of the data storage architecture: - -```mermaid -erDiagram - CUSTOMER ||--o{ ORDER : places - CUSTOMER ||--o{ SUBSCRIPTION : subscribes - ORDER ||--|{ ORDER_ITEM : contains - ORDER ||--|| SHIPMENT : fulfilled-by - ORDER ||--|| PAYMENT : paid-by - PRODUCT ||--o{ ORDER_ITEM : included-in - PRODUCT ||--o{ INVENTORY : stocked-as - WAREHOUSE ||--o{ INVENTORY : holds - SUBSCRIPTION ||--o{ SUBSCRIPTION_PAYMENT : generates - - CUSTOMER { - string id PK - string firstName - string lastName - string email - string phone - datetime createdAt - } - - ORDER { - string id PK - string customerId FK - string status - decimal totalAmount - datetime orderDate - } - - ORDER_ITEM { - string id PK - string orderId FK - string productId FK - int quantity - decimal unitPrice - } - - PRODUCT { - string id PK - string name - string description - decimal price - string category - } - - INVENTORY { - string id PK - string productId FK - string warehouseId FK - int quantity - int reserved - } - - WAREHOUSE { - string id PK - string name - string location - } - - SHIPMENT { - string id PK - string orderId FK - string status - string trackingNumber - datetime shippedDate - } - - PAYMENT { - string id PK - string orderId FK - decimal amount - string paymentMethod - string status - datetime paymentDate - } - - SUBSCRIPTION { - string id PK - string customerId FK - string status - string plan - date startDate - date endDate - string billingCycle - } - - SUBSCRIPTION_PAYMENT { - string id PK - string subscriptionId FK - decimal amount - datetime paymentDate - string status - } -``` - -## Event Flow and Message Schema - -The following diagram shows a sample of our event structure and flow: - -```mermaid -graph TD - classDef publisher fill:#d1e0f0,stroke:#6c8ebf - classDef event fill:#f8cecc,stroke:#b85450 - classDef consumer fill:#d5e8d4,stroke:#82b366 - - OrdersService[Orders Service]:::publisher --> OrderConfirmed[OrderConfirmed Event]:::event - - subgraph OrderConfirmedSchema[ ] - direction TB - OrderConfirmedMessage["OrderConfirmed Schema - { - orderId: string, - userId: string, - orderItems: [ - { - productId: string, - quantity: number, - unitPrice: number - } - ], - totalAmount: number, - timestamp: datetime - }"] - end - - OrderConfirmed --- OrderConfirmedSchema - - OrderConfirmed --> InventoryService[Inventory Service]:::consumer - OrderConfirmed --> ShippingService[Shipping Service]:::consumer - - InventoryService --> InventoryAdjusted[InventoryAdjusted Event]:::event - - subgraph InventoryAdjustedSchema[ ] - direction TB - InventoryAdjustedMessage["InventoryAdjusted Schema - { - productId: string, - warehouseId: string, - quantityChanged: number, - newQuantity: number, - timestamp: datetime - }"] - end - - InventoryAdjusted --- InventoryAdjustedSchema - - InventoryAdjusted --> OrdersService:::consumer - InventoryAdjusted --> NotificationService[Notification Service]:::consumer -``` - -## Conclusion - -This document has provided an in-depth view of the data flows within the FlowMart e-commerce platform. By understanding these flows, developers and stakeholders can better comprehend how data moves through the system and how different components interact with each other. - -For more details on specific architecture components, refer to: -- [High-Level System Overview](./01-high-level-system-overview.mdx) -- [Domain-Level Architecture](./02-domain-level-architecture.mdx) -- [Service-Level Architecture](./03-service-level-architecture.mdx) \ No newline at end of file diff --git a/examples/default/domains/Catalog/entities/category/index.mdx b/examples/default/domains/Catalog/entities/category/index.mdx new file mode 100644 index 000000000..0bfb7a052 --- /dev/null +++ b/examples/default/domains/Catalog/entities/category/index.mdx @@ -0,0 +1,36 @@ +--- +id: category +name: Category +version: 1.0.0 +identifier: categoryId +aggregateRoot: true +summary: A merchandising grouping used to organize products for discovery. +owners: + - product-platform +properties: + - name: categoryId + type: UUID + required: true + description: Unique category identifier. + - name: slug + type: string + required: true + description: Stable URL-safe category key. + - name: displayName + type: string + required: true + description: Customer-facing category name. + - name: parentCategoryId + type: UUID + required: false + description: Parent category for hierarchy navigation. + references: category + relationType: belongsTo + referencesIdentifier: categoryId +--- + +## Overview + +Category defines how products are grouped for browsing. It is owned by the Catalog domain and projected into the Search System for discovery. + + diff --git a/examples/default/domains/Catalog/entities/product-variant/index.mdx b/examples/default/domains/Catalog/entities/product-variant/index.mdx new file mode 100644 index 000000000..365dcd43a --- /dev/null +++ b/examples/default/domains/Catalog/entities/product-variant/index.mdx @@ -0,0 +1,35 @@ +--- +id: product-variant +name: Product Variant +version: 1.0.0 +identifier: variantId +summary: A purchasable variation of a product, such as size or color. +owners: + - product-platform +properties: + - name: variantId + type: UUID + required: true + description: Unique variant identifier. + - name: productId + type: UUID + required: true + description: Product this variant belongs to. + references: product + relationType: belongsTo + referencesIdentifier: productId + - name: sku + type: string + required: true + description: Variant-level SKU. + - name: attributes + type: object + required: true + description: Variant dimensions such as size, color or material. +--- + +## Overview + +Product Variant captures the concrete option a customer can buy. It belongs to a [[entity|product]] and is indexed into search when product data changes. + + diff --git a/examples/default/domains/Catalog/entities/product/index.mdx b/examples/default/domains/Catalog/entities/product/index.mdx new file mode 100644 index 000000000..91826fc3b --- /dev/null +++ b/examples/default/domains/Catalog/entities/product/index.mdx @@ -0,0 +1,37 @@ +--- +id: product +name: Product +version: 1.0.0 +identifier: productId +aggregateRoot: true +summary: The sellable item Acme manages in the product catalog. +owners: + - product-platform +properties: + - name: productId + type: UUID + required: true + description: Unique product identifier. + - name: sku + type: string + required: true + description: Primary merchandising SKU. + - name: name + type: string + required: true + description: Customer-facing product name. + - name: status + type: string + required: true + description: Product publication state. + enum: + - draft + - active + - archived +--- + +## Overview + +The Product is the Catalog domain's core aggregate. It is created and maintained by [[service|product-api]] and published to downstream consumers through product events. + + diff --git a/examples/default/domains/Catalog/entities/search-document/index.mdx b/examples/default/domains/Catalog/entities/search-document/index.mdx new file mode 100644 index 000000000..d9818c4e9 --- /dev/null +++ b/examples/default/domains/Catalog/entities/search-document/index.mdx @@ -0,0 +1,35 @@ +--- +id: search-document +name: Search Document +version: 1.0.0 +identifier: documentId +summary: The denormalized representation of product data indexed by Search. +owners: + - search-platform +properties: + - name: documentId + type: string + required: true + description: Search index document identifier. + - name: productId + type: UUID + required: true + description: Product represented by the document. + references: product + relationType: represents + referencesIdentifier: productId + - name: searchableText + type: string + required: true + description: Text used for matching and ranking. + - name: indexedAt + type: datetime + required: true + description: Time the document was last indexed. +--- + +## Overview + +Search Document is the read model maintained by [[system|search-system]]. It is eventually consistent with the Product Catalog System. + + diff --git a/examples/default/domains/Catalog/index.mdx b/examples/default/domains/Catalog/index.mdx new file mode 100644 index 000000000..0ea42b1e0 --- /dev/null +++ b/examples/default/domains/Catalog/index.mdx @@ -0,0 +1,65 @@ +--- +id: catalog +name: Catalog +version: 1.0.0 +summary: | + The Catalog domain owns everything about products — how they are created, maintained and made discoverable across Acme Inc. It is the source of truth for product data and powers product search across the business. +owners: + - product-platform +systems: + - id: product-catalog-system + version: 1.0.0 + - id: search-system + version: 1.0.0 +entities: + - id: product + version: 1.0.0 + - id: product-variant + version: 1.0.0 + - id: category + version: 1.0.0 + - id: search-document + version: 1.0.0 +badges: + - content: Core domain + backgroundColor: blue + textColor: blue + icon: RectangleGroupIcon + - content: Business Critical + backgroundColor: red + textColor: red + icon: ShieldCheckIcon +--- + +## Overview + +The **Catalog** domain is responsible for the lifecycle of every product Acme Inc sells, and for making those products discoverable. It is split into two internal systems that work together: + + + + + + +### How the systems work together + +The **Product Catalog System** is the authoritative source of product data. Whenever a product is created, updated or deleted it publishes a domain event. The **Search System** subscribes to those events, keeps its search index in sync, and exposes a search API so other teams never have to query the catalog database directly. + +The diagram below shows the domain's systems, how they relate, and the people who interact with them. + + + +## Component map + +These are the components that are part of this domain (parts of the systems in this domain) + + diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/adrs/adr-001-use-transactional-outbox/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/adrs/adr-001-use-transactional-outbox/index.mdx new file mode 100644 index 000000000..2cd394fde --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/adrs/adr-001-use-transactional-outbox/index.mdx @@ -0,0 +1,43 @@ +--- +id: adr-001-use-transactional-outbox +name: 'ADR-001: Publish product events via a transactional outbox' +summary: Product change events are written to an outbox table in the same transaction as the change, then relayed to the broker, so an event is never lost or published for a rolled-back change. +version: 1.0.0 +status: accepted +date: 2026-02-10 +owners: + - product-platform +decisionMakers: + - product-platform +appliesTo: + - type: system + id: product-catalog-system + - type: service + id: product-search-publisher + - type: container + id: product-database +badges: + - content: Messaging + backgroundColor: blue + textColor: blue + - content: Reliability + backgroundColor: green + textColor: green +--- + +## Context + +The Product Catalog System is the source of truth for products, and other systems — notably the [[system|search-system]] — depend on a reliable stream of `product-created`, `product-updated` and `product-deleted` events. Publishing to the broker directly from the [[service|product-api]] request path creates a dual-write problem: if the database commit succeeds but the broker publish fails (or vice versa), the catalog and its consumers drift out of sync. Lost or phantom events are hard to detect and expensive to reconcile. + +## Decision + +The Product API writes every product change **and** a corresponding row in an `outbox` table inside a single database transaction in the [[container|product-database]]. A separate relay, the [[service|product-search-publisher]], reads unpublished outbox rows in order and publishes them to the broker with at-least-once delivery, marking each row published once acknowledged. + +This decouples persistence from publishing: the request path only touches the database, and event delivery becomes an independent, retryable concern. + +## Consequences + +- Events are never lost and never published for a change that rolled back — the change and its event share a transaction boundary. +- Consumers must be **idempotent**, since at-least-once delivery means an event can be redelivered. +- There is a small publish latency between the commit and the relay draining the outbox. +- The outbox table needs a retention/cleanup policy so it doesn't grow unbounded. diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/adrs/adr-002-postgres-as-system-of-record/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/adrs/adr-002-postgres-as-system-of-record/index.mdx new file mode 100644 index 000000000..a7d5077cb --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/adrs/adr-002-postgres-as-system-of-record/index.mdx @@ -0,0 +1,40 @@ +--- +id: adr-002-postgres-as-system-of-record +name: 'ADR-002: Use PostgreSQL as the product system of record' +summary: Product data is stored in a single PostgreSQL database that acts as the authoritative system of record, with derived stores (like the search index) rebuilt from it. +version: 1.0.0 +status: accepted +date: 2026-02-12 +owners: + - product-platform +decisionMakers: + - product-platform +appliesTo: + - type: system + id: product-catalog-system + - type: container + id: product-database +related: + - id: adr-001-use-transactional-outbox +badges: + - content: Data + backgroundColor: purple + textColor: purple +--- + +## Context + +Products are a small-to-medium, strongly relational dataset (products, categories, pricing, lifecycle status) that needs transactional integrity, an enforceable schema, and the ability to write a change and its outbox event atomically. We also want a single, unambiguous source of truth so that derived stores — such as the [[system|search-system]]'s index — can be treated as disposable and rebuilt at any time. + +## Decision + +The Product Catalog System uses a single **PostgreSQL** database, the [[container|product-database]], as the authoritative system of record. It holds the `products` table and the outbox table (see [[adr|adr-001-use-transactional-outbox]]). The [[service|product-api]] is the only writer on the request path; the [[service|product-worker]] performs asynchronous enrichment. + +Any other representation of product data elsewhere in the business is a **derived read model**, not a source of truth, and must be rebuildable by replaying product events. + +## Consequences + +- Strong consistency and transactional outbox writes are straightforward with a single relational store. +- The search index and any future read models can be rebuilt from the catalog, keeping them disposable. +- The database is a scaling focal point; heavy read traffic should be served from read replicas or downstream read models rather than the primary. +- Schema changes need migrations and care, since this is the authoritative store. diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/adrs/adr-003-offload-async-work-to-worker/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/adrs/adr-003-offload-async-work-to-worker/index.mdx new file mode 100644 index 000000000..941e73038 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/adrs/adr-003-offload-async-work-to-worker/index.mdx @@ -0,0 +1,41 @@ +--- +id: adr-003-offload-async-work-to-worker +name: 'ADR-003: Offload long-running catalog work to a dedicated worker' +summary: Slow and asynchronous catalog tasks (enrichment, image processing, bulk imports) run in a separate Product Worker so the Product API stays fast and predictable. +version: 1.0.0 +status: accepted +date: 2026-02-18 +owners: + - product-platform +decisionMakers: + - product-platform +appliesTo: + - type: system + id: product-catalog-system + - type: service + id: product-worker + - type: service + id: product-api +badges: + - content: Architecture + backgroundColor: blue + textColor: blue + - content: Performance + backgroundColor: green + textColor: green +--- + +## Context + +Some catalog operations are slow or bursty: enriching products with derived attributes, processing and optimising images, and ingesting large supplier feeds. Running this work inline in the [[service|product-api]] request path would make API latency unpredictable and couple the responsiveness of create/update/read operations to whatever heavy job happens to be running. + +## Decision + +The Product Catalog System runs a dedicated [[service|product-worker]] for long-running and asynchronous tasks. The [[service|product-api]] handles validation, persistence and the outbox write synchronously, then returns; the worker picks up enrichment, media processing and bulk imports independently, reading from and writing back to the [[container|product-database]]. + +## Consequences + +- The Product API has a small, predictable surface and stays fast under load. +- Heavy work can be scaled, retried and scheduled independently of the API. +- Some product attributes are **eventually** populated after creation, so consumers must not assume every derived field is present immediately. +- There are now two services writing to the [[container|product-database]], so write paths must respect the same invariants and avoid conflicting updates. diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/containers/product-database/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/containers/product-database/index.mdx new file mode 100644 index 000000000..2e3a3ef5e --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/containers/product-database/index.mdx @@ -0,0 +1,45 @@ +--- +id: product-database +name: Product Database +version: 1.0.0 +summary: PostgreSQL database that is the system of record for all product data. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for products, plus the change outbox used to publish events +classification: internal +retention: indefinite +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **Product Database** is the authoritative store for every product in the catalog. The [[service|product-api]] reads from and writes to it, and the [[service|product-worker]] uses it for asynchronous enrichment. It also holds the **outbox** table that the [[service|product-search-publisher]] reads to publish product change events. + +### What does it store? + +- **Products** — one row per product: SKU, name, description, price, currency, category and lifecycle status. +- **Outbox** — one row per product change, used to reliably publish [[event|product-created]], [[event|product-updated]] and [[event|product-deleted]] events. + +### Schema + + + + + + + +### Access patterns + +- The [[service|product-api]] is the only writer to the `products` table on the request path. +- The [[service|product-search-publisher]] is a read-only consumer of the `outbox` table. +- The [[service|product-worker]] reads and writes for asynchronous enrichment jobs. + +### Why an outbox? + +Writing the product change and the event in the **same transaction** guarantees we never persist a change without an event, or publish an event for a change that rolled back. The publisher reads the outbox and emits events with at-least-once delivery. diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/containers/product-database/schema.sql b/examples/default/domains/Catalog/systems/product-catalog-system/containers/product-database/schema.sql new file mode 100644 index 000000000..ad212c2fe --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/containers/product-database/schema.sql @@ -0,0 +1,33 @@ +-- Product Database — system of record for the product catalog + +CREATE TABLE products ( + product_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + sku TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + description TEXT, + price_cents INTEGER NOT NULL CHECK (price_cents >= 0), + currency CHAR(3) NOT NULL, + category TEXT, + status TEXT NOT NULL DEFAULT 'DRAFT' + CHECK (status IN ('DRAFT', 'ACTIVE', 'ARCHIVED')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_products_category ON products (category); +CREATE INDEX idx_products_status ON products (status); + +-- Transactional outbox — change events are written here in the same +-- transaction as the product change, then drained by the Product Search Publisher. +CREATE TABLE outbox ( + id BIGSERIAL PRIMARY KEY, + product_id UUID NOT NULL REFERENCES products (product_id), + event_type TEXT NOT NULL + CHECK (event_type IN ('ProductCreated', 'ProductUpdated', 'ProductDeleted')), + payload JSONB NOT NULL, + occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), + published_at TIMESTAMPTZ +); + +-- Index used by the publisher to find unpublished changes in order. +CREATE INDEX idx_outbox_unpublished ON outbox (id) WHERE published_at IS NULL; diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/index.mdx new file mode 100644 index 000000000..d1edaeea2 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/index.mdx @@ -0,0 +1,72 @@ +--- +id: product-catalog-system +name: Product Catalog System +version: 1.0.0 +summary: | + Internal system that is the source of truth for product data. Owns the product database and publishes product change events for the rest of the business to consume. +owners: + - product-platform +services: + - id: product-api + - id: product-worker + - id: product-search-publisher +containers: + - id: product-database +relationships: + - id: search-system + label: notifies of product changes +actors: + - id: merchandiser + name: Merchandiser + label: creates and updates products + direction: inbound + - id: catalog-admin + name: Catalog Admin + label: manages categories and pricing + direction: inbound +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Product Catalog System** is the system of record for products at Acme Inc. It accepts commands to create, update and delete products, persists them in the [[container|product-database]], and emits domain events whenever product data changes so downstream systems — like the [[system|search-system]] — can react. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|product-api]] | Service | Public-facing API. Handles create/update/delete commands and product reads. | +| [[service\|product-worker]] | Service | Processes long-running and asynchronous catalog work off the request path. | +| [[service\|product-search-publisher]] | Service | Publishes product change events for the Search System to consume. | +| [[container\|product-database]] | Data store | PostgreSQL system of record for all product data. | + +## Messages this system publishes + +When product data changes, the system emits these events: + +- [[event|product-created]] — a new product has been added to the catalog. +- [[event|product-updated]] — an existing product's data has changed. +- [[event|product-deleted]] — a product has been removed from the catalog. +- [[adr|adr-001-use-transactional-outbox]] — a product has been removed from the catalog. + +## Architecture decisions + +The key decisions that shaped this system. Search, filter by status, or sort by clicking a column. + + diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/CreateProduct/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/CreateProduct/index.mdx new file mode 100644 index 000000000..d2b7eff42 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/CreateProduct/index.mdx @@ -0,0 +1,32 @@ +--- +id: create-product +name: Create Product +version: 1.0.0 +summary: | + Command to add a new product to the catalog. +owners: + - product-platform +schemaPath: schema.json +operation: + method: POST + path: /products + statusCodes: ['201', '400', '409'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CreateProduct` is handled by the [[service|product-api]]. It validates the incoming product, writes it to the [[container|product-database]], and on success publishes a [[event|product-created]] event. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/CreateProduct/schema.json b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/CreateProduct/schema.json new file mode 100644 index 000000000..e363e2ea2 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/CreateProduct/schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CreateProduct", + "description": "Command to add a new product to the catalog", + "type": "object", + "properties": { + "sku": { + "description": "Stock keeping unit — must be unique", + "type": "string" + }, + "name": { + "description": "Display name of the product", + "type": "string", + "minLength": 1 + }, + "description": { + "description": "Long-form product description", + "type": "string" + }, + "price": { + "description": "Price in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "currency": { + "description": "ISO 4217 currency code", + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "category": { + "description": "Category the product belongs to", + "type": "string" + }, + "status": { + "description": "Initial lifecycle status — defaults to DRAFT", + "type": "string", + "enum": ["DRAFT", "ACTIVE", "ARCHIVED"] + } + }, + "required": ["sku", "name", "price", "currency"] +} diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/DeleteProduct/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/DeleteProduct/index.mdx new file mode 100644 index 000000000..9188d64e6 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/DeleteProduct/index.mdx @@ -0,0 +1,32 @@ +--- +id: delete-product +name: Delete Product +version: 1.0.0 +summary: | + Command to remove a product from the catalog. +owners: + - product-platform +schemaPath: schema.json +operation: + method: DELETE + path: /products/{productId} + statusCodes: ['204', '404'] +sidebar: + badge: 'DELETE' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`DeleteProduct` is handled by the [[service|product-api]]. It removes a product from the [[container|product-database]] and, on success, publishes a [[event|product-deleted]] event so downstream systems can clean up. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/DeleteProduct/schema.json b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/DeleteProduct/schema.json new file mode 100644 index 000000000..97efc7c2d --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/DeleteProduct/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DeleteProduct", + "description": "Command to remove a product from the catalog", + "type": "object", + "properties": { + "productId": { + "description": "Unique identifier of the product to delete", + "type": "string", + "format": "uuid" + }, + "reason": { + "description": "Optional reason the product is being removed", + "type": "string", + "enum": ["DISCONTINUED", "DUPLICATE", "MERCHANT_REQUEST", "OTHER"] + } + }, + "required": ["productId"] +} diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/UpdateProduct/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/UpdateProduct/index.mdx new file mode 100644 index 000000000..abb07e52f --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/UpdateProduct/index.mdx @@ -0,0 +1,32 @@ +--- +id: update-product +name: Update Product +version: 1.0.0 +summary: | + Command to update an existing product in the catalog. +owners: + - product-platform +schemaPath: schema.json +operation: + method: PATCH + path: /products/{productId} + statusCodes: ['200', '400', '404'] +sidebar: + badge: 'PATCH' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`UpdateProduct` is handled by the [[service|product-api]]. It applies a partial update to an existing product in the [[container|product-database]] and, on success, publishes a [[event|product-updated]] event describing what changed. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/UpdateProduct/schema.json b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/UpdateProduct/schema.json new file mode 100644 index 000000000..5fcfac505 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/commands/UpdateProduct/schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UpdateProduct", + "description": "Command to update an existing product. Only the fields supplied are changed.", + "type": "object", + "properties": { + "productId": { + "description": "Unique identifier of the product to update", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "New display name", + "type": "string", + "minLength": 1 + }, + "description": { + "description": "New long-form description", + "type": "string" + }, + "price": { + "description": "New price in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "currency": { + "description": "ISO 4217 currency code", + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "category": { + "description": "New category", + "type": "string" + }, + "status": { + "description": "New lifecycle status", + "type": "string", + "enum": ["DRAFT", "ACTIVE", "ARCHIVED"] + } + }, + "required": ["productId"] +} diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductCreated/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductCreated/index.mdx new file mode 100644 index 000000000..088514800 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductCreated/index.mdx @@ -0,0 +1,29 @@ +--- +id: product-created +name: Product Created +version: 1.0.0 +summary: | + Published when a new product has been added to the catalog. +owners: + - product-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ProductCreated` is published by the [[service|product-search-publisher]] whenever a new product is successfully added to the catalog by the [[service|product-api]]. The [[system|search-system]] consumes this event to index the new product so it becomes discoverable. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductCreated/schema.json b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductCreated/schema.json new file mode 100644 index 000000000..25a1f8cd6 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductCreated/schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ProductCreated", + "description": "Emitted when a new product is added to the catalog", + "type": "object", + "properties": { + "eventId": { + "description": "Unique identifier for this event", + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "description": "Time the product was created", + "type": "string", + "format": "date-time" + }, + "product": { + "description": "The product that was created", + "type": "object", + "properties": { + "productId": { + "description": "Unique identifier for the product", + "type": "string", + "format": "uuid" + }, + "sku": { + "description": "Stock keeping unit", + "type": "string" + }, + "name": { + "description": "Display name of the product", + "type": "string" + }, + "description": { + "description": "Long-form product description", + "type": "string" + }, + "price": { + "description": "Price in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "currency": { + "description": "ISO 4217 currency code", + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "category": { + "description": "Category the product belongs to", + "type": "string" + }, + "status": { + "description": "Lifecycle status of the product", + "type": "string", + "enum": ["DRAFT", "ACTIVE", "ARCHIVED"] + } + }, + "required": ["productId", "sku", "name", "price", "currency", "status"] + } + }, + "required": ["eventId", "occurredAt", "product"] +} diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductDeleted/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductDeleted/index.mdx new file mode 100644 index 000000000..30473458e --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductDeleted/index.mdx @@ -0,0 +1,29 @@ +--- +id: product-deleted +name: Product Deleted +version: 1.0.0 +summary: | + Published when a product has been removed from the catalog. +owners: + - product-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ProductDeleted` is published by the [[service|product-search-publisher]] when a product is removed from the catalog via the [[service|product-api]]. The [[system|search-system]] consumes this event to remove the product from the search index. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductDeleted/schema.json b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductDeleted/schema.json new file mode 100644 index 000000000..26cc9a5dc --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductDeleted/schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ProductDeleted", + "description": "Emitted when a product is removed from the catalog", + "type": "object", + "properties": { + "eventId": { + "description": "Unique identifier for this event", + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "description": "Time the product was deleted", + "type": "string", + "format": "date-time" + }, + "productId": { + "description": "Unique identifier for the product that was deleted", + "type": "string", + "format": "uuid" + }, + "reason": { + "description": "Optional reason the product was removed", + "type": "string", + "enum": ["DISCONTINUED", "DUPLICATE", "MERCHANT_REQUEST", "OTHER"] + } + }, + "required": ["eventId", "occurredAt", "productId"] +} diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductUpdated/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductUpdated/index.mdx new file mode 100644 index 000000000..44e563b74 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductUpdated/index.mdx @@ -0,0 +1,29 @@ +--- +id: product-updated +name: Product Updated +version: 1.0.0 +summary: | + Published when an existing product's data has changed. +owners: + - product-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ProductUpdated` is published by the [[service|product-search-publisher]] whenever an existing product is changed via the [[service|product-api]]. The [[system|search-system]] consumes this event to re-index the product so search results stay accurate. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductUpdated/schema.json b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductUpdated/schema.json new file mode 100644 index 000000000..ce744bcb8 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/events/ProductUpdated/schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ProductUpdated", + "description": "Emitted when an existing product's data changes", + "type": "object", + "properties": { + "eventId": { + "description": "Unique identifier for this event", + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "description": "Time the product was updated", + "type": "string", + "format": "date-time" + }, + "productId": { + "description": "Unique identifier for the product that changed", + "type": "string", + "format": "uuid" + }, + "changes": { + "description": "The fields that changed and their new values", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "price": { + "description": "Price in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "currency": { + "description": "ISO 4217 currency code", + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "category": { + "type": "string" + }, + "status": { + "description": "Lifecycle status of the product", + "type": "string", + "enum": ["DRAFT", "ACTIVE", "ARCHIVED"] + } + }, + "minProperties": 1 + } + }, + "required": ["eventId", "occurredAt", "productId", "changes"] +} diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/index.mdx new file mode 100644 index 000000000..fa787bcd0 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/index.mdx @@ -0,0 +1,59 @@ +--- +id: product-api +version: 1.0.0 +name: Product API +summary: | + The public-facing API for the product catalog. Handles commands to create, update and delete products, serves product reads, and is the entry point into the Product Catalog System. +styles: + icon: /icons/languages/nodejs.svg +owners: + - product-platform +receives: + - id: create-product + version: 1.0.0 + - id: update-product + version: 1.0.0 + - id: delete-product + version: 1.0.0 + - id: get-product + version: 1.0.0 +writesTo: + - id: product-database +readsFrom: + - id: product-database +repository: + language: TypeScript + url: 'https://github.com/acme/product-api' +specifications: + - type: openapi + path: openapi.yml + name: Product API +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Product API** is the front door to the Product Catalog System. It validates incoming commands, persists product data to the [[container|product-database]], and records every change to the outbox. The [[service|product-search-publisher]] then turns those changes into the domain events the rest of the business consumes. + + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Command handling | Validates and applies [[command\|create-product]], [[command\|update-product]] and [[command\|delete-product]]. | +| Reads | Serves [[query\|get-product]] directly from the product database. | +| Change capture | Records every product change to the outbox in the [[container\|product-database]] for the [[service\|product-search-publisher]] to publish. | +| Persistence | Reads from and writes to the [[container\|product-database]] (system of record). | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/openapi.yml b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/openapi.yml new file mode 100644 index 000000000..695bebdd9 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/openapi.yml @@ -0,0 +1,219 @@ +openapi: 3.0.3 +info: + title: Product API + version: 1.0.0 + description: | + Public-facing API for the Product Catalog System. It is the entry point for + creating, updating, deleting and reading products. Every change is persisted + to the product database and recorded in the outbox so product change events + can be published to the rest of the business. + contact: + name: Product Platform + email: product-platform@acme.test +servers: + - url: https://api.acme.test + description: Production +tags: + - name: Products + description: Manage and read products in the catalog. +paths: + /products: + post: + operationId: createProduct + summary: Create a product + description: Adds a new product to the catalog. On success a `ProductCreated` event is published. + tags: + - Products + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProductRequest' + responses: + '201': + description: The product was created. + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + '400': + description: The request body was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: A product with the same SKU already exists. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /products/{productId}: + parameters: + - $ref: '#/components/parameters/ProductId' + get: + operationId: getProduct + summary: Get a product + description: Returns the current state of a single product by its identifier. + tags: + - Products + responses: + '200': + description: The product was found. + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + '404': + description: No product exists with that identifier. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + patch: + operationId: updateProduct + summary: Update a product + description: Applies a partial update to an existing product. On success a `ProductUpdated` event is published. + tags: + - Products + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProductRequest' + responses: + '200': + description: The product was updated. + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + '400': + description: The request body was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: No product exists with that identifier. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + operationId: deleteProduct + summary: Delete a product + description: Removes a product from the catalog. On success a `ProductDeleted` event is published. + tags: + - Products + parameters: + - name: reason + in: query + required: false + description: Optional reason the product is being removed. + schema: + type: string + enum: [DISCONTINUED, DUPLICATE, MERCHANT_REQUEST, OTHER] + responses: + '204': + description: The product was deleted. + '404': + description: No product exists with that identifier. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + parameters: + ProductId: + name: productId + in: path + required: true + description: Unique identifier of the product. + schema: + type: string + format: uuid + schemas: + Product: + type: object + required: [productId, sku, name, price, currency, status] + properties: + productId: + type: string + format: uuid + sku: + type: string + description: Stock keeping unit. + name: + type: string + description: + type: string + price: + type: integer + description: Price in minor units (e.g. cents). + minimum: 0 + currency: + type: string + pattern: '^[A-Z]{3}$' + category: + type: string + status: + type: string + enum: [DRAFT, ACTIVE, ARCHIVED] + CreateProductRequest: + type: object + required: [sku, name, price, currency] + properties: + sku: + type: string + description: Stock keeping unit — must be unique. + name: + type: string + minLength: 1 + description: + type: string + price: + type: integer + description: Price in minor units (e.g. cents). + minimum: 0 + currency: + type: string + pattern: '^[A-Z]{3}$' + category: + type: string + status: + type: string + description: Initial lifecycle status — defaults to DRAFT. + enum: [DRAFT, ACTIVE, ARCHIVED] + UpdateProductRequest: + type: object + description: Only the fields supplied are changed. + minProperties: 1 + properties: + name: + type: string + minLength: 1 + description: + type: string + price: + type: integer + description: Price in minor units (e.g. cents). + minimum: 0 + currency: + type: string + pattern: '^[A-Z]{3}$' + category: + type: string + status: + type: string + enum: [DRAFT, ACTIVE, ARCHIVED] + Error: + type: object + required: [code, message] + properties: + code: + type: string + message: + type: string diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/queries/GetProduct/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/queries/GetProduct/index.mdx new file mode 100644 index 000000000..8fec5b3d7 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/queries/GetProduct/index.mdx @@ -0,0 +1,32 @@ +--- +id: get-product +name: Get Product +version: 1.0.0 +summary: | + Query to fetch a single product by its identifier. +owners: + - product-platform +schemaPath: schema.json +operation: + method: GET + path: /products/{productId} + statusCodes: ['200', '404'] +sidebar: + badge: 'GET' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`GetProduct` is handled by the [[service|product-api]]. It returns the current state of a single product, read directly from the [[container|product-database]]. Use the [[system|search-system]] when you need to search across many products instead. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/queries/GetProduct/schema.json b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/queries/GetProduct/schema.json new file mode 100644 index 000000000..651038708 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductAPI/queries/GetProduct/schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetProduct", + "description": "Query to fetch a single product by its identifier, and the product returned", + "type": "object", + "properties": { + "request": { + "description": "Parameters used to look up the product", + "type": "object", + "properties": { + "productId": { + "description": "Unique identifier of the product to fetch", + "type": "string", + "format": "uuid" + } + }, + "required": ["productId"] + }, + "response": { + "description": "The product returned by the query", + "type": "object", + "properties": { + "productId": { + "type": "string", + "format": "uuid" + }, + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "price": { + "description": "Price in minor units (e.g. cents)", + "type": "integer" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "category": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["DRAFT", "ACTIVE", "ARCHIVED"] + } + }, + "required": ["productId", "sku", "name", "price", "currency", "status"] + } + }, + "required": ["request", "response"] +} diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductSearchPublisher/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductSearchPublisher/index.mdx new file mode 100644 index 000000000..548200ad1 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductSearchPublisher/index.mdx @@ -0,0 +1,51 @@ +--- +id: product-search-publisher +version: 1.0.0 +name: Product Search Publisher +summary: | + Reads product changes from the product database (outbox) and reliably publishes product-created, product-updated and product-deleted events for the Search System to consume. +styles: + icon: /icons/languages/go.svg +owners: + - product-platform +readsFrom: + - id: product-database +sends: + - id: product-created + version: 1.0.0 + - id: product-updated + version: 1.0.0 + - id: product-deleted + version: 1.0.0 +repository: + language: Go + url: 'https://github.com/acme/product-search-publisher' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Product Search Publisher** is the bridge between the Product Catalog System and the rest of the business. It uses the [transactional outbox pattern](https://microservices.io/patterns/data/transactional-outbox.html): the [[service|product-api]] records every product change in the [[container|product-database]], and this service reliably reads those changes and publishes them as [[event|product-created]], [[event|product-updated]] and [[event|product-deleted]] events. + +This guarantees that product changes are never lost and are delivered to the [[system|search-system]] in order. + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Outbox polling | Reads unpublished product changes from the [[container\|product-database]]. | +| Reliable publishing | Publishes [[event\|product-created]], [[event\|product-updated]] and [[event\|product-deleted]] with at-least-once delivery. | +| Ordering | Preserves per-product ordering of change events. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductWorker/index.mdx b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductWorker/index.mdx new file mode 100644 index 000000000..647b2bb30 --- /dev/null +++ b/examples/default/domains/Catalog/systems/product-catalog-system/services/ProductWorker/index.mdx @@ -0,0 +1,43 @@ +--- +id: product-worker +version: 1.0.0 +name: Product Worker +summary: | + Background worker that handles long-running and asynchronous catalog work off the request path, such as enrichment, image processing and bulk imports. +styles: + icon: /icons/languages/go.svg +owners: + - product-platform +readsFrom: + - id: product-database +writesTo: + - id: product-database +repository: + language: Go + url: 'https://github.com/acme/product-worker' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Product Worker** keeps the [[service|product-api]] fast by taking slow or asynchronous work off the request path. It reads from and writes back to the [[container|product-database]], handling jobs like data enrichment, image processing and bulk catalog imports. + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Enrichment | Augments product records with derived data after they are created or updated. | +| Media | Processes and optimises product images asynchronously. | +| Bulk imports | Ingests large product feeds without blocking the API. | +| Persistence | Reads from and writes to the [[container\|product-database]]. | + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Catalog/systems/search-system/containers/search-index/index.mdx b/examples/default/domains/Catalog/systems/search-system/containers/search-index/index.mdx new file mode 100644 index 000000000..7efe54a0c --- /dev/null +++ b/examples/default/domains/Catalog/systems/search-system/containers/search-index/index.mdx @@ -0,0 +1,39 @@ +--- +id: search-index +name: Search Index +version: 1.0.0 +summary: The denormalised, search-optimised index of products that powers product search. +container_type: searchIndex +technology: opensearch@2 +authoritative: false +access_mode: readWrite +purpose: Serve fast, relevant product search +classification: internal +retention: rebuildable +residency: eu-west-1 +styles: + icon: /icons/analytics/clickhouse.svg +--- + + + +### What is this? + +The **Search Index** is a denormalised, search-optimised copy of the catalog. It is **not** a source of truth — it can be rebuilt at any time by replaying product events from the [[system|product-catalog-system]]. The [[service|search-indexer]] keeps it up to date, and the [[service|search-api]] reads from it to answer [[query|search-products]] queries. + +### What does it store? + +One document per product, optimised for search: + +- Searchable text — name and description, analysed for full-text matching. +- Filterable fields — category, status, price, currency. +- A relevance signal used to rank results. + +### Why is it not authoritative? + +The system of record is the [[container|product-database]]. The index is a derived read model: if it is ever lost or corrupted, we rebuild it by replaying [[event|product-created]], [[event|product-updated]] and [[event|product-deleted]] events. This keeps search fast without coupling other teams to the catalog database. + +### Operational notes + +- **Rebuildable**: a full reindex is a supported, routine operation. +- **Eventually consistent**: changes appear in search shortly after the [[service|search-indexer]] processes the corresponding event. diff --git a/examples/default/domains/Catalog/systems/search-system/flows/ProductSearchIndexing/index.mdx b/examples/default/domains/Catalog/systems/search-system/flows/ProductSearchIndexing/index.mdx new file mode 100644 index 000000000..1e282fa6f --- /dev/null +++ b/examples/default/domains/Catalog/systems/search-system/flows/ProductSearchIndexing/index.mdx @@ -0,0 +1,130 @@ +--- +id: product-search-indexing +name: Product Search Indexing +version: 1.0.0 +summary: | + How a change to a product in the catalog reliably flows into the search index — via the outbox, product events and the indexer — so search stays in sync without coupling to the catalog database. +owners: + - search-platform +steps: + - id: merchant_edits_product + title: Merchant edits a product + actor: + name: Merchant + summary: A merchant creates or updates a product in the catalog. + next_step: + id: product_api + label: Apply change + + - id: product_api + title: Product API + service: + id: product-api + version: 1.0.0 + next_step: + id: product_database + label: Write change + outbox row + + - id: product_database + title: Product Database + container: + id: product-database + next_step: + id: outbox_note + label: Outbox row committed + + - id: outbox_note + title: Outbox pattern + custom: + title: Transactional outbox + color: blue + icon: InboxStackIcon + type: Pattern + summary: The product change and an outbox row are written in the same transaction, so the event can never be lost or out of sync with the data. + properties: + guarantee: 'change and event are atomic' + next_step: + id: product_search_publisher + label: Drain outbox + + - id: product_search_publisher + title: Product Search Publisher + service: + id: product-search-publisher + version: 1.0.0 + next_steps: + - id: product_created + label: Created + - id: product_updated + label: Updated + - id: product_deleted + label: Deleted + + - id: product_created + title: Product Created + message: + id: product-created + version: 1.0.0 + next_step: + id: search_indexer + label: Index product + + - id: product_updated + title: Product Updated + message: + id: product-updated + version: 1.0.0 + next_step: + id: search_indexer + label: Re-index product + + - id: product_deleted + title: Product Deleted + message: + id: product-deleted + version: 1.0.0 + next_step: + id: search_indexer + label: Remove from index + + - id: search_indexer + title: Search Indexer + service: + id: search-indexer + version: 1.0.0 + next_step: + id: search_index + label: Apply to index + + - id: search_index + title: Search Index + container: + id: search-index + next_step: + id: searchable + label: Available to search + + - id: searchable + title: Product is searchable + custom: + title: Product is searchable + color: green + icon: MagnifyingGlassIcon + type: Outcome + summary: The change is reflected in the search index and immediately available to customers via the Search API. + properties: + read_model: 'derived, rebuildable from events' +--- + +## Overview + +**Product Search Indexing** documents how the [[system|search-system]] keeps its index in sync with the catalog. The search index is a derived read model — never a source of truth — so it can be rebuilt at any time by replaying product events. + + + +## How it works + +1. A merchant changes a product via the [[service|product-api]], which writes the change and an outbox row to the [[container|product-database]] in one transaction. +2. The [[service|product-search-publisher]] reliably drains the outbox and publishes [[event|product-created]], [[event|product-updated]] or [[event|product-deleted]]. +3. The [[service|search-indexer]] consumes those events and applies them to the [[container|search-index]]. +4. The change is now searchable via the Search API — with no direct coupling between search and the catalog database. diff --git a/examples/default/domains/Catalog/systems/search-system/index.mdx b/examples/default/domains/Catalog/systems/search-system/index.mdx new file mode 100644 index 000000000..ff89df792 --- /dev/null +++ b/examples/default/domains/Catalog/systems/search-system/index.mdx @@ -0,0 +1,66 @@ +--- +id: search-system +name: Search System +version: 1.0.0 +summary: | + Internal system that keeps product data searchable. Consumes product change events from the Product Catalog System, maintains a search index, and serves fast product search to the rest of the business. +owners: + - search-platform +services: + - id: search-api + - id: search-indexer +containers: + - id: search-index +flows: + - id: product-search-indexing + version: 1.0.0 +relationships: + - id: product-catalog-system + label: indexes product changes from +actors: + - id: shopper + name: Shopper + label: searches for products + direction: inbound + - id: storefront-team + name: Storefront Team + label: integrates product search + direction: inbound +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Search System** makes the catalog discoverable. It subscribes to product change events published by the [[system|product-catalog-system]], updates its [[container|search-index]], and exposes a search API so other teams can find products without ever touching the catalog database directly. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|search-api]] | Service | Public-facing API. Serves product search queries from the index. | +| [[service\|search-indexer]] | Service | Consumes product change events and keeps the search index up to date. | +| [[container\|search-index]] | Data store | The index that powers product search. | + +## Messages this system consumes + +The Search System reacts to product changes published by the Product Catalog System: + +- [[event|product-created]] — index the new product. +- [[event|product-updated]] — re-index the changed product. +- [[event|product-deleted]] — remove the product from the index. diff --git a/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/index.mdx b/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/index.mdx new file mode 100644 index 000000000..24898ef4c --- /dev/null +++ b/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/index.mdx @@ -0,0 +1,44 @@ +--- +id: search-api +version: 1.0.0 +name: Search API +summary: | + Public-facing API that serves fast, relevant product search to the rest of the business, reading from the search index. +styles: + icon: /icons/languages/nodejs.svg +owners: + - search-platform +receives: + - id: search-products + version: 1.0.0 +readsFrom: + - id: search-index +repository: + language: TypeScript + url: 'https://github.com/acme/search-api' +specifications: + - type: openapi + path: openapi.yml + name: Search API +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Search API** is the front door to the Search System. It answers [[query|search-products]] queries by reading from the [[container|search-index]], which is kept current by the [[service|search-indexer]]. Other teams use this API so they never have to query the catalog database directly. + +### Responsibilities + +| Area | Description | +|------|-------------| +| Search | Serves [[query\|search-products]] with full-text matching, filtering and pagination. | +| Reads | Queries the [[container\|search-index]] — never the catalog database. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/openapi.yml b/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/openapi.yml new file mode 100644 index 000000000..d90532519 --- /dev/null +++ b/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/openapi.yml @@ -0,0 +1,138 @@ +openapi: 3.0.3 +info: + title: Search API + version: 1.0.0 + description: | + Public-facing API for the Search System. Serves fast, relevant product + search by reading from the search index, which is kept in sync with the + product catalog. + contact: + name: Search Platform + email: search-platform@acme.test +servers: + - url: https://api.acme.test + description: Production +paths: + /search/products: + get: + operationId: searchProducts + summary: Search the catalog for products + description: | + Runs a full-text search over the product index and returns a paginated + list of matching products. Supports optional filters and pagination. + tags: + - Search + parameters: + - name: query + in: query + required: true + description: Free-text search term. + schema: + type: string + minLength: 1 + example: wireless headphones + - name: category + in: query + required: false + description: Restrict results to a single category. + schema: + type: string + example: audio + - name: status + in: query + required: false + description: Restrict results to products with this lifecycle status. + schema: + type: string + enum: [DRAFT, ACTIVE, ARCHIVED] + - name: minPrice + in: query + required: false + description: Minimum price in minor units (e.g. cents). + schema: + type: integer + minimum: 0 + - name: maxPrice + in: query + required: false + description: Maximum price in minor units (e.g. cents). + schema: + type: integer + minimum: 0 + - name: page + in: query + required: false + description: 1-based page number. + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + required: false + description: Number of results per page. + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + responses: + '200': + description: A paginated list of matching products. + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResults' + '400': + description: The request was invalid (e.g. a missing or empty query). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + SearchResults: + type: object + required: [total, page, pageSize, results] + properties: + total: + type: integer + description: Total number of products matching the query. + page: + type: integer + pageSize: + type: integer + results: + type: array + items: + $ref: '#/components/schemas/ProductSearchResult' + ProductSearchResult: + type: object + required: [productId, sku, name] + properties: + productId: + type: string + format: uuid + sku: + type: string + name: + type: string + category: + type: string + price: + type: integer + description: Price in minor units (e.g. cents). + currency: + type: string + pattern: '^[A-Z]{3}$' + score: + type: number + description: Relevance score for this result. + Error: + type: object + required: [code, message] + properties: + code: + type: string + message: + type: string diff --git a/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/queries/SearchProducts/index.mdx b/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/queries/SearchProducts/index.mdx new file mode 100644 index 000000000..49184611b --- /dev/null +++ b/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/queries/SearchProducts/index.mdx @@ -0,0 +1,32 @@ +--- +id: search-products +name: Search Products +version: 1.0.0 +summary: | + Query to search the catalog for products matching a term, with optional filters and pagination. +owners: + - search-platform +schemaPath: schema.json +operation: + method: GET + path: /search/products + statusCodes: ['200', '400'] +sidebar: + badge: 'GET' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`SearchProducts` is handled by the [[service|search-api]]. It runs a full-text search over the [[container|search-index]] and returns a paginated list of matching products. Use this whenever you need to find products across the catalog, rather than fetching a single product by id with [[query|get-product]]. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/queries/SearchProducts/schema.json b/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/queries/SearchProducts/schema.json new file mode 100644 index 000000000..d9eea41f8 --- /dev/null +++ b/examples/default/domains/Catalog/systems/search-system/services/SearchAPI/queries/SearchProducts/schema.json @@ -0,0 +1,108 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SearchProducts", + "description": "Query to search the catalog for products, and the paginated result returned", + "type": "object", + "properties": { + "request": { + "description": "Search parameters", + "type": "object", + "properties": { + "query": { + "description": "Free-text search term", + "type": "string", + "minLength": 1 + }, + "filters": { + "description": "Optional filters applied to the search", + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["DRAFT", "ACTIVE", "ARCHIVED"] + }, + "minPrice": { + "description": "Minimum price in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "maxPrice": { + "description": "Maximum price in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + } + } + }, + "page": { + "description": "1-based page number", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "pageSize": { + "description": "Number of results per page", + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + } + }, + "required": ["query"] + }, + "response": { + "description": "Paginated search results", + "type": "object", + "properties": { + "total": { + "description": "Total number of products matching the query", + "type": "integer" + }, + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "format": "uuid" + }, + "sku": { + "type": "string" + }, + "name": { + "type": "string" + }, + "category": { + "type": "string" + }, + "price": { + "description": "Price in minor units (e.g. cents)", + "type": "integer" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "score": { + "description": "Relevance score for this result", + "type": "number" + } + }, + "required": ["productId", "sku", "name"] + } + } + }, + "required": ["total", "page", "pageSize", "results"] + } + }, + "required": ["request", "response"] +} diff --git a/examples/default/domains/Catalog/systems/search-system/services/SearchIndexer/index.mdx b/examples/default/domains/Catalog/systems/search-system/services/SearchIndexer/index.mdx new file mode 100644 index 000000000..3c2a7f859 --- /dev/null +++ b/examples/default/domains/Catalog/systems/search-system/services/SearchIndexer/index.mdx @@ -0,0 +1,49 @@ +--- +id: search-indexer +version: 1.0.0 +name: Search Indexer +summary: | + Consumes product change events from the Product Catalog System and keeps the search index up to date so products are discoverable. +styles: + icon: /icons/languages/go.svg +owners: + - search-platform +receives: + - id: product-created + version: 1.0.0 + - id: product-updated + version: 1.0.0 + - id: product-deleted + version: 1.0.0 +writesTo: + - id: search-index +repository: + language: Go + url: 'https://github.com/acme/search-indexer' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Search Indexer** is how the Search System stays in sync with the catalog. It subscribes to [[event|product-created]], [[event|product-updated]] and [[event|product-deleted]] events published by the [[system|product-catalog-system]] and applies each change to the [[container|search-index]]. + + + + + +### Responsibilities + +| Event | Action | +|-------|--------| +| [[event\|product-created]] | Add the new product to the [[container\|search-index]]. | +| [[event\|product-updated]] | Re-index the changed product. | +| [[event\|product-deleted]] | Remove the product from the index. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Catalog/ubiquitous-language.mdx b/examples/default/domains/Catalog/ubiquitous-language.mdx new file mode 100644 index 000000000..60b5ae7d4 --- /dev/null +++ b/examples/default/domains/Catalog/ubiquitous-language.mdx @@ -0,0 +1,54 @@ +--- +dictionary: + - id: Product + name: Product + summary: "A single item Acme Inc sells, with a name, price, category and lifecycle status." + description: | + A product is the central concept of the Catalog domain. It is owned and maintained by the + Product Catalog System, which is the source of truth for all product data. A product carries: + + - A unique product id and a unique SKU + - Display name, description and category + - Price (in minor units) and currency + - A lifecycle status (DRAFT, ACTIVE or ARCHIVED) + + Every change to a product publishes a domain event so the rest of the business — including + search — can react. + icon: Package + - id: SKU + name: SKU + summary: "Stock Keeping Unit — the unique, human-meaningful code that identifies a product." + description: | + A SKU uniquely identifies a product in the catalog and must be unique across all products. + Unlike the internal product id (a UUID), the SKU is the code teams and merchants use day to day. + icon: Tag + - id: Catalog + name: Catalog + summary: "The complete, authoritative collection of products Acme Inc offers." + icon: BookOpen + - id: Search Index + name: Search Index + summary: "A denormalised, search-optimised copy of the catalog that powers product search." + description: | + The search index is a derived read model, not a source of truth. It is kept in sync by the + Search System consuming product change events, and can be rebuilt at any time by replaying + those events. It exists to serve fast, relevant search without coupling consumers to the + catalog database. + icon: Search + - id: Lifecycle Status + name: Lifecycle Status + summary: "Where a product sits in its life: DRAFT, ACTIVE or ARCHIVED." + description: | + - DRAFT — being prepared, not yet visible to customers. + - ACTIVE — live and discoverable. + - ARCHIVED — withdrawn from sale but retained for history. + icon: Workflow + - id: Outbox + name: Outbox + summary: "A table where product changes are recorded in the same transaction, then published as events." + description: | + The outbox is how the Product Catalog System guarantees that a product change and its event + are never out of sync. The Product API writes the change and an outbox row in one transaction; + the Product Search Publisher reliably drains the outbox to the broker. + icon: Mail +--- diff --git a/examples/default/domains/Customer/entities/authentication-session/index.mdx b/examples/default/domains/Customer/entities/authentication-session/index.mdx new file mode 100644 index 000000000..967bdbf94 --- /dev/null +++ b/examples/default/domains/Customer/entities/authentication-session/index.mdx @@ -0,0 +1,35 @@ +--- +id: authentication-session +name: Authentication Session +version: 1.0.0 +identifier: sessionId +summary: A successful customer authentication session. +owners: + - customer-platform +properties: + - name: sessionId + type: UUID + required: true + description: Unique authentication session identifier. + - name: accountId + type: UUID + required: true + description: Account authenticated in the session. + references: customer-account + relationType: belongsTo + referencesIdentifier: accountId + - name: authenticatedAt + type: datetime + required: true + description: Time authentication succeeded. + - name: expiresAt + type: datetime + required: true + description: Time the session expires. +--- + +## Overview + +Authentication Session represents successful login behavior emitted by [[event|customer-authenticated]]. + + diff --git a/examples/default/domains/Customer/entities/customer-account/index.mdx b/examples/default/domains/Customer/entities/customer-account/index.mdx new file mode 100644 index 000000000..032cd7b1a --- /dev/null +++ b/examples/default/domains/Customer/entities/customer-account/index.mdx @@ -0,0 +1,36 @@ +--- +id: customer-account +name: Customer Account +version: 1.0.0 +identifier: accountId +aggregateRoot: true +summary: The identity account used to authenticate a customer. +owners: + - customer-platform +properties: + - name: accountId + type: UUID + required: true + description: Unique account identifier. + - name: customerId + type: UUID + required: true + description: Customer profile linked to this account. + references: customer-profile + relationType: belongsTo + referencesIdentifier: customerId + - name: provider + type: string + required: true + description: Identity provider name. + - name: status + type: string + required: true + description: Account lifecycle state. +--- + +## Overview + +Customer Account belongs to the Identity Provider boundary and should not be queried directly by order, payment or review services. + + diff --git a/examples/default/domains/Customer/entities/customer-address/index.mdx b/examples/default/domains/Customer/entities/customer-address/index.mdx new file mode 100644 index 000000000..cc2b0fa83 --- /dev/null +++ b/examples/default/domains/Customer/entities/customer-address/index.mdx @@ -0,0 +1,35 @@ +--- +id: customer-address +name: Customer Address +version: 1.0.0 +identifier: addressId +summary: A saved address attached to a customer profile. +owners: + - customer-platform +properties: + - name: addressId + type: UUID + required: true + description: Unique address identifier. + - name: customerId + type: UUID + required: true + description: Customer who owns the address. + references: customer-profile + relationType: belongsTo + referencesIdentifier: customerId + - name: countryCode + type: string + required: true + description: ISO country code. + - name: postalCode + type: string + required: true + description: Postal or ZIP code. +--- + +## Overview + +Customer Address supports checkout and fulfilment while remaining owned by the Customer domain. + + diff --git a/examples/default/domains/Customer/entities/customer-profile/index.mdx b/examples/default/domains/Customer/entities/customer-profile/index.mdx new file mode 100644 index 000000000..6cf87760e --- /dev/null +++ b/examples/default/domains/Customer/entities/customer-profile/index.mdx @@ -0,0 +1,33 @@ +--- +id: customer-profile +name: Customer Profile +version: 1.0.0 +identifier: customerId +aggregateRoot: true +summary: The customer details Acme uses for commerce interactions. +owners: + - customer-platform +properties: + - name: customerId + type: UUID + required: true + description: Unique customer identifier. + - name: email + type: string + required: true + description: Customer email address. + - name: displayName + type: string + required: false + description: Preferred customer display name. + - name: status + type: string + required: true + description: Customer profile state. +--- + +## Overview + +Customer Profile is owned by [[system|customer-management-system]] and is separate from authentication credentials. + + diff --git a/examples/default/domains/Customer/index.mdx b/examples/default/domains/Customer/index.mdx new file mode 100644 index 000000000..ba85455db --- /dev/null +++ b/examples/default/domains/Customer/index.mdx @@ -0,0 +1,67 @@ +--- +id: customer +name: Customer +version: 1.0.0 +summary: | + The Customer domain owns who our customers are — how they register, how their profile is maintained, and how they are authenticated across Acme Inc. It is the source of truth for customer identity and profile data. +owners: + - customer-platform +systems: + - id: customer-management-system + version: 1.0.0 + - id: identity-provider + version: 1.0.0 +entities: + - id: customer-profile + version: 1.0.0 + - id: customer-account + version: 1.0.0 + - id: authentication-session + version: 1.0.0 + - id: customer-address + version: 1.0.0 +badges: + - content: Core domain + backgroundColor: blue + textColor: blue + icon: RectangleGroupIcon + - content: Business Critical + backgroundColor: red + textColor: red + icon: ShieldCheckIcon +--- + +## Overview + +The **Customer** domain is responsible for the lifecycle of every customer account at Acme Inc — registration, profile updates, and authentication. It is split into two systems: + + + + + + +### How the systems work together + +The **Customer Management System** owns customer profile data — it accepts registration and update commands and publishes events when customers change. The **Identity Provider** handles authentication: it verifies credentials and emits an event when a customer successfully authenticates, which the rest of the business can react to. + +## System Diagram + +The systems in this domain, how they relate, and the people who interact with them. + + + +## Resource Diagram + +The components that make up this domain. + + diff --git a/examples/default/domains/Customer/systems/customer-management-system/containers/customer-database/index.mdx b/examples/default/domains/Customer/systems/customer-management-system/containers/customer-database/index.mdx new file mode 100644 index 000000000..f080e0dce --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/containers/customer-database/index.mdx @@ -0,0 +1,43 @@ +--- +id: customer-database +name: Customer Database +version: 1.0.0 +summary: PostgreSQL database that is the system of record for all customer profile data. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for customer profiles +classification: confidential +retention: indefinite +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **Customer Database** is the authoritative store for every customer profile at Acme Inc. The [[service|customer-api]] reads from and writes to it. It stores profile data only — credentials and authentication live in the [[system|identity-provider]]'s [[container|user-directory]], not here. + +### What does it store? + +- **Customers** — one row per customer: id, email, name and account status. + +### Schema + + + + + + + +### Access patterns + +- The [[service|customer-api]] is the only writer on the request path. +- No credentials or passwords are stored here — only profile data. Authentication is owned by the [[system|identity-provider]]. + +### Classification + +Customer profile data is **confidential**. Access is role-based and least-privilege; PII handling follows Acme's data policies. diff --git a/examples/default/domains/Customer/systems/customer-management-system/containers/customer-database/schema.sql b/examples/default/domains/Customer/systems/customer-management-system/containers/customer-database/schema.sql new file mode 100644 index 000000000..dc7cb2851 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/containers/customer-database/schema.sql @@ -0,0 +1,14 @@ +-- Customer Database — system of record for customer profiles +-- Note: credentials/authentication are owned by the Identity Provider, not stored here. + +CREATE TABLE customers ( + customer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT NOT NULL UNIQUE, + name TEXT, + status TEXT NOT NULL DEFAULT 'ACTIVE' + CHECK (status IN ('ACTIVE', 'SUSPENDED', 'CLOSED')), + registered_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_customers_status ON customers (status); diff --git a/examples/default/domains/Customer/systems/customer-management-system/index.mdx b/examples/default/domains/Customer/systems/customer-management-system/index.mdx new file mode 100644 index 000000000..1ac794923 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/index.mdx @@ -0,0 +1,60 @@ +--- +id: customer-management-system +name: Customer Management System +version: 1.0.0 +summary: | + Internal system that is the source of truth for customer profile data. Owns the customer database and publishes customer change events for the rest of the business to consume. +owners: + - customer-platform +services: + - id: customer-api +containers: + - id: customer-database +relationships: + - id: identity-provider + label: delegates authentication to +actors: + - id: customer + name: Customer + label: registers and updates their profile + direction: inbound + - id: support-agent + name: Support Agent + label: looks up customer details + direction: inbound +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Customer Management System** is the system of record for customer profiles at Acme Inc. It accepts commands to register and update customers, persists them in the [[container|customer-database]], and emits domain events whenever customer data changes so downstream systems can react. It delegates authentication to the [[system|identity-provider]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|customer-api]] | Service | Public-facing API. Handles register/update commands and customer reads, and publishes customer change events. | +| [[container\|customer-database]] | Data store | PostgreSQL system of record for all customer profile data. | + +## Messages this system publishes + +When customer data changes, the system emits these events: + +- [[event|customer-registered]] — a new customer has registered. +- [[event|customer-updated]] — an existing customer's profile has changed. diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/RegisterCustomer/index.mdx b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/RegisterCustomer/index.mdx new file mode 100644 index 000000000..e63124576 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/RegisterCustomer/index.mdx @@ -0,0 +1,32 @@ +--- +id: register-customer +name: Register Customer +version: 1.0.0 +summary: | + Command to register a new customer. +owners: + - customer-platform +schemaPath: schema.json +operation: + method: POST + path: /customers + statusCodes: ['201', '400', '409'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`RegisterCustomer` is handled by the [[service|customer-api]]. It validates the new customer, writes it to the [[container|customer-database]], and on success publishes a [[event|customer-registered]] event. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/RegisterCustomer/schema.json b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/RegisterCustomer/schema.json new file mode 100644 index 000000000..91d5fc594 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/RegisterCustomer/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RegisterCustomer", + "description": "Command to register a new customer", + "type": "object", + "properties": { + "email": { + "description": "Customer's email address — must be unique", + "type": "string", + "format": "email" + }, + "name": { + "description": "Customer's full name", + "type": "string", + "minLength": 1 + }, + "password": { + "description": "Initial password for the customer's credentials", + "type": "string", + "minLength": 8 + } + }, + "required": ["email", "password"] +} diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/UpdateCustomer/index.mdx b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/UpdateCustomer/index.mdx new file mode 100644 index 000000000..136714bb2 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/UpdateCustomer/index.mdx @@ -0,0 +1,32 @@ +--- +id: update-customer +name: Update Customer +version: 1.0.0 +summary: | + Command to update an existing customer's profile. +owners: + - customer-platform +schemaPath: schema.json +operation: + method: PATCH + path: /customers/{customerId} + statusCodes: ['200', '400', '404'] +sidebar: + badge: 'PATCH' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`UpdateCustomer` is handled by the [[service|customer-api]]. It applies a partial update to an existing customer in the [[container|customer-database]] and, on success, publishes a [[event|customer-updated]] event describing what changed. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/UpdateCustomer/schema.json b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/UpdateCustomer/schema.json new file mode 100644 index 000000000..a9360cea1 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/commands/UpdateCustomer/schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UpdateCustomer", + "description": "Command to update an existing customer. Only the fields supplied are changed.", + "type": "object", + "properties": { + "customerId": { + "description": "Unique identifier of the customer to update", + "type": "string", + "format": "uuid" + }, + "email": { + "description": "New email address", + "type": "string", + "format": "email" + }, + "name": { + "description": "New full name", + "type": "string", + "minLength": 1 + }, + "status": { + "description": "New account status", + "type": "string", + "enum": ["ACTIVE", "SUSPENDED", "CLOSED"] + } + }, + "required": ["customerId"] +} diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerRegistered/index.mdx b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerRegistered/index.mdx new file mode 100644 index 000000000..b2b9adf91 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerRegistered/index.mdx @@ -0,0 +1,29 @@ +--- +id: customer-registered +name: Customer Registered +version: 1.0.0 +summary: | + Published when a new customer has registered. +owners: + - customer-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CustomerRegistered` is published by the [[service|customer-api]] whenever a new customer successfully registers. Downstream systems consume this event to onboard the customer. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerRegistered/schema.json b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerRegistered/schema.json new file mode 100644 index 000000000..b9e1d6503 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerRegistered/schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CustomerRegistered", + "description": "Emitted when a new customer registers", + "type": "object", + "properties": { + "eventId": { + "description": "Unique identifier for this event", + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "description": "Time the customer registered", + "type": "string", + "format": "date-time" + }, + "customer": { + "description": "The customer that registered", + "type": "object", + "properties": { + "customerId": { + "description": "Unique identifier for the customer", + "type": "string", + "format": "uuid" + }, + "email": { + "description": "Customer's email address", + "type": "string", + "format": "email" + }, + "name": { + "description": "Customer's full name", + "type": "string" + }, + "status": { + "description": "Lifecycle status of the customer account", + "type": "string", + "enum": ["ACTIVE", "SUSPENDED", "CLOSED"] + } + }, + "required": ["customerId", "email", "status"] + } + }, + "required": ["eventId", "occurredAt", "customer"] +} diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerUpdated/index.mdx b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerUpdated/index.mdx new file mode 100644 index 000000000..fee2d39e7 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerUpdated/index.mdx @@ -0,0 +1,29 @@ +--- +id: customer-updated +name: Customer Updated +version: 1.0.0 +summary: | + Published when an existing customer's profile has changed. +owners: + - customer-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CustomerUpdated` is published by the [[service|customer-api]] whenever an existing customer's profile changes. Downstream systems consume this event to keep their copy of customer data in sync. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerUpdated/schema.json b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerUpdated/schema.json new file mode 100644 index 000000000..40069594f --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/events/CustomerUpdated/schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CustomerUpdated", + "description": "Emitted when an existing customer's profile changes", + "type": "object", + "properties": { + "eventId": { + "description": "Unique identifier for this event", + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "description": "Time the customer was updated", + "type": "string", + "format": "date-time" + }, + "customerId": { + "description": "Unique identifier for the customer that changed", + "type": "string", + "format": "uuid" + }, + "changes": { + "description": "The fields that changed and their new values", + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["ACTIVE", "SUSPENDED", "CLOSED"] + } + }, + "minProperties": 1 + } + }, + "required": ["eventId", "occurredAt", "customerId", "changes"] +} diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/index.mdx b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/index.mdx new file mode 100644 index 000000000..7c4831bd0 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/index.mdx @@ -0,0 +1,62 @@ +--- +id: customer-api +version: 1.0.0 +name: Customer API +summary: | + The public-facing API for customer profiles. Handles commands to register and update customers, serves customer reads, and publishes customer change events. +styles: + icon: /icons/languages/nodejs.svg +owners: + - customer-platform +receives: + - id: register-customer + version: 1.0.0 + - id: update-customer + version: 1.0.0 + - id: get-customer + version: 1.0.0 +sends: + - id: customer-registered + version: 1.0.0 + - id: customer-updated + version: 1.0.0 +writesTo: + - id: customer-database +readsFrom: + - id: customer-database +repository: + language: TypeScript + url: 'https://github.com/acme/customer-api' +specifications: + - type: openapi + path: openapi.yml + name: Customer API +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Customer API** is the front door to the Customer Management System. It validates incoming commands, persists customer data to the [[container|customer-database]], and publishes domain events (CustomerRegistered, CustomerUpdated) so the rest of the business can react to changes. + + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Command handling | Validates and applies [[command\|register-customer]] and [[command\|update-customer]]. | +| Reads | Serves [[query\|get-customer]] directly from the customer database. | +| Event publishing | Emits [[event\|customer-registered]] and [[event\|customer-updated]] on every change. | +| Persistence | Reads from and writes to the [[container\|customer-database]] (system of record). | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/openapi.yml b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/openapi.yml new file mode 100644 index 000000000..40f6aeb04 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/openapi.yml @@ -0,0 +1,164 @@ +openapi: 3.0.3 +info: + title: Customer API + version: 1.0.0 + description: | + Public-facing API for the Customer Management System. It is the entry point + for registering, updating and reading customers. Every change is persisted + to the customer database and published as a customer change event. + contact: + name: Customer Platform + email: customer-platform@acme.test +servers: + - url: https://api.acme.test + description: Production +tags: + - name: Customers + description: Manage and read customer profiles. +paths: + /customers: + post: + operationId: registerCustomer + summary: Register a customer + description: Registers a new customer. On success a `CustomerRegistered` event is published. + tags: + - Customers + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterCustomerRequest' + responses: + '201': + description: The customer was registered. + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + '400': + description: The request body was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: A customer with the same email already exists. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /customers/{customerId}: + parameters: + - name: customerId + in: path + required: true + description: Unique identifier of the customer. + schema: + type: string + format: uuid + get: + operationId: getCustomer + summary: Get a customer + description: Returns the current state of a single customer by their identifier. + tags: + - Customers + responses: + '200': + description: The customer was found. + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + '404': + description: No customer exists with that identifier. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + patch: + operationId: updateCustomer + summary: Update a customer + description: Applies a partial update to an existing customer. On success a `CustomerUpdated` event is published. + tags: + - Customers + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateCustomerRequest' + responses: + '200': + description: The customer was updated. + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + '400': + description: The request body was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: No customer exists with that identifier. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Customer: + type: object + required: [customerId, email, status] + properties: + customerId: + type: string + format: uuid + email: + type: string + format: email + name: + type: string + status: + type: string + enum: [ACTIVE, SUSPENDED, CLOSED] + registeredAt: + type: string + format: date-time + RegisterCustomerRequest: + type: object + required: [email, password] + properties: + email: + type: string + format: email + name: + type: string + minLength: 1 + password: + type: string + minLength: 8 + UpdateCustomerRequest: + type: object + description: Only the fields supplied are changed. + minProperties: 1 + properties: + email: + type: string + format: email + name: + type: string + minLength: 1 + status: + type: string + enum: [ACTIVE, SUSPENDED, CLOSED] + Error: + type: object + required: [code, message] + properties: + code: + type: string + message: + type: string diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/queries/GetCustomer/index.mdx b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/queries/GetCustomer/index.mdx new file mode 100644 index 000000000..02094e07d --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/queries/GetCustomer/index.mdx @@ -0,0 +1,32 @@ +--- +id: get-customer +name: Get Customer +version: 1.0.0 +summary: | + Query to fetch a single customer by their identifier. +owners: + - customer-platform +schemaPath: schema.json +operation: + method: GET + path: /customers/{customerId} + statusCodes: ['200', '404'] +sidebar: + badge: 'GET' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`GetCustomer` is handled by the [[service|customer-api]]. It returns the current state of a single customer, read directly from the [[container|customer-database]]. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/queries/GetCustomer/schema.json b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/queries/GetCustomer/schema.json new file mode 100644 index 000000000..f42128b64 --- /dev/null +++ b/examples/default/domains/Customer/systems/customer-management-system/services/CustomerAPI/queries/GetCustomer/schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetCustomer", + "description": "Query to fetch a single customer by their identifier, and the customer returned", + "type": "object", + "properties": { + "request": { + "description": "Parameters used to look up the customer", + "type": "object", + "properties": { + "customerId": { + "description": "Unique identifier of the customer to fetch", + "type": "string", + "format": "uuid" + } + }, + "required": ["customerId"] + }, + "response": { + "description": "The customer returned by the query", + "type": "object", + "properties": { + "customerId": { + "type": "string", + "format": "uuid" + }, + "email": { + "type": "string", + "format": "email" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["ACTIVE", "SUSPENDED", "CLOSED"] + }, + "registeredAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["customerId", "email", "status"] + } + }, + "required": ["request", "response"] +} diff --git a/examples/default/domains/Customer/systems/identity-provider/containers/user-directory/index.mdx b/examples/default/domains/Customer/systems/identity-provider/containers/user-directory/index.mdx new file mode 100644 index 000000000..30932eb03 --- /dev/null +++ b/examples/default/domains/Customer/systems/identity-provider/containers/user-directory/index.mdx @@ -0,0 +1,36 @@ +--- +id: user-directory +name: User Directory +version: 1.0.0 +summary: The directory of customer credentials and identities — the source of truth for authentication. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for credentials and login identities +classification: regulated +retention: indefinite +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **User Directory** is the authoritative store for customer credentials and login identities. The [[service|oauth-api]] reads from it to verify sign-in attempts. It holds **only** authentication data — customer profile data lives separately in the [[system|customer-management-system]]'s [[container|customer-database]]. + +### What does it store? + +- **Identities** — one record per customer: the login email and a securely hashed credential. +- **Login metadata** — last login time, multi-factor settings, lockout state. + +### Why is it separate from the customer profile? + +Keeping credentials in a dedicated, **regulated**-classification store isolates the most sensitive data from general profile data. Authentication is owned by the Identity Provider; the Customer Management System never sees raw credentials. + +### Security + +- Credentials are stored only as salted, hashed values — never in plaintext. +- Access is tightly restricted to the [[service|oauth-api]] under least-privilege roles. diff --git a/examples/default/domains/Customer/systems/identity-provider/index.mdx b/examples/default/domains/Customer/systems/identity-provider/index.mdx new file mode 100644 index 000000000..346da9d0a --- /dev/null +++ b/examples/default/domains/Customer/systems/identity-provider/index.mdx @@ -0,0 +1,54 @@ +--- +id: identity-provider +name: Identity Provider +version: 1.0.0 +summary: | + The identity system that authenticates customers. It is the source of truth for credentials and login, verifies sign-in attempts, and emits an event when a customer successfully authenticates. +scope: external +owners: + - customer-platform +services: + - id: oauth-api +containers: + - id: user-directory +relationships: + - id: customer-management-system + label: provides authentication for +actors: + - id: customer + name: Customer + label: signs in + direction: inbound +badges: + - content: Identity + backgroundColor: purple + textColor: purple + icon: LockClosedIcon +--- + +## Overview + +The **Identity Provider** authenticates customers for Acme Inc. It exposes an OAuth-style API, verifies credentials against the [[container|user-directory]], and emits a [[event|customer-authenticated]] event on a successful sign-in so other systems can react. It provides authentication for the [[system|customer-management-system]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|oauth-api]] | Service | OAuth-style authentication API. Handles authentication requests and publishes authentication events. | +| [[container\|user-directory]] | Data store | The directory of customer credentials and identities. | + +## Messages this system publishes + +- [[event|customer-authenticated]] — a customer has successfully authenticated. diff --git a/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/commands/AuthenticateCustomer/index.mdx b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/commands/AuthenticateCustomer/index.mdx new file mode 100644 index 000000000..b888ded2c --- /dev/null +++ b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/commands/AuthenticateCustomer/index.mdx @@ -0,0 +1,32 @@ +--- +id: authenticate-customer +name: Authenticate Customer +version: 1.0.0 +summary: | + Command to authenticate a customer with their credentials. +owners: + - customer-platform +schemaPath: schema.json +operation: + method: POST + path: /oauth/token + statusCodes: ['200', '400', '401'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`AuthenticateCustomer` is handled by the [[service|oauth-api]]. It verifies the supplied credentials against the [[container|user-directory]] and, on success, returns an access token and publishes a [[event|customer-authenticated]] event. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/commands/AuthenticateCustomer/schema.json b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/commands/AuthenticateCustomer/schema.json new file mode 100644 index 000000000..c39e22350 --- /dev/null +++ b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/commands/AuthenticateCustomer/schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AuthenticateCustomer", + "description": "Command to authenticate a customer with their credentials, and the token returned", + "type": "object", + "properties": { + "request": { + "description": "The credentials to authenticate", + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string" + } + }, + "required": ["email", "password"] + }, + "response": { + "description": "The access token issued on a successful sign-in", + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "tokenType": { + "type": "string", + "enum": ["Bearer"] + }, + "expiresIn": { + "description": "Token lifetime in seconds", + "type": "integer" + } + }, + "required": ["accessToken", "tokenType", "expiresIn"] + } + }, + "required": ["request", "response"] +} diff --git a/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/events/CustomerAuthenticated/index.mdx b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/events/CustomerAuthenticated/index.mdx new file mode 100644 index 000000000..9da6701dd --- /dev/null +++ b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/events/CustomerAuthenticated/index.mdx @@ -0,0 +1,29 @@ +--- +id: customer-authenticated +name: Customer Authenticated +version: 1.0.0 +summary: | + Published when a customer has successfully authenticated. +owners: + - customer-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CustomerAuthenticated` is published by the [[service|oauth-api]] whenever a customer successfully signs in. Other systems consume this event for auditing, session tracking, and security monitoring. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/events/CustomerAuthenticated/schema.json b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/events/CustomerAuthenticated/schema.json new file mode 100644 index 000000000..d9d341b7a --- /dev/null +++ b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/events/CustomerAuthenticated/schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CustomerAuthenticated", + "description": "Emitted when a customer successfully authenticates", + "type": "object", + "properties": { + "eventId": { + "description": "Unique identifier for this event", + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "description": "Time the customer authenticated", + "type": "string", + "format": "date-time" + }, + "customerId": { + "description": "Unique identifier of the customer that authenticated", + "type": "string", + "format": "uuid" + }, + "method": { + "description": "How the customer authenticated", + "type": "string", + "enum": ["PASSWORD", "SSO", "MFA"] + }, + "ipAddress": { + "description": "IP address the sign-in came from", + "type": "string" + } + }, + "required": ["eventId", "occurredAt", "customerId", "method"] +} diff --git a/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/index.mdx b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/index.mdx new file mode 100644 index 000000000..e8f3b3371 --- /dev/null +++ b/examples/default/domains/Customer/systems/identity-provider/services/OAuthAPI/index.mdx @@ -0,0 +1,49 @@ +--- +id: oauth-api +version: 1.0.0 +name: OAuth API +summary: | + OAuth-style authentication API for the Identity Provider. Verifies customer credentials and publishes an event when a customer successfully authenticates. +styles: + icon: /icons/languages/nodejs.svg +owners: + - customer-platform +receives: + - id: authenticate-customer + version: 1.0.0 +sends: + - id: customer-authenticated + version: 1.0.0 +readsFrom: + - id: user-directory +repository: + language: TypeScript + url: 'https://github.com/acme/oauth-api' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **OAuth API** is the authentication entry point for Acme Inc. It verifies credentials against the [[container|user-directory]] and, on a successful sign-in, publishes a [[event|customer-authenticated]] event so other systems can react. + + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Authentication | Handles [[command\|authenticate-customer]] and verifies credentials. | +| Event publishing | Emits [[event\|customer-authenticated]] on a successful sign-in. | +| Credentials | Reads from the [[container\|user-directory]] — the source of truth for credentials. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Customer/ubiquitous-language.mdx b/examples/default/domains/Customer/ubiquitous-language.mdx new file mode 100644 index 000000000..ae4de1b54 --- /dev/null +++ b/examples/default/domains/Customer/ubiquitous-language.mdx @@ -0,0 +1,48 @@ +--- +dictionary: + - id: Customer + name: Customer + summary: "A person with an account at Acme Inc, identified by a unique customer id and email." + description: | + The customer is the central concept of the Customer domain. Their profile is owned by the + Customer Management System, which is the source of truth for who a customer is. A customer has: + + - A unique customer id and a unique email + - A display name + - An account status (ACTIVE, SUSPENDED or CLOSED) + + A customer's profile data is kept deliberately separate from their credentials, which live in + the Identity Provider. + icon: User + - id: Identity + name: Identity + summary: "The credentials and login information used to prove a customer is who they say they are." + description: | + An identity is distinct from a customer profile. It lives in the Identity Provider's user + directory and holds the login email and a securely hashed credential. Keeping identity separate + from profile data isolates the most sensitive information. + icon: Fingerprint + - id: Authentication + name: Authentication + summary: "Verifying that a sign-in attempt belongs to a real customer." + description: | + Authentication is owned by the Identity Provider. It verifies credentials against the user + directory and, on success, issues an access token and publishes a CustomerAuthenticated event. + icon: LockKeyhole + - id: Credential + name: Credential + summary: "A secret (such as a password) a customer uses to authenticate. Never stored in plain text." + icon: KeyRound + - id: Account Status + name: Account Status + summary: "Where a customer account sits: ACTIVE, SUSPENDED or CLOSED." + description: | + - ACTIVE — the account is in good standing and can be used. + - SUSPENDED — temporarily blocked (e.g. for review). + - CLOSED — permanently deactivated. + icon: Workflow + - id: User Directory + name: User Directory + summary: "The authoritative store of customer credentials and login identities." + icon: Users +--- diff --git a/examples/default/domains/E-Commerce/index.mdx b/examples/default/domains/E-Commerce/index.mdx deleted file mode 100644 index b2c265bb9..000000000 --- a/examples/default/domains/E-Commerce/index.mdx +++ /dev/null @@ -1,277 +0,0 @@ ---- -id: E-Commerce -name: E-Commerce -version: 1.0.0 -owners: - - dboyne - - full-stack -domains: - - id: Orders - - id: Payment - - id: Subscriptions - - id: MySubdomain -badges: - - content: Core domain - backgroundColor: blue - textColor: blue - icon: RectangleGroupIcon - - content: Business Critical - backgroundColor: yellow - textColor: yellow - icon: ShieldCheckIcon -resourceGroups: - - id: related-resources - title: Core FlowMart Services - items: - - id: InventoryService - type: service - - id: OrdersService - type: service - - id: NotificationService - type: service - - id: ShippingService - type: service - - id: CustomerService - type: service - - id: PaymentService - type: service - - id: AnalyticsService - type: service -attachments: - - url: https://example.com/adr/001 - title: ADR-001 - Use Kafka for asynchronous messaging - description: Learn more about why we chose Kafka for asynchronous messaging in this architecture decision record. - type: 'architecture-decisions' - icon: FileTextIcon - - url: https://example.com/adr/001 - title: ADR-002 - Database per service - description: Learn more about why we chose to use a database per service in this architecture decision record. - icon: FileTextIcon - type: 'architecture-decisions' - - url: https://example.com/c4/e-commerce-system-context.png - title: E-Commerce System Context (C4) - description: The C4 diagram context of the E-Commerce system - type: 'diagrams' - icon: FileBoxIcon ---- - -import Footer from '@catalog/components/footer.astro'; - -The E-Commerce domain is the core business domain of FlowMart, our modern digital marketplace. This domain orchestrates all critical business operations from product discovery to order fulfillment, handling millions of transactions monthly across our global customer base. - - - - - - - - -## Domain Overview - -The E-Commerce domain encapsulates all the core business logic for the FlowMart e-commerce platform. It is built on event-driven microservices architecture. - - - -FlowMart's E-Commerce domain is built on event-driven microservices architecture, enabling: -- Real-time inventory management across multiple warehouses -- Seamless payment processing with multiple providers -- Smart order routing and fulfillment -- Personalized customer notifications -- Subscription-based shopping experiences -- Advanced fraud detection and prevention - -## Core Domains for E-Commerce - -The Orders and Subscription domains are core domains for the E-Commerce domain. - -They are used to manage the orders and subscriptions for the E-Commerce domain. - -
    - - -
    - -The E-Commerce domain is built on the following sub domains: - -- Orders - Core domain for order management -- Payment - A generic domain for payment processing using Stripe as a payment provider -- Subscription - Generic subscription domain handling users subscriptions - - -### FlowMart E-Commerce Database Schema - -This diagram represents the core relational data model behind FlowMart, a fictional event-driven e-commerce platform. It captures the main business entities and their relationships, including customers, orders, products, inventory events, and payments. - -The schema is designed to support a distributed microservices architecture with event-sourced patterns, enabling services like OrderService, InventoryService, and PaymentService to operate independently while maintaining data consistency through asynchronous events. - -```plantuml -@startuml -!define Table(name,desc) class name as "desc" << (T,#E5E7EB) >> -!define PK(x) x -!define FK(x) x - -' ===== Core Tables ===== - -Table(Customers, "Customers") { - PK(customerId): UUID - firstName: VARCHAR - lastName: VARCHAR - email: VARCHAR - phone: VARCHAR - dateRegistered: TIMESTAMP -} - -Table(Orders, "Orders") { - PK(orderId): UUID - FK(customerId): UUID - orderDate: TIMESTAMP - status: VARCHAR - totalAmount: DECIMAL -} - -Table(Products, "Products") { - PK(productId): UUID - name: VARCHAR - description: TEXT - price: DECIMAL - stockQuantity: INT -} - -Table(OrderItems, "Order Items") { - PK(id): UUID - FK(orderId): UUID - FK(productId): UUID - quantity: INT - unitPrice: DECIMAL -} - -Table(Payments, "Payments") { - PK(paymentId): UUID - FK(orderId): UUID - amount: DECIMAL - method: VARCHAR - status: VARCHAR - paidAt: TIMESTAMP -} - -Table(InventoryEvents, "Inventory Events") { - PK(eventId): UUID - FK(productId): UUID - eventType: VARCHAR - quantityChange: INT - eventTime: TIMESTAMP -} - -Table(Subscription, "Subscriptions") { - PK(subscriptionId): UUID - FK(customerId): UUID - plan: VARCHAR - status: VARCHAR - startDate: TIMESTAMP - endDate: TIMESTAMP -} - -' ===== Relationships ===== - -Customers ||--o{ Orders : places -Orders ||--o{ OrderItems : contains -Products ||--o{ OrderItems : includes -Orders ||--o{ Payments : paid_by -Products ||--o{ InventoryEvents : logs -Customers ||--o{ Subscription : subscribes - -@enduml - -``` - -## Target Architecture (Event Storming Results) - -Our target architecture was defined through collaborative event storming sessions with product, engineering, and business stakeholders. This represents our vision for FlowMart's commerce capabilities. - - - - -### Order Processing Flow - -```mermaid -sequenceDiagram - participant Customer - participant OrdersService - participant InventoryService - participant PaymentService - participant NotificationService - participant ShippingService - - Customer->>OrdersService: Place Order - OrdersService->>InventoryService: Check Stock Availability - InventoryService-->>OrdersService: Stock Confirmed - OrdersService->>PaymentService: Process Payment - PaymentService-->>OrdersService: Payment Successful - OrdersService->>InventoryService: Reserve Inventory - OrdersService->>ShippingService: Create Shipping Label - ShippingService-->>OrdersService: Shipping Label Generated - OrdersService->>NotificationService: Send Order Confirmation - NotificationService-->>Customer: Order & Tracking Details -``` - -## Key Business Flows - -### Subscription Management -Our subscription service powers FlowMart's popular "Subscribe & Save" feature: - - - -### Payment Processing -Secure, multi-provider payment processing with fraud detection: - - - -## Core Services - -These services form the backbone of FlowMart's e-commerce operations: - - - -## Performance SLAs - -- Order Processing: < 2 seconds -- Payment Processing: < 3 seconds -- Inventory Updates: Real-time -- Notification Delivery: < 30 seconds - -## Monitoring & Alerts - -- Real-time order volume monitoring -- Payment gateway health checks -- Inventory level alerts -- Customer experience metrics -- System performance dashboards - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/changelog.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/changelog.mdx deleted file mode 100644 index 3f9852aa9..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/changelog.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -createdAt: 2024-08-01 ---- - -### Service added to domain - -Added the InventoryService to the domain. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/channels/orders-domain-eventbus/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/channels/orders-domain-eventbus/index.mdx deleted file mode 100644 index d91047ec8..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/channels/orders-domain-eventbus/index.mdx +++ /dev/null @@ -1,16 +0,0 @@ ---- -id: orders-domain-eventbus -name: Orders Domain EventBus -version: 1.0.0 -summary: | - Amazon Orders Domain EventBus -owners: - - dboyne -routes: - - id: cross-account-bus - - id: notifications-queue ---- - -{/* Information about the Orders Domain EventBus */} - -Full this.... diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/entities/CartItem/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/entities/CartItem/index.mdx deleted file mode 100644 index b7cbfa44b..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/entities/CartItem/index.mdx +++ /dev/null @@ -1,121 +0,0 @@ ---- -id: CartItem -name: CartItem -version: 1.0.0 -identifier: cartItemId -summary: Represents an individual item within a shopping cart. -properties: - - name: cartItemId - type: UUID - required: true - description: Unique identifier for the cart item - - name: cartId - type: UUID - required: true - description: Shopping cart this item belongs to - references: ShoppingCart - referencesIdentifier: cartId - relationType: hasOne - - name: productId - type: UUID - required: true - description: Product being added to cart - references: Product - referencesIdentifier: productId - relationType: hasOne - - name: sku - type: string - required: true - description: Product SKU at time of adding to cart - - name: productName - type: string - required: true - description: Product name snapshot at time of adding to cart - - name: productImage - type: string - required: false - description: Product image URL snapshot - - name: quantity - type: integer - required: true - description: Quantity of this product in the cart - - name: unitPrice - type: decimal - required: true - description: Unit price at time of adding to cart - - name: totalPrice - type: decimal - required: true - description: Total price for this line item (quantity × unit price) - - name: originalPrice - type: decimal - required: false - description: Original product price before any discounts - - name: discountAmount - type: decimal - required: false - description: Discount applied to this line item - - name: productVariant - type: object - required: false - description: Product variant details (size, color, etc.) - properties: - - name: size - type: string - description: Product size if applicable - - name: color - type: string - description: Product color if applicable - - name: style - type: string - description: Product style if applicable - - name: isAvailable - type: boolean - required: true - description: Whether the product is still available - - name: notes - type: string - required: false - description: Customer notes for this item - - name: addedAt - type: DateTime - required: true - description: Date and time when item was added to cart - - name: updatedAt - type: DateTime - required: false - description: Date and time when item was last updated ---- - -## Overview - -The CartItem entity represents individual products within a customer's shopping cart. It maintains snapshots of product information and pricing to ensure consistency during the shopping session. - -### Entity Properties - - -## Relationships - -* **ShoppingCart:** Each cart item belongs to one `ShoppingCart` (identified by `cartId`). -* **Product:** Each cart item references one `Product` (identified by `productId`). - -## Price Calculations - -* **Total Price** = Quantity × Unit Price - Discount Amount -* **Savings** = Original Price - Unit Price (if applicable) - -## Examples - -* **CartItem #1:** iPhone 15 Pro, quantity 1, $999.99 unit price, no discount. -* **CartItem #2:** Running Shoes Size 9, quantity 2, $64.99 unit price (was $129.99). -* **CartItem #3:** T-Shirt Large/Blue, quantity 3, $19.99 unit price. - -## Business Rules - -* Quantity must be greater than zero -* Unit price is captured at time of adding to maintain consistency -* Product availability is checked when cart is accessed -* Unavailable items are marked but not automatically removed -* Total price is recalculated when quantity changes -* Product snapshots prevent price changes from affecting active carts -* Maximum quantity limits may apply per product type \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/entities/Customer/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/entities/Customer/index.mdx deleted file mode 100644 index 2627174ea..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/entities/Customer/index.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -id: Customer -name: Customer -version: 1.0.0 -identifier: customerId -summary: Represents an individual or organization that places orders. - -properties: - - name: customerId - type: UUID - required: true - description: Unique identifier for the customer - - name: firstName - type: string - required: true - description: Customer's first name - - name: lastName - type: string - required: true - description: Customer's last name - - name: email - type: string - required: true - description: Customer's primary email address (unique) - - name: phone - type: string - required: false - description: Customer's phone number - - name: addresses - type: array - items: - type: Address # Assuming an Address value object or entity exists - required: false - description: List of addresses associated with the customer (e.g., shipping, billing) - - name: dateRegistered - type: DateTime - required: true - description: Date and time when the customer registered ---- -## Overview - -The Customer entity holds information about the individuals or organizations who interact with the system, primarily by placing orders. It stores contact details, addresses, and other relevant personal or business information. - -### Entity Properties - - -## Relationships - -* **Order:** A customer can have multiple `Order` entities. The `Order` entity holds a reference (`customerId`) back to the `Customer`. -* **Address:** A customer can have multiple associated `Address` value objects or entities. - -## Examples - -* **Customer A:** Jane Doe, registered on 2023-01-15, with a primary shipping address and a billing address. -* **Customer B:** John Smith, a long-time customer with multiple past orders. - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/entities/Order/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/entities/Order/index.mdx deleted file mode 100644 index bd7845ac2..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/entities/Order/index.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -id: Order -name: Order -version: 1.0.0 -identifier: orderId -aggregateRoot: true -summary: Represents a customer's request to purchase products or services. -properties: - - name: orderId - type: UUID - required: true - description: Unique identifier for the order - - name: orderNumber - type: string - required: true - description: Unique identifier for the order - - name: customerId - type: UUID - required: false - description: Identifier for the customer placing the order test - references: Customer - referencesIdentifier: customerId - relationType: hasOne - - name: orderDate - type: DateTime - required: true - description: Date and time when the order was placed - - name: status - type: string - required: true - description: Current status of the order (e.g., Pending, Processing, Shipped, Delivered, Cancelled) - enum: ['Pending', 'Processing', 'Shipped', 'Delivered', 'Cancelled'] - - name: orderItems - type: array - items: - type: OrderItem # Assuming an OrderItem entity exists - required: true - references: OrderItem - referencesIdentifier: orderItemId - relationType: hasMany - description: List of items included in the order - - name: totalAmount - type: decimal - required: true - description: Total monetary value of the order - - name: shippingAddress - type: Address - required: true - description: Address where the order should be shipped - references: Address - referencesIdentifier: addressId - relationType: hasOne - - name: billingAddress - type: Address - required: true - description: Address for billing purposes - references: Address - referencesIdentifier: addressId - relationType: hasOne - - name: payment - type: Payment - required: false - description: Payment associated with this order - references: Payment - referencesIdentifier: orderId - relationType: hasOne - - name: convertedFromCartId - type: UUID - required: false - description: Shopping cart that was converted to this order - references: ShoppingCart - referencesIdentifier: cartId - relationType: hasOne ---- - -## Overview - -The Order entity captures all details related to a customer's purchase request. It serves as the central aggregate root within the Orders domain, coordinating information about the customer, products ordered, payment, and shipping. - -### Entity Properties - - -## Relationships - -* **Customer:** Each order belongs to one `Customer` (identified by `customerId`). -* **OrderItem:** An order contains one or more `OrderItem` entities detailing the specific products and quantities. -* **Address:** Each order has shipping and billing `Address` entities (identified by `shippingAddress` and `billingAddress`). -* **Payment:** An order is associated with one `Payment` entity for transaction processing. -* **ShoppingCart:** An order can be converted from a `ShoppingCart` (identified by `convertedFromCartId`). -* **Shipment:** An order may lead to one or more `Shipment` entities (not detailed here). - -## Examples - -* **Order #12345:** A customer orders 2 units of Product A and 1 unit of Product B, to be shipped to their home address. Status is 'Processing'. -* **Order #67890:** A customer places a large order for multiple items, requiring special shipping arrangements. Status is 'Pending' until payment confirmation. - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/entities/OrderItem/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/entities/OrderItem/index.mdx deleted file mode 100644 index 67523921f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/entities/OrderItem/index.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -id: OrderItem -name: OrderItem -version: 1.0.0 -identifier: orderItemId -summary: Represents a single item within a customer's order. - -properties: - - name: orderItemId - type: UUID - required: true - description: Unique identifier for the order item - - name: orderId - type: UUID - required: true - description: Identifier for the parent Order - references: Order - relationType: hasOne - - name: productId - type: UUID - required: true - description: Identifier for the product being ordered - references: Product - referencesIdentifier: productId - relationType: hasOne - - name: productName - type: string - required: false # Often denormalized for performance/display - description: Name of the product at the time of order - - name: quantity - type: integer - required: true - description: Number of units of the product ordered - - name: unitPrice - type: decimal - required: true - description: Price per unit of the product at the time of order - - name: totalPrice - type: decimal - required: true - description: Total price for this item line (quantity * unitPrice) ---- - -## Overview - -The OrderItem entity details a specific product and its quantity requested within an `Order`. It holds information about the product, the quantity ordered, and the price calculation for that line item. OrderItems are part of the `Order` aggregate. - -### Entity Properties - - -## Relationships - -* **Order:** Each `OrderItem` belongs to exactly one `Order` (identified by `orderId`). It is a constituent part of the Order aggregate. -* **Product:** Each `OrderItem` refers to one `Product` (identified by `productId`). - -## Examples - -* **OrderItem A (for Order #12345):** Product ID: P001, Quantity: 2, Unit Price: $50.00, Total Price: $100.00 -* **OrderItem B (for Order #12345):** Product ID: P002, Quantity: 1, Unit Price: $75.00, Total Price: $75.00 diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/entities/ShoppingCart/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/entities/ShoppingCart/index.mdx deleted file mode 100644 index 2a7cae974..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/entities/ShoppingCart/index.mdx +++ /dev/null @@ -1,148 +0,0 @@ ---- -id: ShoppingCart -name: ShoppingCart -version: 1.0.0 -identifier: cartId -aggregateRoot: true -summary: Represents a customer's shopping cart containing products before checkout. -properties: - - name: cartId - type: UUID - required: true - description: Unique identifier for the shopping cart - - name: customerId - type: UUID - required: false - description: Customer who owns this cart (null for guest carts) - references: Customer - referencesIdentifier: customerId - relationType: hasOne - - name: sessionId - type: string - required: false - description: Session identifier for guest carts - - name: status - type: string - required: true - description: Current status of the cart - enum: ['active', 'abandoned', 'converted', 'expired'] - - name: cartItems - type: array - items: - type: CartItem - required: false - description: Items in the shopping cart - references: CartItem - referencesIdentifier: cartId - relationType: hasMany - - name: subtotal - type: decimal - required: true - description: Subtotal amount before taxes and shipping - - name: taxAmount - type: decimal - required: false - description: Calculated tax amount - - name: shippingAmount - type: decimal - required: false - description: Calculated shipping amount - - name: discountAmount - type: decimal - required: false - description: Total discount amount applied - - name: totalAmount - type: decimal - required: true - description: Final total amount including taxes and shipping - - name: currency - type: string - required: true - description: Currency code for all amounts - - name: appliedCoupons - type: array - items: - type: string - required: false - description: Coupon codes applied to this cart - - name: shippingAddress - type: Address - required: false - description: Selected shipping address - references: Address - referencesIdentifier: addressId - relationType: hasOne - - name: billingAddress - type: Address - required: false - description: Selected billing address - references: Address - referencesIdentifier: addressId - relationType: hasOne - - name: notes - type: string - required: false - description: Customer notes or special instructions - - name: abandonedAt - type: DateTime - required: false - description: Date and time when cart was abandoned - - name: convertedToOrderId - type: UUID - required: false - description: Order ID if cart was successfully converted - references: Order - referencesIdentifier: orderId - relationType: hasOne - - name: expiresAt - type: DateTime - required: false - description: Date and time when cart expires - - name: createdAt - type: DateTime - required: true - description: Date and time when the cart was created - - name: updatedAt - type: DateTime - required: false - description: Date and time when the cart was last updated ---- - -## Overview - -The ShoppingCart entity manages the customer's shopping experience before checkout. It tracks selected products, quantities, pricing, and supports both registered customer and guest shopping scenarios. - -### Entity Properties - - -## Relationships - -* **Customer:** A cart can belong to one `Customer` (identified by `customerId`). -* **CartItem:** A cart contains multiple `CartItem` entities with product details. -* **Address:** A cart can reference shipping and billing `Address` entities. -* **Order:** A cart can be converted to one `Order` (identified by `convertedToOrderId`). - -## Cart States - -``` -active → abandoned - ↓ ↓ -converted expired -``` - -## Examples - -* **Cart #1:** Customer cart with 3 items, $299.99 total, active status. -* **Cart #2:** Guest cart abandoned after 24 hours, contains 1 high-value item. -* **Cart #3:** Converted cart that became Order #12345, marked as converted. - -## Business Rules - -* Guest carts are identified by session ID when customer ID is null -* Cart totals are recalculated when items are added/removed -* Abandoned carts trigger marketing automation after configured time -* Expired carts are cleaned up after retention period -* Cart conversion creates an order and marks cart as converted -* Inventory is not reserved until checkout begins -* Applied coupons are validated on each cart update -* Cart items maintain snapshot of product prices at time of addition \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/index.mdx deleted file mode 100644 index 992dcbf07..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/index.mdx +++ /dev/null @@ -1,99 +0,0 @@ ---- -id: Orders -name: Orders -version: 0.0.3 -owners: - - dboyne - - full-stack -services: - - id: InventoryService - - id: OrdersService - - id: NotificationService - - id: ShippingService -badges: - - content: Subdomain - backgroundColor: blue - textColor: blue - icon: RectangleGroupIcon -entities: - - id: Order - - id: OrderItem - - id: Customer - - id: ShoppingCart - - id: CartItem -resourceGroups: - - id: related-resources - title: Core resources - items: - - id: InventoryService - type: service - - id: OrdersService - type: service - - id: NotificationService - type: service - - id: ShippingService - type: service ---- - -import Footer from '@catalog/components/footer.astro'; - - - -:::warning - -Please ensure all services are **updated** to the latest version for compatibility and performance improvements. -::: - -The Orders domain handles all operations related to customer orders, from creation to fulfillment. This documentation provides an overview of the events and services involved in the Orders domain, helping developers and stakeholders understand the event-driven architecture - - - - - - -### Architecture for the Orders domain - - - -### Entity Map - -This is an entity map for the Orders domain. It shows the entities and their relationships with external entities in this domain. - - - - - -### Order example (sequence diagram) - -```mermaid -sequenceDiagram - participant Customer - participant OrdersService - participant InventoryService - participant NotificationService - - Customer->>OrdersService: Place Order - OrdersService->>InventoryService: Check Inventory - InventoryService-->>OrdersService: Inventory Available - OrdersService->>InventoryService: Reserve Inventory - OrdersService->>NotificationService: Send Order Confirmation - NotificationService-->>Customer: Order Confirmation - OrdersService->>Customer: Order Placed Successfully - OrdersService->>InventoryService: Update Inventory -``` - -## Flows - -### Cancel Subscription flow -Documented flow when a user cancels their subscription. - - - -### Payment processing flow -Documented flow when a user makes a payment within the order domain - - - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/changelog.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/changelog.mdx deleted file mode 100644 index b917110e9..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/changelog.mdx +++ /dev/null @@ -1,7 +0,0 @@ ---- -createdAt: 2024-08-01 ---- - -### Service receives additional events - -Service now receives [OrderAmended](/docs/events/OrderAmended/0.0.1) and [UpdateInventory](/docs/commands/UpdateInventory/0.0.3) events. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/channels/fiter-by-orders-over-100/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/channels/fiter-by-orders-over-100/index.mdx deleted file mode 100644 index 5cf4e076f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/channels/fiter-by-orders-over-100/index.mdx +++ /dev/null @@ -1,9 +0,0 @@ ---- -id: inventory-service-filter-by-orders-over-100 -name: Filter by Orders over 100 -version: 0.0.1 -summary: | - Custom channel that filters by orders over $100. ---- - -This channel is used to filter by orders over $100. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/AddInventory/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/AddInventory/index.mdx deleted file mode 100644 index a6d00078e..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/AddInventory/index.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -id: AddInventory -name: Add inventory -version: 0.0.3 -summary: | - Command that will add item to a given inventory id -owners: - - dboyne - - asmith - - full-stack - - mobile-devs -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json -sidebar: - badge: POST ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The AddInventory command is issued to add new stock to the inventory. This command is used by the inventory management system to update the quantity of products available in the warehouse or store. - -## Architecture diagram - - - -## Payload example - -```json -{ - "productId": "789e1234-b56c-78d9-e012-3456789fghij", - "quantity": 50, - "warehouseId": "456e7891-c23d-45f6-b78a-123456789abc", - "timestamp": "2024-07-04T14:48:00Z" -} - -``` - -## Schema - - - -
    - - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/AddInventory/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/AddInventory/schema.json deleted file mode 100644 index bf7b6cc19..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/AddInventory/schema.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AddInventoryCommand", - "type": "object", - "properties": { - "productId": { - "type": "string", - "format": "uuid", - "description": "The unique identifier of the product being added to the inventory." - }, - "quantity": { - "type": "integer", - "description": "The quantity of the product being added to the inventory." - }, - "warehouseId": { - "type": "string", - "format": "uuid", - "description": "The unique identifier of the warehouse where the inventory is being added." - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "The date and time when the inventory was added." - } - }, - "required": ["productId", "quantity", "warehouseId", "timestamp"], - "additionalProperties": false - } - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/DeleteInventory/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/DeleteInventory/index.mdx deleted file mode 100644 index dc253a711..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/DeleteInventory/index.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -id: DeleteInventory -name: Delete Inventory -version: 0.0.3 -summary: | - Command that will delete a given inventory item from the system -owners: - - dboyne - - msmith - - asmith - - full-stack - - mobile-devs -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json -sidebar: - badge: DELETE ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The DeleteInventory command is issued to remove a product from the inventory system. This command is used by the inventory management system when a product needs to be completely removed from the warehouse or store catalog, typically due to discontinuation or permanent removal of the item. - -## Architecture diagram - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/UpdateInventory/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/UpdateInventory/index.mdx deleted file mode 100644 index 90374bb0f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/commands/UpdateInventory/index.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -id: UpdateInventory -name: Update inventory -version: 0.0.3 -summary: | - Command that will update a given inventory item -owners: - - dboyne - - msmith - - asmith - - full-stack - - mobile-devs -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json -sidebar: - badge: PUT ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The UpdateInventory command is issued to update the existing stock levels of a product in the inventory. This command is used by the inventory management system to adjust the quantity of products available in the warehouse or store, either by increasing or decreasing the current stock levels. - -## Architecture diagram - - - - - -## Payload example - -```json -{ - "productId": "789e1234-b56c-78d9-e012-3456789fghij", - "quantityChange": -10, - "warehouseId": "456e7891-c23d-45f6-b78a-123456789abc", - "timestamp": "2024-07-04T14:48:00Z" -} -``` - -## Schema (JSON schema) - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/containers/inventory-db/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/containers/inventory-db/index.mdx deleted file mode 100644 index 0974eafbf..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/containers/inventory-db/index.mdx +++ /dev/null @@ -1,195 +0,0 @@ ---- -id: inventory-db -name: Inventory DB -version: 0.0.1 -container_type: database -technology: postgres@14 -authoritative: true -access_mode: readWrite -classification: internal -retention: 5y -residency: eu-west-1 -summary: Authoritative database for product inventory levels, warehouse stock, and inventory movements ---- - - - -### What is this? -Inventory DB is the system of record for real-time inventory tracking across multiple warehouses and fulfillment centers. It maintains accurate stock levels, handles inventory reservations, and tracks all inventory movements (receipts, shipments, adjustments). - -### What does it store? -- **Inventory Levels**: Current stock quantities by product and warehouse -- **Inventory Reservations**: Temporary holds on inventory for pending orders -- **Stock Movements**: Audit trail of all inventory transactions (in, out, adjustments) -- **Reorder Points**: Low-stock thresholds and automatic reorder triggers -- **Warehouse Locations**: Bin/shelf locations for physical inventory management - -### Who writes to it? -- **InventoryService** manages stock levels, reservations, and adjustments -- **OrdersService** creates reservations when orders are placed -- **Warehouse Management System** updates physical inventory counts -- **Receiving System** adds stock when shipments arrive - -### Who reads from it? -- **OrdersService** checks stock availability before order confirmation -- **Inventory Service** monitors low-stock alerts and reorder points -- **Analytics** tracks inventory turnover and stock-out rates -- **Warehouse Operations** for pick/pack workflows -- **Finance** for inventory valuation reports - -### High-level data model -- `inventory_levels`: Current quantities by product and warehouse (frequently updated) -- `inventory_reservations`: Temporary allocations for pending orders (auto-expire) -- `stock_movements`: Immutable log of all inventory changes -- `reorder_rules`: Automated replenishment configuration - -### Common queries -```sql --- Check available inventory for a product (excluding reservations) -SELECT - il.warehouse_id, - il.quantity_on_hand, - COALESCE(SUM(ir.quantity), 0) AS quantity_reserved, - il.quantity_on_hand - COALESCE(SUM(ir.quantity), 0) AS quantity_available -FROM inventory_levels il -LEFT JOIN inventory_reservations ir - ON ir.product_id = il.product_id - AND ir.warehouse_id = il.warehouse_id - AND ir.expires_at > NOW() -WHERE il.product_id = $1 -GROUP BY il.warehouse_id, il.quantity_on_hand; - --- Find low-stock items needing reorder -SELECT - il.product_id, - il.warehouse_id, - il.quantity_on_hand, - rr.reorder_point, - rr.reorder_quantity -FROM inventory_levels il -JOIN reorder_rules rr - ON rr.product_id = il.product_id - AND rr.warehouse_id = il.warehouse_id -WHERE il.quantity_on_hand <= rr.reorder_point - AND rr.enabled = true; - --- Track inventory movements for audit (last 30 days) -SELECT - sm.movement_id, - sm.product_id, - sm.warehouse_id, - sm.movement_type, -- 'RECEIPT', 'SHIPMENT', 'ADJUSTMENT', 'RETURN' - sm.quantity_change, - sm.reason, - sm.created_at, - sm.created_by -FROM stock_movements sm -WHERE sm.product_id = $1 - AND sm.created_at >= NOW() - INTERVAL '30 days' -ORDER BY sm.created_at DESC; - --- Calculate inventory turnover rate -SELECT - p.product_id, - p.product_name, - SUM(CASE WHEN sm.movement_type = 'SHIPMENT' THEN ABS(sm.quantity_change) ELSE 0 END) AS units_sold_30d, - AVG(il.quantity_on_hand) AS avg_inventory_level, - (SUM(CASE WHEN sm.movement_type = 'SHIPMENT' THEN ABS(sm.quantity_change) ELSE 0 END) / - NULLIF(AVG(il.quantity_on_hand), 0)) AS turnover_ratio -FROM stock_movements sm -JOIN inventory_levels il USING (product_id, warehouse_id) -JOIN products p USING (product_id) -WHERE sm.created_at >= NOW() - INTERVAL '30 days' -GROUP BY p.product_id, p.product_name -ORDER BY turnover_ratio DESC; -``` - -### Inventory reservation flow -1. Customer adds item to cart → soft reservation (not written to DB yet) -2. Customer proceeds to checkout → `OrdersService` creates reservation with 15-minute expiry -3. Payment successful → reservation converted to stock movement (`SHIPMENT` type) -4. Payment failed or timeout → reservation auto-expires, stock released -5. Order cancelled → reservation deleted, stock released immediately - -### Access patterns and guidance -- Use indexed lookups by `product_id` and `warehouse_id` -- Reservations have TTL; expired reservations cleaned up hourly -- Stock movements are append-only for audit compliance -- Use pessimistic locking for concurrent inventory updates -- Read replicas for analytics to avoid impacting operational queries - -### Concurrency and consistency -- **Row-level locking**: `SELECT FOR UPDATE` on inventory_levels during reservations -- **Atomic updates**: All inventory changes in transactions (reserve + deduct + log) -- **Idempotency**: Movement records include idempotency keys to prevent duplicates -- **Eventual consistency**: Read model (inventory-readmodel container) synced asynchronously - -### Security and compliance -- Inventory adjustments logged with user identity for audit trail -- Role-based access: warehouse staff vs. system services -- Financial impact tracked for high-value inventory movements -- Historical data retained for 5 years (tax/audit requirements) - -### Requesting access -To request access to Inventory DB: - -1. **Read-only access** (for reporting): - - Submit request via [ServiceNow](https://company.service-now.com) - - Select "Database Access" → "Inventory DB (Read-Only)" - - Approval from inventory team lead - - Access granted within 24 hours - -2. **Write access** (for services): - - Restricted to InventoryService and authorized systems only - - New service integration requires architecture review - - Contact #inventory-team for integration onboarding - -3. **Warehouse operations access**: - - Access via Warehouse Management System only (no direct DB access) - - Contact #warehouse-operations for WMS training - -**Contact**: -- Slack: #inventory-team -- Email: inventory-team@company.com -- On-call: PagerDuty #inventory-oncall - -### Monitoring and alerts -- Stock-out alerts (inventory level = 0 for critical products) -- Negative inventory alerts (data integrity issue) -- Reservation expiry rate (high rate indicates checkout abandonment) -- Database lock contention (alert if wait time > 100ms) -- Replication lag (alert at > 5 seconds) - -### Backup and disaster recovery -- Continuous backups with 5-minute RPO -- Point-in-time recovery window: 35 days -- Cross-region backup replication for DR -- Daily backup validation - -### Performance characteristics -- **Read latency**: p99 < 20ms -- **Write latency**: p99 < 50ms (includes locking) -- **Reservation throughput**: 1,000+ reservations/second -- **Concurrent updates**: Supports high concurrency with row-level locking - -### Local development -- Connection string: `INVENTORY_DB_URL` environment variable -- Docker setup: `docker-compose up inventory-db` -- Seed data: `npm run seed:inventory` -- Test data includes multiple warehouses and stock levels - -### Common issues and troubleshooting -- **Negative inventory**: Check for missing rollback on failed reservations -- **Stuck reservations**: Run cleanup job for expired reservations -- **Lock contention**: Reduce transaction duration, consider optimistic locking -- **Inventory drift**: Reconcile with physical counts, investigate stock movement gaps -- **Replication lag**: Check network latency, increase replication capacity - -### Integration with read model -Inventory DB is the write-side (source of truth). The `inventory-readmodel` container provides optimized read queries: -- Real-time sync via change data capture (CDC) -- Denormalized views for fast lookups -- Eventually consistent (typically < 1 second lag) -- Used by high-traffic read operations (product pages, search) - -For more information, see InventoryService documentation and Inventory Management Playbook. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/containers/inventory-readmodel/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/containers/inventory-readmodel/index.mdx deleted file mode 100644 index 46ddaaca8..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/containers/inventory-readmodel/index.mdx +++ /dev/null @@ -1,122 +0,0 @@ ---- -id: inventory-readmodel -name: Inventory Read Model -version: 0.0.1 -container_type: database -technology: postgres@14 -authoritative: false -access_mode: read -summary: Projection of stock levels from Inventory domain ---- - - - -The Inventory Read Model is a PostgreSQL database that serves as an optimized projection of inventory data for high-performance read operations. This read model is specifically designed to support fast queries for stock levels, product availability, and inventory reporting across our e-commerce platform. - -## Overview - -This read model is maintained by the Inventory Service and provides denormalized views of inventory data that are optimized for query performance. It serves as the primary data source for: - -- Real-time stock level checks during order processing -- Inventory reporting and analytics -- Product availability displays on the storefront -- Low stock alerts and notifications - -## Database Schema - -### Core Tables - -#### `inventory_projection` -- **product_id** (UUID) - Primary key, references the product -- **sku** (VARCHAR) - Stock keeping unit identifier -- **available_quantity** (INTEGER) - Current available stock -- **reserved_quantity** (INTEGER) - Stock reserved for pending orders -- **total_quantity** (INTEGER) - Total physical stock -- **reorder_point** (INTEGER) - Minimum stock threshold -- **last_updated** (TIMESTAMP) - When this record was last updated -- **version** (BIGINT) - Event sourcing version number - -#### `product_locations` -- **product_id** (UUID) - Foreign key to inventory_projection -- **warehouse_id** (UUID) - Warehouse identifier -- **location_code** (VARCHAR) - Specific location within warehouse -- **quantity** (INTEGER) - Quantity at this location -- **last_counted** (TIMESTAMP) - Last physical count date - -#### `stock_movements_summary` -- **product_id** (UUID) - Product identifier -- **movement_date** (DATE) - Date of movement -- **inbound_quantity** (INTEGER) - Total items received -- **outbound_quantity** (INTEGER) - Total items shipped -- **adjustment_quantity** (INTEGER) - Manual adjustments -- **ending_balance** (INTEGER) - End of day balance - -## Data Sources - -This read model is populated from the following event streams: - -- **InventoryReceived** - Updates available quantity when new stock arrives -- **InventoryReserved** - Increases reserved quantity for orders -- **InventoryReleased** - Decreases reserved quantity when reservations expire -- **InventoryAdjusted** - Manual stock adjustments from warehouse operations -- **InventoryTransferred** - Stock movements between locations - -## Performance Characteristics - -- **Read Latency**: < 5ms for single product queries -- **Throughput**: 50,000+ queries per second -- **Data Freshness**: Near real-time (< 100ms from event occurrence) -- **Availability**: 99.95% uptime SLA - -## Usage Patterns - -### High-Frequency Operations -- Stock availability checks during checkout -- Real-time inventory displays on product pages -- Order validation and reservation - -### Reporting Operations -- Daily inventory reports -- Low stock alerts -- Inventory turnover analysis -- Warehouse utilization metrics - -## Maintenance - -### Data Retention -- Transaction-level data: 2 years -- Summary data: 7 years -- Archived data moved to cold storage after retention period - -### Backup Strategy -- Full backups: Daily at 2 AM EST -- Incremental backups: Every 4 hours -- Point-in-time recovery available for 30 days -- Cross-region replication for disaster recovery - -### Monitoring -- Stock level discrepancy alerts -- Query performance monitoring -- Event processing lag alerts -- Database health checks every 5 minutes - -## Security - -- **Access Control**: Role-based access with principle of least privilege -- **Encryption**: Data encrypted at rest using AES-256 -- **Network Security**: VPC isolation with private subnets -- **Audit Logging**: All data access logged for compliance - -## Dependencies - -- **Event Store**: Primary source of inventory events -- **Message Bus**: RabbitMQ for event consumption -- **Cache Layer**: Redis for frequently accessed data -- **Monitoring**: Prometheus and Grafana for metrics - -## Contact Information - -For questions about this read model, please contact: -- **Primary**: Inventory Team (inventory@acmecorp.com) -- **On-call**: Use PagerDuty escalation policy "Inventory-ReadModel" -- **Architecture Questions**: Sarah Mitchell (s.mitchell@acmecorp.com) \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/changelog.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/changelog.mdx deleted file mode 100644 index 691a1ed67..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/changelog.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -createdAt: 2024-08-01 -badges: - - content: ⭐️ JSON Schema - backgroundColor: purple - textColor: purple ---- - -### Added support for JSON Schema - -InventoryAdjusted uses Avro but now also supports JSON Draft 7. - -```json title="Employee JSON Draft" -// labeled-line-markers.jsx -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "Employee", - "properties": { - "Name": { - "type": "string" - }, - "Age": { - "type": "integer" - }, - "Town": { - "type": "string" - } - }, - "required": ["Name", "Age", "Town"] -} - -``` - -Using it with our Kafka Cluster - -## 1. Create a new topic - -```sh -# Create a topic named 'employee_topic' -kafka-topics.sh --create --topic employee_topic --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1 -``` - -## Step 2: Prepare the JSON Message - -Create a JSON file named `employee.json` with the following content: - -```json -{ - "Name": "John Doe", - "Age": 30, - "Town": "Springfield" -} -``` - -## Step 3: Produce the Message to Kafka Topic - -Use the Kafka producer CLI to send the JSON message: - -```sh -cat employee.json | kafka-console-producer.sh --topic employee_topic --bootstrap-server localhost:9092 -``` - -## Step 4: Verify the Message (Optional) - -```sh -kafka-console-consumer.sh --topic employee_topic --from-beginning --bootstrap-server localhost:9092 -``` diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/index.mdx deleted file mode 100644 index bd72ba4ee..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/index.mdx +++ /dev/null @@ -1,245 +0,0 @@ ---- -id: InventoryAdjusted -name: Inventory adjusted -version: 1.0.1 -summary: | - Indicates a change in inventory level -owners: - - dboyne - - msmith - - asmith - - full-stack - - mobile-devs -badges: - - content: Recently updated! - backgroundColor: green - textColor: green - - content: 'Broker:Apache Kafka' - backgroundColor: yellow - textColor: yellow - icon: kafka -schemaPath: schema.avro -draft: - title: Inventory Adjusted 1.0.1 is in draft - message: > - ### New version of Inventory Adjusted is in draft - - - This is a new version of the Inventory Adjusted event. It is not yet ready - for production. We are still working on it and collecting feedback from the - team. - - - You can use this version in lower environments, **but please be aware that - it is still in draft and may change.** - - - You can still use a previous version of the event, [Inventory Adjusted - 1.0.0](/docs/events/InventoryAdjusted/1.0.0), until that version is - deprecated. - - - _If you would like to provide feedback, please contact us at - [feedback@eventcatalog.io](mailto:feedback@eventcatalog.io) or our slack - channel [Order - Management](https://join.slack.com/t/eventcatalog/shared_invite/zt-1q900000000000000000000000000000)_ ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `Inventory Adjusted` event is triggered whenever there is a change in the inventory levels of a product. This could occur due to various reasons such as receiving new stock, sales, returns, or manual adjustments by the inventory management team. The event ensures that all parts of the system that rely on inventory data are kept up-to-date with the latest inventory levels. - - - - - - -## Architecture diagram - - - - - - -## Payload example - -Event example you my see being published. - -```json -{ - "Name": "John Doe", - "Age": 30, - "Department": "Engineering", - "Position": "Software Engineer", - "Salary": 85000.50, - "JoinDate": "2024-01-15" -} -``` - -## Schema (avro) - - - - - -## Producing the Event - -Select the language you want to produce the event in to see an example. - - - - - ```python title="Produce event in Python" frame="terminal" - from kafka import KafkaProducer - import json - from datetime import datetime - - # Kafka configuration - producer = KafkaProducer( - bootstrap_servers=['localhost:9092'], - value_serializer=lambda v: json.dumps(v).encode('utf-8') - ) - - # Event data - event_data = { - "event_id": "abc123", - "timestamp": datetime.utcnow().isoformat() + 'Z', - "product_id": "prod987", - "adjusted_quantity": 10, - "new_quantity": 150, - "adjustment_reason": "restock", - "adjusted_by": "user123" - } - - # Send event to Kafka topic - producer.send('inventory.adjusted', event_data) - producer.flush() - ``` - - - - ```typescript title="Produce event in TypeScript" frame="terminal" - import { Kafka } from 'kafkajs'; - - // Kafka configuration - const kafka = new Kafka({ - clientId: 'inventory-producer', - brokers: ['localhost:9092'] - }); - - const producer = kafka.producer(); - - // Event data - const eventData = { - event_id: "abc123", - timestamp: new Date().toISOString(), - product_id: "prod987", - adjusted_quantity: 10, - new_quantity: 150, - adjustment_reason: "restock", - adjusted_by: "user123" - }; - - // Send event to Kafka topic - async function produceEvent() { - await producer.connect(); - await producer.send({ - topic: 'inventory.adjusted', - messages: [ - { value: JSON.stringify(eventData) } - ], - }); - await producer.disconnect(); - } - - produceEvent().catch(console.error); - ``` - - - - ```java title="Produce event in Java" frame="terminal" - import org.apache.kafka.clients.producer.*; - import org.apache.kafka.common.serialization.StringSerializer; - import com.fasterxml.jackson.databind.ObjectMapper; - import java.util.Properties; - import java.util.HashMap; - import java.util.Map; - import java.time.Instant; - - public class InventoryProducer { - public static void main(String[] args) { - // Kafka configuration - Properties props = new Properties(); - props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); - props.put(ProducerConfig.CLIENT_ID_CONFIG, "inventory-producer"); - props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); - props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); - - Producer producer = new KafkaProducer<>(props); - ObjectMapper mapper = new ObjectMapper(); - - try { - // Event data - Map eventData = new HashMap<>(); - eventData.put("event_id", "abc123"); - eventData.put("timestamp", Instant.now().toString()); - eventData.put("product_id", "prod987"); - eventData.put("adjusted_quantity", 10); - eventData.put("new_quantity", 150); - eventData.put("adjustment_reason", "restock"); - eventData.put("adjusted_by", "user123"); - - // Create producer record - ProducerRecord record = new ProducerRecord<>( - "inventory.adjusted", - mapper.writeValueAsString(eventData) - ); - - // Send event to Kafka topic - producer.send(record, (metadata, exception) -> { - if (exception != null) { - System.err.println("Error producing message: " + exception); - } - }); - - } catch (Exception e) { - e.printStackTrace(); - } finally { - producer.flush(); - producer.close(); - } - } - } - ``` - - - - - -### Consuming the Event - -To consume an Inventory Adjusted event, use the following example Kafka consumer configuration in Python: - -```python title="Consuming the event with python" frame="terminal" -from kafka import KafkaConsumer -import json - -# Kafka configuration -consumer = KafkaConsumer( - 'inventory.adjusted', - bootstrap_servers=['localhost:9092'], - auto_offset_reset='earliest', - enable_auto_commit=True, - group_id='inventory_group', - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# Consume events -for message in consumer: - event_data = json.loads(message.value) - print(f"Received Inventory Adjusted event: {event_data}") -``` - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/schema.avro b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/schema.avro deleted file mode 100644 index ea0986272..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/schema.avro +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "record", - "namespace": "Tutorialspoint", - "name": "Employee", - "doc": "Represents an employee in the organisation HR system.", - "fields": [ - { "name": "Name", "type": "string", "doc": "Full name of the employee." }, - { "name": "Age", "type": "int", "doc": "Age in years at time of record creation.", "default": 0 }, - { "name": "Department", "type": "string", "doc": "The department the employee belongs to.", "default": "Unknown" }, - { "name": "Position", "type": "string", "doc": "The employee's job title or role.", "default": "Unknown" }, - { "name": "Salary", "type": "double", "doc": "Annual salary in the company currency.", "default": 0.0 }, - { "name": "JoinDate", "type": "string", "logicalType": "date", "doc": "Date employee joined the company. Format: YYYY-MM-DD.", "default": "" } - ] -} diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/0.0.1/changelog.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/0.0.1/changelog.mdx deleted file mode 100644 index c200336dc..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/0.0.1/changelog.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -createdAt: 2024-07-01 -badges: - - content: Breaking change - backgroundColor: red - textColor: red ---- - -### Removed fields from schema, added new owners - -`Gender` property has been removed from the Schema of the event - -Also added the [full stackers](/docs/teams/full-stack) team as owners of this event - -```diff lang="json" - { - "type" : "record", - "namespace" : "Tutorialspoint", - "name" : "Employee", - "fields" : [ - { "name" : "Name" , "type" : "string" }, - { "name" : "Age" , "type" : "int" }, -- { "name" : "Gender" , "type" : "string" }, - ] - } -``` - - - - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/0.0.1/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/0.0.1/index.mdx deleted file mode 100644 index ed4c64034..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/0.0.1/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: InventoryAdjusted -name: Inventory adjusted -version: 0.0.1 -summary: | - Indicates a change in inventory level -owners: - - dboyne -schemaPath: 'schema.avro' -badges: - - content: Recently updated! - backgroundColor: green - textColor: green ---- - -:::warning -When firing this event make sure you set the `correlation-id` in the headers. Our schemas have standard metadata make sure you read and follow it. -::: - -### Details - - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/1.0.0/changelog.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/1.0.0/changelog.mdx deleted file mode 100644 index aa47fdc7a..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/1.0.0/changelog.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -createdAt: 2024-07-11 -badges: - - content: New field - backgroundColor: green - textColor: green ---- - -### Added new field to schema - -We added the new town property to the schema for downstream consumers. - -```json ins={"New: Added new Town property to schema:":9-10} -// labeled-line-markers.jsx -{ - "type" : "record", - "namespace" : "Tutorialspoint", - "name" : "Employee", - "fields" : [ - { "name" : "Name" , "type" : "string" }, - { "name" : "Age" , "type" : "int" }, - - { "name" : "Town" , "type" : "string" }, - ] -} -``` diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/1.0.0/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/1.0.0/index.mdx deleted file mode 100644 index f5211107f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryAdjusted/versioned/1.0.0/index.mdx +++ /dev/null @@ -1,106 +0,0 @@ ---- -id: InventoryAdjusted -name: Inventory adjusted -version: 1.0.0 -summary: | - Indicates a change in inventory level -owners: - - dboyne - - msmith - - asmith - - full-stack - - mobile-devs -schemaPath: 'schema.avro' -badges: - - content: Recently updated! - backgroundColor: green - textColor: green - - content: Broker:Apache Kafka - backgroundColor: yellow - textColor: yellow - icon: kafka ---- - -## Overview - -The `Inventory Adjusted` event is triggered whenever there is a change in the inventory levels of a product. This could occur due to various reasons such as receiving new stock, sales, returns, or manual adjustments by the inventory management team. The event ensures that all parts of the system that rely on inventory data are kept up-to-date with the latest inventory levels. - - - -## Event Details - -### Event Name -`inventory.adjusted` - -### Description -This event indicates that the inventory count for one or more products has been adjusted. The event carries the updated inventory details including the product ID, the new quantity, and the reason for the adjustment. - -### Payload -The payload of the `Inventory Adjusted` event includes the following fields: - -```json title="Example of payload" frame="terminal" -{ - "event_id": "string", - "timestamp": "ISO 8601 date-time", - "product_id": "string", - "adjusted_quantity": "integer", - "new_quantity": "integer", - "adjustment_reason": "string", - "adjusted_by": "string" -} -``` - -### Producing the Event - -To produce an Inventory Adjusted event, use the following example Kafka producer configuration in Python: - -```python title="Produce event in Python" frame="terminal" -from kafka import KafkaProducer -import json -from datetime import datetime - -# Kafka configuration -producer = KafkaProducer( - bootstrap_servers=['localhost:9092'], - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# Event data -event_data = { - "event_id": "abc123", - "timestamp": datetime.utcnow().isoformat() + 'Z', - "product_id": "prod987", - "adjusted_quantity": 10, - "new_quantity": 150, - "adjustment_reason": "restock", - "adjusted_by": "user123" -} - -# Send event to Kafka topic -producer.send('inventory.adjusted', event_data) -producer.flush() -``` - -### Consuming the Event - -To consume an Inventory Adjusted event, use the following example Kafka consumer configuration in Python: - -```python title="Consuming the event with python" frame="terminal" -from kafka import KafkaConsumer -import json - -# Kafka configuration -consumer = KafkaConsumer( - 'inventory.adjusted', - bootstrap_servers=['localhost:9092'], - auto_offset_reset='earliest', - enable_auto_commit=True, - group_id='inventory_group', - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# Consume events -for message in consumer: - event_data = json.loads(message.value) - print(f"Received Inventory Adjusted event: {event_data}") -``` \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryGenerated/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryGenerated/index.mdx deleted file mode 100644 index 3a443a80b..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/InventoryGenerated/index.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -id: InventoryGenerated -version: 0.0.1 -name: Inventory Generated -summary: Emitted when new inventory is generated or created for a product -tags: - - inventory - - stock - - warehouse -badges: - - content: New Event - backgroundColor: blue - textColor: white -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The `InventoryGenerated` event is emitted by the Inventory Service when new inventory is created or generated for a product. This event is typically triggered when products are received from suppliers, restocked, or when new inventory records are initialized in the system. - -## Event Flow - -1. InventoryService receives new stock from supplier or warehouse -2. System validates product information and quantities -3. Inventory record is created in the database -4. `InventoryGenerated` event is emitted -5. Downstream services (OrdersService, AnalyticsService) receive the event -6. Product availability is updated across the system - -## Schema - - - -## Example Payload - -```json -{ - "eventId": "evt_inv_20240215_001", - "timestamp": "2024-02-15T10:30:00Z", - "inventoryId": "inv_ABC123XYZ", - "productId": "prod_789456", - "sku": "SKU-2024-WIDGET-BLUE", - "productName": "Premium Widget - Blue", - "quantity": 500, - "unitCost": 12.50, - "totalValue": 6250.00, - "currency": "USD", - "warehouseId": "wh_MAIN_001", - "warehouseName": "Main Distribution Center", - "warehouseLocation": "Aisle 12, Shelf 3, Bin 7", - "supplierId": "supp_ACME_CORP", - "supplierName": "ACME Corporation", - "purchaseOrderId": "PO-2024-0123", - "batchNumber": "BATCH-2024-02-15-001", - "expirationDate": "2025-12-31", - "receivedBy": "user_john_doe", - "receivedAt": "2024-02-15T09:45:00Z", - "status": "active", - "metadata": { - "source": "manual_entry", - "qualityCheck": true, - "temperature": 22.5, - "humidity": 45 - } -} -``` - -## Use Cases - -- **Stock Replenishment**: Triggered when new stock arrives from suppliers -- **Initial Setup**: When setting up inventory for new products -- **Bulk Import**: When importing inventory from external systems -- **Warehouse Transfer**: When inventory is transferred between warehouses - -## Related Events - -- `InventoryAdjusted` - When inventory levels are modified -- `OutOfStock` - When inventory reaches zero -- `LowStockAlert` - When inventory falls below threshold - -
    - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/OutOfStock/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/OutOfStock/index.mdx deleted file mode 100644 index cb1512f5f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/OutOfStock/index.mdx +++ /dev/null @@ -1,100 +0,0 @@ ---- -id: OutOfStock -name: Inventory out of stock -version: 0.0.4 -summary: | - Indicates inventory is out of stock -owners: - - dboyne - - msmith - - asmith - - full-stack - - mobile-devs -badges: - - content: Recently updated! - backgroundColor: green - textColor: green - - content: 'Broker:Apache Kafka' - backgroundColor: yellow - textColor: yellow - icon: kafka ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `Inventory Adjusted` event is triggered whenever there is a change in the inventory levels of a product. This could occur due to various reasons such as receiving new stock, sales, returns, or manual adjustments by the inventory management team. The event ensures that all parts of the system that rely on inventory data are kept up-to-date with the latest inventory levels. - - - -### Payload -The payload of the `Inventory Adjusted` event includes the following fields: - -```json title="Example of payload" frame="terminal" -{ - "event_id": "string", - "timestamp": "ISO 8601 date-time", - "product_id": "string", - "adjusted_quantity": "integer", - "new_quantity": "integer", - "adjustment_reason": "string" -} -``` - -### Producing the Event - -To produce an Inventory Adjusted event, use the following example Kafka producer configuration in Python: - -```python title="Produce event in Python" frame="terminal" -from kafka import KafkaProducer -import json -from datetime import datetime - -# Kafka configuration -producer = KafkaProducer( - bootstrap_servers=['localhost:9092'], - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# Event data -event_data = { - "event_id": "abc123", - "timestamp": datetime.utcnow().isoformat() + 'Z', - "product_id": "prod987", - "adjusted_quantity": 10, - "new_quantity": 150, - "adjustment_reason": "restock", - "adjusted_by": "user123" -} - -# Send event to Kafka topic -producer.send('inventory.adjusted', event_data) -producer.flush() -``` - -### Consuming the Event - -To consume an Inventory Adjusted event, use the following example Kafka consumer configuration in Python: - -```python title="Consuming the event with python" frame="terminal" -from kafka import KafkaConsumer -import json - -# Kafka configuration -consumer = KafkaConsumer( - 'inventory.adjusted', - bootstrap_servers=['localhost:9092'], - auto_offset_reset='earliest', - enable_auto_commit=True, - group_id='inventory_group', - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# Consume events -for message in consumer: - event_data = json.loads(message.value) - print(f"Received Inventory Adjusted event: {event_data}") -``` - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/OutOfStock/versioned/0.0.1/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/OutOfStock/versioned/0.0.1/index.mdx deleted file mode 100644 index 579aafddd..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/events/OutOfStock/versioned/0.0.1/index.mdx +++ /dev/null @@ -1,97 +0,0 @@ ---- -id: OutOfStock -name: Inventory out of stock -version: 0.0.1 -summary: | - Indicates inventory is out of stock -owners: - - dboyne - - msmith - - asmith - - full-stack - - mobile-devs -badges: - - content: Recently updated! - backgroundColor: green - textColor: green - - content: Broker:Apache Kafka - backgroundColor: yellow - textColor: yellow - icon: kafka ---- - -## Overview - -The `Inventory Adjusted` event is triggered whenever there is a change in the inventory levels of a product. This could occur due to various reasons such as receiving new stock, sales, returns, or manual adjustments by the inventory management team. The event ensures that all parts of the system that rely on inventory data are kept up-to-date with the latest inventory levels. - - - -### Payload -The payload of the `Inventory Adjusted` event includes the following fields: - -```json title="Example of payload" frame="terminal" -{ - "event_id": "string", - "timestamp": "ISO 8601 date-time", - "product_id": "string", - "adjusted_quantity": "integer", - "new_quantity": "integer", - "adjustment_reason": "string", - "adjusted_by": "string" -} -``` - -### Producing the Event - -To produce an Inventory Adjusted event, use the following example Kafka producer configuration in Python: - -```python title="Produce event in Python" frame="terminal" -from kafka import KafkaProducer -import json -from datetime import datetime - -# Kafka configuration -producer = KafkaProducer( - bootstrap_servers=['localhost:9092'], - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# Event data -event_data = { - "event_id": "abc123", - "timestamp": datetime.utcnow().isoformat() + 'Z', - "product_id": "prod987", - "adjusted_quantity": 10, - "new_quantity": 150, - "adjustment_reason": "restock", - "adjusted_by": "user123" -} - -# Send event to Kafka topic -producer.send('inventory.adjusted', event_data) -producer.flush() -``` - -### Consuming the Event - -To consume an Inventory Adjusted event, use the following example Kafka consumer configuration in Python: - -```python title="Consuming the event with python" frame="terminal" -from kafka import KafkaConsumer -import json - -# Kafka configuration -consumer = KafkaConsumer( - 'inventory.adjusted', - bootstrap_servers=['localhost:9092'], - auto_offset_reset='earliest', - enable_auto_commit=True, - group_id='inventory_group', - value_serializer=lambda v: json.dumps(v).encode('utf-8') -) - -# Consume events -for message in consumer: - event_data = json.loads(message.value) - print(f"Received Inventory Adjusted event: {event_data}") -``` \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/index.mdx deleted file mode 100644 index 39034ab90..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/index.mdx +++ /dev/null @@ -1,141 +0,0 @@ ---- -id: InventoryService -version: 0.0.2 -name: Inventory Service -summary: | - Service that handles the inventory -owners: - - order-management -receives: - - id: UserSignedUp - - id: OrderConfirmed - from: - - id: inventory-service-filter-by-orders-over-100 - - id: OrderAmended - from: - - id: orders-domain-eventbus - - id: UpdateInventory - from: - - id: inventory-domain-eventbus - - id: AddInventory - from: - - id: inventory-domain-eventbus - - id: DeleteInventory - from: - - id: inventory-domain-eventbus -sends: - - id: InventoryAdjusted - to: - - id: orders-domain-eventbus - - id: OutOfStock - to: - - id: orders-domain-eventbus -writesTo: - - id: inventory-db - version: 0.0.1 - - id: inventory-readmodel - version: 0.0.1 -readsFrom: - - id: inventory-db - version: 0.0.1 - - id: inventory-readmodel - version: 0.0.1 -repository: - language: JavaScript - url: 'https://github.com/event-catalog/pretend-shipping-service' -deprecated: - date: 2026-05-01T00:00:00.000Z - message: > - This service is **being deprecated** and replaced by the new service - **InventoryServiceV2**. - - Please contact the [team for more - information](mailto:inventory-team@example.com) or visit our - [website](https://eventcatalog.dev). ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The Inventory Service is a critical component of the system responsible for managing product stock levels, tracking inventory movements, and ensuring product availability. It interacts with other services to maintain accurate inventory records and supports operations such as order fulfillment, restocking, and inventory audits. - - - - - - - - -## Core features - -| Feature | Description | -|---------|-------------| -| Real-time Stock Tracking | Monitors inventory levels across all warehouses in real-time | -| Automated Reordering | Triggers purchase orders when stock levels fall below defined thresholds | -| Multi-warehouse Support | Manages inventory across multiple warehouse locations | -| Batch Processing | Handles bulk inventory updates and adjustments efficiently | - -## Architecture diagram - - - - - -# Infrastructure - -The Inventory Service is hosted on AWS. - -The diagram below shows the infrastructure of the Inventory Service. The service is hosted on AWS and uses AWS Lambda to handle the inventory requests. The inventory is stored in an AWS Aurora database and the inventory metadata is stored in an AWS S3 bucket. - -```mermaid -architecture-beta - group api(logos:aws) - - service db(logos:aws-aurora)[Inventory DB] in api - service disk1(logos:aws-s3)[Inventory Metadata] in api - service server(logos:aws-lambda)[Inventory Handler] in api - - db:L -- R:server - disk1:T -- B:server -``` - -You can find more information about the Inventory Service infrastructure in the [Inventory Service documentation](https://github.com/event-catalog/pretend-shipping-service/blob/main/README.md). - - - - - - Request API credentials from the Inventory Service team. - - - Run the following command in your project directory: - ```bash - npm install inventory-service-sdk - ``` - - - Use the following code to initialize the Inventory Service client: - - ```js - const InventoryService = require('inventory-service-sdk'); - const client = new InventoryService.Client({ - clientId: 'YOUR_CLIENT_ID', - clientSecret: 'YOUR_CLIENT_SECRET', - apiUrl: 'https://api.inventoryservice.com/v1' - }); -``` - - - - You can now use the client to make API calls. For example, to get all products: - - ```js - client.getProducts() - .then(products => console.log(products)) - .catch(error => console.error(error)); - ``` - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryList/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryList/index.mdx deleted file mode 100644 index edd9c42fd..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryList/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: GetInventoryList -name: List inventory list -version: 0.0.1 -summary: | - GET request that will return inventory list -owners: - - dboyne -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The GetInventoryList message is a query used to retrieve a comprehensive list of all available inventory items within a system. It is designed to return detailed information about each item, such as product names, quantities, availability status, and potentially additional metadata like categories or locations. This query is typically utilized by systems or services that require a real-time view of current stock, ensuring that downstream applications or users have accurate and up-to-date information for decision-making or operational purposes. The GetInventoryList is ideal for use cases such as order processing, stock management, or reporting, providing visibility into the full range of inventory data. - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryList/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryList/schema.json deleted file mode 100644 index da58b2da5..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryList/schema.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetInventoryList", - "description": "A query to retrieve a list of all available inventory items with their details.", - "type": "object", - "properties": { - "filters": { - "type": "object", - "description": "Optional filters to narrow down the inventory search.", - "properties": { - "category": { - "type": "string", - "description": "Filter items by category (e.g., electronics, clothing, etc.)." - }, - "location": { - "type": "string", - "description": "Filter items by storage location or warehouse." - }, - "minStockLevel": { - "type": "integer", - "description": "Filter items with a stock level greater than or equal to this value." - }, - "inStock": { - "type": "boolean", - "description": "Filter items that are currently in stock (true) or out of stock (false)." - } - }, - "additionalProperties": false - }, - "pagination": { - "type": "object", - "description": "Pagination options for the query.", - "properties": { - "page": { - "type": "integer", - "description": "The current page of results.", - "minimum": 1, - "default": 1 - }, - "pageSize": { - "type": "integer", - "description": "The number of items per page.", - "minimum": 1, - "default": 10 - } - }, - "required": ["page", "pageSize"] - } - }, - "required": [], - "additionalProperties": false - } - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryStatus/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryStatus/index.mdx deleted file mode 100644 index 8c1a5289c..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryStatus/index.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: GetInventoryStatus -name: Get inventory status -version: 0.0.1 -summary: | - GET request that will return the current stock status for a specific product. -owners: - - dboyne -badges: - - content: GET Request - backgroundColor: green - textColor: green -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The GetInventoryStatus message is a query designed to retrieve the current stock status for a specific product. - -This query provides detailed information about the available quantity, reserved quantity, and the warehouse location where the product is stored. It is typically used by systems or services that need to determine the real-time availability of a product, enabling efficient stock management, order fulfillment, and inventory tracking processes. - -This query is essential for ensuring accurate stock levels are reported to downstream systems, including e-commerce platforms, warehouse management systems, and sales channels. - - - - - - -### Query using CURL - -Use this snippet to query the inventory status - -```sh title="Example CURL command" -curl -X GET "https://api.yourdomain.com/inventory/status" \ --H "Content-Type: application/json" \ --d '{ - "productId": "12345" -}' -``` \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryStatus/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryStatus/schema.json deleted file mode 100644 index fcf09a10a..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/queries/GetInventoryStatus/schema.json +++ /dev/null @@ -1,447 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetInventoryStatusResponse", - "type": "object", - "definitions": { - "Coordinates": { - "type": "object", - "properties": { - "latitude": { - "type": "number", - "minimum": -90, - "maximum": 90, - "description": "Latitude coordinate" - }, - "longitude": { - "type": "number", - "minimum": -180, - "maximum": 180, - "description": "Longitude coordinate" - } - }, - "required": ["latitude", "longitude"], - "additionalProperties": false - }, - "LocationDetails": { - "type": "object", - "properties": { - "facilityId": { - "type": "string", - "pattern": "^[A-Z]{2}[0-9]{4}$", - "description": "Facility identifier in format CC0000" - }, - "name": { - "type": "string", - "description": "Facility name" - }, - "zone": { - "type": "string", - "enum": ["north", "south", "east", "west", "central"], - "description": "Geographic zone" - }, - "coordinates": { - "$ref": "#/definitions/Coordinates" - }, - "operatingHours": { - "type": "object", - "properties": { - "openTime": { - "type": "string", - "pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$", - "description": "Opening time in HH:MM format" - }, - "closeTime": { - "type": "string", - "pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$", - "description": "Closing time in HH:MM format" - }, - "timeZone": { - "type": "string", - "description": "Time zone identifier" - } - }, - "required": ["openTime", "closeTime", "timeZone"], - "additionalProperties": false - } - }, - "required": ["facilityId", "name", "zone", "coordinates", "operatingHours"], - "additionalProperties": false - }, - "SupplierInfo": { - "type": "object", - "properties": { - "supplierId": { - "type": "string", - "description": "Unique supplier identifier" - }, - "companyName": { - "type": "string", - "description": "Supplier company name" - }, - "tier": { - "type": "string", - "enum": ["primary", "secondary", "backup"], - "description": "Supplier tier level" - }, - "performance": { - "type": "object", - "properties": { - "onTimeDelivery": { - "type": "number", - "minimum": 0, - "maximum": 100, - "description": "On-time delivery percentage" - }, - "qualityRating": { - "type": "number", - "minimum": 1, - "maximum": 5, - "description": "Quality rating (1-5 stars)" - }, - "lastDeliveryDate": { - "type": "string", - "format": "date", - "description": "Date of last delivery" - } - }, - "required": ["onTimeDelivery", "qualityRating"], - "additionalProperties": false - }, - "contractTerms": { - "type": "object", - "properties": { - "minimumOrderQuantity": { - "type": "integer", - "minimum": 1, - "description": "Minimum order quantity" - }, - "pricePerUnit": { - "type": "number", - "minimum": 0, - "description": "Price per unit in USD" - }, - "currency": { - "type": "string", - "enum": ["USD", "EUR", "GBP", "CAD", "JPY"], - "description": "Currency code" - } - }, - "required": ["minimumOrderQuantity", "pricePerUnit", "currency"], - "additionalProperties": false - } - }, - "required": ["supplierId", "companyName", "tier", "performance", "contractTerms"], - "additionalProperties": false - }, - "InventoryMovement": { - "type": "object", - "properties": { - "movementId": { - "type": "string", - "description": "Unique movement identifier" - }, - "type": { - "type": "string", - "enum": ["inbound", "outbound", "transfer", "adjustment"], - "description": "Type of inventory movement" - }, - "quantity": { - "type": "integer", - "description": "Quantity moved (positive for inbound, negative for outbound)" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "When the movement occurred" - }, - "reason": { - "type": "string", - "description": "Reason for the movement" - }, - "reference": { - "type": "object", - "properties": { - "orderId": { - "type": "string", - "description": "Related order ID if applicable" - }, - "transferId": { - "type": "string", - "description": "Related transfer ID if applicable" - }, - "userId": { - "type": "string", - "description": "User who initiated the movement" - } - }, - "additionalProperties": false - } - }, - "required": ["movementId", "type", "quantity", "timestamp", "reason"], - "additionalProperties": false - }, - "QualityMetrics": { - "type": "object", - "properties": { - "inspectionDate": { - "type": "string", - "format": "date", - "description": "Date of last quality inspection" - }, - "grade": { - "type": "string", - "enum": ["A", "B", "C", "D", "F"], - "description": "Quality grade" - }, - "defectRate": { - "type": "number", - "minimum": 0, - "maximum": 100, - "description": "Defect rate percentage" - }, - "expirationTracking": { - "type": "object", - "properties": { - "hasExpiration": { - "type": "boolean", - "description": "Whether product has expiration date" - }, - "nearExpiryCount": { - "type": "integer", - "minimum": 0, - "description": "Count of items near expiration" - }, - "expiredCount": { - "type": "integer", - "minimum": 0, - "description": "Count of expired items" - } - }, - "required": ["hasExpiration"], - "additionalProperties": false - } - }, - "required": ["inspectionDate", "grade", "defectRate", "expirationTracking"], - "additionalProperties": false - } - }, - "properties": { - "productId": { - "type": "string", - "description": "The unique identifier for the product" - }, - "productMetadata": { - "type": "object", - "properties": { - "sku": { - "type": "string", - "description": "Stock keeping unit" - }, - "barcode": { - "type": "string", - "pattern": "^[0-9]{12,13}$", - "description": "Product barcode (UPC/EAN)" - }, - "category": { - "type": "object", - "properties": { - "primary": { - "type": "string", - "description": "Primary category" - }, - "secondary": { - "type": "string", - "description": "Secondary category" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Category tags" - } - }, - "required": ["primary"], - "additionalProperties": false - }, - "dimensions": { - "type": "object", - "properties": { - "length": { - "type": "number", - "minimum": 0, - "description": "Length in centimeters" - }, - "width": { - "type": "number", - "minimum": 0, - "description": "Width in centimeters" - }, - "height": { - "type": "number", - "minimum": 0, - "description": "Height in centimeters" - }, - "weight": { - "type": "number", - "minimum": 0, - "description": "Weight in grams" - } - }, - "required": ["length", "width", "height", "weight"], - "additionalProperties": false - } - }, - "required": ["sku", "barcode", "category", "dimensions"], - "additionalProperties": false - }, - "aggregatedInventory": { - "type": "object", - "properties": { - "totalAvailable": { - "type": "integer", - "minimum": 0, - "description": "Total available quantity across all locations" - }, - "totalReserved": { - "type": "integer", - "minimum": 0, - "description": "Total reserved quantity across all locations" - }, - "totalInTransit": { - "type": "integer", - "minimum": 0, - "description": "Total quantity in transit between locations" - }, - "safetyStock": { - "type": "integer", - "minimum": 0, - "description": "Safety stock level" - }, - "reorderLevel": { - "type": "integer", - "minimum": 0, - "description": "Reorder point threshold" - } - }, - "required": ["totalAvailable", "totalReserved", "totalInTransit", "safetyStock", "reorderLevel"], - "additionalProperties": false - }, - "locationBreakdown": { - "type": "array", - "items": { - "type": "object", - "properties": { - "location": { - "$ref": "#/definitions/LocationDetails" - }, - "inventory": { - "type": "object", - "properties": { - "availableQuantity": { - "type": "integer", - "minimum": 0, - "description": "Available quantity at this location" - }, - "reservedQuantity": { - "type": "integer", - "minimum": 0, - "description": "Reserved quantity at this location" - }, - "lastCountDate": { - "type": "string", - "format": "date", - "description": "Date of last physical count" - } - }, - "required": ["availableQuantity", "reservedQuantity", "lastCountDate"], - "additionalProperties": false - }, - "qualityMetrics": { - "$ref": "#/definitions/QualityMetrics" - } - }, - "required": ["location", "inventory", "qualityMetrics"], - "additionalProperties": false - }, - "minItems": 1, - "description": "Inventory breakdown by location" - }, - "supplierInformation": { - "type": "array", - "items": { - "$ref": "#/definitions/SupplierInfo" - }, - "description": "Information about suppliers for this product" - }, - "recentMovements": { - "type": "array", - "items": { - "$ref": "#/definitions/InventoryMovement" - }, - "maxItems": 10, - "description": "Recent inventory movements (last 10)" - }, - "alerts": { - "type": "object", - "properties": { - "lowStock": { - "type": "boolean", - "description": "Whether product is below reorder level" - }, - "qualityIssues": { - "type": "boolean", - "description": "Whether there are quality concerns" - }, - "supplierDelays": { - "type": "boolean", - "description": "Whether there are supplier delivery delays" - }, - "messages": { - "type": "array", - "items": { - "type": "object", - "properties": { - "severity": { - "type": "string", - "enum": ["info", "warning", "error", "critical"], - "description": "Alert severity level" - }, - "message": { - "type": "string", - "description": "Alert message" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "When the alert was generated" - } - }, - "required": ["severity", "message", "timestamp"], - "additionalProperties": false - }, - "description": "Alert messages" - } - }, - "required": ["lowStock", "qualityIssues", "supplierDelays", "messages"], - "additionalProperties": false - }, - "lastUpdated": { - "type": "string", - "format": "date-time", - "description": "Timestamp when this data was last updated" - } - }, - "required": [ - "productId", - "productMetadata", - "aggregatedInventory", - "locationBreakdown", - "supplierInformation", - "recentMovements", - "alerts", - "lastUpdated" - ], - "additionalProperties": false -} - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/versioned/0.0.1/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/versioned/0.0.1/index.mdx deleted file mode 100644 index 6ee3b58fa..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/InventoryService/versioned/0.0.1/index.mdx +++ /dev/null @@ -1,46 +0,0 @@ ---- -id: InventoryService -version: 0.0.1 -name: Inventory Service -summary: | - Service that handles the inventory -owners: - - dboyne - - full-stack - - mobile-devs -receives: - - id: OrderConfirmed - from: - - id: 'orders.{env}.events' - - id: OrderCancelled - from: - - id: 'orders.{env}.events' - - id: OrderAmended - from: - - id: 'orders.{env}.events' - parameters: - env: staging - - id: UpdateInventory - from: - - id: 'inventory.{env}.events' - parameters: - env: staging -sends: - - id: InventoryAdjusted - to: - - id: 'inventory.{env}.events' - - id: OutOfStock - to: - - id: 'inventory.{env}.events' -repository: - language: JavaScript - url: 'https://github.com/event-catalog/pretend-shipping-service' ---- - -## Overview - -The Inventory Service is a critical component of the system responsible for managing product stock levels, tracking inventory movements, and ensuring product availability. It interacts with other services to maintain accurate inventory records and supports operations such as order fulfillment, restocking, and inventory audits. - -## Architecture diagram - - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/channels/notifications-queue/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/channels/notifications-queue/index.mdx deleted file mode 100644 index 7fed04bfd..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/channels/notifications-queue/index.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -id: notifications-queue -name: Notifications Queue -version: 0.0.1 -summary: | - Queue for notifications -owners: - - order-management ---- - -## Overview - -The Notifications Queue is a queue for notifications. It is used to store notifications that need to be sent to users. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/containers/notifications-queue/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/containers/notifications-queue/index.mdx deleted file mode 100644 index 8e6616505..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/containers/notifications-queue/index.mdx +++ /dev/null @@ -1,186 +0,0 @@ ---- -id: notifications-queue -name: Notifications Queue -version: 0.0.1 -container_type: other -technology: sqs -access_mode: readWrite -classification: internal -retention: 14d -residency: eu-west-1 -summary: Message queue for asynchronous notification delivery across email, SMS, and push channels ---- - - - -### What is this? -Notifications Queue is an AWS SQS FIFO queue that buffers notification requests for asynchronous processing. It ensures reliable, ordered delivery of notifications while decoupling notification producers from delivery channels. - -### What does it store? -- **Notification Messages**: Structured notification payloads with recipient, channel, template, and data -- **Delivery Metadata**: Priority, retry count, scheduled delivery time -- **Batch Groups**: Related notifications grouped for efficient batch processing -- **Dead Letter Queue Messages**: Failed notifications after max retry attempts - -### Queue structure -``` -notifications-queue (Primary Queue) -├── High Priority (MessageGroupId: priority-high) -├── Normal Priority (MessageGroupId: priority-normal) -└── Low Priority (MessageGroupId: priority-low) - -notifications-dlq (Dead Letter Queue) -└── Failed messages after 3 retry attempts -``` - -### Message format -```json -{ - "notification_id": "uuid", - "recipient": { - "customer_id": "uuid", - "email": "customer@example.com", - "phone": "+1234567890", - "device_tokens": ["fcm_token_123"] - }, - "channel": "EMAIL | SMS | PUSH | IN_APP", - "template_id": "order_confirmation", - "template_data": { - "order_id": "12345", - "order_total": "$99.99", - "items_count": 3 - }, - "priority": "HIGH | NORMAL | LOW", - "scheduled_at": "2024-01-15T10:30:00Z", - "metadata": { - "source_service": "OrdersService", - "event_type": "OrderConfirmed", - "idempotency_key": "uuid" - } -} -``` - -### Who writes to it? -- **OrdersService** sends order-related notifications (confirmations, updates, cancellations) -- **PaymentService** sends payment status notifications -- **SubscriptionService** sends subscription lifecycle notifications -- **InventoryService** sends low-stock alerts to internal teams -- **Fraud DetectionService** sends fraud alert notifications - -### Who reads from it? -- **NotificationService Workers** consume messages and dispatch to delivery channels -- **Email Worker**: Processes EMAIL channel notifications via SendGrid/SES -- **SMS Worker**: Processes SMS notifications via Twilio -- **Push Worker**: Processes mobile push notifications via FCM/APNs -- **In-App Worker**: Writes in-app notifications to database - -### Message processing flow -1. Service publishes notification to queue with appropriate priority -2. NotificationService worker polls queue (long polling, 20s) -3. Worker validates message and loads template -4. Renders template with provided data -5. Dispatches to delivery channel (email provider, SMS gateway, etc.) -6. On success: delete message from queue -7. On failure: message returned to queue (visibility timeout) for retry -8. After 3 failures: message moved to DLQ for manual review - -### Queue configuration -- **Queue type**: FIFO (First-In-First-Out) for ordered delivery -- **Message retention**: 14 days -- **Visibility timeout**: 60 seconds (time for worker to process) -- **Delivery delay**: 0 seconds (immediate) -- **Max message size**: 256 KB -- **Dead letter queue**: After 3 receive attempts -- **Throughput**: 3,000 messages/second per message group - -### Priority handling -- **HIGH**: Immediate delivery (fraud alerts, payment failures, security notifications) -- **NORMAL**: Standard delivery (order confirmations, shipping updates) -- **LOW**: Batch delivery (marketing emails, digest notifications) - -Priority implemented via message group IDs - high priority workers process high-priority groups first. - -### Access patterns and guidance -- Use message group IDs to maintain order within notification types -- Include idempotency keys to prevent duplicate notifications -- Set appropriate visibility timeout based on channel latency -- Monitor DLQ and investigate failures -- Use batch sends for multiple notifications to same recipient - -### Monitoring and alerts -- Queue depth monitoring (alert if > 10,000 messages) -- DLQ message count (alert on any messages) -- Message age monitoring (alert if oldest message > 5 minutes) -- Consumer lag (alert if lag > 30 seconds) -- Channel-specific delivery failure rates - -### Security and access control -- **IAM policies**: Service-specific roles for send/receive permissions -- **Encryption**: Messages encrypted at rest with AWS KMS -- **Encryption in transit**: TLS for all queue operations -- **Access logs**: CloudTrail logs all API calls -- **PII protection**: Sensitive data in messages should be references, not full values - -### Requesting access -To request access to Notifications Queue: - -1. **Publish access** (for services sending notifications): - - Submit request via [AWS Access Portal](https://company.awsapps.com) - - Select "SQS Send Permission" → "notifications-queue" - - Requires notification team approval - - IAM policy attached within 1 business day - -2. **Consume access** (for worker services): - - Restricted to NotificationService worker roles only - - New worker setup requires architecture review - - Contact #notifications-team for onboarding - -3. **DLQ access** (for troubleshooting): - - Read-only access via #notifications-oncall - - Write access requires incident ticket - -**Contact**: -- Slack: #notifications-team -- Email: notifications-team@company.com -- On-call: PagerDuty #notifications-oncall - -### Retry and failure handling -- **Transient failures**: Automatic retry via SQS (exponential backoff) -- **Delivery failures**: Logged with error details, retried up to 3 times -- **DLQ messages**: Daily review, manual retry or discard after investigation -- **Circuit breaker**: If channel failure rate > 50%, pause and alert - -### Rate limiting -- **Email**: 100 emails/second (provider limit) -- **SMS**: 10 SMS/second (cost control) -- **Push**: 1,000 push/second -- Per-recipient limits to prevent spam: max 10 notifications/hour - -### Local development -- LocalStack SQS emulation: `docker-compose up localstack` -- Connection: `AWS_ENDPOINT_URL=http://localhost:4566` -- Create queue: `npm run setup:local-queues` -- Send test message: `npm run test:send-notification` - -### Performance characteristics -- **Delivery latency**: p99 < 5 seconds from queue to delivery -- **Throughput**: 10,000+ notifications/minute -- **Message processing time**: p95 < 2 seconds -- **DLQ rate**: < 0.1% of messages - -### Common issues and troubleshooting -- **High queue depth**: Scale up worker instances or check for processing errors -- **Messages in DLQ**: Check worker logs, validate template IDs, verify channel credentials -- **Duplicate notifications**: Ensure idempotency keys are unique and properly checked -- **Slow processing**: Check channel API latencies, may need to increase visibility timeout -- **Message loss**: Verify IAM permissions, check for queue purge events in CloudTrail - -### Best practices -- Always include idempotency key to prevent duplicates -- Use structured logging to correlate queue messages with delivery outcomes -- Set appropriate message retention based on business requirements -- Monitor DLQ regularly and investigate root causes -- Test notification templates before deploying to production -- Use message attributes for filtering and routing - -For more information, see NotificationService documentation and SQS Best Practices Guide. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/index.mdx deleted file mode 100644 index 4ea658534..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/index.mdx +++ /dev/null @@ -1,99 +0,0 @@ ---- -id: NotificationService -version: 0.0.2 -name: Notifications -summary: | - Service that handles notifications across multiple channels (email, SMS, push) -owners: - - order-management -sends: - - id: NotificationSent -receives: - - id: InventoryAdjusted - from: - - id: notifications-queue - - id: PaymentProcessed - from: - - id: notifications-queue - - id: InvoiceGenerated - from: - - id: notifications-queue - - id: OutOfStock - from: - - id: notifications-queue -readsFrom: - - id: notifications-queue - version: 0.0.1 -repository: - language: JavaScript - url: 'https://github.com/event-catalog/pretend-shipping-service' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The Notification Service is responsible for managing and delivering notifications to users and other services. It supports various notification channels such as email, SMS, push notifications, and in-app notifications. The service ensures reliable and timely delivery of messages and integrates with other services to trigger notifications based on specific events. - - - - - - - - -### Core features - -| Feature | Description | -|---------|-------------| -| Multi-Channel Delivery | Supports notifications via email, SMS, push notifications, and in-app messages | -| Template Management | Customizable notification templates with dynamic content placeholders | -| Delivery Status Tracking | Real-time tracking and monitoring of notification delivery status | -| Rate Limiting | Prevents notification flooding through configurable rate limits | -| Priority Queue | Handles urgent notifications with priority delivery mechanisms | -| Batch Processing | Efficiently processes and sends bulk notifications | -| Retry Mechanism | Automatic retry logic for failed notification deliveries | -| Event-Driven Notifications | Triggers notifications based on system events and user actions | - - - -## Architecture diagram - - - - - -## Core Concepts - - - - - Description: A message that is sent to a user or a service. - - Attributes: notificationId, type, recipient, content, channel, status, timestamp - - - - Description: The medium through which the notification is delivered (e.g., email, SMS, push notification). - - Attributes: channelId, name, provider, configuration - - - -## Infrastructure - -The Notification Service is hosted on AWS. - -The diagram below shows the infrastructure of the Notification Service. The service is hosted on AWS and uses AWS Lambda to handle the notification requests. The notification is stored in an AWS Aurora database and the notification metadata is stored in an AWS S3 bucket. - -```mermaid -architecture-beta - group api(logos:aws) - - service db(logos:aws-aurora)[Notification DB] in api - service disk1(logos:aws-s3)[Notification Metadata] in api - service server(logos:aws-lambda)[Notification Handler] in api - - db:L -- R:server - disk1:T -- B:server -``` - -You can find more information about the Notification Service infrastructure in the [Notification Service documentation](https://github.com/event-catalog/pretend-shipping-service/blob/main/README.md). - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetNotificationDetails/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetNotificationDetails/index.mdx deleted file mode 100644 index 48fceec00..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetNotificationDetails/index.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -id: GetNotificationDetails -name: Get notification details -version: 0.0.1 -summary: | - GET request that will return detailed information about a specific notification, identified by its notificationId. -owners: - - dboyne -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json -sidebar: - badge: 'GET' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `GetNotificationDetails` message is a query used to retrieve detailed information about a specific notification identified by its `notificationId`. It provides a comprehensive overview of the notification, including the title, message content, status (read/unread), the date it was created, and any additional metadata related to the notification, such as associated orders or system events. This query is helpful in scenarios where users or systems need detailed insights into a particular notification, such as retrieving full messages or auditing notifications sent to users. - -Use cases include viewing detailed information about order updates, system notifications, or promotional messages, allowing users to view their full notification history and details. - - - - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetNotificationDetails/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetNotificationDetails/schema.json deleted file mode 100644 index 4aafa54b1..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetNotificationDetails/schema.json +++ /dev/null @@ -1,56 +0,0 @@ - -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetNotificationDetailsResponse", - "type": "object", - "properties": { - "notificationId": { - "type": "string", - "description": "The unique identifier for the notification." - }, - "title": { - "type": "string", - "description": "The title or subject of the notification." - }, - "message": { - "type": "string", - "description": "The content or message body of the notification." - }, - "status": { - "type": "string", - "enum": ["unread", "read"], - "description": "The read status of the notification." - }, - "userId": { - "type": "string", - "description": "The unique identifier for the user who received the notification." - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "The date and time when the notification was created." - }, - "type": { - "type": "string", - "description": "The type of the notification, such as order or system." - }, - "metadata": { - "type": "object", - "description": "Additional metadata related to the notification, such as order details.", - "properties": { - "orderId": { - "type": "string", - "description": "The associated order ID, if applicable." - }, - "shippingProvider": { - "type": "string", - "description": "The shipping provider for the associated order, if applicable." - } - }, - "required": ["orderId"], - "additionalProperties": false - } - }, - "required": ["notificationId", "title", "message", "status", "userId", "createdAt", "type"], - "additionalProperties": false -} diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetUserNotifications/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetUserNotifications/index.mdx deleted file mode 100644 index 0b4d0a2f3..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetUserNotifications/index.mdx +++ /dev/null @@ -1,29 +0,0 @@ ---- -id: GetUserNotifications -name: Get user notifications -version: 0.0.1 -summary: | - GET request that will return a list of notifications for a specific user, with options to filter by status (unread or all). -owners: - - dboyne -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json -sidebar: - badge: 'GET' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `GetUserNotifications` message is a query used to retrieve a list of notifications for a specific user. It allows filtering by notification status, such as unread or all notifications. This query is typically utilized by notification services to display user-specific messages, such as order updates, promotional offers, or system notifications. It supports pagination through `limit` and `offset` parameters, ensuring that only a manageable number of notifications are retrieved at once. This query helps users stay informed about important events or updates related to their account, orders, or the platform. - -Use cases include delivering notifications for order updates, promotional campaigns, or general system messages to keep the user informed. - - - - - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetUserNotifications/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetUserNotifications/schema.json deleted file mode 100644 index 033b654de..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/NotificationService/queries/GetUserNotifications/schema.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetUserNotificationsResponse", - "type": "object", - "properties": { - "userId": { - "type": "string", - "description": "The unique identifier for the user." - }, - "notifications": { - "type": "array", - "description": "A list of notifications for the user.", - "items": { - "type": "object", - "properties": { - "notificationId": { - "type": "string", - "description": "The unique identifier for the notification." - }, - "title": { - "type": "string", - "description": "The title or subject of the notification." - }, - "message": { - "type": "string", - "description": "The message body of the notification." - }, - "status": { - "type": "string", - "enum": ["unread", "read"], - "description": "The read status of the notification." - }, - "createdAt": { - "type": "string", - "format": "date-time", - "description": "The date and time when the notification was created." - } - }, - "required": ["notificationId", "title", "message", "status", "createdAt"], - "additionalProperties": false - } - } - }, - "required": ["userId", "notifications"], - "additionalProperties": false - } - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/changelog.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/changelog.mdx deleted file mode 100644 index 111462129..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/changelog.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -createdAt: 2024-08-01 ---- - -This is my entry \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/channels/sqs-channel/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/channels/sqs-channel/index.mdx deleted file mode 100644 index 41ae41734..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/channels/sqs-channel/index.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: orders-service-sqs-channel -name: Orders Service SQS Channel -version: 1.0.0 -summary: | - SQS channel for the Orders Service -owners: - - dboyne ---- - -The Orders Service SQS Channel is used to process messages from the Orders Service. diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/commands/PlaceOrder/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/commands/PlaceOrder/index.mdx deleted file mode 100644 index 5b5c30377..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/commands/PlaceOrder/index.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -id: PlaceOrder -name: Place Order -version: 0.0.1 -summary: | - Command that will place an order -owners: - - dboyne - - msmith - - asmith - - full-stack - - mobile-devs -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: 'schema.json' -sidebar: - badge: 'POST' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The Order Placement Command is a versatile and robust system designed to streamline the process of placing an order. This command takes care of all the essential details needed to complete a purchase, ensuring a smooth and efficient transaction from start to finish. - -## Architecture diagram - - - -## Schema - - - -
    - - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/order-metadata-store/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/order-metadata-store/index.mdx deleted file mode 100644 index 16424189a..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/order-metadata-store/index.mdx +++ /dev/null @@ -1,210 +0,0 @@ ---- -id: order-metadata-store -name: Order Metadata Store -version: 0.0.1 -container_type: objectStore -technology: aws-s3 -summary: Long-term storage of order documents, receipts, and metadata files -access_mode: read -residency: eu-west-1 -retention: 7y ---- - - - -The Order Metadata Store is an AWS S3 bucket that provides scalable, durable object storage for order-related documents and metadata. This storage system handles large files and documents that are associated with orders but don't require real-time database access. - -## Overview - -This S3-based object store serves as the primary repository for: - -- Order confirmation PDFs and receipts -- Customer upload documents (delivery instructions, custom specifications) -- Order processing audit trails and logs -- Large metadata files that exceed database field limits -- Invoice documents and tax records -- Shipping labels and tracking documents - -## Bucket Configuration - -### Structure -- **Bucket Name**: `acmecorp-order-metadata-prod` -- **Region**: `us-east-1` -- **Storage Classes**: - - Standard (first 30 days) - - Standard-IA (30-90 days) - - Glacier (90+ days) -- **Versioning**: Enabled -- **Cross-Region Replication**: Enabled to `us-west-2` - -### Folder Organization -``` -/orders/ - /{year}/ - /{month}/ - /{order-id}/ - /receipts/ - - order-confirmation.pdf - - payment-receipt.pdf - /documents/ - - delivery-instructions.txt - - custom-specifications.json - /audit/ - - processing-log.json - - status-changes.json - /invoices/ - - invoice.pdf - - tax-document.pdf - /shipping/ - - shipping-label.pdf - - tracking-info.json -``` - -## Data Types and Formats - -### Order Receipts -- **Format**: PDF -- **Size Range**: 50KB - 2MB -- **Retention**: 7 years (regulatory compliance) -- **Access Pattern**: High read frequency first 30 days, then infrequent - -### Customer Documents -- **Formats**: PDF, TXT, JSON, JPG, PNG -- **Size Range**: 10KB - 50MB -- **Retention**: 2 years -- **Access Pattern**: Infrequent reads after order completion - -### Audit Trails -- **Format**: JSON, CSV -- **Size Range**: 1KB - 10MB -- **Retention**: 5 years -- **Access Pattern**: Rare access, compliance queries only - -### Invoice Documents -- **Format**: PDF, XML -- **Size Range**: 100KB - 5MB -- **Retention**: 7 years (tax compliance) -- **Access Pattern**: Medium frequency during tax season - -## Performance Characteristics - -- **Upload Throughput**: 1,000+ objects per second -- **Download Latency**: < 100ms for Standard storage -- **Availability**: 99.999999999% (11 9's) durability -- **Consistency**: Strong read-after-write consistency -- **Multi-part Upload**: Enabled for files > 100MB - -## Security and Access Control - -### IAM Policies -- **OrderService Role**: Full read/write access to order folders -- **ReportingService Role**: Read-only access for analytics -- **CustomerService Role**: Read access to customer documents -- **Compliance Role**: Full access for audit purposes - -### Encryption -- **At Rest**: AES-256 with AWS KMS -- **In Transit**: TLS 1.2+ -- **Key Management**: Customer-managed KMS keys with automatic rotation - -### Access Logging -- **CloudTrail**: All API calls logged -- **S3 Access Logs**: Detailed request logging -- **Monitoring**: CloudWatch metrics and alarms -- **Audit**: Quarterly access reviews - -## Lifecycle Management - -### Automated Transitions -1. **0-30 days**: Standard storage class -2. **30-90 days**: Standard-IA (Infrequent Access) -3. **90-365 days**: Glacier storage class -4. **1+ years**: Glacier Deep Archive - -### Data Retention Policies -- **Order receipts**: 7 years (regulatory) -- **Customer documents**: 2 years (business requirement) -- **Audit trails**: 5 years (compliance) -- **Invoice documents**: 7 years (tax law) -- **Automated deletion**: After retention period expires - -## Integration Patterns - -### Write Operations -- Orders Service uploads receipts and confirmations -- Customer portal uploads delivery instructions -- Payment Service stores transaction receipts -- Shipping Service uploads tracking labels - -### Read Operations -- Customer Service retrieves order documents for support -- Reporting Service accesses historical data -- Compliance Service performs audit queries -- External systems via pre-signed URLs - -## Monitoring and Alerting - -### Key Metrics -- **Upload Success Rate**: > 99.9% -- **Download Latency**: < 100ms (95th percentile) -- **Storage Utilization**: Tracked per folder structure -- **Cost Optimization**: Storage class distribution - -### Alerts -- Upload failure rate > 0.1% -- Unusual access patterns (security) -- Storage cost increases > 20% month-over-month -- Lifecycle policy failures - -## Backup and Disaster Recovery - -### Backup Strategy -- **Cross-Region Replication**: Real-time to us-west-2 -- **Versioning**: 30 previous versions retained -- **MFA Delete**: Required for permanent deletion -- **Point-in-Time Recovery**: Via object versioning - -### Disaster Recovery -- **RTO**: 2 hours (switch to backup region) -- **RPO**: < 15 minutes (replication lag) -- **Testing**: Monthly failover tests -- **Documentation**: Runbooks for recovery scenarios - -## Cost Optimization - -### Current Usage (Monthly) -- **Standard Storage**: ~500GB ($11.50) -- **Standard-IA**: ~2TB ($25.60) -- **Glacier**: ~10TB ($40.00) -- **Requests**: ~1M PUT/GET ($4.00) -- **Data Transfer**: ~100GB ($9.00) -- **Total**: ~$90/month - -### Optimization Strategies -- Automated lifecycle transitions -- Regular cleanup of expired objects -- Compression for text-based files -- Cost allocation tags per business unit - -## Dependencies - -- **AWS KMS**: Encryption key management -- **CloudWatch**: Monitoring and metrics -- **CloudTrail**: API call auditing -- **Lambda**: Automated cleanup functions -- **SNS**: Alert notifications - -## Compliance - -- **SOX**: Financial record retention -- **PCI DSS**: Payment card data storage -- **GDPR**: Customer data handling -- **HIPAA**: Healthcare order compliance (when applicable) - -## Contact Information - -For questions about the Order Metadata Store: -- **Primary**: Orders Team (orders@acmecorp.com) -- **Infrastructure**: Michael Chen (m.chen@acmecorp.com) -- **Security**: security@acmecorp.com -- **On-call**: Use PagerDuty escalation policy "Orders-Infrastructure" \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/orders-db/db.sql b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/orders-db/db.sql deleted file mode 100644 index 1aaec2623..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/orders-db/db.sql +++ /dev/null @@ -1,45 +0,0 @@ --- Orders Database - Simple orders table for documentation --- This is fake data for documentation purposes - --- Drop table if it exists (for clean re-runs) -DROP TABLE IF EXISTS orders; - --- Create orders table -CREATE TABLE orders ( - order_id SERIAL PRIMARY KEY, - order_number VARCHAR(50) UNIQUE NOT NULL, - customer_name VARCHAR(100) NOT NULL, - customer_email VARCHAR(100) NOT NULL, - order_status VARCHAR(20) DEFAULT 'pending', - order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - product_name VARCHAR(200) NOT NULL, - quantity INTEGER NOT NULL, - unit_price DECIMAL(10,2) NOT NULL, - total_amount DECIMAL(10,2) NOT NULL, - payment_method VARCHAR(50), - payment_status VARCHAR(20) DEFAULT 'pending', - shipping_address VARCHAR(200), - notes TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Insert sample orders -INSERT INTO orders (order_number, customer_name, customer_email, order_status, product_name, quantity, unit_price, total_amount, payment_method, payment_status, shipping_address) VALUES -('ORD-2024-001', 'John Doe', 'john.doe@email.com', 'delivered', 'MacBook Pro 14"', 1, 1999.99, 1999.99, 'credit_card', 'completed', '123 Main St, New York, NY 10001'), -('ORD-2024-002', 'Jane Smith', 'jane.smith@email.com', 'shipped', 'iPhone 15 Pro', 1, 999.99, 999.99, 'paypal', 'completed', '456 Oak Ave, Los Angeles, CA 90210'), -('ORD-2024-003', 'Michael Johnson', 'michael.johnson@email.com', 'processing', 'AirPods Pro', 2, 249.99, 499.98, 'credit_card', 'completed', '789 Pine Rd, Chicago, IL 60601'), -('ORD-2024-004', 'Emily Davis', 'emily.davis@email.com', 'pending', 'Cotton T-Shirt', 3, 19.99, 59.97, 'debit_card', 'pending', '321 Elm St, Houston, TX 77001'), -('ORD-2024-005', 'David Wilson', 'david.wilson@email.com', 'delivered', 'Smart Watch', 1, 299.99, 299.99, 'credit_card', 'completed', '654 Maple Dr, Phoenix, AZ 85001'), -('ORD-2024-006', 'Sarah Brown', 'sarah.brown@email.com', 'cancelled', 'Travel Backpack', 1, 79.99, 79.99, 'paypal', 'refunded', '987 Cedar Ln, Philadelphia, PA 19101'), -('ORD-2024-007', 'James Miller', 'james.miller@email.com', 'shipped', 'Premium Coffee Beans', 5, 24.99, 124.95, 'credit_card', 'completed', '147 Birch Way, San Antonio, TX 78201'), -('ORD-2024-008', 'Lisa Garcia', 'lisa.garcia@email.com', 'delivered', 'Running Sneakers', 1, 89.99, 89.99, 'apple_pay', 'completed', '258 Spruce St, San Diego, CA 92101'), -('ORD-2024-009', 'Robert Martinez', 'robert.martinez@email.com', 'processing', 'iPad Air', 1, 599.99, 599.99, 'google_pay', 'completed', '369 Willow Ave, Dallas, TX 75201'), -('ORD-2024-010', 'Jennifer Anderson', 'jennifer.anderson@email.com', 'pending', 'The Great Gatsby', 2, 12.99, 25.98, 'credit_card', 'pending', '741 Poplar Rd, San Jose, CA 95101'), -('ORD-2024-011', 'Mark Thompson', 'mark.thompson@email.com', 'delivered', 'Wireless Headphones', 1, 149.99, 149.99, 'paypal', 'completed', '852 Oak Street, Seattle, WA 98101'), -('ORD-2024-012', 'Amanda White', 'amanda.white@email.com', 'shipped', 'Gaming Mouse', 2, 79.99, 159.98, 'credit_card', 'completed', '963 Pine Avenue, Denver, CO 80201'); - --- Create indexes for better performance -CREATE INDEX idx_orders_status ON orders(order_status); -CREATE INDEX idx_orders_date ON orders(order_date); -CREATE INDEX idx_orders_customer_email ON orders(customer_email); \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/orders-db/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/orders-db/index.mdx deleted file mode 100644 index c20b53e32..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/orders-db/index.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -id: orders-db -name: Orders DB -version: 0.0.1 -container_type: database -technology: postgres@14 -authoritative: true -access_mode: readWrite -purpose: System of record for Orders and OrderLines -classification: internal -retention: 7y -residency: eu-west-1 ---- - - - -### What is this? -Orders DB is the primary database for the Orders domain. It is a PostgreSQL 14 database and acts as the system of record for orders and their line items. If you are looking for where an order lives, how to query it, or how order state changes are persisted, you are in the right place. - -### What does it store? -- **Orders**: One row per customer order. Includes status, totals, currency, timestamps. -- **Order Lines**: One row per item inside an order. Includes product, quantities, and pricing. -- **Relationships**: `order_lines` are linked to `orders` by `order_id` (cascading deletes are enabled). - -## Schema and test data for Orders DB - - - - - - - - - -### High-level data model -- An `order` has many `order_lines`. -- Order lifecycle is tracked via the `status` column. Common statuses: `PENDING`, `PAID`, `CANCELLED`, `FULFILLED`, `REFUNDED`. -- Monetary values are stored as integer cents; currency is 3-letter ISO (e.g. `USD`). - - -### Common queries -```sql --- Fetch a single order with its lines -SELECT o.*, l.* -FROM orders o -LEFT JOIN order_lines l USING (order_id) -WHERE o.order_id = $1; - --- List recent orders for a customer (paged) -SELECT * -FROM orders -WHERE customer_id = $1 -ORDER BY created_at DESC -LIMIT $2 OFFSET $3; - --- Daily revenue (rounded to dollars) for the last 30 days -SELECT date_trunc('day', created_at) AS day, - SUM(total_amount_cents)/100.0 AS revenue -FROM orders -WHERE created_at >= now() - interval '30 days' - AND status IN ('PAID','FULFILLED','REFUNDED') -GROUP BY 1 -ORDER BY 1; -``` - -### How data gets here (lifecycle) -1. A user places an order → `OrdersService` validates input and writes to `orders` and `order_lines` in a single transaction. -2. Payment success updates the order `status` to `PAID`. -3. Fulfillment updates the order `status` to `FULFILLED`. -4. Cancellations and refunds update `status` accordingly. -5. Each change emits a domain event that other services consume. - -### Access patterns and guidance -- Use the `order_id` for point lookups; most queries should be indexed by this or `customer_id`. -- Prefer reading from read replicas for analytics/reporting workloads when available. -- Treat `status` as the source of truth for order lifecycle; avoid inferring state from timestamps alone. -- Do not store PII beyond `customer_id` here; card data never resides in this database. - -### Retention and residency -- Retention: 7 years (see frontmatter). Archival or partitioning may be used to keep hot data smaller. -- Residency: `eu-west-1` (see frontmatter). Ensure cross-region access complies with data policies. - -### Backups and recovery -- Automated daily snapshots, plus point-in-time recovery retained per platform policy. -- Test restores should be performed regularly to validate RPO/RTO. - -### Security -- Access is role-based. `OrdersService` has read/write. Downstream services typically have read-only. -- Enforce least privilege at the database role level. Avoid ad-hoc superuser connections. - -### Operational notes -- Consider time-based partitioning on `orders.created_at` for very large datasets. -- Monitor bloat and autovacuum; ensure `vacuum_analyze` runs regularly. -- Keep indexes targeted; avoid over-indexing write-heavy columns. - -### Local development -- Connection string: provided via `ORDERS_DB_URL` environment variable. -- Seed data: use project seed scripts to create sample orders and lines. - -### Gotchas -- Deleting an order cascades to `order_lines` due to `ON DELETE CASCADE`. Prefer status changes over deletes in most cases. -- Monetary math should be done in integer cents to avoid float rounding issues. - -If you’re unsure which table to use or how to fetch something, start with the `orders` table by `order_id`, then join `order_lines` as needed. For more, see the `OrdersService` documentation and related domain events. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/orders-db/test-data.sql b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/orders-db/test-data.sql deleted file mode 100644 index 91d5a5462..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/containers/orders-db/test-data.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Insert sample orders -INSERT INTO orders (order_number, customer_name, customer_email, order_status, product_name, quantity, unit_price, total_amount, payment_method, payment_status, shipping_address) VALUES -('ORD-2024-001', 'John Doe', 'john.doe@email.com', 'delivered', 'MacBook Pro 14"', 1, 1999.99, 1999.99, 'credit_card', 'completed', '123 Main St, New York, NY 10001'), -('ORD-2024-002', 'Jane Smith', 'jane.smith@email.com', 'shipped', 'iPhone 15 Pro', 1, 999.99, 999.99, 'paypal', 'completed', '456 Oak Ave, Los Angeles, CA 90210'), -('ORD-2024-003', 'Michael Johnson', 'michael.johnson@email.com', 'processing', 'AirPods Pro', 2, 249.99, 499.98, 'credit_card', 'completed', '789 Pine Rd, Chicago, IL 60601'), -('ORD-2024-004', 'Emily Davis', 'emily.davis@email.com', 'pending', 'Cotton T-Shirt', 3, 19.99, 59.97, 'debit_card', 'pending', '321 Elm St, Houston, TX 77001'), -('ORD-2024-005', 'David Wilson', 'david.wilson@email.com', 'delivered', 'Smart Watch', 1, 299.99, 299.99, 'credit_card', 'completed', '654 Maple Dr, Phoenix, AZ 85001'), -('ORD-2024-006', 'Sarah Brown', 'sarah.brown@email.com', 'cancelled', 'Travel Backpack', 1, 79.99, 79.99, 'paypal', 'refunded', '987 Cedar Ln, Philadelphia, PA 19101'), -('ORD-2024-007', 'James Miller', 'james.miller@email.com', 'shipped', 'Premium Coffee Beans', 5, 24.99, 124.95, 'credit_card', 'completed', '147 Birch Way, San Antonio, TX 78201'), -('ORD-2024-008', 'Lisa Garcia', 'lisa.garcia@email.com', 'delivered', 'Running Sneakers', 1, 89.99, 89.99, 'apple_pay', 'completed', '258 Spruce St, San Diego, CA 92101'), -('ORD-2024-009', 'Robert Martinez', 'robert.martinez@email.com', 'processing', 'iPad Air', 1, 599.99, 599.99, 'google_pay', 'completed', '369 Willow Ave, Dallas, TX 75201'), -('ORD-2024-010', 'Jennifer Anderson', 'jennifer.anderson@email.com', 'pending', 'The Great Gatsby', 2, 12.99, 25.98, 'credit_card', 'pending', '741 Poplar Rd, San Jose, CA 95101'), -('ORD-2024-011', 'Mark Thompson', 'mark.thompson@email.com', 'delivered', 'Wireless Headphones', 1, 149.99, 149.99, 'paypal', 'completed', '852 Oak Street, Seattle, WA 98101'), -('ORD-2024-012', 'Amanda White', 'amanda.white@email.com', 'shipped', 'Gaming Mouse', 2, 79.99, 159.98, 'credit_card', 'completed', '963 Pine Avenue, Denver, CO 80201'); \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderAmended/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderAmended/index.mdx deleted file mode 100644 index 6fde47bfb..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderAmended/index.mdx +++ /dev/null @@ -1,79 +0,0 @@ ---- -id: OrderAmended -name: Order amended -version: 0.0.1 -summary: | - Indicates an order has been changed -owners: - - order-management -badges: - - content: Recently updated! - backgroundColor: green - textColor: green - - content: 'Broker:Apache Kafka' - backgroundColor: yellow - textColor: yellow - icon: kafka -schemaPath: schema.avro -draft: - title: In draft awaiting feedback - message: > - ### Use with caution, still in draft - - This event is still in draft. It is not yet ready for production. We are - still working on it and collecting feedback from the team. - - If you would like to provide feedback, please contact us at - [feedback@eventcatalog.io](mailto:feedback@eventcatalog.io) or our slack - channel [Order - Management](https://join.slack.com/t/eventcatalog/shared_invite/zt-1q900000000000000000000000000000). - - - If you are looking for a similar event, you can still use the - [OrderConfirmed](/docs/events/OrderConfirmed/0.0.1) event, until it is - deprecated. - - - If you want to use this event in lower environments, you can. We are looking - for feedback on the event and how it is used in the team. ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The OrderAmended event is triggered whenever an existing order is modified. This event ensures that all relevant services are notified of changes to an order, such as updates to order items, quantities, shipping information, or status. The event allows the system to maintain consistency and ensure that all dependent services can react appropriately to the amendments. - - - -## Example payload - -```json title="Example Payload" -{ - "orderId": "123e4567-e89b-12d3-a456-426614174000", - "userId": "123e4567-e89b-12d3-a456-426614174000", - "amendedItems": [ - { - "productId": "789e1234-b56c-78d9-e012-3456789fghij", - "productName": "Example Product", - "oldQuantity": 2, - "newQuantity": 3, - "unitPrice": 29.99, - "totalPrice": 89.97 - } - ], - "orderStatus": "confirmed", - "totalAmount": 150.75, - "timestamp": "2024-07-04T14:48:00Z" -} -``` - -## Schema (Avro)s - - - -## Schema (JSON) - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderAmended/schema.avro b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderAmended/schema.avro deleted file mode 100644 index 60827559d..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderAmended/schema.avro +++ /dev/null @@ -1,296 +0,0 @@ - { - "name": "OrderEvent", - "doc": "Complex order event with nested records, unions, and various Avro types", - "namespace": "com.example.ecommerce", - "type": "record", - "fields": [ - { - "name": "orderId", - "doc": "Unique order identifier", - "type": "string" - }, - { - "name": "timestamp", - "doc": "When the order was created", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } - }, - { - "name": "customer", - "doc": "Customer information - nested record", - "type": { - "type": "record", - "name": "Customer", - "fields": [ - { - "name": "customerId", - "type": "string" - }, - { - "name": "email", - "type": "string" - }, - { - "name": "registrationDate", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } - }, - { - "name": "loyaltyTier", - "doc": "Customer loyalty status", - "type": { - "type": "enum", - "name": "LoyaltyTier", - "symbols": ["BRONZE", "SILVER", "GOLD", "PLATINUM"] - } - } - ] - } - }, - { - "name": "items", - "doc": "Array of order line items", - "type": { - "type": "array", - "items": { - "type": "record", - "name": "LineItem", - "fields": [ - { - "name": "sku", - "type": "string" - }, - { - "name": "quantity", - "type": "int" - }, - { - "name": "price", - "doc": "Price in cents", - "type": { - "type": "long", - "logicalType": "decimal", - "precision": 10, - "scale": 2 - } - }, - { - "name": "discount", - "doc": "Optional discount - union with null", - "type": ["null", { - "type": "record", - "name": "Discount", - "fields": [ - { - "name": "code", - "type": "string" - }, - { - "name": "percentage", - "type": "float" - } - ] - }], - "default": null - } - ] - } - } - }, - { - "name": "shippingAddress", - "doc": "Shipping address - union with multiple record types", - "type": [ - "null", - { - "type": "record", - "name": "USAddress", - "fields": [ - { - "name": "street", - "type": "string" - }, - { - "name": "city", - "type": "string" - }, - { - "name": "state", - "type": "string" - }, - { - "name": "zipCode", - "type": "string" - } - ] - }, - { - "type": "record", - "name": "InternationalAddress", - "fields": [ - { - "name": "street", - "type": "string" - }, - { - "name": "city", - "type": "string" - }, - { - "name": "country", - "type": "string" - }, - { - "name": "postalCode", - "type": "string" - } - ] - } - ], - "default": null - }, - { - "name": "payment", - "doc": "Payment information - union of different payment types", - "type": [ - { - "type": "record", - "name": "CreditCard", - "fields": [ - { - "name": "lastFour", - "type": "string" - }, - { - "name": "expiration", - "type": "string" - }, - { - "name": "issuer", - "type": { - "type": "enum", - "name": "CardIssuer", - "symbols": ["VISA", "MASTERCARD", "AMEX", "DISCOVER"] - } - } - ] - }, - { - "type": "record", - "name": "PayPal", - "fields": [ - { - "name": "accountEmail", - "type": "string" - }, - { - "name": "transactionId", - "type": "string" - } - ] - }, - "null" - ], - "default": null - }, - { - "name": "metadata", - "doc": "Key-value metadata - map type", - "type": { - "type": "map", - "values": "string" - }, - "default": {} - }, - { - "name": "tags", - "doc": "Order tags - array of strings", - "type": { - "type": "array", - "items": "string" - }, - "default": [] - }, - { - "name": "orderStatus", - "doc": "Current order status", - "type": { - "type": "enum", - "name": "OrderStatus", - "symbols": ["PENDING", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED", - "RETURNED"] - } - }, - { - "name": "notes", - "doc": "Optional order notes", - "type": ["null", "string"], - "default": null - }, - { - "name": "trackingInfo", - "doc": "Shipping tracking - complex union with nested record", - "type": [ - "null", - { - "type": "record", - "name": "TrackingInfo", - "fields": [ - { - "name": "carrier", - "type": { - "type": "enum", - "name": "ShippingCarrier", - "symbols": ["FEDEX", "UPS", "USPS", "DHL"] - } - }, - { - "name": "trackingNumber", - "type": "string" - }, - { - "name": "estimatedDelivery", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } - }, - { - "name": "events", - "doc": "Tracking events history", - "type": { - "type": "array", - "items": { - "type": "record", - "name": "TrackingEvent", - "fields": [ - { - "name": "status", - "type": "string" - }, - { - "name": "location", - "type": "string" - }, - { - "name": "timestamp", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - } - } - ] - } - } - } - ] - } - ], - "default": null - } - ] - } \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderAmended/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderAmended/schema.json deleted file mode 100644 index 5ddbc679d..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderAmended/schema.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OrderAmendedEvent", - "type": "object", - "properties": { - "orderId": { - "type": "string", - "format": "uuid", - "description": "The unique identifier of the order that was amended." - }, - "userId": { - "type": "string", - "format": "uuid", - "description": "The unique identifier of the user who placed the order." - }, - "amendedItems": { - "type": "array", - "description": "A list of items that were amended in the order, each containing product details and updated quantities.", - "items": { - "type": "object", - "properties": { - "productId": { - "type": "string", - "format": "uuid", - "description": "The unique identifier of the product." - }, - "productName": { - "type": "string", - "description": "The name of the product." - }, - "oldQuantity": { - "type": "integer", - "description": "The original quantity of the product ordered." - }, - "newQuantity": { - "type": "integer", - "description": "The new quantity of the product ordered." - }, - "unitPrice": { - "type": "number", - "format": "float", - "description": "The price per unit of the product." - }, - "totalPrice": { - "type": "number", - "format": "float", - "description": "The total price for this order item (newQuantity * unitPrice)." - } - }, - "required": ["productId", "productName", "oldQuantity", "newQuantity", "unitPrice", "totalPrice"] - } - }, - "orderStatus": { - "type": "string", - "description": "The current status of the order after the amendment." - }, - "totalAmount": { - "type": "number", - "format": "float", - "description": "The total amount of the order after the amendment." - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "The date and time when the order was amended, in ISO 8601 format." - } - }, - "required": ["orderId", "userId", "amendedItems", "orderStatus", "totalAmount", "timestamp"], - "additionalProperties": false - } - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderCancelled/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderCancelled/index.mdx deleted file mode 100644 index b28c53b1d..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderCancelled/index.mdx +++ /dev/null @@ -1,55 +0,0 @@ ---- -id: OrderCancelled -name: Order cancelled -version: 0.0.1 -summary: | - Indicates an order has been canceled -owners: - - order-management -badges: - - content: Recently updated! - backgroundColor: green - textColor: green - - content: 'Broker:Apache Kafka' - backgroundColor: yellow - textColor: yellow - icon: kafka -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The OrderCancelled event is triggered whenever an existing order is cancelled. This event ensures that all relevant services are notified of the cancellation, allowing them to take appropriate actions such as updating inventory levels, refunding payments, and notifying the user. The event helps maintain consistency across the system by ensuring all dependent services are aware of the order cancellation. - -## Example payload - -```json title="Example payload" -{ - "orderId": "123e4567-e89b-12d3-a456-426614174000", - "userId": "123e4567-e89b-12d3-a456-426614174000", - "orderItems": [ - { - "productId": "789e1234-b56c-78d9-e012-3456789fghij", - "productName": "Example Product", - "quantity": 2, - "unitPrice": 29.99, - "totalPrice": 59.98 - } - ], - "orderStatus": "cancelled", - "totalAmount": 59.98, - "cancellationReason": "Customer requested cancellation", - "timestamp": "2024-07-04T14:48:00Z" -} - -``` - -## Schema - -JSON schema for the event. - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderCancelled/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderCancelled/schema.json deleted file mode 100644 index 52158ad34..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderCancelled/schema.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "OrderCancelledEvent", - "type": "object", - "properties": { - "orderId": { - "type": "string", - "format": "uuid", - "description": "The unique identifier of the order that was cancelled." - }, - "userId": { - "type": "string", - "format": "uuid", - "description": "The unique identifier of the user who placed the order." - }, - "orderItems": { - "type": "array", - "description": "A list of items included in the cancelled order, each containing product details and quantities.", - "items": { - "type": "object", - "properties": { - "productId": { - "type": "string", - "format": "uuid", - "description": "The unique identifier of the product." - }, - "productName": { - "type": "string", - "description": "The name of the product." - }, - "quantity": { - "type": "integer", - "description": "The quantity of the product ordered." - }, - "unitPrice": { - "type": "number", - "format": "float", - "description": "The price per unit of the product." - }, - "totalPrice": { - "type": "number", - "format": "float", - "description": "The total price for this order item (quantity * unit price)." - } - }, - "required": ["productId", "productName", "quantity", "unitPrice", "totalPrice"] - } - }, - "orderStatus": { - "type": "string", - "description": "The current status of the order after cancellation.", - "enum": ["cancelled"] - }, - "totalAmount": { - "type": "number", - "format": "float", - "description": "The total amount of the order that was cancelled." - }, - "cancellationReason": { - "type": "string", - "description": "The reason for the order cancellation, if provided." - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "The date and time when the order was cancelled." - } - }, - "required": ["orderId", "userId", "orderItems", "orderStatus", "totalAmount", "timestamp"], - "additionalProperties": false - } - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderConfirmed/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderConfirmed/index.mdx deleted file mode 100644 index 62ed64f18..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderConfirmed/index.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -id: OrderConfirmed -name: Order confirmed -version: 0.0.1 -summary: | - Indicates an order has been confirmed -owners: - - order-management -badges: - - content: Recently updated! - backgroundColor: green - textColor: green - icon: StarIcon - - content: 'Broker:Apache Kafka' - backgroundColor: yellow - textColor: yellow - icon: kafka -schemaPath: schema.avro -draft: true ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The OrderConfirmed event is triggered when an order has been successfully confirmed. This event notifies relevant services that the order is ready for further processing, such as inventory adjustment, payment finalization, and preparation for shipping. - - - -## Architecture Diagram - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderConfirmed/schema.avro b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderConfirmed/schema.avro deleted file mode 100644 index d0d68ef4f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/events/OrderConfirmed/schema.avro +++ /dev/null @@ -1,74 +0,0 @@ -{ - "type": "record", - "name": "OrderConfirmedEvent", - "namespace": "com.example.events", - "doc": "Event emitted when an order has been confirmed.", - "fields": [ - { - "name": "orderId", - "type": "string", - "doc": "The unique identifier of the confirmed order." - }, - { - "name": "userId", - "type": "string", - "doc": "The unique identifier of the user who placed the order." - }, - { - "name": "orderItems", - "type": { - "type": "array", - "items": { - "type": "record", - "name": "OrderItem", - "fields": [ - { - "name": "productId", - "type": "string", - "doc": "The unique identifier of the product." - }, - { - "name": "productName", - "type": "string", - "doc": "The name of the product." - }, - { - "name": "quantity", - "type": "int", - "doc": "The quantity of the product ordered." - }, - { - "name": "unitPrice", - "type": "double", - "doc": "The price per unit of the product." - }, - { - "name": "totalPrice", - "type": "double", - "doc": "The total price for this order item (quantity * unitPrice)." - } - ] - } - }, - "doc": "A list of items included in the confirmed order." - }, - { - "name": "orderStatus", - "type": "string", - "doc": "The current status of the order after confirmation." - }, - { - "name": "totalAmount", - "type": "double", - "doc": "The total amount of the confirmed order." - }, - { - "name": "confirmationTimestamp", - "type": { - "type": "long", - "logicalType": "timestamp-millis" - }, - "doc": "The date and time when the order was confirmed." - } - ] -} diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/index.mdx deleted file mode 100644 index 5caf463e2..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/index.mdx +++ /dev/null @@ -1,95 +0,0 @@ ---- -id: OrdersService -version: 0.0.3 -name: Orders Service -summary: | - Service that handles orders -owners: - - order-management -receives: - - id: InventoryAdjusted - from: - - id: orders-service-sqs-channel -writesTo: - - id: orders-db - version: 0.0.1 - - id: order-metadata-store - version: 0.0.1 -readsFrom: - - id: orders-db - - id: inventory-readmodel - - id: order-metadata-store -sends: - - id: OrderAmended - to: - - id: orders-domain-eventbus - - id: OrderCancelled - to: - - id: orders-domain-eventbus - - id: OrderConfirmed - to: - - id: orders-domain-eventbus -repository: - language: JavaScript - url: 'https://github.com/event-catalog/pretend-shipping-service' -schemaPath: openapi-v1.yml -specifications: - - type: asyncapi - path: order-service-asyncapi.yaml - - type: openapi - path: openapi-v1.yml - name: v1 API - - type: openapi - path: openapi-v2.yml - name: v2 API ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The Orders Service is responsible for managing customer orders within the system. It handles order creation, updating, status tracking, and interactions with other services such as Inventory, Payment, and Notification services to ensure smooth order processing and fulfillment. - - - - - - - - -### Core features - -| Feature | Description | -|---------|-------------| -| Order Management | Handles order creation, updates, and status tracking | -| Inventory Integration | Validates and processes inventory for incoming orders | -| Payment Processing | Integrates with payment gateways to handle payment transactions | -| Notification Integration | Sends notifications to users and other services | - -## Architecture diagram - - - - - -## Infrastructure - -The Orders Service is hosted on AWS. - -The diagram below shows the infrastructure of the Orders Service. The service is hosted on AWS and uses AWS Lambda to handle the order requests. The order is stored in an AWS Aurora database and the order metadata is stored in an AWS S3 bucket. - -```mermaid -architecture-beta - group api(logos:aws) - - service db(logos:aws-aurora)[Order DB] in api - service disk1(logos:aws-s3)[Order Metadata] in api - service server(logos:aws-lambda)[Order Handler] in api - - db:L -- R:server - disk1:T -- B:server -``` - -You can find more information about the Orders Service infrastructure in the [Orders Service documentation](https://github.com/event-catalog/pretend-shipping-service/blob/main/README.md). - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/openapi-v1.yml b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/openapi-v1.yml deleted file mode 100644 index cee9b9c1a..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/openapi-v1.yml +++ /dev/null @@ -1,290 +0,0 @@ -openapi: 3.0.3 -info: - title: Orders Service API - description: API for managing customer orders - version: 1.0.0 - -servers: - - url: https://api.example.com/v1 - description: Production server - - url: https://staging-api.example.com/v1 - description: Staging server - -paths: - /orders: - get: - summary: List all orders - description: Retrieve a list of orders with optional filtering - parameters: - - name: status - in: query - description: Filter orders by status - schema: - type: string - enum: [pending, processing, completed, cancelled] - - name: customerId - in: query - description: Filter orders by customer ID - schema: - type: string - - name: page - in: query - description: Page number for pagination - schema: - type: integer - minimum: 1 - default: 1 - - name: limit - in: query - description: Number of items per page - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - responses: - '200': - description: Successfully retrieved orders - content: - application/json: - schema: - $ref: '#/components/schemas/OrderList' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - - post: - summary: Create a new order - description: Create a new order for a customer - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OrderCreate' - responses: - '201': - description: Order created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - - /orders/{orderId}: - get: - summary: Get order by ID - description: Retrieve detailed information about a specific order - parameters: - - name: orderId - in: path - required: true - description: Unique identifier of the order - schema: - type: string - responses: - '200': - description: Order found - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '404': - $ref: '#/components/responses/NotFound' - '401': - $ref: '#/components/responses/Unauthorized' - - patch: - summary: Update order status - description: Update the status of an existing order - parameters: - - name: orderId - in: path - required: true - description: Unique identifier of the order - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OrderUpdate' - responses: - '200': - description: Order updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '400': - $ref: '#/components/responses/BadRequest' - '404': - $ref: '#/components/responses/NotFound' - '401': - $ref: '#/components/responses/Unauthorized' - -components: - schemas: - OrderCreate: - type: object - required: - - customerId - - items - properties: - customerId: - type: string - description: Unique identifier of the customer - items: - type: array - items: - $ref: '#/components/schemas/OrderItem' - shippingAddress: - $ref: '#/components/schemas/Address' - billingAddress: - $ref: '#/components/schemas/Address' - - OrderUpdate: - type: object - required: - - status - properties: - status: - type: string - enum: [pending, processing, completed, cancelled] - description: New status of the order - - Order: - type: object - properties: - id: - type: string - description: Unique identifier of the order - customerId: - type: string - description: Unique identifier of the customer - status: - type: string - enum: [pending, processing, completed, cancelled] - items: - type: array - items: - $ref: '#/components/schemas/OrderItem' - totalAmount: - type: number - format: float - shippingAddress: - $ref: '#/components/schemas/Address' - billingAddress: - $ref: '#/components/schemas/Address' - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - - OrderList: - type: object - properties: - items: - type: array - items: - $ref: '#/components/schemas/Order' - pagination: - $ref: '#/components/schemas/Pagination' - - OrderItem: - type: object - required: - - productId - - quantity - properties: - productId: - type: string - quantity: - type: integer - minimum: 1 - price: - type: number - format: float - name: - type: string - - Address: - type: object - required: - - street - - city - - country - - postalCode - properties: - street: - type: string - city: - type: string - state: - type: string - country: - type: string - postalCode: - type: string - - Pagination: - type: object - properties: - totalItems: - type: integer - totalPages: - type: integer - currentPage: - type: integer - itemsPerPage: - type: integer - - Error: - type: object - properties: - code: - type: string - message: - type: string - details: - type: array - items: - type: string - - responses: - BadRequest: - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - Unauthorized: - description: Authentication required - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - NotFound: - description: Resource not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - -security: - - BearerAuth: [] \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/openapi-v2.yml b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/openapi-v2.yml deleted file mode 100644 index 0abbeac83..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/openapi-v2.yml +++ /dev/null @@ -1,248 +0,0 @@ -openapi: 3.0.3 -info: - title: Orders Service API - description: API for managing customer orders - version: 2.0.0 - -servers: - - url: https://api.example.com/v1 - description: Production server - - url: https://staging-api.example.com/v1 - description: Staging server - -paths: - /orders: - post: - summary: Create a new order - description: Create a new order for a customer - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OrderCreate' - responses: - '201': - description: Order created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - - /orders/{orderId}: - get: - summary: Get order by ID - description: Retrieve detailed information about a specific order - parameters: - - name: orderId - in: path - required: true - description: Unique identifier of the order - schema: - type: string - responses: - '200': - description: Order found - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '404': - $ref: '#/components/responses/NotFound' - '401': - $ref: '#/components/responses/Unauthorized' - - patch: - summary: Update order status - description: Update the status of an existing order - parameters: - - name: orderId - in: path - required: true - description: Unique identifier of the order - schema: - type: string - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/OrderUpdate' - responses: - '200': - description: Order updated successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '400': - $ref: '#/components/responses/BadRequest' - '404': - $ref: '#/components/responses/NotFound' - '401': - $ref: '#/components/responses/Unauthorized' - -components: - schemas: - OrderCreate: - type: object - required: - - customerId - - items - properties: - customerId: - type: string - description: Unique identifier of the customer - items: - type: array - items: - $ref: '#/components/schemas/OrderItem' - shippingAddress: - $ref: '#/components/schemas/Address' - billingAddress: - $ref: '#/components/schemas/Address' - - OrderUpdate: - type: object - required: - - status - properties: - status: - type: string - enum: [pending, processing, completed, cancelled] - description: New status of the order - - Order: - type: object - properties: - id: - type: string - description: Unique identifier of the order - customerId: - type: string - description: Unique identifier of the customer - status: - type: string - enum: [pending, processing, completed, cancelled] - items: - type: array - items: - $ref: '#/components/schemas/OrderItem' - totalAmount: - type: number - format: float - shippingAddress: - $ref: '#/components/schemas/Address' - billingAddress: - $ref: '#/components/schemas/Address' - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - - OrderList: - type: object - properties: - items: - type: array - items: - $ref: '#/components/schemas/Order' - pagination: - $ref: '#/components/schemas/Pagination' - - OrderItem: - type: object - required: - - productId - - quantity - properties: - productId: - type: string - quantity: - type: integer - minimum: 1 - price: - type: number - format: float - name: - type: string - - Address: - type: object - required: - - street - - city - - country - - postalCode - properties: - street: - type: string - city: - type: string - state: - type: string - country: - type: string - postalCode: - type: string - - Pagination: - type: object - properties: - totalItems: - type: integer - totalPages: - type: integer - currentPage: - type: integer - itemsPerPage: - type: integer - - Error: - type: object - properties: - code: - type: string - message: - type: string - details: - type: array - items: - type: string - - responses: - BadRequest: - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - Unauthorized: - description: Authentication required - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - NotFound: - description: Resource not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - -security: - - BearerAuth: [] \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/order-service-asyncapi.yaml b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/order-service-asyncapi.yaml deleted file mode 100644 index 180ee9a75..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/order-service-asyncapi.yaml +++ /dev/null @@ -1,148 +0,0 @@ -asyncapi: 3.0.0 - -info: - title: Order service - description: | - This service is in charge of processing order events. - version: '2.0.0' - -servers: - topic: - host: https://mytopic.com - description: Custom Topic. - protocol: HTTPS - -defaultContentType: application/json - -channels: - orderEventsChannel: - address: 'orders.{orderId}' - description: All Order related events are distributed and broadcasted for the interested consumers. - title: Order events channel - messages: - OrderConfirmed: - summary: Order confirmed event - $ref: '#/components/messages/OrderConfirmed' - OrderPlaced: - summary: Order placed event - $ref: '#/components/messages/OrderPlaced' - -operations: - onOrderConfirmation: - summary: Action to confirm an order. - description: The product availability of an order will lead to the confirmation of the order. - title: Order Confirmed - channel: - $ref: '#/channels/orderEventsChannel' - action: send - onOrderPlacement: - summary: Action to place an order. - description: The reception and validation of an order will lead to the placement of the order. - title: Order Placed - channel: - $ref: '#/channels/orderEventsChannel' - action: send - -components: - messages: - OrderConfirmed: - payload: - $ref: '#/components/schemas/OrderConfirmed' - - OrderPlaced: - payload: - $ref: '#/components/schemas/OrderPlaced' - - schemas: - orderId: - description: The unique identifier of an order - type: string - pattern: ^([A-Za-z0-9_-]{21})$ - - userId: - description: The unique identifier of a user - type: string - pattern: ^([A-Za-z0-9_-]{21})$ - - productId: - description: The product unique identifier - type: string - pattern: ^([A-Za-z0-9_-]{21})$ - - Order: - required: - - orderId - - userId - - productId - - price - - quantity - - orderDate - type: object - description: order model - properties: - orderId: - "$ref": "#/components/schemas/orderId" - orderDate: - description: Date of order submition. - type: string - format: date-time - userId: - "$ref": "#/components/schemas/userId" - productId: - "$ref": "#/components/schemas/productId" - price: - type: number - quantity: - type: integer - title: Order - - EventEnvelope: - type: object - allOf: - - $ref: 'https://raw.githubusercontent.com/cloudevents/spec/v1.0.1/spec.json' - properties: - id: - type: string - format: uuid - idempotencykey: - type: string - format: uuid - correlationid: - type: string - format: uuid - causationid: - type: string - format: uuid - - EventType: - type: string - enum: - - "order.placed" - - "order.confirmed" - - OrderConfirmed: - type: object - additionalProperties: false - allOf: - - $ref: '#/components/schemas/EventEnvelope' - properties: - data: - $ref: '#/components/schemas/Order' - type: - $ref: '#/components/schemas/EventType' - - required: - - data - - OrderPlaced: - type: object - additionalProperties: false - allOf: - - $ref: '#/components/schemas/EventEnvelope' - properties: - data: - $ref: '#/components/schemas/Order' - type: - $ref: '#/components/schemas/EventType' - required: - - data diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/queries/GetOrder/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/queries/GetOrder/index.mdx deleted file mode 100644 index ea665e62c..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/queries/GetOrder/index.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -id: GetOrder -name: Get order details -version: 0.0.1 -summary: | - GET request that will return detailed information about a specific order, identified by its orderId. -owners: - - order-management -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json -sidebar: - badge: 'GET' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `GetOrder` message is a query used to retrieve detailed information about a specific order, identified by its `orderId`. It provides information such as the order status (e.g., pending, completed, shipped), the items within the order, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This query can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time order data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/queries/GetOrder/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/queries/GetOrder/schema.json deleted file mode 100644 index e7336cd96..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/queries/GetOrder/schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetOrderQuery", - "type": "object", - "properties": { - "orderId": { - "type": "string", - "format": "uuid", - "description": "The unique identifier of the order being retrieved." - } - }, - "required": ["orderId"], - "additionalProperties": false - } - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/changelog.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/changelog.mdx deleted file mode 100644 index 28c6a1f55..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/changelog.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -createdAt: 2024-08-01 ---- - -Something changed \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/index.mdx deleted file mode 100644 index c9a61bf9f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/index.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -id: OrdersService -version: 0.0.2 -name: Orders Service -summary: | - Service that handles orders -owners: - - dboyne -sends: - - id: AddInventory - version: 0.0.3 -repository: - language: JavaScript - url: 'https://github.com/event-catalog/pretend-shipping-service' -schemaPath: openapi.yml -specifications: - asyncapiPath: order-service-asyncapi.yaml - openapiPath: openapi.yml ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The Orders Service is responsible for managing customer orders within the system. It handles order creation, updating, status tracking, and interactions with other services such as Inventory, Payment, and Notification services to ensure smooth order processing and fulfillment. - -## Architecture diagram - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/openapi.yml b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/openapi.yml deleted file mode 100644 index 4aca8898f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/openapi.yml +++ /dev/null @@ -1,96 +0,0 @@ -openapi: 3.1.0 -info: - title: Simple Task - API - version: 1.0.0 - description: Simple Api - contact: {} - license: - name: apache 2.0 - identifier: apache-2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html - -servers: - - url: https://example.com/ - -paths: - /v1/task/{id}: - put: - summary: Do Simple Task - operationId: DoSimpleTask - responses: - '200': - description: do a task by id - content: - application/json: - schema: - $ref: '#/components/schemas/Task' - '204': - description: No content - '400': - description: Problem with data - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '403': - description: Not Authorized - content: - application/json: - schema: - $ref: '#/components/schemas/Unauthorized' - '404': - description: not found - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - description: Allows to do a simple task - security: - - authorization: [] - parameters: - - in: path - name: id - required: true - schema: - type: string - -components: - schemas: - Task: - properties: - comments: - type: string - creationDate: - type: string - taskId: - type: string - description: - type: string - lastUpdate: - type: string - type: object - additionalProperties: false - Error: - properties: - error: - type: string - required: - - error - type: object - Unauthorized: - properties: - message: - type: string - required: - - message - type: object - securitySchemes: - authorization: - type: http - scheme: bearer diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/order-service-asyncapi.yaml b/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/order-service-asyncapi.yaml deleted file mode 100644 index e561ec461..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/OrdersService/versioned/0.0.2/order-service-asyncapi.yaml +++ /dev/null @@ -1,148 +0,0 @@ -asyncapi: 3.0.0 - -info: - title: Order service - description: | - This service is in charge of processing order events. - version: '1.0.0' - -servers: - topic: - host: https://mytopic.com - description: Custom Topic. - protocol: HTTPS - -defaultContentType: application/json - -channels: - orderEventsChannel: - address: 'orders.{orderId}' - description: All Order related events are distributed and broadcasted for the interested consumers. - title: Order events channel - messages: - OrderConfirmed: - summary: Order confirmed event - $ref: '#/components/messages/OrderConfirmed' - OrderPlaced: - summary: Order placed event - $ref: '#/components/messages/OrderPlaced' - -operations: - onOrderConfirmation: - summary: Action to confirm an order. - description: The product availability of an order will lead to the confirmation of the order. - title: Order Confirmed - channel: - $ref: '#/channels/orderEventsChannel' - action: send - onOrderPlacement: - summary: Action to place an order. - description: The reception and validation of an order will lead to the placement of the order. - title: Order Placed - channel: - $ref: '#/channels/orderEventsChannel' - action: send - -components: - messages: - OrderConfirmed: - payload: - $ref: '#/components/schemas/OrderConfirmed' - - OrderPlaced: - payload: - $ref: '#/components/schemas/OrderPlaced' - - schemas: - orderId: - description: The unique identifier of an order - type: string - pattern: ^([A-Za-z0-9_-]{21})$ - - userId: - description: The unique identifier of a user - type: string - pattern: ^([A-Za-z0-9_-]{21})$ - - productId: - description: The product unique identifier - type: string - pattern: ^([A-Za-z0-9_-]{21})$ - - Order: - required: - - orderId - - userId - - productId - - price - - quantity - - orderDate - type: object - description: order model - properties: - orderId: - "$ref": "#/components/schemas/orderId" - orderDate: - description: Date of order submition. - type: string - format: date-time - userId: - "$ref": "#/components/schemas/userId" - productId: - "$ref": "#/components/schemas/productId" - price: - type: number - quantity: - type: integer - title: Order - - EventEnvelope: - type: object - allOf: - - $ref: 'https://raw.githubusercontent.com/cloudevents/spec/v1.0.1/spec.json' - properties: - id: - type: string - format: uuid - idempotencykey: - type: string - format: uuid - correlationid: - type: string - format: uuid - causationid: - type: string - format: uuid - - EventType: - type: string - enum: - - "order.placed" - - "order.confirmed" - - OrderConfirmed: - type: object - additionalProperties: false - allOf: - - $ref: '#/components/schemas/EventEnvelope' - properties: - data: - $ref: '#/components/schemas/Order' - type: - $ref: '#/components/schemas/EventType' - - required: - - data - - OrderPlaced: - type: object - additionalProperties: false - allOf: - - $ref: '#/components/schemas/EventEnvelope' - properties: - data: - $ref: '#/components/schemas/Order' - type: - $ref: '#/components/schemas/EventType' - required: - - data diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/RegistrationService/events/UserForgotPassword/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/RegistrationService/events/UserForgotPassword/index.mdx deleted file mode 100644 index 22d3b8fbf..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/RegistrationService/events/UserForgotPassword/index.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: UserForgotPasswordEvent -version: 0.0.1 -name: User Forgot Password -summary: | - Event that is sent when a user forgets their password -owners: - - dboyne ---- - -This is a user forgot password event that is sent when a user forgets their password. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/RegistrationService/events/UserSignedUpEvent/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/RegistrationService/events/UserSignedUpEvent/index.mdx deleted file mode 100644 index 3f00f001a..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/RegistrationService/events/UserSignedUpEvent/index.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: UserSignedUpEvent -version: 0.0.1 -name: User Signed Up -summary: | - Event that is sent when a user is signed up -owners: - - dboyne ---- - -This is a user signed up event that is sent when a user is signed up. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/RegistrationService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/RegistrationService/index.mdx deleted file mode 100644 index 2924cd14d..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/RegistrationService/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: RegistrationService -version: 0.0.1 -name: Registration Service -summary: | - Service that handles registration operations -owners: - - dboyne -sends: - - id: UserSignedUpEvent - to: - - id: registrations-domain-eventbus - - id: UserForgotPasswordEvent - to: - - id: registrations-domain-eventbus -receives: - - id: DeliveryFailed - from: - - id: registrations-domain-eventbus ---- - -This is a registration service that sends a registration event. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CancelShipment/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CancelShipment/index.mdx deleted file mode 100644 index 15d952bae..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CancelShipment/index.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -id: CancelShipment -name: Cancel shipment -version: 0.0.1 -summary: | - POST request that will cancel a shipment, identified by its shipmentId. -owners: - - dboyne -schemaPath: schema.json -sidebar: - badge: 'POST' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `CancelShipment` message is a command used to cancel a shipment, identified by its `shipmentId`. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This command can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CancelShipment/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CancelShipment/schema.json deleted file mode 100644 index d6d6d2745..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CancelShipment/schema.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "CancelShipment", - "description": "Schema for cancelling a shipment", - "properties": { - "shipmentId": { - "type": "string", - "description": "Unique identifier for the shipment" - } - }, - "required": ["shipmentId"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CreateReturnLabel/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CreateReturnLabel/index.mdx deleted file mode 100644 index 6c2a91cae..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CreateReturnLabel/index.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -id: CreateReturnLabel -name: Create return label -version: 0.0.1 -summary: | - POST request that will create a return label for a specific shipment, identified by its shipmentId. -owners: - - dboyne -schemaPath: schema.json -sidebar: - badge: 'POST' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `CreateReturnLabel` message is a command used to create a return label for a specific shipment, identified by its `shipmentId`. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This command can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CreateReturnLabel/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CreateReturnLabel/schema.json deleted file mode 100644 index 933b175e6..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CreateReturnLabel/schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "CreateReturnLabel", - "description": "Schema for creating a return shipping label", - "properties": { - "CreateReturnLabel": { - "type": "object", - "properties": { - "shipmentId": { - "type": "string", - "description": "Unique identifier for the shipment" - } - }, - "required": ["shipmentId"] - } - }, - "required": ["CreateReturnLabel"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CreateShipment/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CreateShipment/index.mdx deleted file mode 100644 index 6bdacee49..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/CreateShipment/index.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -id: CreateShipment -name: Create shipment -version: 0.0.1 -summary: | - POST request that will create a shipment for a specific order, identified by its orderId. -owners: - - dboyne -schemaPath: schema.json -sidebar: - badge: 'POST' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `CreateShipment` message is a command used to create a shipment for a specific order, identified by its `orderId`. It provides information such as the order status (e.g., pending, completed, shipped), the items within the order, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This command can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time order data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/UpdateShipmentStatus/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/UpdateShipmentStatus/index.mdx deleted file mode 100644 index b1764a31f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/UpdateShipmentStatus/index.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -id: UpdateShipmentStatus -name: Update shipment status -version: 0.0.1 -summary: | - POST request that will update the status of a shipment, identified by its shipmentId. -owners: - - dboyne -schemaPath: schema.json -sidebar: - badge: 'POST' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `UpdateShipmentStatus` message is a command used to update the status of a shipment, identified by its `shipmentId`. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This command can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/UpdateShipmentStatus/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/UpdateShipmentStatus/schema.json deleted file mode 100644 index f95a1b0cd..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/commands/UpdateShipmentStatus/schema.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "UpdateShipmentStatus", - "description": "Schema for updating a shipment's status", - "properties": { - "UpdateShipmentStatus": { - "type": "object", - "properties": { - "shipmentId": { - "type": "string", - "description": "Unique identifier for the shipment" - }, - "status": { - "type": "string", - "enum": ["pending", "shipped", "delivered", "returned"], - "description": "Current status of the shipment" - }, - "updatedAt": { - "type": "string", - "format": "date-time", - "description": "Timestamp when the status was last updated" - } - }, - "required": ["shipmentId", "status"] - } - }, - "required": ["UpdateShipmentStatus"] -} diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/index.mdx deleted file mode 100644 index 998242391..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: DeliveryFailed -name: Delivery failed -version: 1.0.0 -summary: | - Event that is emitted when a shipment delivery fails. -owners: - - dboyne -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `DeliveryFailed` event is emitted when a shipment delivery fails. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This event can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/schema.json deleted file mode 100644 index d8998d9ce..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/schema.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "DeliveryFailed", - "description": "Schema for delivery failed event", - "properties": { - "shipmentId": { - "type": "number", - "description": "Unique identifier for the shipment" - }, - "orderId": { - "type": "string", - "description": "Identifier for the associated order" - }, - "failureReason": { - "type": "string", - "description": "Reason for the delivery failure" - } - }, - "required": ["shipmentId", "orderId", "failureReason"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.1/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.1/index.mdx deleted file mode 100644 index 3c1c81f92..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.1/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: DeliveryFailed -name: Delivery failed -version: 0.0.1 -summary: | - Event that is emitted when a shipment delivery fails. -owners: - - dboyne -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `DeliveryFailed` event is emitted when a shipment delivery fails. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This event can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.1/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.1/schema.json deleted file mode 100644 index 81fc9a244..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.1/schema.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "DeliveryFailed", - "description": "Schema for delivery failed event", - "properties": { - "shipmentId": { - "type": "string", - "description": "Unique identifier for the shipment" - } - }, - "required": ["shipmentId", "orderId"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.2/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.2/index.mdx deleted file mode 100644 index 768e8a3f9..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.2/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: DeliveryFailed -name: Delivery failed -version: 0.0.2 -summary: | - Event that is emitted when a shipment delivery fails. -owners: - - dboyne -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `DeliveryFailed` event is emitted when a shipment delivery fails. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This event can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.2/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.2/schema.json deleted file mode 100644 index 92371d9e2..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/DeliveryFailed/versioned/0.0.2/schema.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "DeliveryFailed", - "description": "Schema for delivery failed event", - "properties": { - "shipmentId": { - "type": "number", - "description": "Unique identifier for the shipment" - } - }, - "required": ["shipmentId", "orderId"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ReturnInitiated/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ReturnInitiated/index.mdx deleted file mode 100644 index cb7b5ecce..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ReturnInitiated/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: ReturnInitiated -name: Return initiated -version: 0.0.1 -summary: | - Event that is emitted when a return is initiated. -owners: - - dboyne -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `ReturnInitiated` event is emitted when a return is initiated. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This event can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time return data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ReturnInitiated/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ReturnInitiated/schema.json deleted file mode 100644 index 6382f8279..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ReturnInitiated/schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "ReturnInitiated", - "description": "Schema for return initiated event", - "properties": { - "shipmentId": { - "type": "string", - "description": "Unique identifier for the shipment" - }, - "orderId": { - "type": "string", - "description": "Identifier for the associated order" - } - }, - "required": ["shipmentId", "orderId"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentCreated/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentCreated/index.mdx deleted file mode 100644 index ba7e68a53..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentCreated/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: ShipmentCreated -name: Shipment created -version: 0.0.1 -summary: | - Event that is emitted when a shipment is created. -owners: - - dboyne -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `ShipmentCreated` event is emitted when a shipment is created. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This event can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentCreated/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentCreated/schema.json deleted file mode 100644 index a42f2b617..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentCreated/schema.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "ShipmentCreated", - "description": "Schema for shipment created event", - "properties": { - "shipmentId": { - "type": "string", - "description": "Unique identifier for the shipment" - }, - "orderId": { - "type": "string", - "description": "Identifier for the associated order" - }, - "address": { - "type": "object", - "properties": { - "street": { - "type": "string", - "description": "Street address for the shipment" - }, - "city": { - "type": "string", - "description": "City for the shipment" - }, - "state": { - "type": "string", - "description": "State for the shipment" - }, - "postalCode": { - "type": "string", - "description": "Postal code for the shipment" - }, - "country": { - "type": "string", - "description": "Country for the shipment" - } - }, - "required": ["street", "city", "state", "postalCode", "country"] - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "itemId": { - "type": "string", - "description": "Identifier for the item" - }, - "quantity": { - "type": "integer", - "description": "Quantity of the item" - } - }, - "required": ["itemId", "quantity"] - } - }, - "shippingMethod": { - "type": "string", - "description": "Method of shipping (e.g., standard, express)" - } - }, - "required": ["shipmentId", "orderId", "address", "items", "shippingMethod"] -} - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDelivered/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDelivered/index.mdx deleted file mode 100644 index 511af6793..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDelivered/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: ShipmentDelivered -name: Shipment delivered -version: 0.0.1 -summary: | - Event that is emitted when a shipment is delivered. -owners: - - dboyne -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `ShipmentDelivered` event is emitted when a shipment is delivered. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This event can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDelivered/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDelivered/schema.json deleted file mode 100644 index e0fe1211c..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDelivered/schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "ShipmentDelivered", - "description": "Schema for shipment delivered event", - "properties": { - "shipmentId": { - "type": "string", - "description": "Unique identifier for the shipment" - }, - "orderId": { - "type": "string", - "description": "Identifier for the associated order" - } - }, - "required": ["shipmentId", "orderId"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDispatched/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDispatched/index.mdx deleted file mode 100644 index fad0802d8..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDispatched/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: ShipmentDispatched -name: Shipment dispatched -version: 0.0.1 -summary: | - Event that is emitted when a shipment is dispatched. -owners: - - dboyne -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `ShipmentDispatched` event is emitted when a shipment is dispatched. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This event can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDispatched/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDispatched/schema.json deleted file mode 100644 index 31f82e379..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentDispatched/schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "ShipmentDispatched", - "description": "Schema for shipment dispatched event", - "properties": { - "shipmentId": { - "type": "string", - "description": "Unique identifier for the shipment" - }, - "orderId": { - "type": "string", - "description": "Identifier for the associated order" - } - }, - "required": ["shipmentId", "orderId"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentInTransit/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentInTransit/index.mdx deleted file mode 100644 index 73c8a58a5..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentInTransit/index.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: ShipmentInTransit -name: Shipment in transit -version: 0.0.1 -summary: | - Event that is emitted when a shipment is in transit. -owners: - - dboyne -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `ShipmentInTransit` event is emitted when a shipment is in transit. It provides information such as the shipment status (e.g., pending, completed, shipped), the items within the shipment, billing and shipping details, payment information, and the order's total amount. This query is commonly used by systems managing order processing, customer service, or order tracking functionalities. - -This event can be applied in e-commerce systems, marketplaces, or any platform where users and systems need real-time shipment data for tracking, auditing, or managing customer purchases. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentInTransit/schema.json b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentInTransit/schema.json deleted file mode 100644 index 59a728888..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/events/ShipmentInTransit/schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "ShipmentInTransit", - "description": "Schema for shipment in transit event", - "properties": { - "shipmentId": { - "type": "string", - "description": "Unique identifier for the shipment" - }, - "orderId": { - "type": "string", - "description": "Identifier for the associated order" - } - }, - "required": ["shipmentId", "orderId"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/index.mdx deleted file mode 100644 index 8c78b44e6..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/services/ShippingService/index.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -id: ShippingService -version: 0.0.1 -name: Shipping Service -summary: | - Service that handles shipping -owners: - - dboyne -receives: - - id: PaymentProcessed - from: - - id: orders-domain-eventbus -sends: - - id: ShipmentCreated - to: - - id: orders-domain-eventbus - - id: ReturnInitiated - to: - - id: orders-domain-eventbus - - id: ShipmentDispatched - to: - - id: orders-domain-eventbus - - id: ShipmentInTransit - to: - - id: orders-domain-eventbus - - id: ShipmentDelivered - to: - - id: orders-domain-eventbus - - id: DeliveryFailed - to: - - id: orders-domain-eventbus -repository: - language: JavaScript - url: 'https://github.com/event-catalog/pretend-shipping-service' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The Shipping Service is responsible for managing shipping within the system. It handles order creation, updating, status tracking, and interactions with other services such as Inventory, Payment, and Notification services to ensure smooth order processing and fulfillment. - - - - - - -### Core features - -The Shipping Service is responsible for managing shipping within the system. It handles order creation, updating, status tracking, and interactions with other services such as Inventory, Payment, and Notification services to ensure smooth order processing and fulfillment. - - -## Architecture diagram - - - - - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/ubiquitous-language.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/ubiquitous-language.mdx deleted file mode 100644 index 05859cc15..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/ubiquitous-language.mdx +++ /dev/null @@ -1,126 +0,0 @@ ---- -dictionary: - - id: Payment - name: Payment - summary: "The act of paying for magical goods or services." - icon: Wand2 - - id: Purchase Order - name: Purchase Order - summary: "A mystical document issued by a buyer to a seller indicating the types, quantities, and agreed prices for enchanted products or services." - description: | - A purchase order (PO) is a magical document that initiates the buying process between mystical entities. It protects both buyer and seller by clearly documenting the transaction details. Key components include: - - - Unique PO number for tracking - - Detailed item specifications and quantities - - Agreed prices and payment terms - - Delivery requirements and timelines - - Terms and conditions of the purchase - - icon: FileText - - id: Order Line - name: Order Line - summary: "An individual enchanted item within a purchase order, representing a specific magical product or service being ordered." - description: | - Order lines are the fundamental building blocks of any purchase order. Each line represents a distinct item or service and contains critical information for order fulfillment: - - - Product identifier (SKU or part number) - - Quantity ordered - - Unit price and total line value - - Special handling instructions - - Required delivery date - - Order lines drive warehouse picking operations, shipping processes, and financial calculations. They are essential for tracking partial shipments and managing order modifications. - icon: ListOrdered - - id: SKU - name: SKU - summary: "Sorcery Keeping Unit - A unique identifier for distinct magical products and their variants in inventory." - description: | - SKUs are the cornerstone of effective inventory management systems. Each SKU represents a unique combination of product attributes: - - - Product variations (size, color, style) - - Storage location identifiers - - Supplier information - - Reorder points and quantities - - SKUs enable precise inventory tracking, automated reordering, and detailed sales analytics. They are crucial for maintaining optimal stock levels and preventing stockouts or overstock situations. - icon: Tag - - id: Consignment - name: Consignment - summary: "A batch of enchanted goods destined for or delivered to someone." - description: | - A consignment represents the physical movement of goods through the supply chain. It encompasses all aspects of the shipping process: - - - Packaging and labeling requirements - - Transportation method and routing - - Customs documentation for international shipments - - Tracking and proof of delivery - - Consignments may combine multiple orders for efficient shipping and can be tracked as a single unit throughout the delivery process. They are crucial for managing logistics costs and ensuring timely delivery to customers. - icon: Package - - id: Invoice - name: Invoice - summary: "A document issued by a seller to a buyer, listing enchanted goods or services provided and the amount due." - description: | - Invoices are critical for financial transactions, serving as a request for payment from the buyer. They include: - - - Invoice number for tracking - - List of products or services provided - - Total amount due and payment terms - - Seller and buyer contact information - - Due date for payment - - Invoices are essential for accounting, tax purposes, and maintaining cash flow. - icon: Receipt - - id: Supplier - name: Supplier - summary: "An entity that provides enchanted goods or services to another organization." - description: | - Suppliers are key partners in the supply chain, responsible for delivering the necessary products or services. Key aspects include: - - - Supplier identification and contact details - - Product or service offerings - - Pricing and payment terms - - Delivery schedules and reliability - - Effective supplier management ensures quality, cost-effectiveness, and timely delivery. - icon: Truck - - id: Inventory - name: Inventory - summary: "The complete list of enchanted items held in stock by a business." - description: | - Inventory management is crucial for balancing supply and demand. It involves: - - - Tracking stock levels and locations - - Managing reorder points and quantities - - Conducting regular stock audits - - Analyzing inventory turnover rates - - Proper inventory management minimizes costs and maximizes service levels. - icon: Warehouse - - id: Fulfillment - name: Fulfillment - summary: "The process of completing an order and delivering it to the customer." - description: | - Fulfillment encompasses all steps from order receipt to delivery, including: - - - Order processing and picking - - Packaging and shipping - - Delivery tracking and confirmation - - Handling returns and exchanges - - Efficient fulfillment is key to customer satisfaction and operational efficiency. - icon: PackageCheck - - id: Return - name: Return - summary: "The process of sending back enchanted goods to the seller for a refund or exchange." - description: | - Returns management is an important aspect of customer service and inventory control. It involves: - - - Processing return requests and authorizations - - Inspecting returned items for quality - - Restocking or disposing of returned goods - - Issuing refunds or exchanges - - A streamlined returns process enhances customer loyalty and operational efficiency. - icon: PackageX ---- diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/versioned/0.0.1/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/versioned/0.0.1/index.mdx deleted file mode 100644 index 62330c95f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/versioned/0.0.1/index.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -id: Orders -name: Orders -version: 0.0.1 -summary: | - Domain for everything shopping -owners: - - dboyne - - full-stack -services: - - id: InventoryService - version: 0.0.2 -badges: - - content: New domain - backgroundColor: blue - textColor: blue ---- - -## Overview - - diff --git a/examples/default/domains/E-Commerce/subdomains/Orders/versioned/0.0.2/index.mdx b/examples/default/domains/E-Commerce/subdomains/Orders/versioned/0.0.2/index.mdx deleted file mode 100644 index 7a93c8ce7..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Orders/versioned/0.0.2/index.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: Orders -name: Orders -version: 0.0.2 -owners: - - dboyne -services: - - id: InventoryService - version: 0.0.2 - - id: NotificationService - version: 0.0.2 - - id: OrdersService - version: 0.0.2 -badges: - - content: New domain - backgroundColor: blue - textColor: blue ---- - -## Overview - -The Orders domain handles all operations related to customer orders, from creation to fulfillment. This documentation provides an overview of the events and services involved in the Orders domain, helping developers and stakeholders understand the event-driven architecture. - -:::warning -Please ensure all services are updated to the latest version for compatibility and performance improvements. -::: - -## Bounded context - - - -### Order example (sequence diagram) - -```mermaid -sequenceDiagram - participant Customer - participant OrdersService - participant InventoryService - participant NotificationService - - Customer->>OrdersService: Place Order - OrdersService->>InventoryService: Check Inventory - InventoryService-->>OrdersService: Inventory Available - OrdersService->>InventoryService: Reserve Inventory - OrdersService->>NotificationService: Send Order Confirmation - NotificationService-->>Customer: Order Confirmation - OrdersService->>Customer: Order Placed Successfully - OrdersService->>InventoryService: Update Inventory -``` - - diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/channels/payment-domain-eventbus/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/channels/payment-domain-eventbus/index.mdx deleted file mode 100644 index 147e90be1..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/channels/payment-domain-eventbus/index.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -id: payment-domain-eventbus -name: Payment Domain EventBus -version: 1.0.0 -summary: | - Amazon Payment Domain EventBus -owners: - - dboyne -routes: - - id: cross-account-bus ---- - -{/* Information about the Orders Domain EventBus */} - -Full this.... diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/entities/Address/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/entities/Address/index.mdx deleted file mode 100644 index cb17bf85d..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/entities/Address/index.mdx +++ /dev/null @@ -1,162 +0,0 @@ ---- -id: Address -name: Address -version: 1.0.0 -identifier: addressId -summary: Represents shipping and billing addresses for customers and orders. -properties: - - name: addressId - type: UUID - required: true - description: Unique identifier for the address - - name: customerId - type: UUID - required: false - description: Customer this address belongs to - references: Customer - referencesIdentifier: customerId - relationType: hasOne - - name: type - type: string - required: true - description: Type of address - enum: ['billing', 'shipping', 'both'] - - name: firstName - type: string - required: true - description: First name of the recipient - - name: lastName - type: string - required: true - description: Last name of the recipient - - name: company - type: string - required: false - description: Company name if applicable - - name: addressLine1 - type: string - required: true - description: Primary address line (street address) - - name: addressLine2 - type: string - required: false - description: Secondary address line (apartment, suite, etc.) - - name: city - type: string - required: true - description: City name - - name: state - type: string - required: true - description: State or province - - name: postalCode - type: string - required: true - description: Postal or ZIP code - - name: country - type: string - required: true - description: Country code (ISO 3166-1 alpha-2) - - name: phone - type: string - required: false - description: Phone number for delivery contact - - name: isDefault - type: boolean - required: true - description: Whether this is the default address for the customer - - name: isValidated - type: boolean - required: true - description: Whether the address has been validated - - name: validationDetails - type: object - required: false - description: Address validation details - properties: - - name: validatedAt - type: DateTime - description: When the address was validated - - name: validationService - type: string - description: Service used for validation - - name: confidence - type: decimal - description: Validation confidence score - - name: geocoordinates - type: object - required: false - description: Geographic coordinates for the address - properties: - - name: latitude - type: decimal - description: Latitude coordinate - - name: longitude - type: decimal - description: Longitude coordinate - - name: deliveryInstructions - type: string - required: false - description: Special delivery instructions - - name: orders - type: array - items: - type: Order - required: false - description: Orders using this address - references: Order - referencesIdentifier: shippingAddress - relationType: hasMany - - name: payments - type: array - items: - type: Payment - required: false - description: Payments using this as billing address - references: Payment - referencesIdentifier: billingAddress - relationType: hasMany - - name: createdAt - type: DateTime - required: true - description: Date and time when the address was created - - name: updatedAt - type: DateTime - required: false - description: Date and time when the address was last updated ---- - -## Overview - -The Address entity stores shipping and billing addresses for customers, orders, and payments. It supports address validation, geocoding, and delivery instructions to ensure accurate order fulfillment. - -### Entity Properties - - -## Relationships - -* **Customer:** An address can belong to one `Customer` (identified by `customerId`). -* **Order:** An address can be used by multiple `Order` entities for shipping. -* **Payment:** An address can be used by multiple `Payment` entities for billing. - -## Address Types - -* **Billing:** Used for payment processing and invoicing -* **Shipping:** Used for order delivery -* **Both:** Can be used for both billing and shipping - -## Examples - -* **Address #1:** John Doe's home address - default shipping and billing address. -* **Address #2:** Corporate office address - billing only, validated with high confidence. -* **Address #3:** Gift recipient address - shipping only, with special delivery instructions. - -## Business Rules - -* Each customer can have only one default address per type -* Addresses must be validated before being marked as default -* International addresses require country-specific validation -* Geocoordinates are automatically populated when available -* Address changes should create new versions for audit trail -* PO Box addresses may have shipping restrictions -* Address validation improves delivery success rates \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/entities/Invoice/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/entities/Invoice/index.mdx deleted file mode 100644 index ea2277e64..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/entities/Invoice/index.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -id: Invoice -name: Invoice -version: 1.0.0 -identifier: invoiceId -summary: Represents a bill issued to a customer, detailing charges for products or services. - -properties: - - name: invoiceId - type: UUID - required: true - description: Unique identifier for the invoice. - - name: customerId - type: UUID - required: true - description: Identifier of the customer being invoiced - references: Customer - relationType: hasOne - - name: orderId # Optional, if invoice is directly tied to a single order - type: UUID - required: false - description: Identifier of the associated order, if applicable. - - name: subscriptionId # Optional, if invoice is for a subscription period - type: UUID - required: false - description: Identifier of the associated subscription, if applicable. - - name: invoiceNumber - type: string - required: true - description: Human-readable, sequential identifier for the invoice (may have specific format). - - name: issueDate - type: Date - required: true - description: Date the invoice was generated and issued. - - name: dueDate - type: Date - required: true - description: Date by which the payment for the invoice is due. - - name: totalAmount - type: decimal - required: true - description: The total amount due on the invoice. - - name: currency - type: string # ISO 4217 code - required: true - description: Currency of the invoice amount. - - name: status - type: string # (e.g., Draft, Sent, Paid, Overdue, Void) - required: true - description: Current status of the invoice. - - name: billingAddressId # Address used for this specific invoice - type: UUID - required: true - description: Identifier for the billing address used on this invoice. - - name: lineItems - type: array - items: - type: InvoiceLineItem # Assuming a value object or separate entity for line items - required: true - description: List of individual items or services being charged on the invoice. - - name: createdAt - type: DateTime - required: true - description: Timestamp when the invoice record was created. - - name: paidAt # Timestamp when payment was confirmed - type: DateTime - required: false - description: Timestamp when the invoice was marked as paid. ---- - -## Overview - -The Invoice entity represents a formal request for payment issued by the business to a customer. It details the products, services, quantities, prices, taxes, and total amount due, along with payment terms. - -### Entity Properties - - -## Relationships - -* **Customer:** An invoice is issued to one `Customer`. -* **Order/Subscription:** An invoice may be related to one or more `Order`s or a specific `Subscription` period. -* **Payment:** An invoice is settled by one or more `Payment` transactions. -* **InvoiceLineItem:** An invoice contains multiple `InvoiceLineItem`s detailing the charges. -* **BillingProfile:** Invoice generation often uses details from the customer's `BillingProfile`. - -## Examples - -* Invoice #INV-00123 issued to Jane Doe for her monthly subscription renewal, due in 15 days. -* Invoice #INV-00124 issued to Acme Corp for consulting services rendered in the previous month, status Paid. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/entities/Payment/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/entities/Payment/index.mdx deleted file mode 100644 index e906c87d4..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/entities/Payment/index.mdx +++ /dev/null @@ -1,140 +0,0 @@ ---- -id: Payment -name: Payment -version: 1.0.0 -identifier: paymentId -aggregateRoot: true -summary: Represents payment transactions for orders in the e-commerce system. -properties: - - name: paymentId - type: UUID - required: true - description: Unique identifier for the payment - - name: orderId - type: UUID - required: true - description: Order this payment is associated with - references: Order - referencesIdentifier: orderId - relationType: hasOne - - name: customerId - type: UUID - required: true - description: Customer who made the payment - references: Customer - referencesIdentifier: customerId - relationType: hasOne - - name: amount - type: decimal - required: true - description: Payment amount - - name: currency - type: string - required: true - description: Currency code (e.g., USD, EUR, GBP) - - name: paymentMethod - type: string - required: true - description: Payment method used - enum: ['credit_card', 'debit_card', 'paypal', 'stripe', 'bank_transfer', 'apple_pay', 'google_pay'] - - name: paymentMethodDetails - type: object - required: false - description: Additional payment method specific details - properties: - - name: cardLast4 - type: string - description: Last 4 digits of card number - - name: cardType - type: string - description: Card type (Visa, MasterCard, etc.) - - name: expiryMonth - type: integer - description: Card expiry month - - name: expiryYear - type: integer - description: Card expiry year - - name: status - type: string - required: true - description: Current payment status - enum: ['pending', 'processing', 'completed', 'failed', 'cancelled', 'refunded', 'partially_refunded'] - - name: transactionId - type: string - required: false - description: External payment processor transaction ID - - name: gatewayResponse - type: object - required: false - description: Raw response from payment gateway - - name: billingAddress - type: Address - required: true - description: Billing address for the payment - references: Address - referencesIdentifier: addressId - relationType: hasOne - - name: processedAt - type: DateTime - required: false - description: Date and time when payment was processed - - name: failureReason - type: string - required: false - description: Reason for payment failure if applicable - - name: refunds - type: array - items: - type: PaymentRefund - required: false - description: Refunds associated with this payment - references: PaymentRefund - referencesIdentifier: paymentId - relationType: hasMany - - name: createdAt - type: DateTime - required: true - description: Date and time when the payment record was created - - name: updatedAt - type: DateTime - required: false - description: Date and time when the payment record was last updated ---- - -## Overview - -The Payment entity manages all payment transactions in the e-commerce system. It tracks payment details, status, and relationships with orders and customers, supporting various payment methods and refund scenarios. - -### Entity Properties - - -## Relationships - -* **Order:** Each payment belongs to one `Order` (identified by `orderId`). -* **Customer:** Each payment is made by one `Customer` (identified by `customerId`). -* **Address:** Each payment has a billing `Address` (identified by `billingAddress`). -* **PaymentRefund:** A payment can have multiple `PaymentRefund` entities for partial or full refunds. - -## Payment States - -``` -pending → processing → completed - ↓ ↓ ↓ -cancelled failed refunded/partially_refunded -``` - -## Examples - -* **Payment #1:** $299.99 credit card payment for Order #12345, completed successfully. -* **Payment #2:** $150.00 PayPal payment for Order #67890, failed due to insufficient funds. -* **Payment #3:** $500.00 bank transfer, completed with $50.00 partial refund. - -## Business Rules - -* Payment amount must match the order total -* Payment status transitions must follow valid state machine -* Failed payments should include failure reason -* Completed payments cannot be cancelled -* Refunds cannot exceed the original payment amount -* Payment method details are encrypted and PCI compliant -* Transaction IDs from payment gateways must be stored for reconciliation \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/entities/PaymentMethod/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/entities/PaymentMethod/index.mdx deleted file mode 100644 index feec5ece3..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/entities/PaymentMethod/index.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -id: PaymentMethod -name: PaymentMethod -version: 1.0.0 -identifier: paymentMethodId -summary: Represents a payment instrument a customer can use, like a credit card or bank account. - -properties: - - name: paymentMethodId - type: UUID - required: true - description: Unique identifier for the payment method. - - name: customerId - type: UUID - required: true - description: Identifier of the customer who owns this payment method. - references: Customer - relationType: hasOne - - name: type - type: string # (e.g., CreditCard, BankAccount, PayPal, ApplePay) - required: true - description: The type of payment method. - - name: details # Contains type-specific details (masked, tokenized) - type: object - required: true - description: Contains type-specific, often sensitive details (e.g., last 4 digits of card, card brand, bank name, account type, token). **Never store raw PANs or sensitive data.** - # Example structure for CreditCard: - # details: - # brand: "Visa" - # last4: "1234" - # expiryMonth: 12 - # expiryYear: 2025 - # cardholderName: "Jane Doe" - # gatewayToken: "tok_abc123xyz" - - name: isDefault - type: boolean - required: true - description: Indicates if this is the customer's default payment method. - - name: billingAddressId # Link to the billing address associated with this method - type: UUID - required: true - description: Identifier for the billing address verified for this payment method. - - name: status - type: string # (e.g., Active, Expired, Invalid, Removed) - required: true - description: Current status of the payment method. - - name: createdAt - type: DateTime - required: true - description: Timestamp when the payment method was added. - - name: updatedAt - type: DateTime - required: true - description: Timestamp when the payment method was last updated. ---- - -## Overview - -The PaymentMethod entity represents a specific payment instrument registered by a customer, such as a credit card or a linked bank account. It stores necessary (non-sensitive) details required to initiate payments and links to the associated customer and billing address. - -**Security Note:** Sensitive details like full card numbers or bank account numbers should **never** be stored directly. Rely on tokenization provided by payment gateways. - -### Entity Properties - - -## Relationships - -* **Customer:** A payment method belongs to one `Customer`. -* **Address:** Linked to a specific billing `Address`. -* **Payment:** Used to make `Payment` transactions. -* **Subscription:** May be designated as the payment method for a `Subscription`. - -## Examples - -* Jane Doe's default Visa card ending in 1234, expiring 12/2025, status Active. -* John Smith's linked bank account (Chase, Checking), status Active. -* An old MasterCard ending in 5678 belonging to Jane Doe, status Expired. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/entities/PaymentRefund/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/entities/PaymentRefund/index.mdx deleted file mode 100644 index c5a203533..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/entities/PaymentRefund/index.mdx +++ /dev/null @@ -1,80 +0,0 @@ ---- -id: PaymentRefunded -name: PaymentRefunded -version: 1.0.0 -identifier: refundId -aggregateRoot: true -summary: Represents refunded payment transactions for orders in the e-commerce system. -properties: - - name: refundId - type: UUID - required: true - description: Unique identifier for the refund - - name: paymentId - type: UUID - required: true - description: Payment this refund is associated with - references: Payment - referencesIdentifier: paymentId - relationType: hasOne - - name: amount - type: decimal - required: true - description: Refund amount - - name: currency - type: string - required: true - description: Currency code (e.g., USD, EUR, GBP) - - name: status - type: string - required: true - description: Current refund status - enum: ['pending', 'processing', 'completed', 'failed', 'cancelled'] - - name: processedAt - type: DateTime - required: false - description: Date and time when refund was processed - - name: failureReason - type: string - required: false - description: Reason for refund failure if applicable - - name: createdAt - type: DateTime - required: true - description: Date and time when the refund record was created - - name: updatedAt - type: DateTime - required: false - description: Date and time when the refund record was last updated ---- - -## Overview - -The PaymentRefunded entity manages all refund transactions in the e-commerce system. It tracks refund details, status, and relationships with payments, supporting various refund scenarios. - -### Entity Properties - - -## Relationships - -* **Payment:** Each refund belongs to one `Payment` (identified by `paymentId`). - -## Refund States - -``` -pending → processing → completed - ↓ ↓ ↓ -cancelled failed -``` - -## Examples - -* **Refund #1:** $50.00 refund for Payment #12345, completed successfully. -* **Refund #2:** $20.00 refund for Payment #67890, failed due to processing error. - -## Business Rules - -* Refund amount cannot exceed the original payment amount -* Refund status transitions must follow valid state machine -* Failed refunds should include failure reason -* Completed refunds cannot be cancelled \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/entities/Transaction/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/entities/Transaction/index.mdx deleted file mode 100644 index f13ac5fa6..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/entities/Transaction/index.mdx +++ /dev/null @@ -1,77 +0,0 @@ ---- -id: Transaction -name: Transaction -version: 1.0.0 -identifier: transactionId -summary: Represents a low-level interaction with a payment gateway or processor (e.g., authorize, capture, refund, void). - -properties: - - name: transactionId - type: UUID - required: true - description: Unique identifier for this specific gateway transaction. - - name: paymentId - type: UUID - required: true - references: Payment - relationType: hasOne - description: Identifier of the parent Payment this transaction belongs to. - - name: type - type: string # (e.g., Authorize, Capture, Sale, Refund, Void, Verify) - required: true - description: The type of operation performed with the payment gateway. - - name: gatewayReferenceId - type: string - required: true - description: Unique transaction ID provided by the external payment gateway. - - name: amount - type: decimal - required: true - description: The amount associated with this specific transaction operation. - - name: currency - type: string # ISO 4217 code - required: true - description: Currency of the transaction amount. - - name: status - type: string # (e.g., Success, Failure, Pending) - required: true - description: Status reported by the gateway for this specific operation. - - name: responseCode # Gateway-specific code - type: string - required: false - description: Response code returned by the payment gateway. - - name: responseMessage # Gateway-specific message - type: string - required: false - description: Detailed message or reason returned by the gateway. - - name: processedAt - type: DateTime - required: true - description: Timestamp when the transaction was processed by the gateway. - - name: rawRequest # Optional, for debugging - consider security implications - type: text - required: false - description: Raw request payload sent to the gateway (use with caution). - - name: rawResponse # Optional, for debugging - consider security implications - type: text - required: false - description: Raw response payload received from the gateway (use with caution). ---- - -## Overview - -The Transaction entity logs the individual interactions with an external payment processor (like Stripe, PayPal, Adyen) that occur as part of processing a `Payment`. This provides a detailed audit trail of gateway operations, including authorizations, captures, refunds, and any associated success or failure responses. - -### Entity Properties - - -## Relationships - -* **Payment:** A transaction is part of one `Payment`. - -## Examples - -* **Authorization Success:** Type: Authorize, PaymentID: PAY-98765, GatewayRef: auth_abc, Amount: $19.99, Status: Success. -* **Capture Success:** Type: Capture, PaymentID: PAY-98765, GatewayRef: ch_def, Amount: $19.99, Status: Success (following the authorization). -* **Authorization Failure:** Type: Authorize, PaymentID: PAY-98766, GatewayRef: auth_ghi, Amount: $50.00, Status: Failure, ResponseCode: 'declined', ResponseMessage: 'Insufficient Funds'. -* **Refund Success:** Type: Refund, PaymentID: PAY-98760, GatewayRef: re_jkl, Amount: $25.00, Status: Success. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/flows/PaymentProcessed/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/flows/PaymentProcessed/index.mdx deleted file mode 100644 index 8721977be..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/flows/PaymentProcessed/index.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -id: PaymentFlow -name: Payment Flow for customers -version: 1.0.0 -summary: Business flow for processing payments in an e-commerce platform -owners: - - dboyne -steps: - - id: "flow_step" - title: "Flow Step" - flow: - id: SubscriptionRenewed - next_step: - id: "place_order_request" - label: "Subscription Renewed, New Order Placed" - - id: "place_order_request" - title: Place order - message: - id: PlaceOrder - version: 0.0.1 - next_step: - id: "payment_initiated" - label: Initiate payment - - id: "payment_initiated" - title: Payment Initiated - message: - id: PaymentInitiated - version: 0.0.1 - next_steps: - - "payment_processed" - - "payment_failed" - - id: "payment_processed" - title: Payment Processed - message: - id: PaymentProcessed - version: 0.0.1 - next_steps: - - id: "adjust_inventory" - label: Adjust inventory - - id: "send_custom_notification" - label: Notify customer - - id: "payment_failed" - title: Payment Failed - type: node - next_steps: - - id: "failure_notification" - label: Notify customer of failure - - id: "retry_payment" - label: Retry payment - - id: "adjust_inventory" - title: Inventory Adjusted - message: - id: InventoryAdjusted - version: 1.0.1 - next_step: - id: "payment_complete" - label: Complete order - - id: "send_custom_notification" - title: Customer Notified of Payment - type: node - next_step: - id: "payment_complete" - label: Complete order - - id: "failure_notification" - title: Customer Notified of Failure - type: node - - id: "retry_payment" - title: Retry Payment - type: node - next_step: - id: "payment_initiated" - label: Retry payment process - - id: "payment_complete" - title: Payment Complete - message: - id: PaymentComplete - next_step: - id: "order-complete" - label: Order completed - - id: "order-complete" - title: Order Completed - type: node ---- - -### Flow of feature - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/index.mdx deleted file mode 100644 index d17da70e7..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/index.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -id: Payment -name: Payment -version: 0.0.1 -summary: | - Domain that contains payment related services and messages for processing financial transactions. -owners: - - dboyne -services: - - id: PaymentService - version: 0.0.1 - - id: FraudDetectionService - version: 0.0.1 - - id: PaymentGatewayService - version: 0.0.1 -entities: - - id: Invoice - - id: Payment - - id: PaymentMethod - - id: Transaction - - id: Address -badges: - - content: Subdomain - backgroundColor: blue - textColor: blue - icon: BoltIcon ---- - -## Overview - -The Payment Domain encompasses all services and components related to handling financial transactions within the system. It is responsible for managing payments, transactions, billing, fraud detection, and financial records. The domain ensures secure, reliable, and efficient processing of all payment-related activities. - -## Services - -### PaymentService -Core payment orchestration service that coordinates payment workflows and maintains payment state. - -### FraudDetectionService -Analyzes transactions in real-time to detect fraudulent activity using machine learning models and rule-based systems. - -### PaymentGatewayService -Manages integrations with external payment processors (Stripe, PayPal, etc.) and provides a unified interface for payment operations. - -## Cross-Domain Integration - -The Payment domain integrates with: - -- **Subscriptions Domain**: Processes recurring payments for subscriptions -- **Orders Domain**: Handles payments for customer orders -- **Inventory Domain**: Updates based on successful payments - -## Bounded context - - - - diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/containers/fraud-analytics-db/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/containers/fraud-analytics-db/index.mdx deleted file mode 100644 index 49f18a04f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/containers/fraud-analytics-db/index.mdx +++ /dev/null @@ -1,163 +0,0 @@ ---- -id: fraud-analytics-db -name: Fraud Analytics DB -version: 0.0.1 -container_type: dataWarehouse -technology: clickhouse@23 -access_mode: readWrite -classification: confidential -retention: 5y -residency: eu-west-1 -summary: Analytics database for fraud detection patterns, risk scoring, and transaction analysis ---- - - - -### What is this? -Fraud Analytics DB is a ClickHouse columnar database optimized for high-speed analytics on transaction patterns, fraud signals, and risk assessment. It stores historical fraud data and real-time transaction features used by machine learning models. - -### What does it store? -- **Transaction Features**: Extracted features from payment transactions for ML model scoring -- **Fraud Signals**: Device fingerprints, IP addresses, velocity metrics, behavioral patterns -- **Risk Scores**: Historical risk scores and fraud decisions for model training -- **Fraud Cases**: Confirmed fraud incidents with investigation notes -- **Model Predictions**: ML model outputs and confidence scores for analysis - -### Who writes to it? -- **FraudDetectionService** writes real-time transaction features and risk scores -- **Data Pipeline** ingests historical fraud data from payment systems -- **Manual Review Team** updates fraud case outcomes and labels - -### Who reads from it? -- **FraudDetectionService** queries historical patterns for real-time scoring -- **ML Training Pipeline** extracts training data for model retraining -- **Data Science Team** analyzes fraud trends and model performance -- **Business Intelligence** generates fraud metrics and dashboards -- **Compliance Team** audits fraud detection decisions - -### High-level data model -- `transaction_features`: Real-time extracted features (device, location, amount patterns) -- `fraud_signals`: Aggregated signals (velocity, anomaly scores) -- `risk_decisions`: Historical risk decisions and outcomes -- `fraud_labels`: Ground truth labels for confirmed fraud cases -- Time-series data partitioned by day for optimal query performance - -### Common queries -```sql --- Calculate fraud rate by country (last 30 days) -SELECT - country_code, - COUNT(*) as total_transactions, - SUM(is_fraud) as fraud_count, - (SUM(is_fraud) * 100.0 / COUNT(*)) as fraud_rate -FROM transaction_features -WHERE event_date >= today() - 30 -GROUP BY country_code -ORDER BY fraud_rate DESC; - --- Get high-risk transaction patterns -SELECT - customer_id, - COUNT(*) as transaction_count, - AVG(risk_score) as avg_risk_score, - MAX(risk_score) as max_risk_score -FROM risk_decisions -WHERE event_date = today() - AND risk_score > 80 -GROUP BY customer_id -HAVING transaction_count > 5; - --- Analyze model performance over time -SELECT - toStartOfHour(created_at) as hour, - model_version, - AVG(risk_score) as avg_score, - SUM(is_fraud) as actual_fraud_count, - COUNT(*) as total_predictions -FROM risk_decisions -WHERE created_at >= now() - INTERVAL 7 DAY -GROUP BY hour, model_version -ORDER BY hour DESC; - --- Find velocity anomalies (multiple transactions same customer) -SELECT - customer_id, - COUNT(*) as tx_count, - SUM(amount_cents) as total_amount, - arrayDistinct(groupArray(ip_address)) as unique_ips, - arrayDistinct(groupArray(device_fingerprint)) as unique_devices -FROM transaction_features -WHERE event_date = today() - AND created_at >= now() - INTERVAL 1 HOUR -GROUP BY customer_id -HAVING tx_count > 5 OR length(unique_ips) > 3; -``` - -### Access patterns and guidance -- Queries are highly parallelized across columns for fast analytics -- Use `event_date` partition key in WHERE clauses for optimal performance -- Aggregate queries benefit from ClickHouse's native aggregation functions -- Avoid SELECT * on large tables; specify needed columns -- Use materialized views for frequently accessed aggregations - -### Data retention and archiving -- **Hot data**: Last 90 days on high-performance SSD storage -- **Warm data**: 91 days to 2 years on standard storage -- **Cold archive**: 2-5 years on object storage (S3) -- Automated archival jobs run monthly - -### Security and compliance -- Contains sensitive fraud signals and PII (IP addresses, device IDs) -- Access requires security clearance and fraud team membership -- All queries logged for audit trail -- Data anonymized for ML training datasets shared outside fraud team -- GDPR right-to-deletion supported via customer_id purge jobs - -### Requesting access -To request access to Fraud Analytics DB: - -1. **Analyst access** (read-only): - - Submit request via [ServiceNow](https://company.service-now.com) - - Select "Data Analytics Access" → "Fraud Analytics DB" - - Requires fraud team manager approval + security clearance - - Access granted within 2-3 business days - -2. **Data Science access** (read + export): - - Additional approval from Chief Data Officer required - - Data export must be to secure workbench environment only - - Training data exports require anonymization review - -3. **Production write access**: - - Restricted to FraudDetectionService automated processes - - Manual writes require incident ticket and fraud team lead approval - -**Contact**: For access questions: -- Slack: #fraud-detection-team -- Email: fraud-team@company.com -- Data governance: data-governance@company.com - -### Performance characteristics -- **Query latency**: p95 < 500ms for point queries, < 5s for complex aggregations -- **Insert throughput**: 100,000+ events/second -- **Compression ratio**: ~10x (columnar storage) -- **Data freshness**: Near real-time (< 10 second delay) - -### Monitoring and alerts -- Query performance monitoring (slow queries > 10s) -- Data freshness lag alerts -- Storage capacity monitoring (alert at 80%) -- Replication lag alerts for distributed tables - -### Local development -- Local ClickHouse via Docker: `docker-compose up fraud-analytics-db` -- Connection string: `FRAUD_ANALYTICS_DB_URL` environment variable -- Sample dataset available: `npm run seed:fraud-analytics` -- Use ClickHouse client: `clickhouse-client --host localhost --port 9000` - -### Common issues and troubleshooting -- **Slow aggregation queries**: Ensure event_date partition key is used in WHERE clause -- **Out of memory errors**: Reduce query complexity or increase max_memory_usage setting -- **Data duplication**: Use ReplacingMergeTree for deduplication on insert -- **Query timeout**: Increase max_execution_time or optimize query with EXPLAIN - -For more information, see FraudDetectionService documentation and ClickHouse best practices guide. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/containers/ml-model-store/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/containers/ml-model-store/index.mdx deleted file mode 100644 index f27850f19..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/containers/ml-model-store/index.mdx +++ /dev/null @@ -1,182 +0,0 @@ ---- -id: ml-model-store -name: ML Model Store -version: 0.0.1 -container_type: objectStore -technology: s3 -access_mode: read -classification: internal -retention: 3y -residency: eu-west-1 -summary: Object storage for trained ML models, model artifacts, and versioned fraud detection models ---- - - - -### What is this? -ML Model Store is an S3 bucket that stores trained machine learning models, feature extractors, and model artifacts used by the Fraud Detection Service. It provides versioned storage for model rollback and A/B testing. - -### What does it store? -- **Trained Models**: Serialized fraud detection models (TensorFlow SavedModel, ONNX, pickle) -- **Model Metadata**: Model version, training date, performance metrics, feature schema -- **Feature Extractors**: Preprocessing pipelines and feature engineering code -- **Model Configs**: Hyperparameters, training configuration, deployment settings -- **Experiment Results**: Training logs, validation metrics, confusion matrices - -### Storage structure -``` -ml-models/ -├── fraud-detection/ -│ ├── production/ -│ │ ├── v2.3.1/ -│ │ │ ├── model.onnx -│ │ │ ├── metadata.json -│ │ │ ├── feature_schema.json -│ │ │ └── performance_metrics.json -│ │ └── current -> v2.3.1 (symlink) -│ ├── staging/ -│ │ └── v2.4.0-rc1/ -│ └── experiments/ -│ └── exp-2024-01-15-xgboost/ -├── feature-extractors/ -│ └── v1.2.0/ -│ ├── preprocessor.pkl -│ └── feature_config.yaml -└── archived/ - └── deprecated-models/ -``` - -### Who writes to it? -- **ML Training Pipeline** uploads newly trained models after validation -- **Data Science Team** uploads experimental models and feature extractors -- **CI/CD Pipeline** promotes models from staging to production - -### Who reads from it? -- **FraudDetectionService** loads production models on startup and refresh -- **Model Serving Infrastructure** fetches models for deployment -- **A/B Testing Framework** loads multiple model versions for comparison -- **Model Monitoring Service** reads metadata for drift detection - -### Object lifecycle -1. Models trained in ML platform → uploaded to `experiments/` folder -2. Validated models promoted to `staging/` with metadata -3. Approved models moved to `production/` with version tag -4. Old production models archived after 90 days in `archived/` -5. Archived models deleted after 3 years - -### Model metadata format -```json -{ - "model_id": "fraud-detection-v2.3.1", - "version": "2.3.1", - "trained_at": "2024-01-15T10:30:00Z", - "framework": "tensorflow", - "format": "onnx", - "training_dataset": { - "date_range": "2023-10-01 to 2024-01-01", - "total_samples": 5000000, - "fraud_rate": 0.023 - }, - "performance": { - "auc_roc": 0.94, - "precision": 0.89, - "recall": 0.87, - "f1_score": 0.88, - "false_positive_rate": 0.02 - }, - "features": ["transaction_amount", "device_fingerprint", "ip_country", "..."], - "deployment": { - "min_memory_mb": 512, - "inference_latency_p99_ms": 50, - "deployed_at": "2024-01-16T08:00:00Z" - } -} -``` - -### Access patterns -- Models loaded on FraudDetectionService startup (cold start) -- Periodic refresh every 6 hours to pick up new model versions -- Blue-green deployment: new version tested in parallel before full rollout -- Model download cached locally on service instances to reduce S3 calls - -### Versioning strategy -- **Semantic versioning**: major.minor.patch (e.g., 2.3.1) -- **Major**: Breaking changes to feature schema or model API -- **Minor**: Model improvements without breaking changes -- **Patch**: Bug fixes, retraining with same architecture -- Git tags linked to model versions for traceability - -### Security and access control -- **Read access**: FraudDetectionService IAM role only -- **Write access**: ML training pipeline CI/CD role only -- **Encryption**: AES-256 server-side encryption enabled -- **Versioning**: S3 versioning enabled for rollback capability -- **Access logs**: All S3 access logged to audit bucket - -### Requesting access -To request access to ML Model Store: - -1. **Read access** (for service integration): - - Create IAM role request via [AWS Access Portal](https://company.awsapps.com) - - Select "S3 Read Access" → "ml-model-store" - - Requires fraud team lead approval - - Access granted within 1 business day - -2. **Write access** (for ML engineers): - - Submit request via #ml-platform Slack channel - - Requires senior ML engineer approval + security review - - Write access limited to `experiments/` and `staging/` folders - - Production writes restricted to CI/CD pipeline only - -3. **Data Science exploration**: - - Use ML Platform workbench with pre-configured read access - - Contact #ml-platform for workspace setup - -**Contact**: -- ML Platform: #ml-platform -- Fraud ML Team: #fraud-ml-team -- Model governance: ml-governance@company.com - -### Model deployment workflow -1. Train model in ML platform environment -2. Upload to `experiments/` with metadata and performance metrics -3. Validation tests run automatically (schema check, performance baseline) -4. If validated, promote to `staging/` for canary deployment -5. Monitor staging metrics for 24 hours -6. Approve production promotion via deployment ticket -7. CI/CD pipeline moves model to `production/` and updates symlink -8. FraudDetectionService auto-refreshes and loads new model - -### Monitoring and alerts -- **Model staleness**: Alert if production model > 30 days old -- **Download failures**: Alert on 5xx errors from S3 -- **Storage costs**: Monitor bucket size (alert at $500/month) -- **Performance drift**: Compare new model metrics vs. baseline - -### Backup and disaster recovery -- **S3 versioning**: Enabled for accidental deletion protection -- **Cross-region replication**: Models replicated to us-east-1 for DR -- **Backup frequency**: Automatic with S3 durability (99.999999999%) -- **Recovery time**: < 5 minutes (point production symlink to previous version) - -### Local development -- Local MinIO S3-compatible storage: `docker-compose up minio` -- Connection: `AWS_ENDPOINT_URL=http://localhost:9000` -- Seed models: `npm run seed:ml-models` -- CLI access: `aws s3 ls s3://ml-model-store/ --endpoint-url http://localhost:9000` - -### Common issues and troubleshooting -- **Model load timeout**: Increase service timeout, check S3 connectivity -- **Version mismatch**: Ensure feature schema matches model version in metadata -- **Cold start latency**: Pre-warm model cache on service startup -- **S3 rate limits**: Use CloudFront or S3 acceleration for high-traffic models -- **Model size too large**: Compress models with ONNX optimization or quantization - -### Best practices -- Always include metadata.json with model performance metrics -- Test models in staging before production deployment -- Keep last 3 production versions for quick rollback -- Document breaking changes in model version release notes -- Monitor inference latency in production after deployment - -For more information, see ML Platform documentation and Model Deployment Playbook. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/FraudCheckCompleted/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/FraudCheckCompleted/index.mdx deleted file mode 100644 index 450d99711..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/FraudCheckCompleted/index.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -id: FraudCheckCompleted -version: 0.0.1 -name: Fraud Check Completed -summary: Emitted when a fraud check has been completed for a transaction -tags: - - payment - - fraud - - security -badges: - - content: New - backgroundColor: green - textColor: white -schemaPath: schema.proto ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The `FraudCheckCompleted` event is emitted when the fraud detection service has completed its analysis of a payment transaction. This event contains the risk assessment results and recommendations. - -## Schema - - - -## Event Details - -- **Risk Score**: A numerical score from 0-100 indicating fraud risk -- **Decision**: APPROVED, DECLINED, or MANUAL_REVIEW -- **Reasons**: Array of reasons for the decision -- **Confidence**: Confidence level of the fraud detection - -## Example Payload - -```json -{ - "transactionId": "txn_1234567890", - "paymentId": "pay_9876543210", - "riskScore": 25, - "decision": "APPROVED", - "reasons": ["Low risk merchant", "Customer history positive"], - "confidence": 0.92, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/FraudCheckCompleted/schema.proto b/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/FraudCheckCompleted/schema.proto deleted file mode 100644 index 6926bdd95..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/FraudCheckCompleted/schema.proto +++ /dev/null @@ -1,17 +0,0 @@ -syntax = "proto3"; - -package com.example.frauddetection; - -message FraudCheckCompleted { - string transactionId = 1; - string paymentId = 2; - int32 riskScore = 3; - string decision = 4; - repeated string reasons = 5; - double confidence = 6; - string timestamp = 7; -} - -message FraudCheckCompletedEvent { - FraudCheckCompleted fraudCheckCompleted = 1; -} diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/FraudDetected/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/FraudDetected/index.mdx deleted file mode 100644 index 7176bd6a1..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/FraudDetected/index.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -id: FraudDetected -version: 0.0.1 -name: Fraud Detected -summary: Emitted when a fraud is detected in a transaction -tags: - - payment - - fraud - - security -badges: - - content: New - backgroundColor: green - textColor: white ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The `FraudDetected` event is emitted when the fraud detection service identifies a potential fraud in a payment transaction. This event contains the details of the detected fraud. - -## Event Details - -- **Fraud Type**: Type of fraud detected -- **Severity**: LOW, MEDIUM, or HIGH -- **Indicators**: Array of indicators that led to the detection -- **Confidence**: Confidence level of the fraud detection - -## Example Payload - -```json -{ - "transactionId": "txn_1234567890", - "paymentId": "pay_9876543210", - "fraudType": "Stolen Card", - "severity": "HIGH", - "indicators": ["Unusual location", "High transaction amount"], - "confidence": 0.95, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/RiskScoreCalculated/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/RiskScoreCalculated/index.mdx deleted file mode 100644 index 5b0431977..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/events/RiskScoreCalculated/index.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -id: RiskScoreCalculated -version: 0.0.1 -name: Risk Score Calculated -summary: Emitted when a risk score is calculated for a transaction -tags: - - payment - - fraud - - security -badges: - - content: New - backgroundColor: green - textColor: white ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The `RiskScoreCalculated` event is emitted when the fraud detection service calculates a risk score for a payment transaction. This event contains the details of the calculated risk score. - -## Event Details - -- **Risk Score**: Calculated risk score for the transaction -- **Severity**: LOW, MEDIUM, or HIGH -- **Indicators**: Array of indicators used in the calculation -- **Confidence**: Confidence level of the risk score calculation - -## Example Payload - -```json -{ - "transactionId": "txn_1234567890", - "paymentId": "pay_9876543210", - "riskScore": 85, - "severity": "MEDIUM", - "indicators": ["Unusual location", "High transaction amount"], - "confidence": 0.85, - "timestamp": "2024-01-15T10:30:00Z" -} -``` - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/index.mdx deleted file mode 100644 index 57cc044bb..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/FraudDetectionService/index.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -id: FraudDetectionService -version: 0.0.1 -name: Fraud Detection Service -summary: Analyzes payment transactions for fraudulent activity and risk assessment -repository: - url: 'https://github.com/eventcatalog/fraud-detection-service' -receives: - - id: PaymentInitiated - version: 0.0.1 - from: - - id: 'payments.{env}.events' - parameters: - env: staging -sends: - - id: FraudCheckCompleted - version: 0.0.1 - - id: FraudDetected - version: 0.0.1 - - id: RiskScoreCalculated - version: 0.0.1 -writesTo: - - id: fraud-analytics-db - version: 0.0.1 -readsFrom: - - id: fraud-analytics-db - version: 0.0.1 - - id: ml-model-store - version: 0.0.1 - - id: payment-cache - version: 0.0.1 -owners: - - dboyne ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The Fraud Detection Service is responsible for analyzing payment transactions in real-time to detect potential fraudulent activity. It uses machine learning models and rule-based systems to assess risk and prevent financial losses. - - - -## Key Features - -- **Real-time Transaction Analysis**: Analyzes transactions as they occur -- **Machine Learning Models**: Uses ML to identify suspicious patterns -- **Risk Scoring**: Calculates risk scores for each transaction -- **Automated Blocking**: Can automatically block high-risk transactions -- **Manual Review Queue**: Flags medium-risk transactions for manual review - -## API Endpoints - -### REST API -- `POST /api/fraud/check` - Submit transaction for fraud check -- `GET /api/fraud/risk-score/{transactionId}` - Get risk score for transaction -- `PUT /api/fraud/override/{transactionId}` - Manual override of fraud decision - -## Configuration - -```yaml -fraud_detection: - risk_thresholds: - high: 80 - medium: 50 - low: 20 - auto_block_threshold: 90 - ml_model_version: "2.3.1" -``` - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/commands/ProcessPayment/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/commands/ProcessPayment/index.mdx deleted file mode 100644 index 5dc3b66fb..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/commands/ProcessPayment/index.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -id: ProcessPayment -version: 0.0.1 -name: Process Payment -summary: Command to process a payment through the payment gateway -tags: - - payment - - command - - cross-domain -badges: - - content: Command - backgroundColor: blue - textColor: white -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The `ProcessPayment` command is used to initiate payment processing through the payment gateway. This command can be triggered by various sources including the Billing Service for subscription payments. - -## Command Sources - -This command can be triggered by: -- **BillingService** (Subscriptions Domain) - For recurring subscription payments -- **OrdersService** (Orders Domain) - For one-time order payments -- **PaymentService** (Payment Domain) - For payment retries - -## Command Flow - -```mermaid -sequenceDiagram - participant BS as BillingService - participant PGS as PaymentGatewayService - participant FDS as FraudDetectionService - participant EXT as External Gateway - - BS->>PGS: ProcessPayment - PGS->>FDS: Check Fraud - FDS-->>PGS: Risk Assessment - PGS->>EXT: Process Payment - EXT-->>PGS: Payment Result - PGS-->>BS: PaymentProcessed/Failed -``` - -## Schema - - - -## Example Request - -```json -{ - "paymentId": "pay_123456", - "amount": 49.99, - "currency": "USD", - "paymentMethod": { - "type": "card", - "token": "tok_visa_4242" - }, - "metadata": { - "subscriptionId": "sub_ABC123", - "invoiceId": "inv_123456", - "customerId": "cust_XYZ789" - }, - "idempotencyKey": "sub_ABC123_2024_02", - "captureImmediately": true -} -``` - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/commands/ProcessPayment/schema.json b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/commands/ProcessPayment/schema.json deleted file mode 100644 index 04ba14cdd..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/commands/ProcessPayment/schema.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "paymentId": { - "type": "string", - "description": "Unique identifier for this payment" - }, - "amount": { - "type": "number", - "description": "Payment amount" - }, - "currency": { - "type": "string", - "description": "Currency code (ISO 4217)" - }, - "paymentMethod": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["card", "bank_transfer", "paypal", "digital_wallet"] - }, - "token": { - "type": "string", - "description": "Payment method token" - } - }, - "required": ["type", "token"] - }, - "metadata": { - "type": "object", - "properties": { - "subscriptionId": { - "type": "string" - }, - "invoiceId": { - "type": "string" - }, - "customerId": { - "type": "string" - }, - "orderId": { - "type": "string" - } - } - }, - "idempotencyKey": { - "type": "string", - "description": "Key to prevent duplicate payments" - }, - "captureImmediately": { - "type": "boolean", - "description": "Whether to capture payment immediately or just authorize" - } - }, - "required": ["paymentId", "amount", "currency", "paymentMethod", "idempotencyKey"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/events/PaymentFailed/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/events/PaymentFailed/index.mdx deleted file mode 100644 index 3b0739721..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/events/PaymentFailed/index.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -id: PaymentFailed -version: 0.0.1 -name: Payment Failed -summary: Emitted when a payment attempt fails -badges: - - content: Error Event - backgroundColor: red - textColor: white -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The `PaymentFailed` event is emitted when a payment attempt fails for any reason. This event is consumed by various services to handle payment failures appropriately. - -## Consumers - -This event is consumed by: -- **BillingService** (Subscriptions Domain) - To handle subscription payment failures -- **OrdersService** (Orders Domain) - To handle order payment failures -- **NotificationService** (Orders Domain) - To notify customers of payment failures - -## Failure Reasons - -Common failure reasons include: -- **insufficient_funds** - Card has insufficient funds -- **card_declined** - Card was declined by issuer -- **expired_card** - Card has expired -- **fraud_suspected** - Payment flagged as potentially fraudulent -- **network_error** - Payment gateway network error -- **invalid_payment_method** - Payment method is invalid - -## Schema - - - -## Example Payload - -```json -{ - "paymentId": "pay_123456", - "amount": 49.99, - "currency": "USD", - "failureReason": "insufficient_funds", - "failureMessage": "Your card has insufficient funds", - "metadata": { - "subscriptionId": "sub_ABC123", - "invoiceId": "inv_123456", - "customerId": "cust_XYZ789" - }, - "canRetry": true, - "nextRetryAt": "2024-02-02T00:00:00Z", - "timestamp": "2024-02-01T10:30:00Z" -} -``` - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/events/PaymentFailed/schema.json b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/events/PaymentFailed/schema.json deleted file mode 100644 index c807c58ca..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/events/PaymentFailed/schema.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "paymentId": { - "type": "string", - "description": "Unique identifier for the failed payment" - }, - "amount": { - "type": "number", - "description": "Payment amount that failed" - }, - "currency": { - "type": "string", - "description": "Currency code (ISO 4217)" - }, - "failureReason": { - "type": "string", - "enum": [ - "insufficient_funds", - "card_declined", - "expired_card", - "fraud_suspected", - "network_error", - "invalid_payment_method", - "authentication_required", - "other" - ], - "description": "Reason for payment failure" - }, - "failureMessage": { - "type": "string", - "description": "Human-readable failure message" - }, - "metadata": { - "type": "object", - "properties": { - "subscriptionId": { - "type": "string" - }, - "invoiceId": { - "type": "string" - }, - "customerId": { - "type": "string" - }, - "orderId": { - "type": "string" - } - } - }, - "canRetry": { - "type": "boolean", - "description": "Whether this payment can be retried" - }, - "nextRetryAt": { - "type": "string", - "format": "date-time", - "description": "Suggested time for next retry attempt" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "When the failure occurred" - } - }, - "required": ["paymentId", "amount", "currency", "failureReason", "timestamp"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/index.mdx deleted file mode 100644 index 7affca0bf..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentGatewayService/index.mdx +++ /dev/null @@ -1,74 +0,0 @@ ---- -id: PaymentGatewayService -version: 0.0.1 -name: Payment Gateway Service -summary: Manages integration with external payment processors (Stripe, PayPal, etc.) -tags: - - payment - - gateway - - integration -repository: - url: https://github.com/eventcatalog/payment-gateway-service -receives: - - id: ProcessPayment - version: 0.0.1 - from: - - id: 'payment-domain-eventbus' - - id: FraudCheckCompleted - version: 0.0.1 -sends: - - id: PaymentFailed - version: 0.0.1 -owners: - - dboyne ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The Payment Gateway Service acts as an abstraction layer between our payment system and external payment processors. It handles the complexity of integrating with multiple payment providers and provides a unified interface for payment operations. - - - -## Supported Payment Providers - -- **Stripe**: Credit/debit cards, digital wallets -- **PayPal**: PayPal accounts, PayPal Credit -- **Square**: In-person and online payments -- **Adyen**: Global payment processing -- **Braintree**: Multiple payment methods - -## Key Features - -- **Multi-provider Support**: Switch between providers seamlessly -- **Retry Logic**: Automatic retry for failed transactions -- **Tokenization**: Secure card data handling -- **Webhook Management**: Handles provider webhooks -- **Currency Conversion**: Support for 150+ currencies - -## API Endpoints - -### REST API -- `POST /api/gateway/authorize` - Authorize a payment -- `POST /api/gateway/capture` - Capture an authorized payment -- `POST /api/gateway/refund` - Process a refund -- `GET /api/gateway/status/{transactionId}` - Get transaction status - -## Configuration - -```yaml -payment_gateway: - providers: - stripe: - api_key: ${STRIPE_API_KEY} - webhook_secret: ${STRIPE_WEBHOOK_SECRET} - paypal: - client_id: ${PAYPAL_CLIENT_ID} - client_secret: ${PAYPAL_CLIENT_SECRET} - retry: - max_attempts: 3 - backoff_ms: 1000 -``` - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/containers/payment-cache/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/containers/payment-cache/index.mdx deleted file mode 100644 index 2aa046201..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/containers/payment-cache/index.mdx +++ /dev/null @@ -1,158 +0,0 @@ ---- -id: payment-cache -name: Payment Cache -version: 0.0.1 -container_type: cache -technology: redis@7 -access_mode: readWrite -classification: internal -retention: 24h -residency: eu-west-1 -summary: High-performance cache for payment session data and idempotency checks ---- - - - -### What is this? -Payment Cache is a Redis 7 cluster used for caching payment session data, rate limiting, and idempotency key tracking. It provides sub-millisecond latency for payment processing flows and reduces load on the primary database. - -### What does it store? -- **Payment Sessions**: Temporary payment context during checkout (15-minute TTL) -- **Idempotency Keys**: Fast duplicate payment prevention (24-hour TTL) -- **Rate Limiting Counters**: Per-customer payment attempt limits -- **Payment Method Cache**: Recently used payment methods (1-hour TTL) -- **Gateway Status**: Payment gateway health check results (5-minute TTL) - -### Who writes to it? -- **PaymentService** writes session data, idempotency keys, and cached payment methods -- **PaymentGatewayService** updates gateway health status -- **FraudDetectionService** writes rate limiting counters - -### Who reads from it? -- **PaymentService** reads session data and checks idempotency keys before processing -- **FraudDetectionService** reads rate limiting counters for risk assessment -- **Frontend APIs** check payment session validity - -### Key patterns and structure -```redis -# Payment session keys (TTL: 15 minutes) -payment:session:{session_id} -> { - "customer_id": "uuid", - "order_id": "uuid", - "amount_cents": 9999, - "currency": "USD", - "payment_method_id": "uuid", - "created_at": "2024-01-15T10:30:00Z" -} - -# Idempotency keys (TTL: 24 hours) -payment:idempotency:{idempotency_key} -> {payment_id} - -# Rate limiting (TTL: 1 hour) -payment:ratelimit:{customer_id}:hour -> count -payment:ratelimit:{customer_id}:day -> count - -# Payment method cache (TTL: 1 hour) -payment:method:{payment_method_id} -> { - "type": "CARD", - "last_four": "4242", - "brand": "VISA", - "customer_id": "uuid" -} - -# Gateway health status (TTL: 5 minutes) -payment:gateway:{gateway_name}:status -> "UP" | "DOWN" | "DEGRADED" -``` - -### Common operations -```javascript -// Create payment session -await redis.setex( - `payment:session:${sessionId}`, - 900, // 15 minutes - JSON.stringify(sessionData) -); - -// Check idempotency key -const existingPaymentId = await redis.get(`payment:idempotency:${key}`); -if (existingPaymentId) { - return { duplicate: true, paymentId: existingPaymentId }; -} - -// Set idempotency key -await redis.setex(`payment:idempotency:${key}`, 86400, paymentId); - -// Increment rate limit counter -const attempts = await redis.incr(`payment:ratelimit:${customerId}:hour`); -if (attempts === 1) { - await redis.expire(`payment:ratelimit:${customerId}:hour`, 3600); -} -if (attempts > 10) { - throw new RateLimitError('Too many payment attempts'); -} - -// Cache payment method -await redis.setex( - `payment:method:${methodId}`, - 3600, - JSON.stringify(paymentMethod) -); -``` - -### Configuration -- **Cluster mode**: Enabled for high availability (3 primary + 3 replica nodes) -- **Eviction policy**: `allkeys-lru` (automatically evict least recently used keys) -- **Max memory**: 4GB per node -- **Persistence**: AOF disabled (cache data is ephemeral) -- **Connection pooling**: Min 10, Max 50 connections per service instance - -### Access patterns and guidance -- Always set appropriate TTLs to prevent memory bloat -- Use Redis Cluster key hashing for scalability -- Idempotency checks must be atomic (use `SETNX` or Lua scripts) -- Rate limiting uses `INCR` with `EXPIRE` for atomic counters -- Payment sessions should be invalidated on completion or expiry - -### Monitoring and alerts -- Cache hit rate monitored (target: > 85%) -- Memory usage alerts at 80% capacity -- Connection pool exhaustion alerts -- Slow command alerts (> 10ms) -- Replication lag monitoring (target: < 1 second) - -### High availability -- Multi-AZ deployment across 3 availability zones -- Automatic failover with Redis Sentinel -- Read replicas for read-heavy operations -- Connection retry logic in application code - -### Security -- Redis AUTH enabled with strong password rotation -- TLS encryption for all connections -- VPC isolation - no public internet access -- Network ACLs restrict access to payment services only - -### Backup and disaster recovery -- No persistent backups (cache data is ephemeral and can be rebuilt) -- Cluster configuration backed up for rapid rebuild -- Runbooks for cache warmup after total failure - -### Local development -- Connection string: `PAYMENT_CACHE_URL` environment variable -- Local Redis via Docker: `docker run -d -p 6379:6379 redis:7-alpine` -- Test data can be seeded via `npm run seed:payment-cache` - -### Common issues and troubleshooting -- **Cache misses on payment sessions**: Check TTL configuration, may need adjustment for slow checkouts -- **Idempotency key collisions**: Ensure clients generate unique keys (recommend UUID v4) -- **Rate limiting false positives**: Review rate limit thresholds, may need per-tier limits -- **Memory pressure**: Monitor eviction rates, consider increasing cluster size -- **Connection timeouts**: Check connection pool settings and network latency - -### Performance characteristics -- **Read latency**: p99 < 5ms -- **Write latency**: p99 < 10ms -- **Throughput**: 50,000+ ops/sec per node -- **TTL precision**: 1-second granularity - -For more information, see the PaymentService caching strategy documentation. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/containers/payments-db/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/containers/payments-db/index.mdx deleted file mode 100644 index 356191794..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/containers/payments-db/index.mdx +++ /dev/null @@ -1,155 +0,0 @@ ---- -id: payments-db -name: Payments DB -version: 0.0.1 -container_type: database -technology: postgres@15 -authoritative: true -access_mode: readWrite -classification: regulated -retention: 10y -residency: eu-west-1 -summary: Primary database for payment transactions and payment method records ---- - - - -### What is this? -Payments DB is the authoritative database for all payment transactions, payment methods, and related financial data. It uses PostgreSQL 15 and serves as the system of record for payment processing, refunds, and transaction history. - -### What does it store? -- **Payments**: Transaction records including amount, currency, status, timestamps, and gateway references -- **Payment Methods**: Tokenized payment method details (cards, bank accounts) with PCI-compliant storage -- **Refunds**: Refund records linked to original payment transactions -- **Payment Attempts**: Historical record of all payment attempts for debugging and analytics -- **Relationships**: Payment methods belong to customers, payments reference orders and payment methods - -### Who writes to it? -- **PaymentService** creates payment records, processes transactions, and manages payment methods -- **PaymentGatewayService** updates payment status based on gateway responses -- Transaction records are immutable after settlement for audit compliance - -### Who reads from it? -- **FraudDetectionService** analyzes payment patterns and transaction history -- **BillingService** retrieves payment history for invoice generation -- **OrdersService** checks payment status for order fulfillment -- **SubscriptionService** validates payment methods for recurring charges -- **Finance/Analytics** teams for reconciliation and reporting - -### High-level data model -- A `payment` belongs to an `order` and uses a `payment_method` -- Payment lifecycle tracked via `status`: `INITIATED`, `AUTHORIZED`, `CAPTURED`, `FAILED`, `REFUNDED`, `CANCELLED` -- Monetary values stored as integer cents to avoid floating-point issues -- All transactions are idempotent using `idempotency_key` -- Tables: `payments`, `payment_methods`, `refunds` - -### Common queries -```sql --- Get payment details with method info -SELECT p.*, pm.type AS payment_method_type, pm.last_four, pm.brand -FROM payments p -LEFT JOIN payment_methods pm USING (payment_method_id) -WHERE p.payment_id = $1; - --- List customer payments (for payment history) -SELECT * -FROM payments -WHERE customer_id = $1 -ORDER BY created_at DESC -LIMIT $2 OFFSET $3; - --- Find failed payments for retry analysis -SELECT payment_id, order_id, failure_reason, created_at -FROM payments -WHERE status = 'FAILED' - AND created_at >= now() - interval '24 hours' -ORDER BY created_at DESC; - --- Calculate daily payment volume -SELECT date_trunc('day', captured_at) AS day, - COUNT(*) AS transaction_count, - SUM(amount_cents)/100.0 AS total_amount -FROM payments -WHERE status = 'CAPTURED' - AND captured_at >= now() - interval '30 days' -GROUP BY 1 -ORDER BY 1; -``` - -### Lifecycle and data flow -1. Customer initiates payment → `PaymentService` creates record with status `INITIATED` -2. Payment gateway authorization → status updated to `AUTHORIZED` -3. Capture after order fulfillment → status updated to `CAPTURED` -4. Refunds create separate `refunds` record linked to original payment -5. All state changes emit domain events for downstream consumers - -### Security and compliance -- **PCI DSS Level 1 compliance**: Card data tokenized via payment gateway, no raw card numbers stored -- **Encryption at rest**: Database encrypted using AWS KMS -- **Encryption in transit**: TLS 1.3 for all connections -- **Access control**: Role-based access, audit logging enabled -- **PII handling**: Customer PII encrypted, access logged for GDPR compliance - -### Access patterns and guidance -- Use `payment_id` for point lookups; indexed by `order_id` and `customer_id` -- Query by `idempotency_key` to prevent duplicate charges -- Payments are immutable after `CAPTURED` status - use refunds for reversals -- Never store raw card numbers - always use gateway tokens -- Use read replicas for analytics queries to avoid impacting transaction processing - -### Retention and residency -- **Retention**: 10 years (regulatory requirement for financial records) -- **Residency**: `eu-west-1` (GDPR compliance, data localization) -- Archived records older than 3 years moved to cold storage - -### Backups and recovery -- Automated continuous backups with 35-day retention -- Point-in-time recovery with 5-minute RPO -- Cross-region backup replication for disaster recovery -- Monthly restore testing to validate RTO objectives - -### Monitoring and alerts -- Transaction latency monitored (p99 < 200ms) -- Failed payment rate alerts (threshold: > 5%) -- Database connection pool monitoring -- Slow query alerts for queries > 1 second - -### Requesting access -To request access to the Payments DB: - -1. **Development/Staging access**: - - Submit an access request via [ServiceNow](https://company.service-now.com) - - Select "Database Access Request" → "Payments DB (Non-Production)" - - Specify read-only or read-write access level - - Approval required from Payment Service team lead - - Access granted within 24 hours - -2. **Production access**: - - Production read access requires manager approval + security review - - Production write access is restricted to PaymentService automated deployments only - - For emergency production access, contact on-call via PagerDuty - - All production queries are logged and audited for compliance - -3. **Analytics/Reporting access**: - - Use the read-replica endpoint for reporting queries - - Request access via #payments-data Slack channel - - Data export requests require data governance approval due to PCI/GDPR - -**Contact**: For questions, contact the Payment Service team: -- Slack: #payments-team -- Email: payments-team@company.com -- Team lead: @dboyne - -### Local development -- Connection string: `PAYMENTS_DB_URL` environment variable -- Seed data available via `npm run seed:payments` -- Test payment methods use gateway test tokens -- Local development uses Docker: `docker-compose up payments-db` - -### Common issues and troubleshooting -- **Duplicate payments**: Always include `idempotency_key` in payment creation -- **Stuck authorizations**: Run daily job to release authorizations older than 7 days -- **Gateway timeouts**: Implement retry logic with exponential backoff -- **Currency mismatches**: Validate order currency matches payment currency - -For more information, see the PaymentService documentation and PCI compliance guidelines. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentComplete/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentComplete/index.mdx deleted file mode 100644 index 1f72d1fe0..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentComplete/index.mdx +++ /dev/null @@ -1,37 +0,0 @@ ---- -id: PaymentComplete -name: Payment Complete -version: 0.0.2 -summary: Event is triggered when a user completes a payment through the Payment Service -owners: - - dboyne ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The Payment Complete event is triggered when a user successfully completes a payment through the Payment Service. This event signifies the end of the payment process and confirms that the payment has been processed successfully. - - - -### Payload Example - -```json -{ - "userId": "123e4567-e89b-12d3-a456-426614174000", - "orderId": "789e1234-b56c-78d9-e012-3456789fghij", - "amount": 100.50, - "paymentMethod": "CreditCard", - "timestamp": "2024-07-04T14:48:00Z", - "status": "Completed" -} -``` - -### Security Considerations - -- **Authentication**: Ensure that only authenticated users can complete a payment, and the userId in the payload matches the authenticated user. -- **Data Validation**: Validate all input data to prevent injection attacks or other malicious input. -- **Sensitive Data Handling**: Avoid including sensitive information (e.g., credit card numbers) in the event payload. Use secure channels and encryption for such data. - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentInitiated/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentInitiated/index.mdx deleted file mode 100644 index 63cbb32f2..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentInitiated/index.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -id: PaymentInitiated -name: Payment Initiated -version: 0.0.1 -summary: Event is triggered when a user initiates a payment through the Payment Service -owners: - - dboyne ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The Payment Initiated event is triggered when a user initiates a payment through the Payment Service. This event signifies the beginning of the payment process and contains all necessary information to process the payment. - - - -### Payload Example - -```json -{ - "userId": "123e4567-e89b-12d3-a456-426614174000", - "orderId": "789e1234-b56c-78d9-e012-3456789fghij", - "amount": 100.50, - "paymentMethod": "CreditCard", - "timestamp": "2024-07-04T14:48:00Z" -} -``` - -### Security Considerations - -- **Authentication**: Ensure that only authenticated users can initiate a payment, and the userId in the payload matches the authenticated user. -- **Data Validation**: Validate all input data to prevent injection attacks or other malicious input. -- **Sensitive Data Handling**: Avoid including sensitive information (e.g., credit card numbers) in the event payload. Use secure channels and encryption for such data. - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentProcessed/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentProcessed/index.mdx deleted file mode 100644 index 26998397f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentProcessed/index.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: PaymentProcessed -name: Payment Processed -version: 1.0.0 -summary: Event is triggered after the payment has been successfully processed -owners: - - dboyne ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The PaymentProcessed event is triggered after the payment has been successfully processed by the Payment Service. This event signifies that a payment has been confirmed, and it communicates the outcome to other services and components within the system. - - - -### Payload Example - -```json -{ - "transactionId": "123e4567-e89b-12d3-a456-426614174000", - "userId": "123e4567-e89b-12d3-a456-426614174000", - "orderId": "789e1234-b56c-78d9-e012-3456789fghij", - "amount": 100.50, - "paymentMethod": "CreditCard", - "status": "confirmed", - "confirmationDetails": { - "gatewayResponse": "Approved", - "transactionId": "abc123" - }, - "timestamp": "2024-07-04T14:48:00Z" -} -``` - -### Security Considerations - -- **Data Validation**: Ensure that all data in the event payload is validated before publishing to prevent injection attacks or other malicious activities. -- **Sensitive Data Handling**: Avoid including sensitive information (e.g., full credit card numbers) in the event payload. Use secure channels and encryption for such data. -- **Authentication and Authorization**: Ensure that only authorized services can publish or consume PaymentProcessed events. - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentProcessed/versioned/0.0.1/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentProcessed/versioned/0.0.1/index.mdx deleted file mode 100644 index 7c7eeed7c..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/events/PaymentProcessed/versioned/0.0.1/index.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -id: PaymentProcessed -name: Payment Processed -version: 0.0.1 -summary: Event is triggered after the payment has been successfully processed -owners: - - dboyne ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The PaymentProcessed event is triggered after the payment has been successfully processed by the Payment Service. This event signifies that a payment has been confirmed, and it communicates the outcome to other services and components within the system. - - - -### Payload Example - -```json -{ - "transactionId": "123e4567-e89b-12d3-a456-426614174000", - "userId": "123e4567-e89b-12d3-a456-426614174000", - "orderId": "789e1234-b56c-78d9-e012-3456789fghij", - "amount": 100.50, - "paymentMethod": "CreditCard", - "status": "confirmed", - "confirmationDetails": { - "gatewayResponse": "Approved", - "transactionId": "abc123" - }, - "timestamp": "2024-07-04T14:48:00Z" -} -``` - -### Security Considerations - -- **Data Validation**: Ensure that all data in the event payload is validated before publishing to prevent injection attacks or other malicious activities. -- **Sensitive Data Handling**: Avoid including sensitive information (e.g., full credit card numbers) in the event payload. Use secure channels and encryption for such data. -- **Authentication and Authorization**: Ensure that only authorized services can publish or consume PaymentProcessed events. - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/index.mdx deleted file mode 100644 index b8dfa43d2..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/index.mdx +++ /dev/null @@ -1,76 +0,0 @@ ---- -id: PaymentService -name: Payment Service -version: 0.0.1 -summary: | - Service that handles payments -owners: - - dboyne -receives: - - id: PaymentInitiated - version: 0.0.1 - from: - - id: 'payments.{env}.events' - parameters: - env: staging -sends: - - id: PaymentProcessed -writesTo: - - id: payments-db - version: 0.0.1 - - id: payment-cache - version: 0.0.1 -readsFrom: - - id: order-metadata-store - version: 0.0.1 - - id: payments-db - version: 0.0.1 - - id: payment-cache - version: 0.0.1 -repository: - language: JavaScript - url: 'https://github.com/event-catalog/pretend-shipping-service' ---- - -The Payment Service is a crucial component of our system that handles all payment-related operations. It processes payments, manages transactions, and communicates with other services through events. Using an event-driven architecture, it ensures that all actions are asynchronous, decoupled, and scalable. - -### Core features - -| Feature | Description | -|---------|-------------| -| Payment Processing | Processes payments and manages transactions | -| Event-Driven Architecture | Ensures asynchronous, decoupled, and scalable operations | -| Integration with Payment Gateways | Interfaces with external payment providers | - - - - - -## Infrastructure - -The Payment Service is hosted on AWS. - -The diagram below shows the infrastructure of the Payment Service. The service is hosted on AWS and uses AWS Lambda to handle the payment requests. The payment is stored in an AWS Aurora database and the payment metadata is stored in an AWS S3 bucket. - -```mermaid -architecture-beta - group api(logos:aws) - - service db(logos:aws-aurora)[Payment DB] in api - service disk1(logos:aws-s3)[Payment Metadata] in api - service server(logos:aws-lambda)[Payment Handler] in api - - db:L -- R:server - disk1:T -- B:server -``` - -You can find more information about the Payment Service infrastructure in the [Payment Service documentation](https://github.com/event-catalog/pretend-payment-service/blob/main/README.md). - -### Key Components -- Payment API: Exposes endpoints for initiating payments and querying payment status. -- Payment Processor: Handles the core payment processing logic. -- Event Bus: Manages the communication between services using events. -- Payment Gateway: Interfaces with external payment providers. -- Transaction Service: Manages transaction records and states. -- Notification Service: Sends notifications related to payment status changes. -- Database: Stores transaction data and payment status. diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/queries/GetPaymentStatus/index.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/queries/GetPaymentStatus/index.mdx deleted file mode 100644 index 8d1679a97..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/queries/GetPaymentStatus/index.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -id: GetPaymentStatus -name: Get payment status -version: 0.0.1 -summary: | - GET request that will return the payment status for a specific order, identified by its orderId. -owners: - - dboyne -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json -sidebar: - badge: 'GET' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `GetPaymentStatus` message is a query used to retrieve the payment status for a specific order, identified by its `orderId`. This query returns the current status of the payment, such as whether it is pending, completed, failed, or refunded. It is used by systems that need to track the lifecycle of payments associated with orders, ensuring that the payment has been successfully processed or identifying if any issues occurred during the transaction. - -This query is useful in scenarios such as order management, refund processing, or payment auditing, ensuring that users or systems have real-time visibility into the payment status for a given order. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/queries/GetPaymentStatus/schema.json b/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/queries/GetPaymentStatus/schema.json deleted file mode 100644 index 3ee0a321e..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/services/PaymentService/queries/GetPaymentStatus/schema.json +++ /dev/null @@ -1,40 +0,0 @@ - -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetPaymentStatusResponse", - "type": "object", - "properties": { - "orderId": { - "type": "string", - "description": "The unique identifier for the order." - }, - "paymentStatus": { - "type": "string", - "enum": ["pending", "completed", "failed", "refunded"], - "description": "The current payment status of the order." - }, - "amount": { - "type": "number", - "description": "The amount paid for the order." - }, - "currency": { - "type": "string", - "description": "The currency in which the payment was made (e.g., USD, EUR)." - }, - "paymentMethod": { - "type": "string", - "description": "The payment method used for the transaction (e.g., Credit Card, PayPal)." - }, - "transactionId": { - "type": "string", - "description": "The unique identifier for the payment transaction." - }, - "paymentDate": { - "type": "string", - "format": "date-time", - "description": "The date and time when the payment was processed." - } - }, - "required": ["orderId", "paymentStatus", "amount", "currency", "paymentMethod", "transactionId", "paymentDate"], - "additionalProperties": false -} diff --git a/examples/default/domains/E-Commerce/subdomains/Payment/ubiquitous-language.mdx b/examples/default/domains/E-Commerce/subdomains/Payment/ubiquitous-language.mdx deleted file mode 100644 index 85eec09bf..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Payment/ubiquitous-language.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -dictionary: - - id: Transaction - name: Transaction - summary: "The process of transferring funds from one party to another." - icon: CreditCard - - id: Invoice - name: Invoice - summary: "A document issued by a seller to a buyer, listing goods or services provided and the amount due." - description: | - Invoices are critical for financial transactions, serving as a request for payment from the buyer. They include: - - - Invoice number for tracking - - List of products or services provided - - Total amount due and payment terms - - Seller and buyer contact information - - Due date for payment - - Invoices are essential for accounting, tax purposes, and maintaining cash flow. - icon: CreditCard - - id: Payment Method - name: Payment Method - summary: "The means by which a payment is made, such as credit card or bank transfer." - description: | - Payment methods are the various ways customers can pay for goods or services. Common methods include: - - - Credit and debit cards - - Bank transfers - - Digital wallets - - Cash on delivery - - Offering multiple payment methods can enhance customer satisfaction and increase sales. - icon: Wallet - - id: Receipt - name: Receipt - summary: "A document acknowledging that a payment has been made." - description: | - Receipts serve as proof of payment and are important for record-keeping. They typically include: - - - Receipt number - - Date and time of payment - - Amount paid - - Payment method used - - Details of the transaction - - Receipts are essential for both customers and businesses to track financial transactions. - icon: Receipt - - id: Refund - name: Refund - summary: "The process of returning funds to a customer for a returned product or service." - description: | - Refunds are issued when a customer returns a product or cancels a service. The process involves: - - - Verifying the return or cancellation - - Processing the refund through the original payment method - - Updating financial records - - Efficient refund processes can improve customer satisfaction and loyalty. - icon: RotateCcw - - id: Currency - name: Currency - summary: "The system of money in general use in a particular country." - description: | - Currency is the medium of exchange for goods and services. Key aspects include: - - - Currency code (e.g., USD, EUR) - - Exchange rates - - Currency symbols - - Understanding currency is crucial for international transactions and financial reporting. - icon: DollarSign - - id: Payment Gateway - name: Payment Gateway - summary: "A service that authorizes and processes payments for online and offline transactions." - description: | - Payment gateways facilitate the transfer of payment information between the customer and the merchant. They ensure secure and efficient transactions by: - - - Encrypting sensitive data - - Authorizing payments - - Providing transaction reports - - Choosing a reliable payment gateway is essential for business operations. - icon: Server - - id: Chargeback - name: Chargeback - summary: "A demand by a credit card provider for a retailer to make good the loss on a fraudulent or disputed transaction." - description: | - Chargebacks occur when a customer disputes a transaction, and the funds are returned to their account. The process involves: - - - Investigating the dispute - - Providing evidence to the payment processor - - Resolving the issue with the customer - - Managing chargebacks effectively can prevent financial losses and maintain customer trust. - icon: AlertCircle ---- diff --git a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Category/index.mdx b/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Category/index.mdx deleted file mode 100644 index c6691a44f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Category/index.mdx +++ /dev/null @@ -1,124 +0,0 @@ ---- -id: Category -name: Category -version: 1.0.0 -identifier: categoryId -aggregateRoot: true -summary: Represents a product category with hierarchical structure support. -properties: - - name: categoryId - type: UUID - required: true - description: Unique identifier for the category - - name: name - type: string - required: true - description: Name of the category - - name: description - type: string - required: false - description: Description of the category - - name: slug - type: string - required: true - description: URL-friendly identifier for the category - - name: parentCategoryId - type: UUID - required: false - description: Parent category for hierarchical structure - references: Category - referencesIdentifier: categoryId - relationType: hasOne - - name: childCategories - type: array - items: - type: Category - required: false - description: Subcategories under this category - references: Category - referencesIdentifier: parentCategoryId - relationType: hasMany - - name: level - type: integer - required: true - description: Depth level in the category hierarchy (0 = root) - - name: isActive - type: boolean - required: true - description: Whether the category is currently active - - name: sortOrder - type: integer - required: false - description: Display order within the same level - - name: icon - type: string - required: false - description: Icon URL or identifier for the category - - name: imageUrl - type: string - required: false - description: Category banner or thumbnail image - - name: seoTitle - type: string - required: false - description: SEO-optimized title for the category page - - name: seoDescription - type: string - required: false - description: SEO meta description for the category page - - name: products - type: array - items: - type: Product - required: false - description: Products belonging to this category - references: Product - referencesIdentifier: categoryId - relationType: hasMany - - name: createdAt - type: DateTime - required: true - description: Date and time when the category was created - - name: updatedAt - type: DateTime - required: false - description: Date and time when the category was last updated ---- - -## Overview - -The Category entity organizes products into a hierarchical structure, supporting multi-level categorization. It enables efficient product discovery and navigation through the e-commerce catalog. - -### Entity Properties - - -## Relationships - -* **Parent Category:** Each category can have one parent `Category` (identified by `parentCategoryId`). -* **Child Categories:** A category can have multiple child `Category` entities creating a hierarchy. -* **Products:** A category contains multiple `Product` entities (identified by `categoryId`). - -## Hierarchy Examples - -``` -Electronics (Level 0) -├── Computers (Level 1) -│ ├── Laptops (Level 2) -│ ├── Desktops (Level 2) -│ └── Tablets (Level 2) -├── Mobile Phones (Level 1) -│ ├── Smartphones (Level 2) -│ └── Feature Phones (Level 2) -└── Audio (Level 1) - ├── Headphones (Level 2) - └── Speakers (Level 2) -``` - -## Business Rules - -* Root categories have `parentCategoryId` as null and `level` as 0 -* Child categories must have a valid `parentCategoryId` -* Category slugs must be unique across the entire catalog -* Categories cannot be deleted if they contain products or subcategories -* Inactive categories should hide all associated products from public view -* Maximum hierarchy depth should be limited (e.g., 5 levels) \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Inventory/index.mdx b/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Inventory/index.mdx deleted file mode 100644 index 6c7707db9..000000000 --- a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Inventory/index.mdx +++ /dev/null @@ -1,116 +0,0 @@ ---- -id: Inventory -name: Inventory -version: 1.0.0 -identifier: inventoryId -summary: Tracks stock levels and availability for products. -properties: - - name: inventoryId - type: UUID - required: true - description: Unique identifier for the inventory record - - name: productId - type: UUID - required: true - description: Product this inventory record tracks - references: Product - referencesIdentifier: productId - relationType: hasOne - - name: sku - type: string - required: true - description: Stock Keeping Unit matching the product SKU - - name: quantityOnHand - type: integer - required: true - description: Current available stock quantity - - name: quantityReserved - type: integer - required: true - description: Quantity reserved for pending orders - - name: quantityAvailable - type: integer - required: true - description: Calculated available quantity (onHand - reserved) - - name: minimumStockLevel - type: integer - required: true - description: Minimum stock level before reorder alert - - name: maximumStockLevel - type: integer - required: false - description: Maximum stock level for inventory management - - name: reorderPoint - type: integer - required: true - description: Stock level that triggers reorder process - - name: reorderQuantity - type: integer - required: true - description: Quantity to order when restocking - - name: unitCost - type: decimal - required: false - description: Cost per unit for inventory valuation - - name: warehouseLocation - type: string - required: false - description: Physical location or bin where item is stored - - name: lastRestockedAt - type: DateTime - required: false - description: Date and time of last restock - - name: lastSoldAt - type: DateTime - required: false - description: Date and time of last sale - - name: isTrackingEnabled - type: boolean - required: true - description: Whether inventory tracking is enabled for this product - - name: backorderAllowed - type: boolean - required: true - description: Whether backorders are allowed when out of stock - - name: createdAt - type: DateTime - required: true - description: Date and time when the inventory record was created - - name: updatedAt - type: DateTime - required: false - description: Date and time when the inventory record was last updated ---- - -## Overview - -The Inventory entity manages stock levels and availability for products in the e-commerce system. It tracks current quantities, reserved stock, and provides reorder management capabilities. - -### Entity Properties - - -## Relationships - -* **Product:** Each inventory record belongs to one `Product` (identified by `productId`). -* **OrderItem:** Inventory quantities are affected by `OrderItem` entities when orders are placed. - -## Stock Calculations - -* **Available Quantity** = Quantity On Hand - Quantity Reserved -* **Reorder Needed** = Quantity Available <= Reorder Point -* **Stock Value** = Quantity On Hand × Unit Cost - -## Examples - -* **Inventory #1:** iPhone 15 Pro - 25 on hand, 5 reserved, 20 available, reorder at 10 units. -* **Inventory #2:** Running Shoes Size 9 - 0 on hand, 2 reserved, backorder allowed. - -## Business Rules - -* Quantity on hand cannot be negative -* Quantity reserved cannot exceed quantity on hand -* Available quantity is automatically calculated -* Reorder alerts are triggered when available = reorder point -* Stock reservations are created when orders are placed -* Stock is decremented when orders are shipped -* Inventory adjustments must be logged for audit trail \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Product/index.mdx b/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Product/index.mdx deleted file mode 100644 index 59738ca8f..000000000 --- a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Product/index.mdx +++ /dev/null @@ -1,115 +0,0 @@ ---- -id: Product -name: Product -version: 1.0.0 -identifier: productId -aggregateRoot: true -summary: Represents a product or service available for purchase in the e-commerce system. -properties: - - name: productId - type: UUID - required: true - description: Unique identifier for the product - - name: name - type: string - required: true - description: Name of the product - - name: description - type: string - required: false - description: Detailed description of the product - - name: sku - type: string - required: true - description: Stock Keeping Unit - unique product identifier - - name: price - type: decimal - required: true - description: Current selling price of the product - - name: categoryId - type: UUID - required: true - description: Category this product belongs to - references: Category - referencesIdentifier: categoryId - relationType: hasOne - - name: brand - type: string - required: false - description: Brand name of the product - - name: weight - type: decimal - required: false - description: Weight of the product in kilograms - - name: dimensions - type: object - required: false - description: Product dimensions (length, width, height) - properties: - - name: length - type: decimal - - name: width - type: decimal - - name: height - type: decimal - - name: isActive - type: boolean - required: true - description: Whether the product is currently available for sale - - name: createdAt - type: DateTime - required: true - description: Date and time when the product was created - - name: updatedAt - type: DateTime - required: false - description: Date and time when the product was last updated - - name: images - type: array - items: - type: string - required: false - description: URLs of product images - - name: inventory - type: Inventory - required: false - description: Inventory information for this product - references: Inventory - referencesIdentifier: productId - relationType: hasOne - - name: reviews - type: array - items: - type: Review - required: false - description: Customer reviews for this product - references: Review - referencesIdentifier: productId - relationType: hasMany ---- - -## Overview - -The Product entity represents items or services available for purchase in the e-commerce system. It serves as an aggregate root containing all product-related information including pricing, categorization, inventory details, and customer reviews. - -### Entity Properties - - -## Relationships - -* **Category:** Each product belongs to one `Category` (identified by `categoryId`). -* **Inventory:** Each product has one `Inventory` record tracking stock levels. -* **Review:** A product can have multiple `Review` entities from customers. -* **OrderItem:** Products are referenced in `OrderItem` entities when included in orders. - -## Examples - -* **Product #1:** "iPhone 15 Pro" - Electronics category, $999.99, with 50 units in stock and 4.5-star reviews. -* **Product #2:** "Running Shoes" - Sports category, $129.99, various sizes available, with detailed size chart. - -## Business Rules - -* Products must have a unique SKU across the entire catalog -* Products cannot be deleted if they have associated order items -* Price changes should be tracked for audit purposes -* Products must belong to an active category to be purchasable \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Review/index.mdx b/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Review/index.mdx deleted file mode 100644 index 4b5d0b2f2..000000000 --- a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/entities/Review/index.mdx +++ /dev/null @@ -1,154 +0,0 @@ ---- -id: Review -name: Review -version: 1.0.0 -identifier: reviewId -summary: Represents customer reviews and ratings for products. -properties: - - name: reviewId - type: UUID - required: true - description: Unique identifier for the review - - name: productId - type: UUID - required: true - description: Product being reviewed - references: Product - referencesIdentifier: productId - relationType: hasOne - - name: customerId - type: UUID - required: true - description: Customer who wrote the review - references: Customer - referencesIdentifier: customerId - relationType: hasOne - - name: orderId - type: UUID - required: false - description: Order associated with this review (for verified purchases) - references: Order - referencesIdentifier: orderId - relationType: hasOne - - name: rating - type: integer - required: true - description: Rating given by customer (1-5 stars) - minimum: 1 - maximum: 5 - - name: title - type: string - required: false - description: Review title or headline - - name: content - type: string - required: true - description: Review content and comments - - name: isVerifiedPurchase - type: boolean - required: true - description: Whether this review is from a verified purchase - - name: isRecommended - type: boolean - required: false - description: Whether customer recommends this product - - name: helpfulVotes - type: integer - required: true - description: Number of helpful votes received - - name: totalVotes - type: integer - required: true - description: Total number of votes received - - name: status - type: string - required: true - description: Current review status - enum: ['pending', 'approved', 'rejected', 'flagged'] - - name: moderationNotes - type: string - required: false - description: Internal moderation notes - - name: images - type: array - items: - type: string - required: false - description: URLs of images uploaded with the review - - name: pros - type: array - items: - type: string - required: false - description: List of positive aspects mentioned - - name: cons - type: array - items: - type: string - required: false - description: List of negative aspects mentioned - - name: merchantResponse - type: object - required: false - description: Response from merchant to this review - properties: - - name: content - type: string - description: Merchant response content - - name: respondedAt - type: DateTime - description: When merchant responded - - name: respondedBy - type: string - description: Who responded for the merchant - - name: createdAt - type: DateTime - required: true - description: Date and time when the review was created - - name: updatedAt - type: DateTime - required: false - description: Date and time when the review was last updated - - name: moderatedAt - type: DateTime - required: false - description: Date and time when the review was moderated ---- - -## Overview - -The Review entity captures customer feedback and ratings for products. It supports verified purchase validation, content moderation, community voting, and merchant responses to build trust and provide valuable product insights. - -### Entity Properties - - -## Relationships - -* **Product:** Each review belongs to one `Product` (identified by `productId`). -* **Customer:** Each review is written by one `Customer` (identified by `customerId`). -* **Order:** Each review can be linked to one `Order` for purchase verification (identified by `orderId`). - -## Review Lifecycle - -``` -submitted → pending → approved → published - ↓ ↓ - rejected flagged -``` - -## Examples - -* **Review #1:** 5-star review for iPhone 15 Pro, verified purchase, "Excellent camera quality!" -* **Review #2:** 3-star review for Running Shoes, helpful votes: 15/20, includes photos -* **Review #3:** 1-star review flagged for inappropriate content, pending moderation - -## Business Rules - -* Reviews can only be submitted by customers who purchased the product -* Rating must be between 1-5 stars -* Verified purchase reviews are given higher weight in calculations -* Inappropriate content is flagged and requires moderation -* Customers can only review the same product once per purchase -* Helpful votes help surface most valuable reviews -* Merchant responses are limited to one per review -* Reviews older than 2 years may have reduced weight in calculations \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/index.mdx b/examples/default/domains/E-Commerce/subdomains/ProductCatalog/index.mdx deleted file mode 100644 index b5ab60b69..000000000 --- a/examples/default/domains/E-Commerce/subdomains/ProductCatalog/index.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -id: ProductCatalog -name: Product Catalog -version: 0.0.1 -summary: Manages product information, categories, inventory, and customer reviews in the e-commerce system. -owners: - - dboyne -entities: - - id: Product - - id: Category - - id: Inventory - - id: Review -services: - - id: InventoryService ---- - -## Overview - -The Product Catalog subdomain is responsible for managing all product-related information in the e-commerce system. This includes product details, hierarchical categorization, inventory tracking, and customer reviews. - -## Core Responsibilities - -### Product Management -- Maintain product information including pricing, descriptions, and specifications -- Support product variants (size, color, style) -- Handle product lifecycle (active, discontinued, draft) -- Manage product relationships and cross-selling - -### Category Management -- Organize products into hierarchical categories -- Support multi-level category structures -- Maintain category metadata and SEO information -- Handle category navigation and filtering - -### Inventory Management -- Track stock levels and availability -- Manage reorder points and stock alerts -- Handle inventory reservations and allocations -- Support warehouse and location management - -### Review Management -- Collect and manage customer product reviews -- Calculate review metrics and ratings -- Moderate review content -- Support review helpfulness and responses - -## Key Entities - -- **Product**: Central aggregate containing all product information -- **Category**: Hierarchical product categorization system -- **Inventory**: Stock tracking and availability management -- **Review**: Customer feedback and rating system - -## Business Rules - -- Products must belong to an active category -- Inventory levels affect product availability -- Reviews require verified purchases -- Category hierarchies have maximum depth limits \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/channels/domain-eventbus/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/channels/domain-eventbus/index.mdx deleted file mode 100644 index 6a168e1b4..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/channels/domain-eventbus/index.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -id: subscription-domain-eventbus -name: Subscription Domain EventBus -version: 1.0.0 -summary: | - Amazon Subscription Domain EventBus -owners: - - dboyne -routes: - - id: cross-account-bus ---- - -{/* Information about the Orders Domain EventBus */} - -Full this.... diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/entities/BillingProfile/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/entities/BillingProfile/index.mdx deleted file mode 100644 index 460bcbc68..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/entities/BillingProfile/index.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -id: BillingProfile -name: BillingProfile -version: 1.0.0 -identifier: billingProfileId -summary: Stores billing-related contact information and preferences for a customer, often used for invoices and communication. - -properties: - - name: billingProfileId - type: UUID - required: true - description: Unique identifier for the billing profile. - - name: customerId - type: UUID - required: true - description: Identifier of the customer this billing profile belongs to. - references: Customer - referencesIdentifier: customerId - relationType: hasOne - - name: billingEmail - type: string - required: false # May default to customer's primary email - description: Specific email address for sending invoices and billing notifications - - name: companyName # Optional, for B2B - type: string - required: false - description: Company name for billing purposes. - - name: taxId # Optional, for B2B or specific regions - type: string - required: false - description: Tax identification number (e.g., VAT ID, EIN). - - name: billingAddressId - type: UUID - required: true - description: Identifier for the primary billing address associated with this profile. - - name: preferredPaymentMethodId # Optional default for invoices/subscriptions - type: UUID - required: false - description: Customer's preferred payment method for charges related to this profile. - - name: createdAt - type: DateTime - required: true - description: Timestamp when the billing profile was created. - - name: updatedAt - type: DateTime - required: true - description: Timestamp when the billing profile was last updated. ---- - -## Overview - -The BillingProfile entity consolidates billing-specific details for a customer, such as the billing address, contact email for invoices, tax information, and potentially preferred payment methods. This might be distinct from the customer's general contact information or shipping addresses. - -### Entity Properties - - -## Relationships - -* **Customer:** A billing profile belongs to one `Customer`. A customer might potentially have multiple profiles in complex scenarios, but often just one. -* **Address:** Linked to a primary billing `Address`. -* **PaymentMethod:** May specify a preferred `PaymentMethod`. -* **Invoice:** Invoices are typically generated using information from the BillingProfile. -* **Subscription:** Subscriptions may use the associated customer's BillingProfile for charging. - -## Examples - -* Jane Doe's personal billing profile with her home address and primary email. -* Acme Corp's billing profile with their HQ address, VAT ID, and accounts payable email address. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/entities/SubscriptionPeriod/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/entities/SubscriptionPeriod/index.mdx deleted file mode 100644 index 21c9f1f05..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/entities/SubscriptionPeriod/index.mdx +++ /dev/null @@ -1,73 +0,0 @@ ---- -id: SubscriptionPeriod -name: SubscriptionPeriod -version: 1.0.0 -identifier: subscriptionPeriodId -summary: Represents a single billing cycle or interval within a subscription's lifetime. - -properties: - - name: subscriptionPeriodId - type: UUID - required: true - description: Unique identifier for this specific subscription period. - - name: subscriptionId - type: UUID - required: true - description: Identifier of the parent Subscription this period belongs to. - - name: planId # Denormalized for easier lookup? - type: UUID - required: true - description: Identifier of the Plan active during this period - - name: startDate - type: Date - required: true - description: The start date of this billing period. - - name: endDate - type: Date - required: true - description: The end date of this billing period. - - name: invoiceId # Optional, links to the invoice generated for this period - type: UUID - required: false - description: Identifier of the invoice created for this period's charge. - - name: paymentId # Optional, links to the payment made for this period's invoice - type: UUID - required: false - description: Identifier of the payment that settled the invoice for this period. - - name: status - type: string # (e.g., Active, Billed, Paid, Unpaid, PastDue) - required: true - description: Status specific to this period (reflects invoicing/payment state). - - name: amountBilled - type: decimal - required: false # May only be set once invoiced - description: The actual amount billed for this period (could differ from plan due to promotions, usage, etc.). - - name: currency - type: string # ISO 4217 code - required: false - description: Currency of the billed amount. - - name: createdAt - type: DateTime - required: true - description: Timestamp when this period record was created (often at the start of the period). ---- - -## Overview - -The SubscriptionPeriod entity tracks the state and details of a specific billing cycle within a `Subscription`. It links the subscription to the relevant invoice and payment for that interval and records the exact dates and amount billed. - -### Entity Properties - - -## Relationships - -* **Subscription:** A subscription period belongs to one `Subscription`. -* **Plan:** Reflects the `Plan` active during this period. -* **Invoice:** May be associated with one `Invoice` generated for this period. -* **Payment:** May be associated with one `Payment` that settled the period's invoice. - -## Examples - -* Period for Jane Doe's 'Pro Plan' from 2024-05-01 to 2024-05-31, invoiced via #INV-00123, status Paid. -* Period for Acme Corp's 'Enterprise Plan' from 2024-04-15 to 2024-05-14, status Billed, awaiting payment. -* The first period (trial) for a new subscription from 2024-05-20 to 2024-06-19, status Active, amountBilled $0.00. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/CancelSubscription/changelog.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/CancelSubscription/changelog.mdx deleted file mode 100644 index c8eb76e22..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/CancelSubscription/changelog.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -createdAt: 2024-08-20 ---- - -Send an email to the customer confirming the successful cancellation. diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/CancelSubscription/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/CancelSubscription/index.mdx deleted file mode 100644 index 772757393..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/CancelSubscription/index.mdx +++ /dev/null @@ -1,70 +0,0 @@ ---- -id: "CancelSubscription" -name: "User Cancels Subscription" -version: "1.0.0" -summary: "Flow for when a user has cancelled a subscription" -owners: - - subscriptions-management -steps: - - id: "cancel_subscription_initiated" - title: "Cancels Subscription" - summary: "User cancels their subscription" - actor: - name: "User" - next_step: - id: "cancel_subscription_request" - label: "Initiate subscription cancellation" - - - id: "cancel_subscription_request" - title: "Cancel Subscription" - message: - id: "CancelSubscription" - version: "0.0.1" - next_step: - id: "subscription_service" - label: "Proceed to subscription service" - - - id: "stripe_integration" - title: "Stripe" - externalSystem: - name: "Stripe" - summary: "3rd party payment system" - url: "https://stripe.com/" - next_step: - id: "subscription_service" - label: "Return to subscription service" - - - id: "subscription_service" - title: "Subscription Service" - service: - id: "SubscriptionService" - version: "0.0.1" - next_steps: - - id: "stripe_integration" - label: "Cancel subscription via Stripe" - - id: "subscription_cancelled" - label: "Successful cancellation" - - id: "subscription_rejected" - label: "Failed cancellation" - - - id: "subscription_cancelled" - title: "Subscription has been Cancelled" - message: - id: "UserSubscriptionCancelled" - version: "0.0.1" - next_step: - id: "notification_service" - label: "Email customer" - - - id: "subscription_rejected" - title: "Subscription cancellation has been rejected" - - - id: "notification_service" - title: "Notifications Service" - service: - id: "NotificationService" - version: "0.0.2" - ---- - - diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/CancelSubscription/versioned/0.0.1/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/CancelSubscription/versioned/0.0.1/index.mdx deleted file mode 100644 index 53cc9f7d5..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/CancelSubscription/versioned/0.0.1/index.mdx +++ /dev/null @@ -1,51 +0,0 @@ ---- -id: "CancelSubscription" -name: "User Cancels Subscription" -version: "0.0.1" -summary: "Flow for when a user has cancelled a subscription" -owners: - - subscriptions-management -steps: - - id: "cancel_subscription_initiated" - title: "Cancels Subscription" - summary: "User cancels their subscription" - actor: - name: "User" - next_step: - id: "cancel_subscription_request" - label: "Initiate subscription cancellation" - - - id: "cancel_subscription_request" - title: "Cancel Subscription" - message: - id: "CancelSubscription" - version: "0.0.1" - next_step: - id: "subscription_service" - label: "Proceed to subscription service" - - - id: "stripe_integration" - title: "Stripe" - externalSystem: - name: "Stripe" - summary: "3rd party payment system" - url: "https://stripe.com/" - next_step: - id: "subscription_service" - label: "Return to subscription service" - - - id: "subscription_service" - title: "Subscription Service" - service: - id: "SubscriptionService" - version: "0.0.1" - next_steps: - - id: "stripe_integration" - label: "Cancel subscription via Stripe" - - id: "subscription_cancelled" - label: "Successful cancellation" - - id: "subscription_rejected" - label: "Failed cancellation" ---- - - diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/SubscriptionRenewed/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/SubscriptionRenewed/index.mdx deleted file mode 100644 index 568329293..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/flows/SubscriptionRenewed/index.mdx +++ /dev/null @@ -1,304 +0,0 @@ ---- -id: "SubscriptionRenewed" -name: "Subscription Renewal Flow" -version: "1.0.0" -summary: "Business flow for automatic subscription renewals and related processes" -owners: - - subscriptions-management -steps: - - id: "renewal_timer_triggered" - title: "Renewal Period Reached" - custom: - title: "Renewal Timer" - color: "orange" - icon: "ClockIcon" - type: "Scheduler" - summary: "Automated timer triggers the subscription renewal process" - height: 8 - properties: - subscription_id: "sub_12345678" - renewal_type: "Automatic" - billing_cycle: "Monthly" - next_billing_date: "2024-08-01" - menu: - - label: "View scheduler configuration" - url: "https://docs.example.com/scheduler" - - label: "Subscription timing documentation" - url: "https://docs.example.com/subscription-timing" - next_step: - id: "check_subscription_status" - label: "Verify subscription status" - - - id: "check_subscription_status" - title: "Check Subscription Status" - service: - id: "SubscriptionService" - version: "0.0.1" - next_steps: - - id: "payment_approval_check" - label: "Subscription active, proceed to payment" - - id: "subscription_expired" - label: "Subscription has expired" - - id: "subscription_canceled" - label: "Subscription was canceled" - - - id: "subscription_expired" - title: "Subscription Expired" - type: "node" - next_step: - id: "send_renewal_notification" - label: "Notify customer to renew" - - - id: "subscription_canceled" - title: "Subscription Canceled" - type: "node" - next_step: - id: "send_reactivation_offer" - label: "Send special reactivation offer" - - - id: "send_renewal_notification" - title: "Send Renewal Notification" - service: - id: "NotificationService" - version: "0.0.2" - next_step: - id: "await_customer_action" - label: "Wait for customer response" - - - id: "send_reactivation_offer" - title: "Send Reactivation Offer" - service: - id: "NotificationService" - version: "0.0.2" - next_step: - id: "await_customer_action" - label: "Wait for customer response" - - - id: "await_customer_action" - title: "Await Customer Action" - custom: - title: "Customer Decision Point" - color: "purple" - icon: "UserIcon" - type: "Decision" - height: 8 - summary: "Waiting period for customer to take action on notification" - properties: - timeout_period: "7 days" - options: "Renew, Upgrade, Cancel, Ignore" - next_steps: - - id: "manual_renewal_flow" - label: "Customer manually renews" - - id: "flow_ends" - label: "No action taken" - - - id: "manual_renewal_flow" - title: "Manual Renewal Flow" - type: "node" - next_step: - id: "payment_initiated" - label: "Process payment" - - - id: "payment_approval_check" - title: "Check Payment Approval" - message: - id: "GetPaymentStatus" - version: "0.0.1" - next_steps: - - id: "payment_initiated" - label: "Payment approved, proceed with billing" - - id: "payment_method_invalid" - label: "Invalid payment method" - - - id: "payment_method_invalid" - title: "Invalid Payment Method" - type: "node" - next_step: - id: "request_payment_update" - label: "Request updated payment method" - - - id: "request_payment_update" - title: "Request Payment Update" - service: - id: "NotificationService" - version: "0.0.2" - next_step: - id: "await_updated_payment" - label: "Wait for payment update" - - - id: "await_updated_payment" - title: "Await Updated Payment Method" - actor: - name: "Customer" - summary: "Customer is waiting for payment update" - next_steps: - - id: "payment_initiated" - label: "Payment method updated" - - id: "subscription_grace_period" - label: "No update received" - - - id: "subscription_grace_period" - title: "Grace Period" - custom: - title: "Subscription Grace Period" - color: "yellow" - icon: "ShieldExclamationIcon" - type: "Timer" - summary: "Limited period where subscription remains active despite payment failure" - properties: - duration: "7 days" - status: "At risk" - next_steps: - - id: "payment_initiated" - label: "Payment updated during grace period" - - id: "subscription_suspended" - label: "Grace period expired" - - - id: "subscription_suspended" - title: "Subscription Suspended" - message: - id: "UserSubscriptionCancelled" - version: "0.0.1" - next_step: - id: "send_suspension_notification" - label: "Notify customer of suspension" - - - id: "send_suspension_notification" - title: "Send Suspension Notification" - service: - id: "NotificationService" - version: "0.0.2" - next_step: - id: "flow_ends" - label: "Flow ends" - - - id: "payment_initiated" - title: "Process Payment" - message: - id: "PaymentInitiated" - version: "0.0.1" - next_step: - id: "payment_gateway" - label: "Send to payment gateway" - - - id: "payment_gateway" - title: "Payment Gateway" - externalSystem: - name: "Stripe" - summary: "3rd party payment processor" - url: "https://stripe.com/" - next_steps: - - id: "payment_processed" - label: "Payment successful" - - id: "payment_failed" - label: "Payment failed" - - - id: "payment_failed" - title: "Payment Failed" - type: "node" - next_step: - id: "retry_payment" - label: "Retry payment" - - - id: "retry_payment" - title: "Retry Payment" - custom: - title: "Payment Retry Logic" - color: "red" - icon: "ArrowPathIcon" - type: "Processor" - summary: "Automated retry logic for failed payments" - properties: - max_attempts: 3 - backoff_interval: "24 hours" - current_attempt: 1 - next_steps: - - id: "payment_initiated" - label: "Retry payment" - - id: "subscription_grace_period" - label: "Max retries exceeded" - - - id: "payment_processed" - title: "Payment Processed" - message: - id: "PaymentProcessed" - version: "1.0.0" - next_step: - id: "update_subscription_status" - label: "Update subscription" - - - id: "update_subscription_status" - title: "Update Subscription" - service: - id: "SubscriptionService" - version: "0.0.1" - next_step: - id: "send_renewal_confirmation" - label: "Confirm renewal to customer" - - - id: "send_renewal_confirmation" - title: "Send Renewal Confirmation" - service: - id: "NotificationService" - version: "0.0.2" - next_step: - id: "analyze_customer_usage" - label: "Analyze customer usage patterns" - - - id: "analyze_customer_usage" - title: "Analyze Usage Patterns" - custom: - title: "Usage Analytics" - color: "blue" - icon: "ChartBarIcon" - type: "Analytics" - summary: "Analyze customer usage patterns to identify upsell opportunities" - properties: - metrics_analyzed: "Feature usage, Resource consumption, Access patterns" - lookback_period: "90 days" - menu: - - label: "View analytics dashboard" - url: "https://analytics.example.com/subscriptions" - - label: "Documentation" - url: "https://docs.example.com/analytics" - next_steps: - - id: "send_upgrade_recommendation" - label: "Usage suggests upgrade opportunity" - - id: "flow_ends" - label: "No upgrade opportunity identified" - - - id: "send_upgrade_recommendation" - title: "Send Upgrade Recommendation" - service: - id: "NotificationService" - version: "0.0.2" - next_step: - id: "flow_ends" - label: "Flow completed" - - - id: "flow_ends" - title: "Flow Completed" - type: "node" - ---- - -## Subscription Renewal Flow - -This flow documents the process of automatic subscription renewals, including handling various edge cases such as payment failures, expired subscriptions, and customer interactions. - - - -### Key Components - -* **Automatic Renewal Process**: Triggered by a scheduled timer when the subscription renewal period is reached -* **Payment Processing**: Integration with payment gateway and handling of payment failures -* **Customer Notifications**: Various notifications sent throughout the process -* **Grace Period Handling**: Special handling when payments fail with a grace period before subscription suspension -* **Usage Analytics**: Analysis of customer usage patterns to identify upgrade opportunities - -### Business Rules - -1. Subscriptions are renewed automatically unless explicitly canceled -2. Failed payments trigger a retry process (up to 3 attempts) -3. Customers receive a 7-day grace period before subscription suspension -4. Usage patterns are analyzed to provide personalized upgrade recommendations diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/index.mdx deleted file mode 100644 index ce7d3ae19..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/index.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -id: Subscriptions -name: Subscriptions -version: 0.0.1 -summary: | - Manages subscription lifecycle, billing cycles, and plan management for recurring revenue streams. -owners: - - subscriptions-management -services: - - id: BillingService - version: 0.0.1 -entities: - - id: BillingProfile - - id: SubscriptionPeriod -badges: - - content: Subdomain - backgroundColor: blue - textColor: blue - ---- - -## Overview - -The Subscriptions Domain is responsible for managing all aspects of subscription-based services within our e-commerce platform. This includes subscription lifecycle management, recurring billing, plan management, and integration with payment systems. - -## Core Capabilities - -- **Subscription Lifecycle**: Create, update, pause, resume, and cancel subscriptions -- **Billing Cycles**: Manage monthly, quarterly, and annual billing cycles -- **Plan Management**: Define and manage subscription plans and pricing -- **Trial Periods**: Support free trials and promotional periods -- **Usage-based Billing**: Track and bill based on usage metrics - -## Services - -### BillingService -Handles billing cycle calculations, invoice generation, and payment scheduling. Coordinates with the Payment domain for processing recurring payments. - - -## Cross-Domain Integration - -The Subscriptions domain integrates closely with: - -- **Payment Domain**: For processing recurring payments and handling payment failures -- **Orders Domain**: For managing subscription-based product orders -- **Customer Domain**: For customer account and profile management - -## Domain Events - -Key events in the Subscriptions domain: -- `SubscriptionPaymentDue` - Triggers payment collection -- `PlanMigrationCompleted` - Subscription plan changed - -## Bounded context - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/InvoiceGenerated/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/InvoiceGenerated/index.mdx deleted file mode 100644 index ccaa824e3..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/InvoiceGenerated/index.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -id: InvoiceGenerated -version: 0.0.1 -name: Invoice Generated -summary: Emitted when an invoice is generated for a subscription -tags: - - billing - - payment - - cross-domain -badges: - - content: Cross-Domain - backgroundColor: purple - textColor: white -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The `InvoiceGenerated` event is emitted by the Billing Service when an invoice has been successfully generated for a subscription. This event notifies other services that a new invoice is available and ready for payment processing. - -## Cross-Domain Communication - -This event facilitates communication between: -- **Source**: Subscriptions Domain (BillingService) -- **Target**: Payment Domain (PaymentService), Notification Services, Accounting Systems - -## Schema - - - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/InvoiceGenerated/schema.json b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/InvoiceGenerated/schema.json deleted file mode 100644 index 83c1e9120..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/InvoiceGenerated/schema.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "InvoiceGenerated", - "description": "Schema for invoice generated event", - "properties": { - "invoiceId": { - "type": "string", - "description": "Unique identifier for the invoice" - }, - "invoiceNumber": { - "type": "string", - "description": "Human-readable invoice number" - }, - "subscriptionId": { - "type": "string", - "description": "Unique identifier for the subscription" - }, - "customerId": { - "type": "string", - "description": "Unique identifier for the customer" - }, - "invoiceDate": { - "type": "string", - "format": "date-time", - "description": "Date and time when the invoice was generated" - }, - "dueDate": { - "type": "string", - "format": "date-time", - "description": "Date and time when the invoice payment is due" - }, - "status": { - "type": "string", - "enum": ["generated", "sent", "paid", "overdue", "cancelled"], - "description": "Current status of the invoice" - }, - "lineItems": { - "type": "array", - "description": "List of items on the invoice", - "items": { - "type": "object", - "properties": { - "description": { - "type": "string", - "description": "Description of the line item" - }, - "quantity": { - "type": "number", - "description": "Quantity of the item" - }, - "unitPrice": { - "type": "number", - "description": "Price per unit" - }, - "total": { - "type": "number", - "description": "Total price for this line item (quantity * unitPrice)" - } - }, - "required": ["description", "quantity", "unitPrice", "total"] - } - }, - "subtotal": { - "type": "number", - "description": "Subtotal amount before taxes" - }, - "tax": { - "type": "number", - "description": "Tax amount applied to the invoice" - }, - "total": { - "type": "number", - "description": "Total amount including taxes" - }, - "currency": { - "type": "string", - "description": "Currency code (ISO 4217)" - }, - "billingPeriod": { - "type": "object", - "description": "Billing period covered by this invoice", - "properties": { - "start": { - "type": "string", - "format": "date", - "description": "Start date of the billing period" - }, - "end": { - "type": "string", - "format": "date", - "description": "End date of the billing period" - } - }, - "required": ["start", "end"] - }, - "planId": { - "type": "string", - "description": "Subscription plan identifier" - } - }, - "required": [ - "invoiceId", - "invoiceNumber", - "subscriptionId", - "customerId", - "invoiceDate", - "dueDate", - "status", - "lineItems", - "subtotal", - "tax", - "total", - "currency", - "billingPeriod", - "planId" - ] -} - diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/SubscriptionPaymentDue/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/SubscriptionPaymentDue/index.mdx deleted file mode 100644 index d1cc3e02d..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/SubscriptionPaymentDue/index.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -id: SubscriptionPaymentDue -version: 0.0.1 -name: Subscription Payment Due -summary: Emitted when a subscription payment is due for collection -tags: - - billing - - payment - - cross-domain -badges: - - content: Cross-Domain - backgroundColor: purple - textColor: white -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The `SubscriptionPaymentDue` event is emitted by the Billing Service when a subscription's billing cycle is due for payment. This event triggers the payment collection process in the Payment domain. - -## Cross-Domain Communication - -This event facilitates communication between: -- **Source**: Subscriptions Domain (BillingService) -- **Target**: Payment Domain (PaymentService, FraudDetectionService) - -## Schema - - - -## Event Flow - -1. BillingService calculates when payment is due -2. Emits `SubscriptionPaymentDue` event -3. PaymentService receives and initiates payment -4. FraudDetectionService performs risk assessment -5. Payment is processed through PaymentGatewayService - -## Example Payload - -```json -{ - "subscriptionId": "sub_ABC123", - "customerId": "cust_XYZ789", - "invoiceId": "inv_123456", - "amount": 49.99, - "currency": "USD", - "dueDate": "2024-02-01T00:00:00Z", - "billingPeriod": { - "start": "2024-02-01", - "end": "2024-02-29" - }, - "planId": "pro-monthly", - "retryAttempt": 0 -} -``` - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/SubscriptionPaymentDue/schema.json b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/SubscriptionPaymentDue/schema.json deleted file mode 100644 index d1ed1aba9..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/events/SubscriptionPaymentDue/schema.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "subscriptionId": { - "type": "string", - "description": "Unique identifier for the subscription" - }, - "customerId": { - "type": "string", - "description": "Unique identifier for the customer" - }, - "invoiceId": { - "type": "string", - "description": "Unique identifier for the invoice" - }, - "amount": { - "type": "number", - "description": "Amount due for payment" - }, - "currency": { - "type": "string", - "description": "Currency code (ISO 4217)" - }, - "dueDate": { - "type": "string", - "format": "date-time", - "description": "When the payment is due" - }, - "billingPeriod": { - "type": "object", - "properties": { - "start": { - "type": "string", - "format": "date" - }, - "end": { - "type": "string", - "format": "date" - } - }, - "required": ["start", "end"] - }, - "planId": { - "type": "string", - "description": "Subscription plan identifier" - }, - "retryAttempt": { - "type": "integer", - "description": "Number of retry attempts for failed payments" - } - }, - "required": ["subscriptionId", "customerId", "invoiceId", "amount", "currency", "dueDate"] -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/index.mdx deleted file mode 100644 index 82e3eebdb..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/index.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -id: BillingService -version: 0.0.1 -name: Billing Service -summary: >- - Manages billing cycles, invoice generation, and payment scheduling for - subscriptions -tags: - - billing - - subscriptions - - invoicing -repository: - url: 'https://github.com/eventcatalog/billing-service' -receives: - - id: PaymentProcessed -sends: - - id: InvoiceGenerated - - id: SubscriptionPaymentDue -owners: - - dboyne -attachments: - - url: 'https://example.com/adr/001' - title: ADR-001 - Use Kafka for asynchronous messaging - type: architecture-decisions - icon: FileTextIcon - - url: 'https://example.com/adr/001' - title: ADR-002 - Database per service - icon: FileTextIcon - type: architecture-decisions -specifications: - - type: graphql - path: schema.graphql - name: GraphQL API -readsFrom: - - id: order-metadata-store ---- - -import Footer from '@catalog/components/footer.astro' - -## Overview - -The Billing Service is responsible for managing all billing-related operations for subscriptions. It calculates billing cycles, generates invoices, and coordinates with payment services to ensure timely payment collection. - -## Key Features - -- **Billing Cycle Management**: Handles daily, weekly, monthly, quarterly, and annual billing cycles -- **Invoice Generation**: Creates detailed invoices with line items and tax calculations -- **Payment Scheduling**: Schedules recurring payments based on billing cycles -- **Proration**: Calculates prorated charges for mid-cycle changes -- **Dunning Management**: Handles failed payment retry logic - -## API Endpoints - -### REST API -- `GET /api/billing/invoice/{subscriptionId}` - Get current invoice -- `GET /api/billing/history/{subscriptionId}` - Get billing history -- `POST /api/billing/preview` - Preview upcoming charges -- `PUT /api/billing/retry/{invoiceId}` - Retry failed payment - -## Billing Cycle States - -```mermaid -stateDiagram-v2 - [*] --> Scheduled - Scheduled --> Processing - Processing --> Paid - Processing --> Failed - Failed --> Retrying - Retrying --> Paid - Retrying --> Suspended - Paid --> [*] - Suspended --> [*] -``` - -## Configuration - -```yaml -billing_service: - cycles: - - daily - - weekly - - monthly - - quarterly - - annual - retry_attempts: 3 - retry_interval_days: [1, 3, 7] - invoice_generation_lead_days: 7 -``` - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/schema.graphql b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/schema.graphql deleted file mode 100644 index ee85bbb50..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/BillingService/schema.graphql +++ /dev/null @@ -1,191 +0,0 @@ -scalar DateTime -scalar Money - -enum BillingCycle { - DAILY - WEEKLY - MONTHLY - QUARTERLY - ANNUAL -} - -enum InvoiceStatus { - DRAFT - PENDING - PAID - FAILED - CANCELLED - REFUNDED -} - -enum PaymentStatus { - SCHEDULED - PROCESSING - PAID - FAILED - RETRYING - SUSPENDED -} - -type Subscription { - id: ID! - customerId: ID! - planId: ID! - status: String! - billingCycle: BillingCycle! - currentPeriodStart: DateTime! - currentPeriodEnd: DateTime! - cancelAtPeriodEnd: Boolean! - createdAt: DateTime! - updatedAt: DateTime! -} - -type LineItem { - id: ID! - description: String! - quantity: Int! - unitPrice: Money! - totalAmount: Money! - taxAmount: Money! - proratedDays: Int -} - -type Invoice { - id: ID! - subscriptionId: ID! - invoiceNumber: String! - status: InvoiceStatus! - subtotal: Money! - taxAmount: Money! - totalAmount: Money! - dueDate: DateTime! - paidAt: DateTime - lineItems: [LineItem!]! - billingPeriodStart: DateTime! - billingPeriodEnd: DateTime! - createdAt: DateTime! - updatedAt: DateTime! -} - -type Payment { - id: ID! - invoiceId: ID! - amount: Money! - status: PaymentStatus! - attemptCount: Int! - nextRetryAt: DateTime - failureReason: String - processedAt: DateTime - createdAt: DateTime! -} - -type BillingHistory { - subscriptionId: ID! - invoices: [Invoice!]! - totalPages: Int! - currentPage: Int! -} - -type ChargePreview { - subscriptionId: ID! - nextBillingDate: DateTime! - upcomingCharges: [LineItem!]! - proratedCredits: [LineItem!]! - totalAmount: Money! -} - -type RetryResult { - success: Boolean! - payment: Payment - errorMessage: String -} - -input ChargePreviewInput { - subscriptionId: ID! - planChanges: [PlanChangeInput!] - addons: [AddonInput!] -} - -input PlanChangeInput { - newPlanId: ID! - effectiveDate: DateTime -} - -input AddonInput { - addonId: ID! - quantity: Int! -} - -type Query { - """Get current invoice for a subscription""" - currentInvoice(subscriptionId: ID!): Invoice - - """Get billing history for a subscription with pagination""" - billingHistory( - subscriptionId: ID! - page: Int = 1 - limit: Int = 10 - ): BillingHistory! - - """Preview upcoming charges for a subscription""" - previewCharges(input: ChargePreviewInput!): ChargePreview! - - """Get invoice by ID""" - invoice(id: ID!): Invoice - - """Get payment details""" - payment(id: ID!): Payment - - """Get all failed payments for retry""" - failedPayments(subscriptionId: ID!): [Payment!]! -} - -type Mutation { - """Retry a failed payment""" - retryPayment(invoiceId: ID!): RetryResult! - - """Generate invoice for subscription""" - generateInvoice(subscriptionId: ID!): Invoice! - - """Process immediate payment""" - processPayment(invoiceId: ID!): Payment! - - """Cancel invoice""" - cancelInvoice(invoiceId: ID!): Invoice! - - """Apply credit to subscription""" - applyCredit( - subscriptionId: ID! - amount: Money! - reason: String! - ): Invoice! -} - -type Subscription { - """Real-time updates for billing events""" - billingEvents(subscriptionId: ID!): BillingEvent! -} - -union BillingEvent = InvoiceGenerated | PaymentProcessed | PaymentFailed | PaymentRetrying - -type InvoiceGenerated { - invoice: Invoice! - subscription: Subscription! -} - -type PaymentProcessed { - payment: Payment! - invoice: Invoice! -} - -type PaymentFailed { - payment: Payment! - invoice: Invoice! - retryScheduledAt: DateTime -} - -type PaymentRetrying { - payment: Payment! - attemptNumber: Int! - nextRetryAt: DateTime! -} \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/commands/CancelSubscription/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/commands/CancelSubscription/index.mdx deleted file mode 100644 index 019154688..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/commands/CancelSubscription/index.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -id: CancelSubscription -name: Cancel subscription -version: 0.0.1 -summary: | - Command that will try and cancel a users subscription -owners: - - subscriptions-management -badges: - - content: New! - backgroundColor: green - textColor: green -sidebar: - badge: 'POST' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `CancelSubscription` command will try and cancel a subscription for the user. - -## Architecture diagram - - - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/commands/SubscribeUser/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/commands/SubscribeUser/index.mdx deleted file mode 100644 index d99c9f821..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/commands/SubscribeUser/index.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -id: SubscribeUser -name: Subscribe user -version: 0.0.1 -summary: | - Command that will try and subscribe a given user -owners: - - subscriptions-management -badges: - - content: New! - backgroundColor: green - textColor: green -sidebar: - badge: 'POST' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `SubscribeUser` command represents when a new user wants to subscribe to our service. - -## Architecture diagram - - - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/containers/subscriptions-db/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/containers/subscriptions-db/index.mdx deleted file mode 100644 index a89615252..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/containers/subscriptions-db/index.mdx +++ /dev/null @@ -1,167 +0,0 @@ ---- -id: subscriptions-db -name: Subscriptions DB -version: 0.0.1 -container_type: database -technology: postgres@15 -authoritative: true -access_mode: readWrite -classification: internal -retention: 7y -residency: eu-west-1 -summary: Primary database for subscription plans, user subscriptions, and recurring billing cycles ---- - - - -### What is this? -Subscriptions DB is the system of record for all subscription-related data including subscription plans, customer subscriptions, billing cycles, and subscription lifecycle events. It uses PostgreSQL 15 with specialized extensions for time-series billing data. - -### What does it store? -- **Subscription Plans**: Available plans with pricing, billing frequency, and features -- **Customer Subscriptions**: Active and historical subscriptions with status tracking -- **Billing Cycles**: Recurring billing periods, next billing dates, and payment status -- **Subscription Changes**: Audit trail of plan changes, upgrades, downgrades, cancellations -- **Usage Tracking**: For metered billing features and overage calculations - -### Who writes to it? -- **SubscriptionService** manages subscription lifecycle (create, update, cancel, renew) -- **BillingService** updates billing cycle status and payment attempts -- **PaymentService** updates payment method associations - -### Who reads from it? -- **BillingService** reads upcoming billing cycles for charge processing -- **PaymentService** checks subscription status for payment validation -- **OrdersService** validates subscription benefits for order processing -- **Analytics** tracks subscription metrics (churn, MRR, LTV) -- **Customer Support** views subscription history and status - -### High-level data model -- `subscription_plans`: Catalog of available plans and pricing tiers -- `subscriptions`: Customer subscriptions linked to plans and payment methods -- `billing_cycles`: Time-based records of billing periods -- Subscription status: `TRIAL`, `ACTIVE`, `PAST_DUE`, `CANCELLED`, `EXPIRED` - -### Common queries -```sql --- Get active subscriptions with next billing date -SELECT s.subscription_id, s.customer_id, sp.name AS plan_name, - bc.next_billing_date, bc.amount_cents -FROM subscriptions s -JOIN subscription_plans sp USING (plan_id) -JOIN billing_cycles bc ON bc.subscription_id = s.subscription_id -WHERE s.status = 'ACTIVE' - AND bc.next_billing_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '7 days'; - --- Calculate Monthly Recurring Revenue (MRR) -SELECT - sp.name AS plan_name, - COUNT(*) AS active_subscriptions, - SUM(bc.amount_cents)/100.0 AS monthly_revenue -FROM subscriptions s -JOIN subscription_plans sp USING (plan_id) -JOIN billing_cycles bc ON bc.subscription_id = s.subscription_id -WHERE s.status = 'ACTIVE' - AND sp.billing_frequency = 'MONTHLY' -GROUP BY sp.name; - --- Find subscriptions due for renewal today -SELECT s.subscription_id, s.customer_id, s.payment_method_id, - bc.amount_cents, bc.currency -FROM subscriptions s -JOIN billing_cycles bc ON bc.subscription_id = s.subscription_id -WHERE bc.next_billing_date = CURRENT_DATE - AND s.status = 'ACTIVE' - AND bc.status = 'PENDING'; - --- Customer subscription history (for support) -SELECT s.subscription_id, sp.name, s.status, - s.started_at, s.cancelled_at, - CASE WHEN s.status = 'ACTIVE' - THEN DATE_PART('day', NOW() - s.started_at) - ELSE DATE_PART('day', s.cancelled_at - s.started_at) - END AS subscription_days -FROM subscriptions s -JOIN subscription_plans sp USING (plan_id) -WHERE s.customer_id = $1 -ORDER BY s.started_at DESC; -``` - -### Subscription lifecycle -1. Customer selects plan → `SubscriptionService` creates subscription with `TRIAL` or `ACTIVE` status -2. Billing cycle created with `next_billing_date` -3. BillingService processes payment on billing date -4. On success: cycle renewed, `next_billing_date` updated -5. On failure: subscription status → `PAST_DUE`, retry logic triggered -6. Cancellation: status → `CANCELLED`, billing cycles stop -7. Expiration: status → `EXPIRED` after grace period - -### Access patterns and guidance -- Use indexed lookups by `customer_id` and `subscription_id` -- Billing date queries use `next_billing_date` index for daily batch processing -- Status filtering is common; maintain composite index on `(status, next_billing_date)` -- Subscription changes are append-only for audit trail -- Use read replicas for analytics and reporting queries - -### Security and compliance -- Payment method tokens stored, not raw card data -- Customer PII encrypted at rest -- Access logged for compliance audits -- GDPR right-to-deletion supported via customer_id purge -- Financial data retention: 7 years for tax/regulatory requirements - -### Requesting access -To request access to Subscriptions DB: - -1. **Development access** (read-only): - - Submit ticket via [ServiceNow](https://company.service-now.com) - - Select "Database Access" → "Subscriptions DB (Dev)" - - Approval from subscriptions team lead required - - Access granted within 24 hours - -2. **Analytics access** (read-replica): - - Request via #subscriptions-data Slack channel - - Use read-replica endpoint for reporting queries - - No approval needed for read-only analytics access - -3. **Production write access**: - - Restricted to SubscriptionService and BillingService only - - Manual production writes require incident ticket + approval - - Emergency access via on-call: PagerDuty #subscriptions-oncall - -**Contact**: -- Slack: #subscriptions-team -- Email: subscriptions-team@company.com -- Team lead: subscriptions-management team - -### Monitoring and alerts -- Failed billing cycle alerts (> 5% failure rate) -- Subscription churn rate monitoring (weekly) -- Database replication lag (alert at > 10 seconds) -- Past-due subscription count (daily alert if > threshold) - -### Backup and disaster recovery -- Automated daily snapshots with 35-day retention -- Point-in-time recovery with 5-minute RPO -- Cross-region backup replication -- Monthly restore drills - -### Performance characteristics -- **Read latency**: p99 < 50ms -- **Write latency**: p99 < 100ms -- **Daily billing batch**: processes 50,000+ subscriptions in < 1 hour -- **Connection pool**: 20-100 connections depending on load - -### Local development -- Connection string: `SUBSCRIPTIONS_DB_URL` environment variable -- Docker setup: `docker-compose up subscriptions-db` -- Seed data: `npm run seed:subscriptions` -- Test data includes trial, active, and cancelled subscriptions - -### Common issues and troubleshooting -- **Duplicate billing cycles**: Check idempotency in billing cycle creation -- **Missed renewals**: Verify cron job schedule and timezone handling -- **Past-due stuck subscriptions**: Run retry job, check payment method validity -- **Timezone issues**: All dates stored in UTC, convert for display only - -For more information, see SubscriptionService documentation and Billing Runbooks. \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/events/UserSubscriptionCancelled/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/events/UserSubscriptionCancelled/index.mdx deleted file mode 100644 index 8c976ebe2..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/events/UserSubscriptionCancelled/index.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -id: UserSubscriptionCancelled -name: User subscription cancelled -version: 0.0.1 -summary: | - An event that is triggered when a users subscription has been cancelled -owners: - - subscriptions-management -badges: - - content: New! - backgroundColor: green - textColor: green ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `UserSubscriptionCancelled` event is triggered when a users subscription has been cancelled. - -## Architecture diagram - - - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/events/UserSubscriptionStarted/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/events/UserSubscriptionStarted/index.mdx deleted file mode 100644 index ba1b28edb..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/events/UserSubscriptionStarted/index.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -id: UserSubscriptionStarted -name: User subscription started -version: 0.0.1 -summary: | - An event that is triggered when a new user subscription has started -owners: - - subscriptions-management -badges: - - content: New! - backgroundColor: green - textColor: green ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `UserSubscriptionStarted` event is triggered when a user starts a new subscription with our service. - -## Architecture diagram - - - -
    \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/index.mdx deleted file mode 100644 index af199ddaf..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/index.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -id: SubscriptionService -version: 0.0.1 -name: Subscription Service -summary: | - Service that handles subscriptions -owners: - - subscriptions-management -receives: - - id: PaymentProcessed - version: 0.0.1 - from: - - id: 'payments.{env}.events' - parameters: - env: staging -sends: - - id: UserSubscriptionStarted - version: 0.0.1 - to: - - id: 'subscription-domain-eventbus' - - id: UserSubscriptionCancelled - version: 0.0.1 - to: - - id: 'subscription-domain-eventbus' -writesTo: - - id: subscriptions-db - version: 0.0.1 -readsFrom: - - id: subscriptions-db - version: 0.0.1 - - id: payments-db - version: 0.0.1 -repository: - language: JavaScript - url: 'https://github.com/event-catalog/pretend-subscription-service' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The subscription Service is responsible for handling customer subscriptions in our system. It handles new subscriptions, cancelling subscriptions and updating them. - - - - - - - - -### Core features - -| Feature | Description | -|---------|-------------| -| Subscription Management | Handles subscription creation, updates, and status tracking | -| Payment Processing | Integrates with payment gateways to handle payment transactions | -| Notification Integration | Sends notifications to users and other services | -| Multi-Channel Fulfillment | Supports multiple fulfillment channels (e.g., shipping, in-store pickup) | - -## Architecture diagram - - - - - -## Infrastructure - -The Subscription Service is hosted on AWS. - -The diagram below shows the infrastructure of the Subscription Service. The service is hosted on AWS and uses AWS Lambda to handle the subscription requests. The subscription is stored in an AWS Aurora database and the subscription metadata is stored in an AWS S3 bucket. - -```mermaid -architecture-beta - group api(logos:aws) - - service db(logos:aws-aurora)[Subscription DB] in api - service disk1(logos:aws-s3)[Subscription Metadata] in api - service server(logos:aws-lambda)[Subscription Handler] in api - - db:L -- R:server - disk1:T -- B:server -``` - -You can find more information about the Subscription Service infrastructure in the [Subscription Service documentation](https://github.com/event-catalog/pretend-subscription-service/blob/main/README.md). - -
    diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/index.mdx deleted file mode 100644 index b6c969c18..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/index.mdx +++ /dev/null @@ -1,28 +0,0 @@ ---- -id: GetSubscriptionStatus -name: Get subscription status -version: 0.0.2 -summary: | - GET request that will return the current subscription status for a specific user, identified by their userId. -owners: - - subscriptions-management -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json -sidebar: - badge: 'GET' ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `GetSubscriptionStatus` message is a query used to retrieve the current subscription status for a specific user, identified by their `userId`. This query returns detailed information about the user's subscription, such as its current status (active, canceled, expired), the subscription tier or plan, and the next billing date. It is typically used by systems that manage user subscriptions, billing, and renewal processes to ensure that users are aware of their subscription details and any upcoming renewals. - -This query is particularly useful in managing subscriptions for SaaS products, media services, or any recurring payment-based services where users need to manage and view their subscription information. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/schema.json b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/schema.json deleted file mode 100644 index eab69d297..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/schema.json +++ /dev/null @@ -1,46 +0,0 @@ - -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetSubscriptionStatusResponse", - "type": "object", - "properties": { - "userId": { - "type": "string", - "description": "The unique identifier for the user." - }, - "subscriptionStatus": { - "type": "string", - "enum": ["active", "canceled", "expired", "pending"], - "description": "The current status of the user's subscription." - }, - "subscriptionPlan": { - "type": "string", - "description": "The name or tier of the subscription plan." - }, - "nextBillingDate": { - "type": "string", - "format": "date-time", - "description": "The date and time of the next billing or renewal." - }, - "billingFrequency": { - "type": "string", - "enum": ["monthly", "yearly"], - "description": "The frequency of the billing cycle." - }, - "amount": { - "type": "number", - "description": "The amount to be billed for the subscription." - }, - "currency": { - "type": "string", - "description": "The currency in which the subscription is billed (e.g., USD, EUR)." - }, - "lastPaymentDate": { - "type": "string", - "format": "date-time", - "description": "The date and time when the last payment was processed." - } - }, - "required": ["userId", "subscriptionStatus", "subscriptionPlan", "nextBillingDate", "billingFrequency", "amount", "currency", "lastPaymentDate"], - "additionalProperties": false -} diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/versioned/0.0.1/index.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/versioned/0.0.1/index.mdx deleted file mode 100644 index b46ab0e33..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/versioned/0.0.1/index.mdx +++ /dev/null @@ -1,26 +0,0 @@ ---- -id: GetSubscriptionStatus -name: Get subscription status -version: 0.0.1 -summary: | - GET request that will return the current subscription status for a specific user, identified by their userId. -owners: - - dboyne -badges: - - content: Recently updated! - backgroundColor: green - textColor: green -schemaPath: schema.json ---- - -import Footer from '@catalog/components/footer.astro'; - -## Overview - -The `GetSubscriptionStatus` message is a query used to retrieve the current subscription status for a specific user, identified by their `userId`. This query returns detailed information about the user's subscription, such as its current status (active, canceled, expired), the subscription tier or plan, and the next billing date. It is typically used by systems that manage user subscriptions, billing, and renewal processes to ensure that users are aware of their subscription details and any upcoming renewals. - -This query is particularly useful in managing subscriptions for SaaS products, media services, or any recurring payment-based services where users need to manage and view their subscription information. - - - - \ No newline at end of file diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/versioned/0.0.1/schema.json b/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/versioned/0.0.1/schema.json deleted file mode 100644 index eab69d297..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/services/SubscriptionService/queries/GetSubscriptionStatus/versioned/0.0.1/schema.json +++ /dev/null @@ -1,46 +0,0 @@ - -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GetSubscriptionStatusResponse", - "type": "object", - "properties": { - "userId": { - "type": "string", - "description": "The unique identifier for the user." - }, - "subscriptionStatus": { - "type": "string", - "enum": ["active", "canceled", "expired", "pending"], - "description": "The current status of the user's subscription." - }, - "subscriptionPlan": { - "type": "string", - "description": "The name or tier of the subscription plan." - }, - "nextBillingDate": { - "type": "string", - "format": "date-time", - "description": "The date and time of the next billing or renewal." - }, - "billingFrequency": { - "type": "string", - "enum": ["monthly", "yearly"], - "description": "The frequency of the billing cycle." - }, - "amount": { - "type": "number", - "description": "The amount to be billed for the subscription." - }, - "currency": { - "type": "string", - "description": "The currency in which the subscription is billed (e.g., USD, EUR)." - }, - "lastPaymentDate": { - "type": "string", - "format": "date-time", - "description": "The date and time when the last payment was processed." - } - }, - "required": ["userId", "subscriptionStatus", "subscriptionPlan", "nextBillingDate", "billingFrequency", "amount", "currency", "lastPaymentDate"], - "additionalProperties": false -} diff --git a/examples/default/domains/E-Commerce/subdomains/Subscriptions/ubiquitous-language.mdx b/examples/default/domains/E-Commerce/subdomains/Subscriptions/ubiquitous-language.mdx deleted file mode 100644 index b594f4dbe..000000000 --- a/examples/default/domains/E-Commerce/subdomains/Subscriptions/ubiquitous-language.mdx +++ /dev/null @@ -1,129 +0,0 @@ ---- -dictionary: - - id: Subscription - name: Subscription - summary: "A recurring agreement where a customer pays for access to a product or service at regular intervals." - description: | - A subscription represents an ongoing relationship between customer and provider. Key aspects include: - - - Subscription plan details - - Billing frequency - - Access rights and limitations - - Start and end dates - - Auto-renewal settings - - Subscriptions provide predictable revenue for businesses and convenient access for customers. - icon: Repeat - - id: Billing Cycle - name: Billing Cycle - summary: "The recurring period for which a subscription is billed." - description: | - Billing cycles define when payments are collected and services are provided. They include: - - - Cycle duration (monthly, annually, etc.) - - Billing date - - Payment due dates - - Pro-ration rules - - Grace periods - - Clear billing cycles help manage customer expectations and cash flow. - icon: Calendar - - id: Plan - name: Plan - summary: "A specific subscription offering with defined features, pricing, and terms." - description: | - Subscription plans define the service levels available to customers. They specify: - - - Feature sets and limitations - - Pricing structure - - Billing frequency options - - Usage allowances - - Additional benefits - - Well-designed plans cater to different customer segments and needs. - icon: Layout - - id: Trial Period - name: Trial Period - summary: "A promotional period allowing customers to test a subscription service before committing." - description: | - Trial periods help customers evaluate the service risk-free. They include: - - - Duration of trial - - Available features - - Conversion process - - Payment information requirements - - Trial end notifications - - Effective trials can increase conversion rates to paid subscriptions. - icon: Clock - - id: Auto-Renewal - name: Auto-Renewal - summary: "Automatic continuation of a subscription at the end of each billing period." - description: | - Auto-renewal ensures service continuity for customers. The process includes: - - - Renewal notifications - - Payment processing - - Service extension - - Failed payment handling - - Cancellation options - - Auto-renewal reduces churn and maintains steady revenue streams. - icon: RefreshCw - - id: Upgrade - name: Upgrade - summary: "The process of moving to a higher-tier subscription plan." - description: | - Upgrades allow customers to access more features or capacity. This involves: - - - Plan comparison - - Pro-rated billing adjustments - - Feature activation - - Data migration if needed - - Service level changes - - Smooth upgrade processes encourage customer growth. - icon: ArrowUpCircle - - id: Downgrade - name: Downgrade - summary: "The process of moving to a lower-tier subscription plan." - description: | - Downgrades adjust service levels to match customer needs. This includes: - - - Feature reduction - - Billing adjustments - - Data retention policies - - Service level changes - - Customer retention strategies - - Flexible downgrade options can prevent complete cancellations. - icon: ArrowDownCircle - - id: Cancellation - name: Cancellation - summary: "The termination of a subscription service." - description: | - Cancellation processes handle the end of subscription relationships. They cover: - - - Notice periods - - Final billing - - Data retention/export - - Service access termination - - Reactivation options - - Clear cancellation policies protect both customer and provider interests. - icon: XCircle - - id: Usage Limit - name: Usage Limit - summary: "Restrictions on service usage within a subscription plan." - description: | - Usage limits define the boundaries of service consumption. They include: - - - Quantitative restrictions - - Monitoring systems - - Overage handling - - Alert mechanisms - - Upgrade triggers - - Well-defined limits help manage resources and encourage appropriate plan selection. - icon: Gauge ---- diff --git a/examples/default/domains/Fulfilment/entities/inventory-reservation/index.mdx b/examples/default/domains/Fulfilment/entities/inventory-reservation/index.mdx new file mode 100644 index 000000000..90be074fc --- /dev/null +++ b/examples/default/domains/Fulfilment/entities/inventory-reservation/index.mdx @@ -0,0 +1,39 @@ +--- +id: inventory-reservation +name: Inventory Reservation +version: 1.0.0 +identifier: reservationId +aggregateRoot: true +summary: Stock held for a checkout or order before fulfilment. +owners: + - fulfilment-platform +properties: + - name: reservationId + type: UUID + required: true + description: Unique reservation identifier. + - name: orderId + type: UUID + required: false + description: Order associated with the reservation once created. + references: order + relationType: reservedFor + referencesIdentifier: orderId + - name: productId + type: UUID + required: true + description: Product being reserved. + references: product + relationType: reserves + referencesIdentifier: productId + - name: expiresAt + type: datetime + required: true + description: Time the reservation expires if not committed. +--- + +## Overview + +Inventory Reservation is created during checkout by [[command|reserve-inventory]] and released if the saga fails. + + diff --git a/examples/default/domains/Fulfilment/entities/shipment/index.mdx b/examples/default/domains/Fulfilment/entities/shipment/index.mdx new file mode 100644 index 000000000..112fc465e --- /dev/null +++ b/examples/default/domains/Fulfilment/entities/shipment/index.mdx @@ -0,0 +1,36 @@ +--- +id: shipment +name: Shipment +version: 1.0.0 +identifier: shipmentId +aggregateRoot: true +summary: A carrier delivery for a packed customer order. +owners: + - fulfilment-platform +properties: + - name: shipmentId + type: UUID + required: true + description: Unique shipment identifier. + - name: orderId + type: UUID + required: true + description: Order being shipped. + references: order + relationType: ships + referencesIdentifier: orderId + - name: carrierReference + type: string + required: false + description: External carrier tracking reference. + - name: status + type: string + required: true + description: Shipment delivery status. +--- + +## Overview + +Shipment is created by the Shipping System and updated from carrier events such as [[event|shipment-created]] and [[event|shipment-delivered]]. + + diff --git a/examples/default/domains/Fulfilment/entities/stock-item/index.mdx b/examples/default/domains/Fulfilment/entities/stock-item/index.mdx new file mode 100644 index 000000000..5fc776df4 --- /dev/null +++ b/examples/default/domains/Fulfilment/entities/stock-item/index.mdx @@ -0,0 +1,36 @@ +--- +id: stock-item +name: Stock Item +version: 1.0.0 +identifier: stockItemId +aggregateRoot: true +summary: The inventory record for a product variant at a location. +owners: + - fulfilment-platform +properties: + - name: stockItemId + type: UUID + required: true + description: Unique stock item identifier. + - name: productId + type: UUID + required: true + description: Product held in stock. + references: product + relationType: stocks + referencesIdentifier: productId + - name: locationId + type: string + required: true + description: Warehouse or fulfilment location. + - name: availableQuantity + type: integer + required: true + description: Quantity available to reserve. +--- + +## Overview + +Stock Item is the Inventory System's view of available stock for a product at a fulfilment location. + + diff --git a/examples/default/domains/Fulfilment/entities/warehouse-pick/index.mdx b/examples/default/domains/Fulfilment/entities/warehouse-pick/index.mdx new file mode 100644 index 000000000..8629f8403 --- /dev/null +++ b/examples/default/domains/Fulfilment/entities/warehouse-pick/index.mdx @@ -0,0 +1,35 @@ +--- +id: warehouse-pick +name: Warehouse Pick +version: 1.0.0 +identifier: pickId +summary: A warehouse task to pick items for a completed order. +owners: + - fulfilment-platform +properties: + - name: pickId + type: UUID + required: true + description: Unique warehouse pick identifier. + - name: orderId + type: UUID + required: true + description: Order being picked. + references: order + relationType: fulfils + referencesIdentifier: orderId + - name: assignedTo + type: string + required: false + description: Warehouse worker or automation lane assigned. + - name: status + type: string + required: true + description: Pick task status. +--- + +## Overview + +Warehouse Pick represents the operational task handled by the Warehouse System before an order can be packed and shipped. + + diff --git a/examples/default/domains/Fulfilment/index.mdx b/examples/default/domains/Fulfilment/index.mdx new file mode 100644 index 000000000..804091abb --- /dev/null +++ b/examples/default/domains/Fulfilment/index.mdx @@ -0,0 +1,83 @@ +--- +id: fulfilment +name: Fulfilment +version: 1.0.0 +summary: | + The Fulfilment domain gets a confirmed order to the customer. It reserves stock, picks and packs orders in the warehouse, and hands them to carriers for delivery. +owners: + - fulfilment-platform +systems: + - id: inventory-system + version: 1.0.0 + - id: warehouse-system + version: 1.0.0 + - id: shipping-system + version: 1.0.0 + - id: carrier + version: 1.0.0 +entities: + - id: stock-item + version: 1.0.0 + - id: inventory-reservation + version: 1.0.0 + - id: warehouse-pick + version: 1.0.0 + - id: shipment + version: 1.0.0 +badges: + - content: Core domain + backgroundColor: blue + textColor: blue + icon: RectangleGroupIcon + - content: Business Critical + backgroundColor: red + textColor: red + icon: ShieldCheckIcon +--- + +## Overview + +The **Fulfilment** domain is responsible for the physical side of an order — making sure stock is available, picking and packing it, and shipping it to the customer. It owns three internal systems and integrates with external carriers: + + + + + + + + +### How the systems work together + +When an order is completed, the **Warehouse System** picks and packs it, having relied on the **Inventory System** to reserve stock earlier in checkout. Once packed, the **Shipping System** creates a shipment with an external **Carrier**, which delivers it to the customer and reports progress back. + +## System Diagram + +The systems in this domain, how they relate, and the people who interact with them. + + + +## Resource Diagram + +The components that make up this domain. + + diff --git a/examples/default/domains/Fulfilment/systems/carrier/index.mdx b/examples/default/domains/Fulfilment/systems/carrier/index.mdx new file mode 100644 index 000000000..5b67f43b0 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/carrier/index.mdx @@ -0,0 +1,52 @@ +--- +id: carrier +name: Carrier +version: 1.0.0 +scope: external +summary: | + External carrier that delivers shipments to customers. It accepts shipment requests and reports delivery progress back. +owners: + - fulfilment-platform +services: + - id: carrier-shipping-api + - id: carrier-tracking-api +relationships: + - id: shipping-system + label: delivers shipments for +badges: + - content: External + backgroundColor: yellow + textColor: yellow + icon: GlobeAltIcon +--- + +## Overview + +The **Carrier** is the external delivery company Acme Inc uses to ship orders to customers. It is owned and operated by a third party — modelled here as an **external system** so the catalog shows how the [[system|shipping-system]] integrates with it. + +It receives [[command|create-shipment]] and reports progress back via [[event|shipment-created]], [[event|shipment-delivered]] and [[event|shipment-failed]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|carrier-shipping-api]] | Service | Accepts shipment requests and dispatches parcels. | +| [[service\|carrier-tracking-api]] | Service | Reports delivery progress back to Acme Inc. | + +## Messages this system publishes + +- [[event\|shipment-created]] — a shipment has been created and dispatched. +- [[event\|shipment-delivered]] — a shipment was delivered to the customer. +- [[event\|shipment-failed]] — a shipment could not be delivered. diff --git a/examples/default/domains/Fulfilment/systems/carrier/services/CarrierShippingAPI/index.mdx b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierShippingAPI/index.mdx new file mode 100644 index 000000000..569692358 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierShippingAPI/index.mdx @@ -0,0 +1,35 @@ +--- +id: carrier-shipping-api +version: 1.0.0 +name: Shipping API +summary: | + The carrier's API for creating shipments. Acme Inc's Shipping System calls it to dispatch packed orders. +styles: + icon: /icons/languages/nodejs.svg +owners: + - fulfilment-platform +receives: + - id: create-shipment + version: 1.0.0 +repository: + language: External + url: 'https://github.com/acme/carrier-integration' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Shipping API** is the carrier's external interface for creating shipments. The [[system|shipping-system]] sends [[command|create-shipment]], and the carrier dispatches the parcel and begins reporting progress through its [[service|carrier-tracking-api]]. + + + + + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentCreated/index.mdx b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentCreated/index.mdx new file mode 100644 index 000000000..70f90f5de --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentCreated/index.mdx @@ -0,0 +1,29 @@ +--- +id: shipment-created +name: Shipment Created +version: 1.0.0 +summary: | + Published by the carrier when a shipment has been created and dispatched. +owners: + - fulfilment-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ShipmentCreated` is published by the carrier's [[service|carrier-tracking-api]] when a shipment has been created and dispatched. Acme Inc consumes it to give the customer a tracking number and expected delivery date. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentCreated/schema.json b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentCreated/schema.json new file mode 100644 index 000000000..872fd4173 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentCreated/schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ShipmentCreated", + "description": "Published when a shipment has been created and dispatched", + "type": "object", + "properties": { + "shipmentId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "trackingNumber": { "type": "string" }, + "carrier": { "type": "string" }, + "estimatedDelivery": { "type": "string", "format": "date" }, + "createdAt": { "type": "string", "format": "date-time" } + }, + "required": ["shipmentId", "orderId", "trackingNumber", "createdAt"] +} diff --git a/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentDelivered/index.mdx b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentDelivered/index.mdx new file mode 100644 index 000000000..9ff7eb04f --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentDelivered/index.mdx @@ -0,0 +1,29 @@ +--- +id: shipment-delivered +name: Shipment Delivered +version: 1.0.0 +summary: | + Published by the carrier when a shipment has been delivered to the customer. +owners: + - fulfilment-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ShipmentDelivered` is published by the carrier's [[service|carrier-tracking-api]] when a shipment reaches the customer. This is the happy-path end of the fulfilment journey — Acme Inc consumes it to close out the order and prompt the customer for feedback. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentDelivered/schema.json b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentDelivered/schema.json new file mode 100644 index 000000000..d8966a5d4 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentDelivered/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ShipmentDelivered", + "description": "Published when a shipment has been delivered to the customer", + "type": "object", + "properties": { + "shipmentId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "deliveredAt": { "type": "string", "format": "date-time" }, + "signedBy": { "type": "string", "description": "Name of the person who accepted delivery, if captured" } + }, + "required": ["shipmentId", "orderId", "deliveredAt"] +} diff --git a/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentFailed/index.mdx b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentFailed/index.mdx new file mode 100644 index 000000000..a008409c5 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentFailed/index.mdx @@ -0,0 +1,29 @@ +--- +id: shipment-failed +name: Shipment Failed +version: 1.0.0 +summary: | + Published by the carrier when a shipment could not be delivered. +owners: + - fulfilment-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ShipmentFailed` is published by the carrier's [[service|carrier-tracking-api]] when a shipment cannot be delivered — for example a failed delivery attempt or a lost parcel. Acme Inc consumes it to trigger a redelivery, refund, or customer follow-up. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentFailed/schema.json b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentFailed/schema.json new file mode 100644 index 000000000..56691ec17 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/events/ShipmentFailed/schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ShipmentFailed", + "description": "Published when a shipment could not be delivered", + "type": "object", + "properties": { + "shipmentId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "reason": { + "type": "string", + "enum": ["FAILED_DELIVERY", "LOST", "DAMAGED", "RETURNED_TO_SENDER"] + }, + "failedAt": { "type": "string", "format": "date-time" } + }, + "required": ["shipmentId", "orderId", "reason", "failedAt"] +} diff --git a/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/index.mdx b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/index.mdx new file mode 100644 index 000000000..727f92185 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/carrier/services/CarrierTrackingAPI/index.mdx @@ -0,0 +1,39 @@ +--- +id: carrier-tracking-api +version: 1.0.0 +name: Tracking API +summary: | + The carrier's API for reporting delivery progress. It notifies Acme Inc when a shipment is created, delivered, or fails. +styles: + icon: /icons/languages/nodejs.svg +owners: + - fulfilment-platform +sends: + - id: shipment-created + version: 1.0.0 + - id: shipment-delivered + version: 1.0.0 + - id: shipment-failed + version: 1.0.0 +repository: + language: External + url: 'https://github.com/acme/carrier-integration' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Tracking API** is how the carrier reports delivery progress back to Acme Inc. After a shipment is created via the [[service|carrier-shipping-api]], it emits [[event|shipment-created]], then [[event|shipment-delivered]] on success or [[event|shipment-failed]] if delivery cannot be completed. + + + + + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/containers/inventory-database/index.mdx b/examples/default/domains/Fulfilment/systems/inventory-system/containers/inventory-database/index.mdx new file mode 100644 index 000000000..bd2a00b8b --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/containers/inventory-database/index.mdx @@ -0,0 +1,35 @@ +--- +id: inventory-database +name: Inventory Database +version: 1.0.0 +summary: PostgreSQL database that is the system of record for stock levels and reservations. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for stock levels and reservations +classification: internal +retention: 3y +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **Inventory Database** is the authoritative store for stock levels at Acme Inc. The [[service|inventory-service]] reads from and writes to it as stock is reserved and released. + +### What does it store? + +- **Stock** — one row per product: available quantity and reserved quantity. +- **Reservations** — one row per reservation: id, order, items and status. + +### Schema + + + + + + diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/containers/inventory-database/schema.sql b/examples/default/domains/Fulfilment/systems/inventory-system/containers/inventory-database/schema.sql new file mode 100644 index 000000000..ff2a97fbb --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/containers/inventory-database/schema.sql @@ -0,0 +1,26 @@ +-- Inventory Database — system of record for stock levels and reservations + +CREATE TABLE stock ( + product_id UUID PRIMARY KEY, + available INTEGER NOT NULL DEFAULT 0 CHECK (available >= 0), + reserved INTEGER NOT NULL DEFAULT 0 CHECK (reserved >= 0), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE reservations ( + reservation_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + order_id UUID, + cart_id UUID, + status TEXT NOT NULL DEFAULT 'HELD' + CHECK (status IN ('HELD', 'RELEASED', 'CONSUMED')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE reservation_items ( + reservation_id UUID NOT NULL REFERENCES reservations (reservation_id) ON DELETE CASCADE, + product_id UUID NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + PRIMARY KEY (reservation_id, product_id) +); + +CREATE INDEX idx_reservations_order ON reservations (order_id); diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/index.mdx b/examples/default/domains/Fulfilment/systems/inventory-system/index.mdx new file mode 100644 index 000000000..3419079bc --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/index.mdx @@ -0,0 +1,49 @@ +--- +id: inventory-system +name: Inventory System +version: 1.0.0 +summary: | + Internal system that tracks stock and reserves it for orders. It is the source of truth for available inventory and confirms whether stock can be reserved. +owners: + - fulfilment-platform +services: + - id: inventory-service +containers: + - id: inventory-database +relationships: + - id: warehouse-system + label: provides stock for +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Inventory System** owns stock levels at Acme Inc. During checkout it receives [[command|reserve-inventory]] and confirms whether stock is available — replying with [[event|inventory-reserved]] or [[event|inventory-unavailable]]. It also handles [[command|release-inventory]] (when a reservation is no longer needed) and serves [[query|get-stock-level]] lookups from the [[container|inventory-database]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|inventory-service]] | Service | Reserves and releases stock, serves stock-level queries, and publishes inventory events. | +| [[container\|inventory-database]] | Data store | PostgreSQL system of record for stock levels and reservations. | + +## Messages this system publishes + +- [[event\|inventory-reserved]] — stock was successfully reserved for an order. +- [[event\|inventory-unavailable]] — stock could not be reserved. diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/commands/ReleaseInventory/index.mdx b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/commands/ReleaseInventory/index.mdx new file mode 100644 index 000000000..38112a099 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/commands/ReleaseInventory/index.mdx @@ -0,0 +1,32 @@ +--- +id: release-inventory +name: Release Inventory +version: 1.0.0 +summary: | + Command to release a previously held inventory reservation. +owners: + - fulfilment-platform +schemaPath: schema.json +operation: + method: POST + path: /reservations/{reservationId}/release + statusCodes: ['200', '404'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ReleaseInventory` is handled by the [[service|inventory-service]]. It releases a reservation that is no longer needed — for example when checkout fails after stock was reserved, or an order is cancelled. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/commands/ReleaseInventory/schema.json b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/commands/ReleaseInventory/schema.json new file mode 100644 index 000000000..6cdf190e5 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/commands/ReleaseInventory/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReleaseInventory", + "description": "Command to release a previously held inventory reservation", + "type": "object", + "properties": { + "reservationId": { "type": "string", "format": "uuid" }, + "reason": { + "type": "string", + "enum": ["CHECKOUT_FAILED", "ORDER_CANCELLED", "EXPIRED"] + } + }, + "required": ["reservationId"] +} diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryReserved/index.mdx b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryReserved/index.mdx new file mode 100644 index 000000000..eb43f0380 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryReserved/index.mdx @@ -0,0 +1,29 @@ +--- +id: inventory-reserved +name: Inventory Reserved +version: 1.0.0 +summary: | + Published when stock has been successfully reserved for an order. +owners: + - fulfilment-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`InventoryReserved` is published by the [[service|inventory-service]] when stock for an order has been successfully reserved. The Ordering domain's checkout saga relies on this to continue placing the order. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryReserved/schema.json b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryReserved/schema.json new file mode 100644 index 000000000..c659aa8ad --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryReserved/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InventoryReserved", + "description": "Published when stock has been successfully reserved for an order", + "type": "object", + "properties": { + "reservationId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "cartId": { "type": "string", "format": "uuid" }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "productId": { "type": "string", "format": "uuid" }, + "quantity": { "type": "integer", "minimum": 1 } + }, + "required": ["productId", "quantity"] + } + }, + "reservedAt": { "type": "string", "format": "date-time" } + }, + "required": ["reservationId", "items", "reservedAt"] +} diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryUnavailable/index.mdx b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryUnavailable/index.mdx new file mode 100644 index 000000000..a9adad0c0 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryUnavailable/index.mdx @@ -0,0 +1,29 @@ +--- +id: inventory-unavailable +name: Inventory Unavailable +version: 1.0.0 +summary: | + Published when stock could not be reserved for an order. +owners: + - fulfilment-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`InventoryUnavailable` is published by the [[service|inventory-service]] when stock for an order cannot be reserved. The Ordering domain's checkout saga uses it to fail checkout and cancel the order. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryUnavailable/schema.json b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryUnavailable/schema.json new file mode 100644 index 000000000..f57559daf --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/events/InventoryUnavailable/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InventoryUnavailable", + "description": "Published when stock could not be reserved for an order", + "type": "object", + "properties": { + "orderId": { "type": "string", "format": "uuid" }, + "cartId": { "type": "string", "format": "uuid" }, + "unavailableItems": { + "type": "array", + "items": { + "type": "object", + "properties": { + "productId": { "type": "string", "format": "uuid" }, + "requested": { "type": "integer", "minimum": 1 }, + "available": { "type": "integer", "minimum": 0 } + }, + "required": ["productId", "requested", "available"] + } + }, + "checkedAt": { "type": "string", "format": "date-time" } + }, + "required": ["unavailableItems", "checkedAt"] +} diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/index.mdx b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/index.mdx new file mode 100644 index 000000000..7a3a43ff1 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/index.mdx @@ -0,0 +1,57 @@ +--- +id: inventory-service +version: 1.0.0 +name: Inventory Service +summary: | + The system of record for stock. Reserves and releases inventory, serves stock-level queries, and publishes events when stock is reserved or unavailable. +styles: + icon: /icons/languages/java.svg +owners: + - fulfilment-platform +receives: + - id: reserve-inventory + version: 1.0.0 + - id: release-inventory + version: 1.0.0 + - id: get-stock-level + version: 1.0.0 +sends: + - id: inventory-reserved + version: 1.0.0 + - id: inventory-unavailable + version: 1.0.0 +writesTo: + - id: inventory-database +readsFrom: + - id: inventory-database +repository: + language: Java + url: 'https://github.com/acme/inventory-service' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Inventory Service** is the heart of the Inventory System. It receives [[command|reserve-inventory]] during checkout and replies with [[event|inventory-reserved]] or [[event|inventory-unavailable]]. It also handles [[command|release-inventory]] and serves [[query|get-stock-level]] reads, all backed by the [[container|inventory-database]]. + + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Reservations | Handles [[command\|reserve-inventory]] and [[command\|release-inventory]]. | +| Lookups | Serves [[query\|get-stock-level]] from the [[container\|inventory-database]]. | +| Event publishing | Emits [[event\|inventory-reserved]] and [[event\|inventory-unavailable]]. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/queries/GetStockLevel/index.mdx b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/queries/GetStockLevel/index.mdx new file mode 100644 index 000000000..67c740a7a --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/queries/GetStockLevel/index.mdx @@ -0,0 +1,32 @@ +--- +id: get-stock-level +name: Get Stock Level +version: 1.0.0 +summary: | + Query to fetch the current available stock level for a product. +owners: + - fulfilment-platform +schemaPath: schema.json +operation: + method: GET + path: /stock/{productId} + statusCodes: ['200', '404'] +sidebar: + badge: 'GET' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`GetStockLevel` is handled by the [[service|inventory-service]]. It returns the current available stock for a product, read directly from the [[container|inventory-database]]. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/queries/GetStockLevel/schema.json b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/queries/GetStockLevel/schema.json new file mode 100644 index 000000000..3e947c58e --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/inventory-system/services/InventoryService/queries/GetStockLevel/schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetStockLevel", + "description": "Query to fetch the current available stock level for a product, and the level returned", + "type": "object", + "properties": { + "request": { + "type": "object", + "properties": { + "productId": { "type": "string", "format": "uuid" } + }, + "required": ["productId"] + }, + "response": { + "type": "object", + "properties": { + "productId": { "type": "string", "format": "uuid" }, + "available": { "type": "integer", "minimum": 0 }, + "reserved": { "type": "integer", "minimum": 0 } + }, + "required": ["productId", "available"] + } + }, + "required": ["request", "response"] +} diff --git a/examples/default/domains/Fulfilment/systems/shipping-system/index.mdx b/examples/default/domains/Fulfilment/systems/shipping-system/index.mdx new file mode 100644 index 000000000..9e37f5fda --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/shipping-system/index.mdx @@ -0,0 +1,47 @@ +--- +id: shipping-system +name: Shipping System +version: 1.0.0 +summary: | + Internal system that hands packed orders to carriers. It reacts to ready-for-shipping orders and creates shipments with an external carrier. +owners: + - fulfilment-platform +services: + - id: shipping-api + - id: carrier-adapter +relationships: + - id: carrier + label: creates shipments with +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Shipping System** connects Acme Inc to its carriers. When the [[system|warehouse-system]] publishes [[event|order-ready-for-shipping]], the [[service|shipping-api]] selects a carrier and the [[service|carrier-adapter]] sends a [[command|create-shipment]] to the external [[system|carrier]] to dispatch the parcel. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|shipping-api]] | Service | Receives ready-for-shipping orders and selects a carrier. | +| [[service\|carrier-adapter]] | Service | Translates and sends shipment requests to the external carrier. | + +## Messages this system sends + +- [[command\|create-shipment]] — create a shipment with the carrier. diff --git a/examples/default/domains/Fulfilment/systems/shipping-system/services/CarrierAdapter/commands/CreateShipment/index.mdx b/examples/default/domains/Fulfilment/systems/shipping-system/services/CarrierAdapter/commands/CreateShipment/index.mdx new file mode 100644 index 000000000..ec50cdffd --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/shipping-system/services/CarrierAdapter/commands/CreateShipment/index.mdx @@ -0,0 +1,32 @@ +--- +id: create-shipment +name: Create Shipment +version: 1.0.0 +summary: | + Command to create a shipment with a carrier for a packed order. +owners: + - fulfilment-platform +schemaPath: schema.json +operation: + method: POST + path: /shipments + statusCodes: ['201', '400', '422'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CreateShipment` is sent by the [[service|carrier-adapter]] to the external [[system|carrier]] to dispatch a packed order. The carrier responds asynchronously with [[event|shipment-created]] and later [[event|shipment-delivered]] or [[event|shipment-failed]]. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/shipping-system/services/CarrierAdapter/commands/CreateShipment/schema.json b/examples/default/domains/Fulfilment/systems/shipping-system/services/CarrierAdapter/commands/CreateShipment/schema.json new file mode 100644 index 000000000..7e111d8dd --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/shipping-system/services/CarrierAdapter/commands/CreateShipment/schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CreateShipment", + "description": "Command to create a shipment with a carrier for a packed order", + "type": "object", + "properties": { + "orderId": { "type": "string", "format": "uuid" }, + "carrier": { "type": "string", "description": "The carrier to ship with" }, + "service": { + "type": "string", + "enum": ["STANDARD", "EXPRESS", "NEXT_DAY"] + }, + "parcelCount": { "type": "integer", "minimum": 1 }, + "destination": { + "type": "object", + "properties": { + "line1": { "type": "string" }, + "city": { "type": "string" }, + "postcode": { "type": "string" }, + "country": { "type": "string", "pattern": "^[A-Z]{2}$" } + }, + "required": ["line1", "city", "postcode", "country"] + } + }, + "required": ["orderId", "destination"] +} diff --git a/examples/default/domains/Fulfilment/systems/shipping-system/services/CarrierAdapter/index.mdx b/examples/default/domains/Fulfilment/systems/shipping-system/services/CarrierAdapter/index.mdx new file mode 100644 index 000000000..1d39e421c --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/shipping-system/services/CarrierAdapter/index.mdx @@ -0,0 +1,35 @@ +--- +id: carrier-adapter +version: 1.0.0 +name: Carrier Adapter +summary: | + Translates Acme Inc shipment requests into the external carrier's format and sends them to create a shipment. +styles: + icon: /icons/languages/go.svg +owners: + - fulfilment-platform +sends: + - id: create-shipment + version: 1.0.0 +repository: + language: Go + url: 'https://github.com/acme/carrier-adapter' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Carrier Adapter** is the anti-corruption layer between Acme Inc and its carriers. It takes a shipment request from the [[service|shipping-api]], maps it to the external [[system|carrier]]'s API, and sends a [[command|create-shipment]] to dispatch the parcel. + + + + + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/shipping-system/services/ShippingAPI/index.mdx b/examples/default/domains/Fulfilment/systems/shipping-system/services/ShippingAPI/index.mdx new file mode 100644 index 000000000..cfb8f2f9d --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/shipping-system/services/ShippingAPI/index.mdx @@ -0,0 +1,35 @@ +--- +id: shipping-api +version: 1.0.0 +name: Shipping API +summary: | + Receives ready-for-shipping orders, selects a carrier, and kicks off shipment creation. +styles: + icon: /icons/languages/nodejs.svg +owners: + - fulfilment-platform +receives: + - id: order-ready-for-shipping + version: 1.0.0 +repository: + language: TypeScript + url: 'https://github.com/acme/shipping-api' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Shipping API** is the entry point to the Shipping System. It consumes [[event|order-ready-for-shipping]] from the [[system|warehouse-system]], selects an appropriate carrier, and hands off to the [[service|carrier-adapter]] to create the shipment. + + + + + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/warehouse-system/containers/warehouse-database/index.mdx b/examples/default/domains/Fulfilment/systems/warehouse-system/containers/warehouse-database/index.mdx new file mode 100644 index 000000000..8386572f5 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/warehouse-system/containers/warehouse-database/index.mdx @@ -0,0 +1,35 @@ +--- +id: warehouse-database +name: Warehouse Database +version: 1.0.0 +summary: PostgreSQL database that stores picking jobs and their status. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for picking and packing jobs +classification: internal +retention: 1y +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **Warehouse Database** stores the picking and packing jobs for the warehouse. The [[service|warehouse-service]] creates jobs and the [[service|picking-worker]] updates them as orders are picked. + +### What does it store? + +- **Picking Jobs** — one row per order being fulfilled: order, status and timestamps. +- **Picking Job Items** — the items to pick for each job. + +### Schema + + + + + + diff --git a/examples/default/domains/Fulfilment/systems/warehouse-system/containers/warehouse-database/schema.sql b/examples/default/domains/Fulfilment/systems/warehouse-system/containers/warehouse-database/schema.sql new file mode 100644 index 000000000..742b44d30 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/warehouse-system/containers/warehouse-database/schema.sql @@ -0,0 +1,21 @@ +-- Warehouse Database — picking and packing jobs + +CREATE TABLE picking_jobs ( + job_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + order_id UUID NOT NULL, + status TEXT NOT NULL DEFAULT 'CREATED' + CHECK (status IN ('CREATED', 'PICKING', 'PACKED')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE picking_job_items ( + job_id UUID NOT NULL REFERENCES picking_jobs (job_id) ON DELETE CASCADE, + product_id UUID NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + picked BOOLEAN NOT NULL DEFAULT false, + PRIMARY KEY (job_id, product_id) +); + +CREATE INDEX idx_picking_jobs_order ON picking_jobs (order_id); +CREATE INDEX idx_picking_jobs_status ON picking_jobs (status); diff --git a/examples/default/domains/Fulfilment/systems/warehouse-system/index.mdx b/examples/default/domains/Fulfilment/systems/warehouse-system/index.mdx new file mode 100644 index 000000000..d99b11b04 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/warehouse-system/index.mdx @@ -0,0 +1,53 @@ +--- +id: warehouse-system +name: Warehouse System +version: 1.0.0 +summary: | + Internal system that picks and packs orders in the warehouse. It reacts to completed orders, organises picking, and signals when an order is packed and ready to ship. +owners: + - fulfilment-platform +services: + - id: warehouse-service + - id: picking-worker +containers: + - id: warehouse-database +relationships: + - id: shipping-system + label: hands packed orders to + - id: inventory-system + label: consumes reserved stock from +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Warehouse System** turns a completed order into a packed parcel. When the Ordering domain publishes [[event|order-completed]], the [[service|warehouse-service]] creates a picking job, the [[service|picking-worker]] works through it, and the system publishes [[event|order-packed]] and then [[event|order-ready-for-shipping]] for the [[system|shipping-system]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|warehouse-service]] | Service | Receives completed orders and orchestrates picking and packing. | +| [[service\|picking-worker]] | Service | Works through picking jobs on the warehouse floor. | +| [[container\|warehouse-database]] | Data store | PostgreSQL store of picking jobs and their status. | + +## Messages this system publishes + +- [[event\|order-packed]] — an order has been picked and packed. +- [[event\|order-ready-for-shipping]] — a packed order is ready to be shipped. diff --git a/examples/default/domains/Fulfilment/systems/warehouse-system/services/PickingWorker/events/OrderPacked/index.mdx b/examples/default/domains/Fulfilment/systems/warehouse-system/services/PickingWorker/events/OrderPacked/index.mdx new file mode 100644 index 000000000..5036db236 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/warehouse-system/services/PickingWorker/events/OrderPacked/index.mdx @@ -0,0 +1,29 @@ +--- +id: order-packed +name: Order Packed +version: 1.0.0 +summary: | + Published when an order has been picked and packed in the warehouse. +owners: + - fulfilment-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`OrderPacked` is published by the [[service|picking-worker]] when an order has been fully picked and packed. The [[service|warehouse-service]] consumes it to mark the order ready and publish [[event|order-ready-for-shipping]]. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/warehouse-system/services/PickingWorker/events/OrderPacked/schema.json b/examples/default/domains/Fulfilment/systems/warehouse-system/services/PickingWorker/events/OrderPacked/schema.json new file mode 100644 index 000000000..f588ac7f7 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/warehouse-system/services/PickingWorker/events/OrderPacked/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OrderPacked", + "description": "Published when an order has been picked and packed in the warehouse", + "type": "object", + "properties": { + "orderId": { "type": "string", "format": "uuid" }, + "pickingJobId": { "type": "string", "format": "uuid" }, + "parcelCount": { "type": "integer", "minimum": 1 }, + "packedAt": { "type": "string", "format": "date-time" } + }, + "required": ["orderId", "packedAt"] +} diff --git a/examples/default/domains/Fulfilment/systems/warehouse-system/services/PickingWorker/index.mdx b/examples/default/domains/Fulfilment/systems/warehouse-system/services/PickingWorker/index.mdx new file mode 100644 index 000000000..c6033c8be --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/warehouse-system/services/PickingWorker/index.mdx @@ -0,0 +1,39 @@ +--- +id: picking-worker +version: 1.0.0 +name: Picking Worker +summary: | + Works through picking jobs on the warehouse floor and signals when an order has been picked and packed. +styles: + icon: /icons/languages/go.svg +owners: + - fulfilment-platform +sends: + - id: order-packed + version: 1.0.0 +readsFrom: + - id: warehouse-database +writesTo: + - id: warehouse-database +repository: + language: Go + url: 'https://github.com/acme/picking-worker' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Picking Worker** processes picking jobs created by the [[service|warehouse-service]]. As warehouse staff pick and pack each order, the worker updates the job in the [[container|warehouse-database]] and publishes [[event|order-packed]] when the order is complete. + + + + + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/warehouse-system/services/WarehouseService/events/OrderReadyForShipping/index.mdx b/examples/default/domains/Fulfilment/systems/warehouse-system/services/WarehouseService/events/OrderReadyForShipping/index.mdx new file mode 100644 index 000000000..58a86231c --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/warehouse-system/services/WarehouseService/events/OrderReadyForShipping/index.mdx @@ -0,0 +1,29 @@ +--- +id: order-ready-for-shipping +name: Order Ready For Shipping +version: 1.0.0 +summary: | + Published when a packed order is ready to be handed to a carrier for shipping. +owners: + - fulfilment-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`OrderReadyForShipping` is published by the [[service|warehouse-service]] once an order has been packed. The [[system|shipping-system]] consumes it to create a shipment with a carrier. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Fulfilment/systems/warehouse-system/services/WarehouseService/events/OrderReadyForShipping/schema.json b/examples/default/domains/Fulfilment/systems/warehouse-system/services/WarehouseService/events/OrderReadyForShipping/schema.json new file mode 100644 index 000000000..f903e40aa --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/warehouse-system/services/WarehouseService/events/OrderReadyForShipping/schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OrderReadyForShipping", + "description": "Published when a packed order is ready to be handed to a carrier", + "type": "object", + "properties": { + "orderId": { "type": "string", "format": "uuid" }, + "customerId": { "type": "string", "format": "uuid" }, + "parcelCount": { "type": "integer", "minimum": 1 }, + "shippingAddress": { + "type": "object", + "properties": { + "line1": { "type": "string" }, + "city": { "type": "string" }, + "postcode": { "type": "string" }, + "country": { "type": "string", "pattern": "^[A-Z]{2}$" } + }, + "required": ["line1", "city", "postcode", "country"] + }, + "readyAt": { "type": "string", "format": "date-time" } + }, + "required": ["orderId", "shippingAddress", "readyAt"] +} diff --git a/examples/default/domains/Fulfilment/systems/warehouse-system/services/WarehouseService/index.mdx b/examples/default/domains/Fulfilment/systems/warehouse-system/services/WarehouseService/index.mdx new file mode 100644 index 000000000..b08b802f5 --- /dev/null +++ b/examples/default/domains/Fulfilment/systems/warehouse-system/services/WarehouseService/index.mdx @@ -0,0 +1,43 @@ +--- +id: warehouse-service +version: 1.0.0 +name: Warehouse Service +summary: | + Orchestrates picking and packing in the warehouse. It reacts to completed orders, creates picking jobs, and signals when an order is ready to ship. +styles: + icon: /icons/languages/java.svg +owners: + - fulfilment-platform +receives: + - id: order-completed + version: 1.0.0 +sends: + - id: order-ready-for-shipping + version: 1.0.0 +writesTo: + - id: warehouse-database +readsFrom: + - id: warehouse-database +repository: + language: Java + url: 'https://github.com/acme/warehouse-service' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Warehouse Service** runs warehousing. When the Ordering domain publishes [[event|order-completed]], it creates a picking job in the [[container|warehouse-database]] for the [[service|picking-worker]] to work through. Once the order is packed it publishes [[event|order-ready-for-shipping]] for the [[system|shipping-system]]. + + + + + + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Fulfilment/ubiquitous-language.mdx b/examples/default/domains/Fulfilment/ubiquitous-language.mdx new file mode 100644 index 000000000..880e4c151 --- /dev/null +++ b/examples/default/domains/Fulfilment/ubiquitous-language.mdx @@ -0,0 +1,44 @@ +--- +dictionary: + - id: Fulfilment + name: Fulfilment + summary: "Everything that gets a confirmed order physically to the customer — stock, packing and shipping." + icon: PackageCheck + - id: Stock + name: Stock + summary: "The quantity of a product physically available to sell." + description: | + Stock is owned by the Inventory System. Each product has an available quantity and a reserved + quantity. Stock is the source of truth for whether an order can be fulfilled. + icon: Boxes + - id: Reservation + name: Reservation + summary: "A temporary hold on stock for a specific order, taken during checkout." + description: | + A reservation guarantees stock is set aside for an order while payment is taken. It is held, + then either consumed when the order ships or released if checkout fails. + icon: BookmarkCheck + - id: Picking + name: Picking + summary: "Collecting an order's items from the warehouse shelves." + icon: Hand + - id: Packing + name: Packing + summary: "Boxing up a picked order into one or more parcels ready for shipping." + icon: Package + - id: Shipment + name: Shipment + summary: "A packed order handed to a carrier for delivery, tracked from dispatch to delivery." + description: | + A shipment is created with an external carrier once an order is packed. Its lifecycle — + created, delivered or failed — is reported back by the carrier. + icon: Truck + - id: Carrier + name: Carrier + summary: "The external delivery company that transports shipments to customers." + icon: Globe + - id: Tracking Number + name: Tracking Number + summary: "A code from the carrier that lets the customer follow a shipment's progress." + icon: ScanBarcode +--- diff --git a/examples/default/domains/Ordering/entities/checkout-session/index.mdx b/examples/default/domains/Ordering/entities/checkout-session/index.mdx new file mode 100644 index 000000000..b247c6c8a --- /dev/null +++ b/examples/default/domains/Ordering/entities/checkout-session/index.mdx @@ -0,0 +1,36 @@ +--- +id: checkout-session +name: Checkout Session +version: 1.0.0 +identifier: checkoutSessionId +aggregateRoot: true +summary: The orchestration state for turning a checked-out cart into an order. +owners: + - ordering-platform +properties: + - name: checkoutSessionId + type: UUID + required: true + description: Unique checkout session identifier. + - name: cartId + type: UUID + required: true + description: Cart being checked out. + references: cart + relationType: startsFrom + referencesIdentifier: cartId + - name: status + type: string + required: true + description: Current checkout state. + - name: startedAt + type: datetime + required: true + description: Time checkout orchestration started. +--- + +## Overview + +Checkout Session tracks the saga managed by [[service|checkout-orchestrator]] across inventory reservation, payment authorization and order creation. + + diff --git a/examples/default/domains/Ordering/entities/order-line/index.mdx b/examples/default/domains/Ordering/entities/order-line/index.mdx new file mode 100644 index 000000000..f0159af8d --- /dev/null +++ b/examples/default/domains/Ordering/entities/order-line/index.mdx @@ -0,0 +1,38 @@ +--- +id: order-line +name: Order Line +version: 1.0.0 +identifier: orderLineId +summary: A purchased product and quantity within an order. +owners: + - ordering-platform +properties: + - name: orderLineId + type: UUID + required: true + description: Unique order line identifier. + - name: orderId + type: UUID + required: true + description: Order this line belongs to. + references: order + relationType: belongsTo + referencesIdentifier: orderId + - name: productId + type: UUID + required: true + description: Product purchased. + references: product + relationType: references + referencesIdentifier: productId + - name: quantity + type: integer + required: true + description: Quantity purchased. +--- + +## Overview + +Order Line snapshots the products bought at checkout. It should not replace the Catalog domain's authoritative product data. + + diff --git a/examples/default/domains/Ordering/entities/order-status-history/index.mdx b/examples/default/domains/Ordering/entities/order-status-history/index.mdx new file mode 100644 index 000000000..384955a68 --- /dev/null +++ b/examples/default/domains/Ordering/entities/order-status-history/index.mdx @@ -0,0 +1,35 @@ +--- +id: order-status-history +name: Order Status History +version: 1.0.0 +identifier: orderStatusHistoryId +summary: Audit history of order lifecycle state changes. +owners: + - ordering-platform +properties: + - name: orderStatusHistoryId + type: UUID + required: true + description: Unique status history record. + - name: orderId + type: UUID + required: true + description: Order whose status changed. + references: order + relationType: belongsTo + referencesIdentifier: orderId + - name: status + type: string + required: true + description: New order status. + - name: changedAt + type: datetime + required: true + description: Time the status changed. +--- + +## Overview + +Order Status History provides an audit trail for customer support, fulfilment exceptions and order lifecycle debugging. + + diff --git a/examples/default/domains/Ordering/entities/order/index.mdx b/examples/default/domains/Ordering/entities/order/index.mdx new file mode 100644 index 000000000..685a479ca --- /dev/null +++ b/examples/default/domains/Ordering/entities/order/index.mdx @@ -0,0 +1,36 @@ +--- +id: order +name: Order +version: 1.0.0 +identifier: orderId +aggregateRoot: true +summary: The durable customer order created after successful checkout. +owners: + - ordering-platform +properties: + - name: orderId + type: UUID + required: true + description: Unique order identifier. + - name: customerId + type: UUID + required: true + description: Customer who placed the order. + references: customer-profile + relationType: belongsTo + referencesIdentifier: customerId + - name: status + type: string + required: true + description: Current order lifecycle state. + - name: totalAmount + type: decimal + required: true + description: Total order value at creation. +--- + +## Overview + +Order is the main aggregate in [[system|order-management-system]]. It emits order lifecycle events used by Fulfilment and support workflows. + + diff --git a/examples/default/domains/Ordering/flows/PlaceAnOrder/index.mdx b/examples/default/domains/Ordering/flows/PlaceAnOrder/index.mdx new file mode 100644 index 000000000..97eaca900 --- /dev/null +++ b/examples/default/domains/Ordering/flows/PlaceAnOrder/index.mdx @@ -0,0 +1,158 @@ +--- +id: place-an-order +name: Place an Order +version: 1.0.0 +summary: | + The end-to-end journey from a customer checking out their cart to a confirmed order — spanning the Shopping and Ordering domains, including the checkout saga and its failure path. +owners: + - ordering-platform +steps: + - id: shopper_checks_out + title: Shopper checks out + actor: + name: Shopper + summary: The customer confirms their cart and starts checkout. + next_step: + id: checkout_cart_command + label: Check out cart + + - id: checkout_cart_command + title: Checkout Cart + message: + id: checkout-cart + version: 1.0.0 + next_step: + id: cart_api + label: Price and finalise cart + + - id: cart_api + title: Cart API + service: + id: cart-api + version: 1.0.0 + next_step: + id: cart_checked_out + label: Publish checkout + + - id: cart_checked_out + title: Cart Checked Out + message: + id: cart-checked-out + version: 1.0.0 + next_step: + id: checkout_api + label: Start checkout flow + + - id: checkout_api + title: Checkout API + service: + id: checkout-api + version: 1.0.0 + next_step: + id: checkout_orchestrator + label: Run checkout saga + + - id: checkout_orchestrator + title: Checkout Orchestrator + service: + id: checkout-orchestrator + version: 1.0.0 + next_steps: + - id: reserve_inventory + label: Reserve inventory + - id: authorize_payment + label: Authorize payment + + - id: reserve_inventory + title: Reserve Inventory + message: + id: reserve-inventory + version: 1.0.0 + next_step: + id: authorize_payment + label: Inventory held + + - id: authorize_payment + title: Authorize Payment + message: + id: authorize-payment + version: 1.0.0 + next_steps: + - id: create_order + label: Payment authorized + - id: checkout_failed + label: Authorization declined + + - id: create_order + title: Create Order + message: + id: create-order + version: 1.0.0 + next_step: + id: order_service + label: Persist order + + - id: order_service + title: Order Service + service: + id: order-service + version: 1.0.0 + next_step: + id: order_created + label: Publish order created + + - id: order_created + title: Order Created + message: + id: order-created + version: 1.0.0 + next_step: + id: order_confirmed + label: Confirm to customer + + - id: order_confirmed + title: Order confirmed + custom: + title: Order confirmed + color: green + icon: CheckCircleIcon + type: Outcome + summary: The customer's order is placed and confirmed. Fulfilment begins. + properties: + happy_path: 'true' + + - id: checkout_failed + title: Checkout failed + custom: + title: Checkout failed + color: red + icon: ExclamationTriangleIcon + type: Exception + summary: A checkout step failed (e.g. payment declined). The orchestrator compensates earlier steps and cancels the order. + properties: + common_causes: 'payment declined, out of stock' + next_action: 'release reservation, void authorization, cancel order' + next_step: + id: order_cancelled + label: Cancel order + + - id: order_cancelled + title: Order Cancelled + message: + id: order-cancelled + version: 1.0.0 +--- + +## Overview + +**Place an Order** is the headline business flow at Acme Inc. It begins in the [[domain|shopping]] domain, when a shopper checks out their cart, and finishes in the [[domain|ordering]] domain, with a confirmed order — or a compensated, cancelled one if a step fails. + + + +## How it works + +1. The shopper checks out — the [[service|cart-api]] prices the cart and publishes [[event|cart-checked-out]]. +2. The [[service|checkout-api]] picks this up and hands it to the [[service|checkout-orchestrator]]. +3. The orchestrator runs the saga: [[command|reserve-inventory]] then [[command|authorize-payment]]. +4. On success it sends [[command|create-order]] to the [[service|order-service]], which publishes [[event|order-created]]. +5. If any step fails, the orchestrator compensates the earlier steps and the order is cancelled via [[event|order-cancelled]]. diff --git a/examples/default/domains/Ordering/index.mdx b/examples/default/domains/Ordering/index.mdx new file mode 100644 index 000000000..3c7418b90 --- /dev/null +++ b/examples/default/domains/Ordering/index.mdx @@ -0,0 +1,70 @@ +--- +id: ordering +name: Ordering +version: 1.0.0 +summary: | + The Ordering domain turns a checked-out cart into a confirmed order. It orchestrates checkout — reserving inventory, authorizing payment and creating the order — and owns the lifecycle of every order. +owners: + - ordering-platform +systems: + - id: checkout-system + version: 1.0.0 + - id: order-management-system + version: 1.0.0 +flows: + - id: place-an-order + version: 1.0.0 +entities: + - id: checkout-session + version: 1.0.0 + - id: order + version: 1.0.0 + - id: order-line + version: 1.0.0 + - id: order-status-history + version: 1.0.0 +badges: + - content: Core domain + backgroundColor: blue + textColor: blue + icon: RectangleGroupIcon + - content: Business Critical + backgroundColor: red + textColor: red + icon: ShieldCheckIcon +--- + +## Overview + +The **Ordering** domain is responsible for everything that happens once a customer commits to buy — from the moment their cart is checked out to a confirmed, fulfilled order. It is split into two systems: + + + + + + +### How the systems work together + +When the [[domain|shopping]] domain checks out a cart, the **Checkout System** takes over. It orchestrates the steps needed to place an order — reserving inventory, authorizing payment, and finally asking the **Order Management System** to create the order. The Order Management System then owns that order for the rest of its life, publishing events as it is created, completed or cancelled. + +## System Diagram + +The systems in this domain, how they relate, and the people who interact with them. + + + +## Resource Diagram + +The components that make up this domain. + + diff --git a/examples/default/domains/Ordering/systems/checkout-system/flows/CheckoutSaga/index.mdx b/examples/default/domains/Ordering/systems/checkout-system/flows/CheckoutSaga/index.mdx new file mode 100644 index 000000000..90fa12e5d --- /dev/null +++ b/examples/default/domains/Ordering/systems/checkout-system/flows/CheckoutSaga/index.mdx @@ -0,0 +1,101 @@ +--- +id: checkout-saga +name: Checkout Saga +version: 1.0.0 +summary: | + How the Checkout Orchestrator turns a checked-out cart into an order — reserving inventory, authorizing payment and creating the order, with compensation when a step fails. +owners: + - ordering-platform +steps: + - id: cart_checked_out + title: Cart Checked Out + message: + id: cart-checked-out + version: 1.0.0 + next_step: + id: checkout_api + label: Receive checkout + + - id: checkout_api + title: Checkout API + service: + id: checkout-api + version: 1.0.0 + next_step: + id: orchestrator + label: Start saga + + - id: orchestrator + title: Checkout Orchestrator + service: + id: checkout-orchestrator + version: 1.0.0 + next_step: + id: reserve_inventory + label: Step 1 — reserve inventory + + - id: reserve_inventory + title: Reserve Inventory + message: + id: reserve-inventory + version: 1.0.0 + next_steps: + - id: authorize_payment + label: Reserved + - id: compensate_release + label: Out of stock + + - id: authorize_payment + title: Authorize Payment + message: + id: authorize-payment + version: 1.0.0 + next_steps: + - id: create_order + label: Authorized + - id: compensate_release + label: Declined + + - id: create_order + title: Create Order + message: + id: create-order + version: 1.0.0 + next_step: + id: saga_complete + label: Order created + + - id: saga_complete + title: Saga complete + custom: + title: Saga complete + color: green + icon: CheckCircleIcon + type: Outcome + summary: Inventory reserved, payment authorized and order created. The saga succeeds. + + - id: compensate_release + title: Compensate + custom: + title: Compensate and abort + color: red + icon: ArrowUturnLeftIcon + type: Compensation + summary: A step failed. The orchestrator releases any reservation and voids any authorization, then aborts the saga without creating an order. + properties: + actions: 'release reservation, void authorization' +--- + +## Overview + +The **Checkout Saga** is the heart of the [[system|checkout-system]]. It coordinates the steps that turn a checked-out cart into a confirmed order. Each step has a compensating action so a failure never leaves the customer in an inconsistent state. + + + +## Steps + +1. **Reserve inventory** — [[command|reserve-inventory]] holds stock for the cart's items. +2. **Authorize payment** — [[command|authorize-payment]] places a hold on the order total. +3. **Create order** — [[command|create-order]] persists the order in the [[system|order-management-system]]. + +If reservation or authorization fails, the orchestrator **compensates** — releasing the reservation and voiding the authorization — and aborts without creating an order. diff --git a/examples/default/domains/Ordering/systems/checkout-system/index.mdx b/examples/default/domains/Ordering/systems/checkout-system/index.mdx new file mode 100644 index 000000000..b1332cd8a --- /dev/null +++ b/examples/default/domains/Ordering/systems/checkout-system/index.mdx @@ -0,0 +1,57 @@ +--- +id: checkout-system +name: Checkout System +version: 1.0.0 +summary: | + Internal system that orchestrates checkout. When a cart is checked out it reserves inventory, authorizes payment, and asks the Order Management System to create the order. +owners: + - ordering-platform +services: + - id: checkout-api + - id: checkout-orchestrator +flows: + - id: checkout-saga + version: 1.0.0 +relationships: + - id: order-management-system + label: creates orders via +actors: + - id: shopper + name: Shopper + label: confirms checkout + direction: inbound +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Checkout System** drives the checkout flow at Acme Inc. When the [[domain|shopping]] domain checks out a cart, the [[service|checkout-api]] receives the [[command|checkout-cart]] command and hands it to the [[service|checkout-orchestrator]]. The orchestrator runs the checkout saga — reserving inventory via [[command|reserve-inventory]], authorizing payment via [[command|authorize-payment]], and finally creating the order via [[command|create-order]] against the [[system|order-management-system]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|checkout-api]] | Service | Entry point for checkout. Receives the checkout command and starts the flow. | +| [[service\|checkout-orchestrator]] | Service | Runs the checkout saga — reserve inventory, authorize payment, create order. | + +## Messages this system sends + +- [[command\|reserve-inventory]] — reserve stock for the items in the cart. +- [[command\|authorize-payment]] — authorize payment for the order total. +- [[command\|create-order]] — create the order in the Order Management System. diff --git a/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutAPI/index.mdx b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutAPI/index.mdx new file mode 100644 index 000000000..96fd384bc --- /dev/null +++ b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutAPI/index.mdx @@ -0,0 +1,43 @@ +--- +id: checkout-api +version: 1.0.0 +name: Checkout API +summary: | + The entry point to the Checkout System. Receives the checkout command from the Shopping domain and starts the checkout flow. +styles: + icon: /icons/languages/nodejs.svg +owners: + - ordering-platform +receives: + - id: checkout-cart + version: 1.0.0 +repository: + language: TypeScript + url: 'https://github.com/acme/checkout-api' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Checkout API** is the front door to the Checkout System. When the [[domain|shopping]] domain checks out a cart, this service receives the [[command|checkout-cart]] command, validates it, and hands the work to the [[service|checkout-orchestrator]] to run the checkout saga. + + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Checkout entry | Receives and validates [[command\|checkout-cart]] from the Shopping domain. | +| Orchestration handoff | Starts a checkout saga in the [[service\|checkout-orchestrator]]. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/AuthorizePayment/index.mdx b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/AuthorizePayment/index.mdx new file mode 100644 index 000000000..d0489ec1e --- /dev/null +++ b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/AuthorizePayment/index.mdx @@ -0,0 +1,32 @@ +--- +id: authorize-payment +name: Authorize Payment +version: 1.0.0 +summary: | + Command to authorize payment for the total of a checked-out cart. +owners: + - ordering-platform +schemaPath: schema.json +operation: + method: POST + path: /payments/authorize + statusCodes: ['201', '400', '402', '409'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`AuthorizePayment` is sent by the [[service|checkout-orchestrator]] to authorize payment for the order total. Authorization places a hold on the customer's payment method; the funds are captured once the order is created. If a later step fails, the authorization is voided. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/AuthorizePayment/schema.json b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/AuthorizePayment/schema.json new file mode 100644 index 000000000..f6cc128e4 --- /dev/null +++ b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/AuthorizePayment/schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AuthorizePayment", + "description": "Command to authorize payment for the total of a checked-out cart", + "type": "object", + "properties": { + "cartId": { + "description": "Identifier of the cart being checked out", + "type": "string", + "format": "uuid" + }, + "customerId": { + "description": "Identifier of the customer paying", + "type": "string", + "format": "uuid" + }, + "amount": { + "description": "Amount to authorize, in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "currency": { + "description": "ISO 4217 currency code", + "type": "string", + "pattern": "^[A-Z]{3}$" + } + }, + "required": ["cartId", "amount", "currency"] +} diff --git a/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/ReserveInventory/index.mdx b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/ReserveInventory/index.mdx new file mode 100644 index 000000000..a713a0228 --- /dev/null +++ b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/ReserveInventory/index.mdx @@ -0,0 +1,32 @@ +--- +id: reserve-inventory +name: Reserve Inventory +version: 1.0.0 +summary: | + Command to reserve stock for the items in a checked-out cart. +owners: + - ordering-platform +schemaPath: schema.json +operation: + method: POST + path: /reservations + statusCodes: ['201', '400', '409'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ReserveInventory` is sent by the [[service|checkout-orchestrator]] to hold stock for the items in a checked-out cart. A successful reservation guarantees the inventory is available while payment is authorized. If a later checkout step fails, the reservation is released. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/ReserveInventory/schema.json b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/ReserveInventory/schema.json new file mode 100644 index 000000000..c7702c1df --- /dev/null +++ b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/commands/ReserveInventory/schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReserveInventory", + "description": "Command to reserve stock for the items in a checked-out cart", + "type": "object", + "properties": { + "cartId": { + "description": "Identifier of the cart being checked out", + "type": "string", + "format": "uuid" + }, + "customerId": { + "description": "Identifier of the customer checking out", + "type": "string", + "format": "uuid" + }, + "items": { + "description": "The items to reserve", + "type": "array", + "items": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "format": "uuid" + }, + "quantity": { + "type": "integer", + "minimum": 1 + } + }, + "required": ["productId", "quantity"] + } + } + }, + "required": ["cartId", "items"] +} diff --git a/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/index.mdx b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/index.mdx new file mode 100644 index 000000000..8f2904c56 --- /dev/null +++ b/examples/default/domains/Ordering/systems/checkout-system/services/CheckoutOrchestrator/index.mdx @@ -0,0 +1,54 @@ +--- +id: checkout-orchestrator +version: 1.0.0 +name: Checkout Orchestrator +summary: | + Runs the checkout saga. Reserves inventory, authorizes payment and creates the order, coordinating the steps that turn a checked-out cart into a confirmed order. +styles: + icon: /icons/languages/nodejs.svg +owners: + - ordering-platform +sends: + - id: reserve-inventory + version: 1.0.0 + - id: authorize-payment + version: 1.0.0 + - id: create-order + version: 1.0.0 +repository: + language: TypeScript + url: 'https://github.com/acme/checkout-orchestrator' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Checkout Orchestrator** is the brain of the Checkout System. Once the [[service|checkout-api]] hands it a checkout, it runs the checkout saga step by step: + +1. **Reserve inventory** — sends [[command|reserve-inventory]] to hold stock for the cart's items. +2. **Authorize payment** — sends [[command|authorize-payment]] to authorize the order total. +3. **Create the order** — sends [[command|create-order]] to the [[system|order-management-system]]. + +If any step fails, the orchestrator compensates the earlier steps (releasing the reservation, voiding the authorization) so the customer is never left in an inconsistent state. + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Inventory | Reserves stock via [[command\|reserve-inventory]]. | +| Payment | Authorizes the order total via [[command\|authorize-payment]]. | +| Order creation | Creates the order via [[command\|create-order]] against the [[system\|order-management-system]]. | +| Compensation | Rolls back earlier steps if a later step fails. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/containers/order-database/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/containers/order-database/index.mdx new file mode 100644 index 000000000..11dd5802d --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/containers/order-database/index.mdx @@ -0,0 +1,39 @@ +--- +id: order-database +name: Order Database +version: 1.0.0 +summary: PostgreSQL database that is the system of record for orders and their items. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for orders +classification: confidential +retention: 7y +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **Order Database** is the authoritative store for orders at Acme Inc. The [[service|order-service]] reads from and writes to it as orders are created, cancelled and completed. + +### What does it store? + +- **Orders** — one row per order: id, customer, status, total and timestamps. +- **Order Items** — one row per item in an order: product, quantity and unit price. + +### Schema + + + + + + + +### Retention + +Orders are financial records and are retained for **7 years** (see frontmatter) to meet accounting and audit requirements. diff --git a/examples/default/domains/Ordering/systems/order-management-system/containers/order-database/schema.sql b/examples/default/domains/Ordering/systems/order-management-system/containers/order-database/schema.sql new file mode 100644 index 000000000..554b70834 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/containers/order-database/schema.sql @@ -0,0 +1,24 @@ +-- Order Database — system of record for orders + +CREATE TABLE orders ( + order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + cart_id UUID, + customer_id UUID NOT NULL, + status TEXT NOT NULL DEFAULT 'CREATED' + CHECK (status IN ('CREATED', 'COMPLETED', 'CANCELLED')), + total_cents INTEGER NOT NULL CHECK (total_cents >= 0), + currency CHAR(3) NOT NULL DEFAULT 'USD', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE order_items ( + order_id UUID NOT NULL REFERENCES orders (order_id) ON DELETE CASCADE, + product_id UUID NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + unit_price_cents INTEGER NOT NULL CHECK (unit_price_cents >= 0), + PRIMARY KEY (order_id, product_id) +); + +CREATE INDEX idx_orders_customer ON orders (customer_id); +CREATE INDEX idx_orders_status ON orders (status); diff --git a/examples/default/domains/Ordering/systems/order-management-system/flows/OrderCancellation/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/flows/OrderCancellation/index.mdx new file mode 100644 index 000000000..b13d842b2 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/flows/OrderCancellation/index.mdx @@ -0,0 +1,105 @@ +--- +id: order-cancellation +name: Order Cancellation +version: 1.0.0 +summary: | + How an order is cancelled — whether requested by the customer or triggered by a failure — and the events that tell the rest of the business to unwind it. +owners: + - ordering-platform +steps: + - id: cancellation_trigger + title: Cancellation requested + actor: + name: Shopper or System + summary: A customer asks to cancel, or a downstream failure (payment, inventory) forces a cancellation. + next_step: + id: cancel_order_command + label: Cancel order + + - id: cancel_order_command + title: Cancel Order + message: + id: cancel-order + version: 1.0.0 + next_step: + id: order_service + label: Handle cancellation + + - id: order_service + title: Order Service + service: + id: order-service + version: 1.0.0 + next_steps: + - id: eligibility_check + label: Check eligibility + - id: not_cancellable + label: Already completed + + - id: eligibility_check + title: Cancellation policy check + custom: + title: Cancellation policy + color: blue + icon: ClipboardDocumentCheckIcon + type: Policy + summary: An order can only be cancelled before it is completed. Completed orders must go through returns instead. + properties: + rule: 'status must be CREATED' + next_step: + id: order_database + label: Mark cancelled + + - id: order_database + title: Order Database + container: + id: order-database + next_step: + id: order_cancelled + label: Persist + publish + + - id: order_cancelled + title: Order Cancelled + message: + id: order-cancelled + version: 1.0.0 + next_step: + id: downstream_unwind + label: Unwind downstream + + - id: downstream_unwind + title: Downstream unwind + custom: + title: Release and refund + color: orange + icon: ArrowUturnLeftIcon + type: Compensation + summary: Consumers of OrderCancelled release reserved inventory, void or refund the payment, and stop fulfilment. + properties: + consumers: 'inventory, payment, fulfilment' + + - id: not_cancellable + title: Cannot cancel + custom: + title: Cannot cancel + color: red + icon: ExclamationTriangleIcon + type: Exception + summary: The order has already been completed and cannot be cancelled. The customer is directed to the returns process. + properties: + next_action: 'start a return' +--- + +## Overview + +**Order Cancellation** documents how the [[system|order-management-system]] cancels an order. Cancellation can be requested by a shopper or forced by a downstream failure during checkout. Only orders that have not yet completed can be cancelled. + + + +## How it works + +1. A [[command|cancel-order]] command reaches the [[service|order-service]]. +2. The service checks the cancellation policy — only `CREATED` orders are cancellable. +3. If eligible, the order is marked cancelled in the [[container|order-database]] and [[event|order-cancelled]] is published. +4. Downstream consumers react to [[event|order-cancelled]] to release inventory, refund payment and stop fulfilment. +5. If the order is already completed, cancellation is rejected and the customer is sent to the returns process. diff --git a/examples/default/domains/Ordering/systems/order-management-system/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/index.mdx new file mode 100644 index 000000000..58fda2096 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/index.mdx @@ -0,0 +1,53 @@ +--- +id: order-management-system +name: Order Management System +version: 1.0.0 +summary: | + Internal system that is the source of truth for orders. It creates orders, tracks their lifecycle, and publishes events as orders are created, completed and cancelled. +owners: + - ordering-platform +services: + - id: order-service +containers: + - id: order-database +flows: + - id: order-cancellation + version: 1.0.0 +relationships: + - id: checkout-system + label: receives orders from +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Order Management System** owns the order at Acme Inc. It accepts the [[command|create-order]] command from the [[system|checkout-system]], persists orders in the [[container|order-database]], and serves order lookups via [[query|get-order]]. As an order moves through its lifecycle it publishes [[event|order-created]], [[event|order-completed]] and [[event|order-cancelled]] events for the rest of the business to react to. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|order-service]] | Service | Creates, cancels and serves orders, and publishes order lifecycle events. | +| [[container\|order-database]] | Data store | PostgreSQL system of record for orders and their items. | + +## Messages this system publishes + +- [[event\|order-created]] — a new order has been created. +- [[event\|order-completed]] — an order has been fulfilled and completed. +- [[event\|order-cancelled]] — an order has been cancelled. diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/asyncapi.yml b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/asyncapi.yml new file mode 100644 index 000000000..ff77c2538 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/asyncapi.yml @@ -0,0 +1,233 @@ +asyncapi: 3.0.0 + +info: + title: Order Service + version: 1.0.0 + description: | + The event-driven interface of the Order Service — the system of record for orders. + + The service consumes commands to create and cancel orders, and publishes events as + orders move through their lifecycle (created, completed, cancelled). Query access + (`GetOrder`) is served over the REST API and is not part of this AsyncAPI document. + contact: + name: Ordering Platform + email: ordering-platform@acme.test + +defaultContentType: application/json + +servers: + production: + host: broker.acme.test:9092 + protocol: kafka + description: Production Kafka cluster. + +channels: + orderCommands: + address: ordering.commands + title: Order commands + description: Commands the Order Service consumes to create and cancel orders. + messages: + CreateOrder: + $ref: '#/components/messages/CreateOrder' + CancelOrder: + $ref: '#/components/messages/CancelOrder' + + orderEvents: + address: ordering.orders + title: Order lifecycle events + description: Events the Order Service publishes as orders move through their lifecycle. + messages: + OrderCreated: + $ref: '#/components/messages/OrderCreated' + OrderCompleted: + $ref: '#/components/messages/OrderCompleted' + OrderCancelled: + $ref: '#/components/messages/OrderCancelled' + +operations: + receiveCreateOrder: + action: receive + title: Create Order + summary: Create a new order from a checked-out cart. + channel: + $ref: '#/channels/orderCommands' + messages: + - $ref: '#/channels/orderCommands/messages/CreateOrder' + + receiveCancelOrder: + action: receive + title: Cancel Order + summary: Cancel an existing order. + channel: + $ref: '#/channels/orderCommands' + messages: + - $ref: '#/channels/orderCommands/messages/CancelOrder' + + publishOrderCreated: + action: send + title: Order Created + summary: Published when a new order has been created. + channel: + $ref: '#/channels/orderEvents' + messages: + - $ref: '#/channels/orderEvents/messages/OrderCreated' + + publishOrderCompleted: + action: send + title: Order Completed + summary: Published when an order has been fulfilled and completed. + channel: + $ref: '#/channels/orderEvents' + messages: + - $ref: '#/channels/orderEvents/messages/OrderCompleted' + + publishOrderCancelled: + action: send + title: Order Cancelled + summary: Published when an order has been cancelled. + channel: + $ref: '#/channels/orderEvents' + messages: + - $ref: '#/channels/orderEvents/messages/OrderCancelled' + +components: + messages: + CreateOrder: + name: CreateOrder + title: Create Order + summary: Command to create a new order from a checked-out cart. + payload: + $ref: '#/components/schemas/CreateOrder' + CancelOrder: + name: CancelOrder + title: Cancel Order + summary: Command to cancel an existing order. + payload: + $ref: '#/components/schemas/CancelOrder' + OrderCreated: + name: OrderCreated + title: Order Created + summary: Published when a new order has been created. + payload: + $ref: '#/components/schemas/OrderCreated' + OrderCompleted: + name: OrderCompleted + title: Order Completed + summary: Published when an order has been fulfilled and completed. + payload: + $ref: '#/components/schemas/OrderCompleted' + OrderCancelled: + name: OrderCancelled + title: Order Cancelled + summary: Published when an order has been cancelled. + payload: + $ref: '#/components/schemas/OrderCancelled' + + schemas: + OrderItem: + type: object + required: [productId, quantity, unitPrice] + properties: + productId: + type: string + format: uuid + quantity: + type: integer + minimum: 1 + unitPrice: + type: integer + description: Price per unit in minor units (e.g. cents). + + CancellationReason: + type: string + enum: [CUSTOMER_REQUESTED, PAYMENT_FAILED, OUT_OF_STOCK, FRAUD] + + CreateOrder: + type: object + description: Command to create a new order from a checked-out cart. + required: [cartId, customerId, items, total, currency] + properties: + cartId: + type: string + format: uuid + customerId: + type: string + format: uuid + items: + type: array + items: + $ref: '#/components/schemas/OrderItem' + total: + type: integer + minimum: 0 + description: Order total in minor units (e.g. cents). + currency: + type: string + pattern: '^[A-Z]{3}$' + paymentAuthorizationId: + type: string + description: Identifier of the payment authorization for this order. + + CancelOrder: + type: object + description: Command to cancel an existing order. + required: [orderId, reason] + properties: + orderId: + type: string + format: uuid + reason: + $ref: '#/components/schemas/CancellationReason' + + OrderCreated: + type: object + description: Published when a new order has been created. + required: [orderId, customerId, total, currency, createdAt] + properties: + orderId: + type: string + format: uuid + customerId: + type: string + format: uuid + total: + type: integer + description: Order total in minor units (e.g. cents). + currency: + type: string + pattern: '^[A-Z]{3}$' + createdAt: + type: string + format: date-time + + OrderCompleted: + type: object + description: Published when an order has been fulfilled and completed. + required: [orderId, customerId, completedAt] + properties: + orderId: + type: string + format: uuid + customerId: + type: string + format: uuid + completedAt: + type: string + format: date-time + + OrderCancelled: + type: object + description: Published when an order has been cancelled. + required: [orderId, reason, cancelledAt] + properties: + orderId: + type: string + format: uuid + customerId: + type: string + format: uuid + reason: + $ref: '#/components/schemas/CancellationReason' + cancelledAt: + type: string + format: date-time diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CancelOrder/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CancelOrder/index.mdx new file mode 100644 index 000000000..3d109146a --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CancelOrder/index.mdx @@ -0,0 +1,32 @@ +--- +id: cancel-order +name: Cancel Order +version: 1.0.0 +summary: | + Command to cancel an existing order. +owners: + - ordering-platform +schemaPath: schema.json +operation: + method: POST + path: /orders/{orderId}/cancel + statusCodes: ['200', '404', '409'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CancelOrder` is handled by the [[service|order-service]]. It cancels an order that has not yet been completed — releasing any reservations and voiding the payment authorization where needed. On success the Order Service publishes an [[event|order-cancelled]] event. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CancelOrder/schema.json b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CancelOrder/schema.json new file mode 100644 index 000000000..a67eede17 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CancelOrder/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CancelOrder", + "description": "Command to cancel an existing order", + "type": "object", + "properties": { + "orderId": { + "description": "Identifier of the order to cancel", + "type": "string", + "format": "uuid" + }, + "reason": { + "description": "Why the order is being cancelled", + "type": "string", + "enum": ["CUSTOMER_REQUESTED", "PAYMENT_FAILED", "OUT_OF_STOCK", "FRAUD"] + } + }, + "required": ["orderId", "reason"] +} diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CreateOrder/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CreateOrder/index.mdx new file mode 100644 index 000000000..86eaff6b9 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CreateOrder/index.mdx @@ -0,0 +1,32 @@ +--- +id: create-order +name: Create Order +version: 1.0.0 +summary: | + Command to create a new order from a checked-out cart. +owners: + - ordering-platform +schemaPath: schema.json +operation: + method: POST + path: /orders + statusCodes: ['201', '400', '409'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CreateOrder` is sent by the [[service|checkout-orchestrator]] and handled by the [[service|order-service]]. It is the final step of the checkout saga — once inventory is reserved and payment is authorized, this command creates the order. On success the Order Service publishes an [[event|order-created]] event. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CreateOrder/schema.json b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CreateOrder/schema.json new file mode 100644 index 000000000..796d29dc7 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/commands/CreateOrder/schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CreateOrder", + "description": "Command to create a new order from a checked-out cart", + "type": "object", + "properties": { + "cartId": { + "description": "Identifier of the cart this order was created from", + "type": "string", + "format": "uuid" + }, + "customerId": { + "description": "Identifier of the customer placing the order", + "type": "string", + "format": "uuid" + }, + "items": { + "description": "The items in the order", + "type": "array", + "items": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "format": "uuid" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "unitPrice": { + "type": "integer", + "description": "Price per unit in minor units (e.g. cents)" + } + }, + "required": ["productId", "quantity", "unitPrice"] + } + }, + "total": { + "description": "Order total in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "currency": { + "description": "ISO 4217 currency code", + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "paymentAuthorizationId": { + "description": "Identifier of the payment authorization for this order", + "type": "string" + } + }, + "required": ["cartId", "customerId", "items", "total", "currency"] +} diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCancelled/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCancelled/index.mdx new file mode 100644 index 000000000..6a5c9fb4e --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCancelled/index.mdx @@ -0,0 +1,29 @@ +--- +id: order-cancelled +name: Order Cancelled +version: 1.0.0 +summary: | + Published when an order has been cancelled. +owners: + - ordering-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`OrderCancelled` is published by the [[service|order-service]] when an order is cancelled — whether at the customer's request or because a downstream step (payment, inventory) failed. Downstream systems consume it to release inventory, refund or void payment, and stop fulfilment. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCancelled/schema.json b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCancelled/schema.json new file mode 100644 index 000000000..d01f5531a --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCancelled/schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OrderCancelled", + "description": "Published when an order has been cancelled", + "type": "object", + "properties": { + "orderId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "reason": { + "type": "string", + "enum": ["CUSTOMER_REQUESTED", "PAYMENT_FAILED", "OUT_OF_STOCK", "FRAUD"] + }, + "cancelledAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["orderId", "reason", "cancelledAt"] +} diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCompleted/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCompleted/index.mdx new file mode 100644 index 000000000..e78b1611e --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCompleted/index.mdx @@ -0,0 +1,29 @@ +--- +id: order-completed +name: Order Completed +version: 1.0.0 +summary: | + Published when an order has been fulfilled and completed. +owners: + - ordering-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`OrderCompleted` is published by the [[service|order-service]] when an order has been fully fulfilled. It marks the end of the happy path for an order. Downstream systems consume it to close out fulfilment, capture payment, and update reporting. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCompleted/schema.json b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCompleted/schema.json new file mode 100644 index 000000000..9fc2b0049 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCompleted/schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OrderCompleted", + "description": "Published when an order has been fulfilled and completed", + "type": "object", + "properties": { + "orderId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "completedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["orderId", "customerId", "completedAt"] +} diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/index.mdx new file mode 100644 index 000000000..b87b1e969 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/index.mdx @@ -0,0 +1,29 @@ +--- +id: order-created +name: Order Created +version: 1.0.0 +summary: | + Published when a new order has been created. +owners: + - ordering-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`OrderCreated` is published by the [[service|order-service]] when a new order is created from a checked-out cart. Downstream systems consume this event to begin fulfilment, send confirmation to the customer, and update reporting. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/schema.json b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/schema.json new file mode 100644 index 000000000..8d4cf1f43 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OrderCreated", + "description": "Published when a new order has been created", + "type": "object", + "properties": { + "orderId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "total": { + "type": "integer", + "description": "Order total in minor units (e.g. cents)" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "status": { + "type": "string", + "enum": ["created"], + "description": "Initial lifecycle status for the order" + }, + "items": { + "type": "array", + "description": "Line items captured at order creation time", + "items": { + "type": "object", + "properties": { + "sku": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "unitPrice": { + "type": "integer", + "description": "Unit price in minor units (e.g. cents)" + } + }, + "required": ["sku", "quantity", "unitPrice"] + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["orderId", "customerId", "total", "currency", "status", "items", "createdAt"] +} diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.3.0/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.3.0/index.mdx new file mode 100644 index 000000000..ecd02be6f --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.3.0/index.mdx @@ -0,0 +1,25 @@ +--- +id: order-created +name: Order Created +version: 0.3.0 +summary: | + Published when a new order has been created. This early contract only included identifiers and the creation timestamp. +owners: + - ordering-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`OrderCreated` version `0.3.0` was the first stable order creation event contract used by downstream fulfilment consumers. + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.3.0/schema.json b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.3.0/schema.json new file mode 100644 index 000000000..c3aa1344f --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.3.0/schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OrderCreated", + "description": "Published when a new order has been created", + "type": "object", + "properties": { + "orderId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["orderId", "customerId", "createdAt"] +} diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.6.0/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.6.0/index.mdx new file mode 100644 index 000000000..ce5332e0b --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.6.0/index.mdx @@ -0,0 +1,25 @@ +--- +id: order-created +name: Order Created +version: 0.6.0 +summary: | + Published when a new order has been created. This version added the order total and currency for payment reconciliation. +owners: + - ordering-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`OrderCreated` version `0.6.0` added monetary fields so consumers could reconcile order totals without calling the Order API. + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.6.0/schema.json b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.6.0/schema.json new file mode 100644 index 000000000..44c4686ca --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/events/OrderCreated/versioned/0.6.0/schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OrderCreated", + "description": "Published when a new order has been created", + "type": "object", + "properties": { + "orderId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "total": { + "type": "integer", + "description": "Order total in minor units (e.g. cents)" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["orderId", "customerId", "total", "currency", "createdAt"] +} diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/index.mdx new file mode 100644 index 000000000..84c0e168f --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/index.mdx @@ -0,0 +1,64 @@ +--- +id: order-service +version: 1.0.0 +name: Order Service +summary: | + The system of record for orders. Creates and cancels orders, serves order lookups, and publishes events as orders move through their lifecycle. +styles: + icon: /icons/languages/java.svg +owners: + - ordering-platform +receives: + - id: create-order + version: 1.0.0 + - id: cancel-order + version: 1.0.0 + - id: get-order + version: 1.0.0 +sends: + - id: order-created + version: 1.0.0 + - id: order-completed + version: 1.0.0 + - id: order-cancelled + version: 1.0.0 +writesTo: + - id: order-database +readsFrom: + - id: order-database +repository: + language: Java + url: 'https://github.com/acme/order-service' +specifications: + - type: asyncapi + path: asyncapi.yml + name: Order Service AsyncAPI +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Order Service** is the heart of the Order Management System. It receives [[command|create-order]] from the [[system|checkout-system]], persists orders to the [[container|order-database]], and serves order lookups via [[query|get-order]]. It also handles [[command|cancel-order]]. As orders move through their lifecycle it publishes [[event|order-created]], [[event|order-completed]] and [[event|order-cancelled]] events. + + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Order creation | Handles [[command\|create-order]] from the Checkout System and persists the order. | +| Order cancellation | Handles [[command\|cancel-order]] and compensates downstream where needed. | +| Order lookups | Serves [[query\|get-order]] from the [[container\|order-database]]. | +| Event publishing | Emits [[event\|order-created]], [[event\|order-completed]] and [[event\|order-cancelled]]. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/queries/GetOrder/index.mdx b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/queries/GetOrder/index.mdx new file mode 100644 index 000000000..7659bac71 --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/queries/GetOrder/index.mdx @@ -0,0 +1,32 @@ +--- +id: get-order +name: Get Order +version: 1.0.0 +summary: | + Query to fetch a single order by its identifier. +owners: + - ordering-platform +schemaPath: schema.json +operation: + method: GET + path: /orders/{orderId} + statusCodes: ['200', '404'] +sidebar: + badge: 'GET' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`GetOrder` is handled by the [[service|order-service]]. It returns the current state of a single order, read directly from the [[container|order-database]]. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/queries/GetOrder/schema.json b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/queries/GetOrder/schema.json new file mode 100644 index 000000000..40582a9be --- /dev/null +++ b/examples/default/domains/Ordering/systems/order-management-system/services/OrderService/queries/GetOrder/schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetOrder", + "description": "Query to fetch a single order by its identifier, and the order returned", + "type": "object", + "properties": { + "request": { + "description": "Parameters used to look up the order", + "type": "object", + "properties": { + "orderId": { + "description": "Unique identifier of the order to fetch", + "type": "string", + "format": "uuid" + } + }, + "required": ["orderId"] + }, + "response": { + "description": "The order returned by the query", + "type": "object", + "properties": { + "orderId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": ["CREATED", "COMPLETED", "CANCELLED"] + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "productId": { "type": "string", "format": "uuid" }, + "quantity": { "type": "integer", "minimum": 1 }, + "unitPrice": { "type": "integer" } + }, + "required": ["productId", "quantity", "unitPrice"] + } + }, + "total": { + "type": "integer", + "description": "Order total in minor units (e.g. cents)" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["orderId", "customerId", "status", "items", "total", "currency"] + } + }, + "required": ["request", "response"] +} diff --git a/examples/default/domains/Ordering/ubiquitous-language.mdx b/examples/default/domains/Ordering/ubiquitous-language.mdx new file mode 100644 index 000000000..ee2ace336 --- /dev/null +++ b/examples/default/domains/Ordering/ubiquitous-language.mdx @@ -0,0 +1,53 @@ +--- +dictionary: + - id: Order + name: Order + summary: "A confirmed intent to purchase, created from a checked-out cart and owned for its whole lifecycle." + description: | + The order is the central concept of the Ordering domain. It is owned by the Order Management + System, which is the source of truth for orders. An order has: + + - A unique order id and the customer it belongs to + - A set of order items (product, quantity and unit price) + - A total and currency + - A status (CREATED, COMPLETED or CANCELLED) + + An order is immutable in its essentials once created — its lifecycle is expressed through events, + not by editing it in place. + icon: ClipboardList + - id: Checkout + name: Checkout + summary: "The flow that turns a checked-out cart into a confirmed order." + description: | + Checkout is owned by the Checkout System. It is the orchestration that runs after the Shopping + domain checks out a cart — reserving inventory, authorizing payment and creating the order. + icon: CreditCard + - id: Saga + name: Saga + summary: "A multi-step process with compensating actions, used to coordinate checkout across systems." + description: | + The Checkout Orchestrator runs checkout as a saga: a sequence of steps (reserve inventory, + authorize payment, create order) where each step has a compensating action that undoes it if a + later step fails. This keeps the customer consistent without distributed transactions. + icon: Workflow + - id: Reservation + name: Reservation + summary: "A temporary hold on stock for the items in a cart, taken during checkout." + icon: PackageCheck + - id: Authorization + name: Authorization + summary: "A hold placed on a customer's payment method for the order total, captured when the order is created." + icon: ShieldCheck + - id: Order Item + name: Order Item + summary: "A single product line within an order: a product, a quantity and the price paid per unit." + icon: ListOrdered + - id: Order Status + name: Order Status + summary: "Where an order sits in its life: CREATED, COMPLETED or CANCELLED." + description: | + - CREATED — the order exists and is being fulfilled. + - COMPLETED — the order has been fully fulfilled. + - CANCELLED — the order was cancelled before completion. + icon: Workflow +--- diff --git a/examples/default/domains/Payments/entities/fraud-check/index.mdx b/examples/default/domains/Payments/entities/fraud-check/index.mdx new file mode 100644 index 000000000..f700c2a2f --- /dev/null +++ b/examples/default/domains/Payments/entities/fraud-check/index.mdx @@ -0,0 +1,39 @@ +--- +id: fraud-check +name: Fraud Check +version: 1.0.0 +identifier: fraudCheckId +summary: Fraud screening result associated with a payment attempt. +owners: + - payments-platform +properties: + - name: fraudCheckId + type: UUID + required: true + description: Unique fraud check identifier. + - name: paymentId + type: UUID + required: true + description: Payment attempt being screened. + references: payment + relationType: screens + referencesIdentifier: paymentId + - name: result + type: string + required: true + description: Screening result. + enum: + - passed + - failed + - review + - name: checkedAt + type: datetime + required: true + description: Time screening completed. +--- + +## Overview + +Fraud Check documents the screening outcome from [[system|fraud-detection]] so payment decisions are auditable. + + diff --git a/examples/default/domains/Payments/entities/payment-authorization/index.mdx b/examples/default/domains/Payments/entities/payment-authorization/index.mdx new file mode 100644 index 000000000..29413fc6d --- /dev/null +++ b/examples/default/domains/Payments/entities/payment-authorization/index.mdx @@ -0,0 +1,35 @@ +--- +id: payment-authorization +name: Payment Authorization +version: 1.0.0 +identifier: authorizationId +summary: Approval from a payment provider to reserve funds for an order. +owners: + - payments-platform +properties: + - name: authorizationId + type: UUID + required: true + description: Unique authorization identifier. + - name: paymentId + type: UUID + required: true + description: Payment this authorization belongs to. + references: payment + relationType: belongsTo + referencesIdentifier: paymentId + - name: providerReference + type: string + required: true + description: External payment provider reference. + - name: authorizedAt + type: datetime + required: true + description: Time authorization was granted. +--- + +## Overview + +Payment Authorization captures the result of [[command|authorize-payment]] and links Acme payment state to provider state. + + diff --git a/examples/default/domains/Payments/entities/payment/index.mdx b/examples/default/domains/Payments/entities/payment/index.mdx new file mode 100644 index 000000000..889df22e3 --- /dev/null +++ b/examples/default/domains/Payments/entities/payment/index.mdx @@ -0,0 +1,36 @@ +--- +id: payment +name: Payment +version: 1.0.0 +identifier: paymentId +aggregateRoot: true +summary: Acme's internal record of payment intent and outcome for an order. +owners: + - payments-platform +properties: + - name: paymentId + type: UUID + required: true + description: Unique payment identifier. + - name: orderId + type: UUID + required: true + description: Order being paid for. + references: order + relationType: paysFor + referencesIdentifier: orderId + - name: amount + type: decimal + required: true + description: Amount requested. + - name: status + type: string + required: true + description: Payment lifecycle status. +--- + +## Overview + +Payment is the Payments domain aggregate. It records Acme's view of payment state regardless of external provider retries or webhook order. + + diff --git a/examples/default/domains/Payments/entities/refund/index.mdx b/examples/default/domains/Payments/entities/refund/index.mdx new file mode 100644 index 000000000..36ff90861 --- /dev/null +++ b/examples/default/domains/Payments/entities/refund/index.mdx @@ -0,0 +1,36 @@ +--- +id: refund +name: Refund +version: 1.0.0 +identifier: refundId +aggregateRoot: true +summary: A request to return money to a customer after payment. +owners: + - payments-platform +properties: + - name: refundId + type: UUID + required: true + description: Unique refund identifier. + - name: paymentId + type: UUID + required: true + description: Payment being refunded. + references: payment + relationType: refunds + referencesIdentifier: paymentId + - name: amount + type: decimal + required: true + description: Refund amount. + - name: status + type: string + required: true + description: Refund lifecycle state. +--- + +## Overview + +Refund is owned by Payments and records refund requests and provider outcomes such as [[event|refund-processed]]. + + diff --git a/examples/default/domains/Payments/index.mdx b/examples/default/domains/Payments/index.mdx new file mode 100644 index 000000000..8a2d2d49a --- /dev/null +++ b/examples/default/domains/Payments/index.mdx @@ -0,0 +1,70 @@ +--- +id: payments +name: Payments +version: 1.0.0 +summary: | + The Payments domain takes payment for orders and processes refunds. It orchestrates authorization and capture through an external payment processor, and screens payments for fraud. +owners: + - payments-platform +systems: + - id: payment-processing-system + version: 1.0.0 + - id: stripe + version: 1.0.0 + - id: fraud-detection + version: 1.0.0 +entities: + - id: payment + version: 1.0.0 + - id: payment-authorization + version: 1.0.0 + - id: refund + version: 1.0.0 + - id: fraud-check + version: 1.0.0 +badges: + - content: Core domain + backgroundColor: blue + textColor: blue + icon: RectangleGroupIcon + - content: Business Critical + backgroundColor: red + textColor: red + icon: ShieldCheckIcon +--- + +## Overview + +The **Payments** domain is responsible for moving money. It authorizes and captures payment for orders, issues refunds, and screens payments for fraud. It owns one internal system and integrates with two external providers: + + + + + + + +### How the systems work together + +The **Payment Processing System** is the internal orchestrator. When the Ordering domain authorizes a payment, it requests payment via the external **Stripe** system and, in parallel, asks **Fraud Detection** to screen the payment. Stripe reports back whether the payment succeeded or failed, and the Payment Processing System records the outcome. + +## System Diagram + +The systems in this domain, how they relate, and the people who interact with them. + + + diff --git a/examples/default/domains/Payments/systems/fraud-detection/index.mdx b/examples/default/domains/Payments/systems/fraud-detection/index.mdx new file mode 100644 index 000000000..9980011f3 --- /dev/null +++ b/examples/default/domains/Payments/systems/fraud-detection/index.mdx @@ -0,0 +1,49 @@ +--- +id: fraud-detection +name: Fraud Detection +version: 1.0.0 +scope: external +summary: | + External provider that screens payments for fraud. It consumes payment requests and returns a pass or fail verdict. +owners: + - payments-platform +services: + - id: fraud-api +relationships: + - id: payment-processing-system + label: screens payments for +badges: + - content: External + backgroundColor: yellow + textColor: yellow + icon: GlobeAltIcon +--- + +## Overview + +**Fraud Detection** is an external provider Acme Inc uses to screen payments for fraud. It is owned and operated by a third party — modelled here as an **external system** so the catalog shows how the [[system|payment-processing-system]] integrates with it. + +It consumes [[event|payment-requested]] and returns a verdict via [[event|fraud-check-passed]] or [[event|fraud-check-failed]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|fraud-api]] | Service | Screens a payment for fraud and returns a pass/fail verdict. | + +## Messages this system publishes + +- [[event\|fraud-check-passed]] — a payment passed fraud screening. +- [[event\|fraud-check-failed]] — a payment failed fraud screening. diff --git a/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckFailed/index.mdx b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckFailed/index.mdx new file mode 100644 index 000000000..ec4dcf580 --- /dev/null +++ b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckFailed/index.mdx @@ -0,0 +1,29 @@ +--- +id: fraud-check-failed +name: Fraud Check Failed +version: 1.0.0 +summary: | + Published when a payment fails fraud screening. +owners: + - payments-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`FraudCheckFailed` is published by the [[service|fraud-api]] when a payment is flagged as fraudulent. The [[system|payment-processing-system]] uses it to block the charge and cancel the order. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckFailed/schema.json b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckFailed/schema.json new file mode 100644 index 000000000..8deb7fd1a --- /dev/null +++ b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckFailed/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FraudCheckFailed", + "description": "Published when a payment fails fraud screening", + "type": "object", + "properties": { + "paymentId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "score": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Fraud risk score; higher is riskier" + }, + "reason": { + "type": "string", + "enum": ["HIGH_RISK_SCORE", "BLOCKLISTED", "VELOCITY", "MISMATCH"] + }, + "checkedAt": { "type": "string", "format": "date-time" } + }, + "required": ["paymentId", "orderId", "reason", "checkedAt"] +} diff --git a/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckPassed/index.mdx b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckPassed/index.mdx new file mode 100644 index 000000000..551254e24 --- /dev/null +++ b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckPassed/index.mdx @@ -0,0 +1,29 @@ +--- +id: fraud-check-passed +name: Fraud Check Passed +version: 1.0.0 +summary: | + Published when a payment passes fraud screening. +owners: + - payments-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`FraudCheckPassed` is published by the [[service|fraud-api]] when a payment clears fraud screening. The [[system|payment-processing-system]] uses it to allow the charge to proceed. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckPassed/schema.json b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckPassed/schema.json new file mode 100644 index 000000000..979e2cab9 --- /dev/null +++ b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/events/FraudCheckPassed/schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FraudCheckPassed", + "description": "Published when a payment passes fraud screening", + "type": "object", + "properties": { + "paymentId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "score": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Fraud risk score; lower is safer" + }, + "checkedAt": { "type": "string", "format": "date-time" } + }, + "required": ["paymentId", "orderId", "checkedAt"] +} diff --git a/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/index.mdx b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/index.mdx new file mode 100644 index 000000000..8999426d2 --- /dev/null +++ b/examples/default/domains/Payments/systems/fraud-detection/services/FraudAPI/index.mdx @@ -0,0 +1,41 @@ +--- +id: fraud-api +version: 1.0.0 +name: Fraud API +summary: | + External fraud screening API. It consumes payment requests and returns a pass or fail verdict for each. +styles: + icon: /icons/languages/nodejs.svg +owners: + - payments-platform +receives: + - id: payment-requested + version: 1.0.0 +sends: + - id: fraud-check-passed + version: 1.0.0 + - id: fraud-check-failed + version: 1.0.0 +repository: + language: External + url: 'https://github.com/acme/fraud-api' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Fraud API** screens payments for fraud. It consumes [[event|payment-requested]] from the [[system|payment-processing-system]] and returns a verdict — [[event|fraud-check-passed]] or [[event|fraud-check-failed]] — which the Payments domain uses to allow or block the charge. + + + + + + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Payments/systems/payment-processing-system/containers/payment-database/index.mdx b/examples/default/domains/Payments/systems/payment-processing-system/containers/payment-database/index.mdx new file mode 100644 index 000000000..2422aa6b2 --- /dev/null +++ b/examples/default/domains/Payments/systems/payment-processing-system/containers/payment-database/index.mdx @@ -0,0 +1,39 @@ +--- +id: payment-database +name: Payment Database +version: 1.0.0 +summary: PostgreSQL database that is the system of record for payments and refunds. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for payments and refunds +classification: confidential +retention: 7y +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **Payment Database** is the authoritative store for payments and refunds at Acme Inc. The [[service|payment-api]] and [[service|payment-worker]] read from and write to it as payments are authorized, charged and refunded. + +### What does it store? + +- **Payments** — one row per payment: id, order, amount, status and timestamps. +- **Refunds** — one row per refund: id, payment, amount, status and timestamps. + +### Schema + + + + + + + +### Retention + +Payments and refunds are financial records and are retained for **7 years** (see frontmatter) to meet accounting and audit requirements. diff --git a/examples/default/domains/Payments/systems/payment-processing-system/containers/payment-database/schema.sql b/examples/default/domains/Payments/systems/payment-processing-system/containers/payment-database/schema.sql new file mode 100644 index 000000000..a53415c14 --- /dev/null +++ b/examples/default/domains/Payments/systems/payment-processing-system/containers/payment-database/schema.sql @@ -0,0 +1,25 @@ +-- Payment Database — system of record for payments and refunds + +CREATE TABLE payments ( + payment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + order_id UUID NOT NULL, + customer_id UUID, + amount_cents INTEGER NOT NULL CHECK (amount_cents >= 0), + currency CHAR(3) NOT NULL DEFAULT 'USD', + status TEXT NOT NULL DEFAULT 'REQUESTED' + CHECK (status IN ('REQUESTED', 'SUCCEEDED', 'FAILED')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE refunds ( + refund_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + payment_id UUID NOT NULL REFERENCES payments (payment_id), + amount_cents INTEGER NOT NULL CHECK (amount_cents >= 0), + status TEXT NOT NULL DEFAULT 'REQUESTED' + CHECK (status IN ('REQUESTED', 'PROCESSED', 'FAILED')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_payments_order ON payments (order_id); +CREATE INDEX idx_payments_status ON payments (status); diff --git a/examples/default/domains/Payments/systems/payment-processing-system/index.mdx b/examples/default/domains/Payments/systems/payment-processing-system/index.mdx new file mode 100644 index 000000000..60dde9539 --- /dev/null +++ b/examples/default/domains/Payments/systems/payment-processing-system/index.mdx @@ -0,0 +1,53 @@ +--- +id: payment-processing-system +name: Payment Processing System +version: 1.0.0 +summary: | + Internal system that orchestrates payment for orders. It authorizes payments, requests charges from the external payment processor, handles the outcome, and issues refunds. +owners: + - payments-platform +services: + - id: payment-api + - id: payment-worker +containers: + - id: payment-database +relationships: + - id: stripe + label: requests charges from + - id: fraud-detection + label: screens payments with +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Payment Processing System** is the internal heart of the Payments domain. When the Ordering domain sends [[command|authorize-payment]], the [[service|payment-api]] records the intent in the [[container|payment-database]] and the [[service|payment-worker]] requests a charge from the external [[system|stripe]] via [[event|payment-requested]]. Stripe reports the outcome back as [[event|payment-succeeded]] or [[event|payment-failed]], which the worker records. The system also issues refunds via [[event|refund-requested]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|payment-api]] | Service | Receives authorization requests and records payment intent. | +| [[service\|payment-worker]] | Service | Drives charges and refunds against Stripe and records outcomes. | +| [[container\|payment-database]] | Data store | PostgreSQL system of record for payments and refunds. | + +## Messages this system publishes + +- [[event\|payment-requested]] — a charge has been requested from the payment processor. +- [[event\|refund-requested]] — a refund has been requested from the payment processor. diff --git a/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentAPI/index.mdx b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentAPI/index.mdx new file mode 100644 index 000000000..e958eb81b --- /dev/null +++ b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentAPI/index.mdx @@ -0,0 +1,46 @@ +--- +id: payment-api +version: 1.0.0 +name: Payment API +summary: | + Receives payment authorization requests from the Ordering domain and records the payment intent, kicking off the charge against the payment processor. +styles: + icon: /icons/languages/nodejs.svg +owners: + - payments-platform +receives: + - id: authorize-payment + version: 1.0.0 +writesTo: + - id: payment-database +readsFrom: + - id: payment-database +repository: + language: TypeScript + url: 'https://github.com/acme/payment-api' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Payment API** is the entry point to the Payment Processing System. It receives [[command|authorize-payment]] from the Ordering domain, records the payment intent in the [[container|payment-database]], and leaves the [[service|payment-worker]] to drive the charge against [[system|stripe]]. + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Authorization | Receives and validates [[command\|authorize-payment]]. | +| Persistence | Records payment intent in the [[container\|payment-database]]. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/PaymentRequested/index.mdx b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/PaymentRequested/index.mdx new file mode 100644 index 000000000..425194162 --- /dev/null +++ b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/PaymentRequested/index.mdx @@ -0,0 +1,29 @@ +--- +id: payment-requested +name: Payment Requested +version: 1.0.0 +summary: | + Published when the Payment Processing System requests a charge from the payment processor. +owners: + - payments-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`PaymentRequested` is published by the [[service|payment-worker]] to ask the external [[system|stripe]] to charge a customer for an order. Stripe responds with [[event|payment-succeeded]] or [[event|payment-failed]]. The [[system|fraud-detection]] system also consumes this event to screen the payment. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/PaymentRequested/schema.json b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/PaymentRequested/schema.json new file mode 100644 index 000000000..b25ed842b --- /dev/null +++ b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/PaymentRequested/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PaymentRequested", + "description": "Published when a charge is requested from the payment processor", + "type": "object", + "properties": { + "paymentId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "customerId": { "type": "string", "format": "uuid" }, + "amount": { + "type": "integer", + "minimum": 0, + "description": "Amount to charge, in minor units (e.g. cents)" + }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, + "requestedAt": { "type": "string", "format": "date-time" } + }, + "required": ["paymentId", "orderId", "amount", "currency", "requestedAt"] +} diff --git a/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/RefundRequested/index.mdx b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/RefundRequested/index.mdx new file mode 100644 index 000000000..ffedefba9 --- /dev/null +++ b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/RefundRequested/index.mdx @@ -0,0 +1,29 @@ +--- +id: refund-requested +name: Refund Requested +version: 1.0.0 +summary: | + Published when the Payment Processing System requests a refund from the payment processor. +owners: + - payments-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`RefundRequested` is published by the [[service|payment-worker]] to ask the external [[system|stripe]] to refund a previous charge — for example when an order is cancelled after payment. Stripe responds with [[event|refund-processed]]. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/RefundRequested/schema.json b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/RefundRequested/schema.json new file mode 100644 index 000000000..db69eb603 --- /dev/null +++ b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/events/RefundRequested/schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RefundRequested", + "description": "Published when a refund is requested from the payment processor", + "type": "object", + "properties": { + "refundId": { "type": "string", "format": "uuid" }, + "paymentId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "amount": { + "type": "integer", + "minimum": 0, + "description": "Amount to refund, in minor units (e.g. cents)" + }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, + "reason": { "type": "string" }, + "requestedAt": { "type": "string", "format": "date-time" } + }, + "required": ["refundId", "paymentId", "amount", "currency", "requestedAt"] +} diff --git a/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/index.mdx b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/index.mdx new file mode 100644 index 000000000..a303776d2 --- /dev/null +++ b/examples/default/domains/Payments/systems/payment-processing-system/services/PaymentWorker/index.mdx @@ -0,0 +1,56 @@ +--- +id: payment-worker +version: 1.0.0 +name: Payment Worker +summary: | + Drives charges and refunds against the external payment processor and records the outcomes. It requests payments and refunds, and consumes the success/failure events Stripe returns. +styles: + icon: /icons/languages/go.svg +owners: + - payments-platform +receives: + - id: payment-succeeded + version: 1.0.0 + - id: payment-failed + version: 1.0.0 +sends: + - id: payment-requested + version: 1.0.0 + - id: refund-requested + version: 1.0.0 +writesTo: + - id: payment-database +readsFrom: + - id: payment-database +repository: + language: Go + url: 'https://github.com/acme/payment-worker' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Payment Worker** drives money movement. It requests charges from the external [[system|stripe]] via [[event|payment-requested]] and refunds via [[event|refund-requested]], then consumes the [[event|payment-succeeded]] and [[event|payment-failed]] events Stripe returns and records the outcome in the [[container|payment-database]]. + + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Charging | Requests charges via [[event\|payment-requested]]. | +| Refunds | Requests refunds via [[event\|refund-requested]]. | +| Outcomes | Consumes [[event\|payment-succeeded]] / [[event\|payment-failed]] and records them. | +| Persistence | Reads from and writes to the [[container\|payment-database]]. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Payments/systems/stripe/index.mdx b/examples/default/domains/Payments/systems/stripe/index.mdx new file mode 100644 index 000000000..a97cbffb6 --- /dev/null +++ b/examples/default/domains/Payments/systems/stripe/index.mdx @@ -0,0 +1,52 @@ +--- +id: stripe +name: Stripe +version: 1.0.0 +scope: external +summary: | + External payment processor used by Acme Inc to charge cards and issue refunds. It receives payment and refund requests and reports outcomes back via webhook events. +owners: + - payments-platform +services: + - id: stripe-payments-api + - id: stripe-webhook-endpoint +relationships: + - id: payment-processing-system + label: processes payments for +badges: + - content: External + backgroundColor: yellow + textColor: yellow + icon: GlobeAltIcon +--- + +## Overview + +[Stripe](https://stripe.com) is the external payment processor Acme Inc uses to charge customer cards and issue refunds. It is owned and operated by Stripe — modelled here as an **external system** so the catalog shows how the [[system|payment-processing-system]] integrates with it. + +Stripe receives [[event|payment-requested]] and [[event|refund-requested]], and reports outcomes back via [[event|payment-succeeded]], [[event|payment-failed]] and [[event|refund-processed]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|stripe-payments-api]] | Service | Accepts charge and refund requests and processes them. | +| [[service\|stripe-webhook-endpoint]] | Service | Delivers payment and refund outcomes back to Acme Inc as webhook events. | + +## Messages this system publishes + +- [[event\|payment-succeeded]] — a charge succeeded. +- [[event\|payment-failed]] — a charge failed. +- [[event\|refund-processed]] — a refund was processed. diff --git a/examples/default/domains/Payments/systems/stripe/services/StripePaymentsAPI/index.mdx b/examples/default/domains/Payments/systems/stripe/services/StripePaymentsAPI/index.mdx new file mode 100644 index 000000000..b821545b8 --- /dev/null +++ b/examples/default/domains/Payments/systems/stripe/services/StripePaymentsAPI/index.mdx @@ -0,0 +1,37 @@ +--- +id: stripe-payments-api +version: 1.0.0 +name: Payments API +summary: | + Stripe's API for charging cards and issuing refunds. Acme Inc's Payment Processing System calls it to request payments and refunds. +styles: + icon: /icons/payments/stripe.svg +owners: + - payments-platform +receives: + - id: payment-requested + version: 1.0.0 + - id: refund-requested + version: 1.0.0 +repository: + language: External + url: 'https://github.com/stripe/stripe-node' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Payments API** is Stripe's external interface for charging cards and issuing refunds. The [[system|payment-processing-system]] sends [[event|payment-requested]] and [[event|refund-requested]], and Stripe processes them and reports the outcome back through its [[service|stripe-webhook-endpoint]]. + + + + + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentFailed/index.mdx b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentFailed/index.mdx new file mode 100644 index 000000000..5dfbcb287 --- /dev/null +++ b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentFailed/index.mdx @@ -0,0 +1,29 @@ +--- +id: payment-failed +name: Payment Failed +version: 1.0.0 +summary: | + Published by Stripe when a charge fails. +owners: + - payments-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`PaymentFailed` is delivered by Stripe's [[service|stripe-webhook-endpoint]] when a requested charge fails — for example a declined card. The [[service|payment-worker]] consumes it and records the payment as failed, which ultimately leads the order to be cancelled. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentFailed/schema.json b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentFailed/schema.json new file mode 100644 index 000000000..fd0e83885 --- /dev/null +++ b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentFailed/schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PaymentFailed", + "description": "Published when a charge fails", + "type": "object", + "properties": { + "paymentId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "reason": { + "type": "string", + "enum": ["CARD_DECLINED", "INSUFFICIENT_FUNDS", "EXPIRED_CARD", "PROCESSING_ERROR"] + }, + "failedAt": { "type": "string", "format": "date-time" } + }, + "required": ["paymentId", "orderId", "reason", "failedAt"] +} diff --git a/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentSucceeded/index.mdx b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentSucceeded/index.mdx new file mode 100644 index 000000000..ed59c5311 --- /dev/null +++ b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentSucceeded/index.mdx @@ -0,0 +1,29 @@ +--- +id: payment-succeeded +name: Payment Succeeded +version: 1.0.0 +summary: | + Published by Stripe when a charge succeeds. +owners: + - payments-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`PaymentSucceeded` is delivered by Stripe's [[service|stripe-webhook-endpoint]] when a requested charge succeeds. The [[service|payment-worker]] consumes it and records the payment as succeeded. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentSucceeded/schema.json b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentSucceeded/schema.json new file mode 100644 index 000000000..91755e4b3 --- /dev/null +++ b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/PaymentSucceeded/schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PaymentSucceeded", + "description": "Published when a charge succeeds", + "type": "object", + "properties": { + "paymentId": { "type": "string", "format": "uuid" }, + "orderId": { "type": "string", "format": "uuid" }, + "amount": { "type": "integer", "description": "Amount charged, in minor units (e.g. cents)" }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, + "processorReference": { "type": "string", "description": "Stripe's charge identifier" }, + "succeededAt": { "type": "string", "format": "date-time" } + }, + "required": ["paymentId", "orderId", "amount", "currency", "succeededAt"] +} diff --git a/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/RefundProcessed/index.mdx b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/RefundProcessed/index.mdx new file mode 100644 index 000000000..3c9fa3a24 --- /dev/null +++ b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/RefundProcessed/index.mdx @@ -0,0 +1,29 @@ +--- +id: refund-processed +name: Refund Processed +version: 1.0.0 +summary: | + Published by Stripe when a refund has been processed. +owners: + - payments-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`RefundProcessed` is delivered by Stripe's [[service|stripe-webhook-endpoint]] when a requested refund has been processed. The [[service|payment-worker]] consumes it and records the refund as complete. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/RefundProcessed/schema.json b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/RefundProcessed/schema.json new file mode 100644 index 000000000..70e376974 --- /dev/null +++ b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/events/RefundProcessed/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RefundProcessed", + "description": "Published when a refund has been processed", + "type": "object", + "properties": { + "refundId": { "type": "string", "format": "uuid" }, + "paymentId": { "type": "string", "format": "uuid" }, + "amount": { "type": "integer", "description": "Amount refunded, in minor units (e.g. cents)" }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, + "processedAt": { "type": "string", "format": "date-time" } + }, + "required": ["refundId", "paymentId", "amount", "currency", "processedAt"] +} diff --git a/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/index.mdx b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/index.mdx new file mode 100644 index 000000000..9b53316b8 --- /dev/null +++ b/examples/default/domains/Payments/systems/stripe/services/StripeWebhookEndpoint/index.mdx @@ -0,0 +1,39 @@ +--- +id: stripe-webhook-endpoint +version: 1.0.0 +name: Webhook Endpoint +summary: | + Stripe's webhook delivery for payment and refund outcomes. It notifies Acme Inc whether a charge succeeded or failed, and when a refund is processed. +styles: + icon: /icons/payments/stripe.svg +owners: + - payments-platform +sends: + - id: payment-succeeded + version: 1.0.0 + - id: payment-failed + version: 1.0.0 + - id: refund-processed + version: 1.0.0 +repository: + language: External + url: 'https://github.com/stripe/stripe-node' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Webhook Endpoint** is how Stripe reports outcomes back to Acme Inc. After processing a request from the [[service|stripe-payments-api]], it delivers [[event|payment-succeeded]], [[event|payment-failed]] or [[event|refund-processed]] to the [[system|payment-processing-system]]. + + + + + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Payments/ubiquitous-language.mdx b/examples/default/domains/Payments/ubiquitous-language.mdx new file mode 100644 index 000000000..e043f11da --- /dev/null +++ b/examples/default/domains/Payments/ubiquitous-language.mdx @@ -0,0 +1,41 @@ +--- +dictionary: + - id: Payment + name: Payment + summary: "A charge taken from a customer to pay for an order." + description: | + A payment is the central concept of the Payments domain. It is owned by the Payment Processing + System, which records every payment's status (REQUESTED, SUCCEEDED or FAILED). The actual money + movement happens at the external payment processor. + icon: CreditCard + - id: Authorization + name: Authorization + summary: "A hold placed on a customer's payment method, confirming funds are available before capture." + icon: ShieldCheck + - id: Capture + name: Capture + summary: "Taking the held funds from an authorized payment — turning an authorization into an actual charge." + icon: Banknote + - id: Refund + name: Refund + summary: "Returning money to a customer for a previous payment, in full or in part." + icon: Undo2 + - id: Payment Processor + name: Payment Processor + summary: "The external provider (Stripe) that charges cards and issues refunds on Acme Inc's behalf." + description: | + The payment processor is a third party. Acme Inc never stores raw card details — it delegates + the actual charge and refund to the processor and reacts to the outcomes it reports back. + icon: Building2 + - id: Fraud Screening + name: Fraud Screening + summary: "Checking a payment for signs of fraud before allowing the charge to proceed." + description: | + Fraud screening is performed by an external Fraud Detection provider. It returns a verdict — + passed or failed — that determines whether a payment is allowed or blocked. + icon: ShieldAlert + - id: Webhook + name: Webhook + summary: "A callback the payment processor sends to report the outcome of a charge or refund." + icon: Webhook +--- diff --git a/examples/default/domains/Reviews/asyncapi.yml b/examples/default/domains/Reviews/asyncapi.yml new file mode 100644 index 000000000..b85d9b379 --- /dev/null +++ b/examples/default/domains/Reviews/asyncapi.yml @@ -0,0 +1,181 @@ +asyncapi: 3.0.0 + +info: + title: Review Moderation Worker + version: 1.0.0 + description: | + The event-driven interface of the Review Moderation Worker. + + The worker consumes submitted and flagged reviews, screens them for spam, abuse + and policy violations, and publishes the outcome (published or rejected). + contact: + name: Reviews Platform + email: reviews-platform@acme.test + +defaultContentType: application/json + +servers: + production: + host: broker.acme.test:9092 + protocol: kafka + description: Production Kafka cluster. + +channels: + reviewIntake: + address: reviews.intake + title: Reviews to moderate + description: Reviews that need screening — newly submitted or flagged for re-moderation. + messages: + ReviewSubmitted: + $ref: '#/components/messages/ReviewSubmitted' + ReviewFlagged: + $ref: '#/components/messages/ReviewFlagged' + + reviewOutcomes: + address: reviews.outcomes + title: Moderation outcomes + description: The decisions the worker publishes after screening a review. + messages: + ReviewPublished: + $ref: '#/components/messages/ReviewPublished' + ReviewRejected: + $ref: '#/components/messages/ReviewRejected' + +operations: + receiveReviewSubmitted: + action: receive + title: Review Submitted + summary: Screen a newly submitted review. + channel: + $ref: '#/channels/reviewIntake' + messages: + - $ref: '#/channels/reviewIntake/messages/ReviewSubmitted' + + receiveReviewFlagged: + action: receive + title: Review Flagged + summary: Re-screen a previously published review that has been flagged. + channel: + $ref: '#/channels/reviewIntake' + messages: + - $ref: '#/channels/reviewIntake/messages/ReviewFlagged' + + publishReviewPublished: + action: send + title: Review Published + summary: Published when a review passes moderation. + channel: + $ref: '#/channels/reviewOutcomes' + messages: + - $ref: '#/channels/reviewOutcomes/messages/ReviewPublished' + + publishReviewRejected: + action: send + title: Review Rejected + summary: Published when a review fails moderation. + channel: + $ref: '#/channels/reviewOutcomes' + messages: + - $ref: '#/channels/reviewOutcomes/messages/ReviewRejected' + +components: + messages: + ReviewSubmitted: + name: ReviewSubmitted + title: Review Submitted + summary: A customer submitted a review; it awaits moderation. + payload: + $ref: '#/components/schemas/ReviewSubmitted' + ReviewFlagged: + name: ReviewFlagged + title: Review Flagged + summary: A published review was flagged and needs re-screening. + payload: + $ref: '#/components/schemas/ReviewFlagged' + ReviewPublished: + name: ReviewPublished + title: Review Published + summary: A review passed moderation and is visible on the storefront. + payload: + $ref: '#/components/schemas/ReviewPublished' + ReviewRejected: + name: ReviewRejected + title: Review Rejected + summary: A review failed moderation and will not be shown. + payload: + $ref: '#/components/schemas/ReviewRejected' + + schemas: + ReviewSubmitted: + type: object + properties: + reviewId: + type: string + format: uuid + productId: + type: string + customerId: + type: string + rating: + type: integer + minimum: 1 + maximum: 5 + title: + type: string + body: + type: string + submittedAt: + type: string + format: date-time + required: [reviewId, productId, customerId, rating, submittedAt] + ReviewFlagged: + type: object + properties: + reviewId: + type: string + format: uuid + productId: + type: string + reason: + type: string + enum: [spam, abuse, off_topic, inappropriate, other] + flagCount: + type: integer + minimum: 1 + flaggedAt: + type: string + format: date-time + required: [reviewId, productId, reason, flaggedAt] + ReviewPublished: + type: object + properties: + reviewId: + type: string + format: uuid + productId: + type: string + customerId: + type: string + rating: + type: integer + minimum: 1 + maximum: 5 + publishedAt: + type: string + format: date-time + required: [reviewId, productId, rating, publishedAt] + ReviewRejected: + type: object + properties: + reviewId: + type: string + format: uuid + productId: + type: string + reason: + type: string + enum: [spam, abuse, off_topic, policy_violation] + rejectedAt: + type: string + format: date-time + required: [reviewId, productId, reason, rejectedAt] diff --git a/examples/default/domains/Reviews/containers/rating-cache/index.mdx b/examples/default/domains/Reviews/containers/rating-cache/index.mdx new file mode 100644 index 000000000..d52dc76ed --- /dev/null +++ b/examples/default/domains/Reviews/containers/rating-cache/index.mdx @@ -0,0 +1,29 @@ +--- +id: rating-cache +name: Rating Cache +version: 1.0.0 +summary: Redis cache that serves each product's aggregate star rating with low latency. +container_type: cache +technology: redis@7 +authoritative: false +access_mode: readWrite +purpose: Fast read store for aggregate product ratings +classification: internal +retention: transient +residency: eu-west-1 +--- + + + +### What is this? + +The **Rating Cache** holds the aggregate rating (average score and review count) for each product. It is a derived read model — never a source of truth — so it can be rebuilt at any time by replaying published reviews. + +### What does it store? + +- **Aggregate rating** — one entry per product: average rating, review count and last-updated timestamp. + +### Access patterns + +- The [[service|rating-aggregator]] writes the aggregate rating whenever a review is published. +- The [[service|review-api]] reads it to serve [[query|get-product-reviews]]. diff --git a/examples/default/domains/Reviews/containers/review-database/index.mdx b/examples/default/domains/Reviews/containers/review-database/index.mdx new file mode 100644 index 000000000..6b61a44ac --- /dev/null +++ b/examples/default/domains/Reviews/containers/review-database/index.mdx @@ -0,0 +1,33 @@ +--- +id: review-database +name: Review Database +version: 1.0.0 +summary: PostgreSQL database that is the system of record for reviews and their moderation state. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for reviews, ratings and moderation decisions +classification: internal +retention: indefinite +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **Review Database** is the authoritative store for every review in the Reviews & Ratings domain. The [[service|review-api]] writes new reviews to it, and the [[service|review-moderation-worker]] records each moderation decision against the stored review. + +### What does it store? + +- **Reviews** — one row per review: product, customer, rating, title, body and lifecycle status (`submitted`, `published`, `rejected`). +- **Moderation decisions** — the outcome and reason for each moderated review, kept for audit. + +### Access patterns + +- The [[service|review-api]] writes new reviews and reads published reviews. +- The [[service|review-moderation-worker]] reads submitted reviews and writes moderation outcomes. +- The [[service|rating-aggregator]] reads published reviews to recompute aggregates. diff --git a/examples/default/domains/Reviews/entities/moderation-decision/index.mdx b/examples/default/domains/Reviews/entities/moderation-decision/index.mdx new file mode 100644 index 000000000..610f9a0f4 --- /dev/null +++ b/examples/default/domains/Reviews/entities/moderation-decision/index.mdx @@ -0,0 +1,38 @@ +--- +id: moderation-decision +name: Moderation Decision +version: 1.0.0 +identifier: moderationDecisionId +summary: The outcome of reviewing submitted customer feedback. +owners: + - reviews-platform +properties: + - name: moderationDecisionId + type: UUID + required: true + description: Unique moderation decision identifier. + - name: reviewId + type: UUID + required: true + description: Review being moderated. + references: review + relationType: decides + referencesIdentifier: reviewId + - name: outcome + type: string + required: true + description: Moderation result. + enum: + - published + - rejected + - name: decidedAt + type: datetime + required: true + description: Time the decision was made. +--- + +## Overview + +Moderation Decision records why a review became published or rejected and supports audit of [[service|review-moderation-worker]] behavior. + + diff --git a/examples/default/domains/Reviews/entities/rating-summary/index.mdx b/examples/default/domains/Reviews/entities/rating-summary/index.mdx new file mode 100644 index 000000000..40871dcf4 --- /dev/null +++ b/examples/default/domains/Reviews/entities/rating-summary/index.mdx @@ -0,0 +1,35 @@ +--- +id: rating-summary +name: Rating Summary +version: 1.0.0 +identifier: productId +summary: Aggregated review statistics for a product. +owners: + - reviews-platform +properties: + - name: productId + type: UUID + required: true + description: Product being summarized. + references: product + relationType: summarizes + referencesIdentifier: productId + - name: averageRating + type: decimal + required: true + description: Average published rating. + - name: reviewCount + type: integer + required: true + description: Number of published reviews. + - name: updatedAt + type: datetime + required: true + description: Time the summary was recalculated. +--- + +## Overview + +Rating Summary is the read model maintained by [[service|rating-aggregator]] and stored in [[container|rating-cache]]. + + diff --git a/examples/default/domains/Reviews/entities/review-flag/index.mdx b/examples/default/domains/Reviews/entities/review-flag/index.mdx new file mode 100644 index 000000000..bff385fd4 --- /dev/null +++ b/examples/default/domains/Reviews/entities/review-flag/index.mdx @@ -0,0 +1,35 @@ +--- +id: review-flag +name: Review Flag +version: 1.0.0 +identifier: flagId +summary: A customer report that a review may violate policy. +owners: + - reviews-platform +properties: + - name: flagId + type: UUID + required: true + description: Unique review flag identifier. + - name: reviewId + type: UUID + required: true + description: Review being reported. + references: review + relationType: flags + referencesIdentifier: reviewId + - name: reason + type: string + required: true + description: Reason selected by the customer. + - name: flaggedAt + type: datetime + required: true + description: Time the review was flagged. +--- + +## Overview + +Review Flag captures [[command|flag-review]] interactions and can send a published review back through moderation. + + diff --git a/examples/default/domains/Reviews/entities/review-vote/index.mdx b/examples/default/domains/Reviews/entities/review-vote/index.mdx new file mode 100644 index 000000000..f9cd217f7 --- /dev/null +++ b/examples/default/domains/Reviews/entities/review-vote/index.mdx @@ -0,0 +1,38 @@ +--- +id: review-vote +name: Review Vote +version: 1.0.0 +identifier: voteId +summary: A customer's helpfulness vote on a product review. +owners: + - reviews-platform +properties: + - name: voteId + type: UUID + required: true + description: Unique review vote identifier. + - name: reviewId + type: UUID + required: true + description: Review being voted on. + references: review + relationType: votesOn + referencesIdentifier: reviewId + - name: customerId + type: UUID + required: true + description: Customer who voted. + references: customer-profile + relationType: submittedBy + referencesIdentifier: customerId + - name: votedAt + type: datetime + required: true + description: Time the vote was submitted. +--- + +## Overview + +Review Vote captures [[command|vote-review-helpful]] interactions and contributes to review helpfulness ranking. + + diff --git a/examples/default/domains/Reviews/entities/review/index.mdx b/examples/default/domains/Reviews/entities/review/index.mdx new file mode 100644 index 000000000..d9872f4fc --- /dev/null +++ b/examples/default/domains/Reviews/entities/review/index.mdx @@ -0,0 +1,63 @@ +--- +id: review +name: Review +version: 1.0.0 +identifier: reviewId +aggregateRoot: true +summary: A customer's review and rating of a product, including its moderation lifecycle. +owners: + - reviews-platform +properties: + - name: reviewId + type: UUID + required: true + description: Unique identifier for the review. + - name: productId + type: string + required: true + description: The product being reviewed. + references: product + relationType: reviews + referencesIdentifier: productId + - name: customerId + type: string + required: true + description: The customer who wrote the review. + references: customer-profile + relationType: writtenBy + referencesIdentifier: customerId + - name: rating + type: integer + required: true + description: Star rating from 1 to 5. + - name: title + type: string + required: false + description: Short headline for the review. + - name: body + type: string + required: true + description: The review text. + - name: status + type: string + required: true + description: Lifecycle status of the review. + enum: + - submitted + - published + - rejected + - name: submittedAt + type: datetime + required: true + description: When the review was submitted. +--- + +## Overview + +The **Review** is the core aggregate of the Reviews & Ratings domain. It captures a customer's rating and written feedback for a product, along with the moderation `status` that controls whether it is visible on the storefront. + +A review moves through three states: + +1. **submitted** — stored by the [[service|review-api]], awaiting moderation. +2. **published** — passed moderation and visible (the [[service|rating-aggregator]] folds it into the product's rating). +3. **rejected** — failed moderation and never shown, but kept for audit. diff --git a/examples/default/domains/Reviews/flows/review-submission/index.mdx b/examples/default/domains/Reviews/flows/review-submission/index.mdx new file mode 100644 index 000000000..f6797dd1a --- /dev/null +++ b/examples/default/domains/Reviews/flows/review-submission/index.mdx @@ -0,0 +1,120 @@ +--- +id: review-submission +name: Review Submission +version: 1.0.0 +summary: | + How a customer's product review travels from submission, through moderation, to being published and folded into the product's aggregate rating. +owners: + - reviews-platform +steps: + - id: customer_submits + title: Customer submits a review + actor: + name: Customer + summary: A customer writes a review and rating for a product they purchased. + next_step: + id: review_api + label: Submit review + + - id: review_api + title: Review API + service: + id: review-api + version: 1.0.0 + next_step: + id: review_database + label: Store review + + - id: review_database + title: Review Database + container: + id: review-database + next_step: + id: review_submitted + label: Review stored + + - id: review_submitted + title: Review Submitted + message: + id: review-submitted + version: 1.0.0 + next_step: + id: moderation_worker + label: Screen review + + - id: moderation_worker + title: Review Moderation Worker + service: + id: review-moderation-worker + version: 1.0.0 + next_steps: + - id: review_published + label: Approved + - id: review_rejected + label: Rejected + + - id: review_published + title: Review Published + message: + id: review-published + version: 1.0.0 + next_step: + id: rating_aggregator + label: Update rating + + - id: review_rejected + title: Review Rejected + message: + id: review-rejected + version: 1.0.0 + next_step: + id: rejected_outcome + label: Not shown + + - id: rating_aggregator + title: Rating Aggregator + service: + id: rating-aggregator + version: 1.0.0 + next_step: + id: rating_cache + label: Write aggregate + + - id: rating_cache + title: Rating Cache + container: + id: rating-cache + next_step: + id: published_outcome + label: Visible on storefront + + - id: published_outcome + title: Review is live + custom: + title: Review is live + color: green + icon: StarIcon + type: Outcome + summary: The review is published and the product's aggregate rating is updated and served from the cache. + + - id: rejected_outcome + title: Review rejected + custom: + title: Review rejected + color: red + icon: NoSymbolIcon + type: Outcome + summary: The review failed moderation. It is kept for audit but never shown on the storefront. +--- + +## Overview + +**Review Submission** documents the journey of a customer review from submission through moderation to publication. A review is only ever folded into a product's rating once it has been approved. + + + +## How it works + +1. A customer submits a review via the [[service|review-api]], which stores it in the [[container|review-database]] and publishes [[event|review-submitted]]. +2. The [[service|review-moderation-worker]] screens the review and publishes either [[event|review-published]] or [[event|review-rejected]]. +3. On approval, the [[service|rating-aggregator]] updates the product's aggregate rating in the [[container|rating-cache]] and the review goes live. diff --git a/examples/default/domains/Reviews/index.mdx b/examples/default/domains/Reviews/index.mdx new file mode 100644 index 000000000..0ab26e2dc --- /dev/null +++ b/examples/default/domains/Reviews/index.mdx @@ -0,0 +1,93 @@ +--- +id: reviews +name: Reviews & Ratings +version: 1.0.0 +summary: | + The Reviews & Ratings domain owns customer feedback on products — how reviews are submitted, moderated, published and aggregated into the star ratings shown across the storefront. +owners: + - reviews-platform +services: + - id: review-api + version: 1.0.0 + - id: review-moderation-worker + version: 1.0.0 + - id: rating-aggregator + version: 1.0.0 +entities: + - id: review + version: 1.0.0 + - id: moderation-decision + version: 1.0.0 + - id: rating-summary + version: 1.0.0 + - id: review-vote + version: 1.0.0 + - id: review-flag + version: 1.0.0 +flows: + - id: review-submission + version: 1.0.0 +receives: + - id: submit-review + version: 1.0.0 + - id: flag-review + version: 1.0.0 + - id: vote-review-helpful + version: 1.0.0 + - id: get-product-reviews + version: 1.0.0 +sends: + - id: review-submitted + version: 1.0.0 + - id: review-published + version: 1.0.0 + - id: review-rejected + version: 1.0.0 + - id: review-flagged + version: 1.0.0 + - id: review-helpful-voted + version: 1.0.0 + - id: rating-updated + version: 1.0.0 +specifications: + - type: openapi + path: openapi.yml + name: Review API + - type: asyncapi + path: asyncapi.yml + name: Reviews Eventing +badges: + - content: Supporting domain + backgroundColor: blue + textColor: blue + icon: RectangleGroupIcon +--- + +## Overview + +The **Reviews & Ratings** domain is responsible for everything to do with customer feedback on products: accepting reviews, moderating them for quality and abuse, publishing the approved ones, and aggregating them into the star ratings shown across the storefront. + +Unlike most other domains in the catalog, Reviews & Ratings is modelled **without systems** — its services, data stores and flows hang directly off the domain. + +### What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|review-api]] | Service | Accepts review submissions and serves product reviews. | +| [[service\|review-moderation-worker]] | Service | Screens submitted reviews and publishes or rejects them. | +| [[service\|rating-aggregator]] | Service | Keeps each product's aggregate rating in sync as reviews are published. | +| [[container\|review-database]] | Data Store | System of record for reviews and their moderation state. | +| [[container\|rating-cache]] | Data Store | Fast read store for aggregate product ratings. | +| [[entity\|review]] | Entity | The core review aggregate. | + +The diagram below shows the services, data stores and messages that make up this domain. + + + +## How it works + +1. A customer submits a review via the [[service\|review-api]], which stores it and publishes [[event\|review-submitted]]. +2. The [[service\|review-moderation-worker]] screens the review and publishes either [[event\|review-published]] or [[event\|review-rejected]]. +3. The [[service\|rating-aggregator]] consumes [[event\|review-published]] and updates the product's aggregate rating, publishing [[event\|rating-updated]]. + +Once a review is live, customers can interact with it: [[command\|vote-review-helpful]] updates its helpful count (publishing [[event\|review-helpful-voted]]), and [[command\|flag-review]] reports it for re-moderation (publishing [[event\|review-flagged]], which the [[service\|review-moderation-worker]] re-screens). diff --git a/examples/default/domains/Reviews/openapi.yml b/examples/default/domains/Reviews/openapi.yml new file mode 100644 index 000000000..4f1046a66 --- /dev/null +++ b/examples/default/domains/Reviews/openapi.yml @@ -0,0 +1,209 @@ +openapi: 3.0.3 +info: + title: Review API + version: 1.0.0 + description: | + Public-facing API for the Reviews & Ratings domain. It is the entry point for + submitting reviews, flagging reviews, voting reviews helpful, and reading the + published reviews and aggregate rating for a product. + contact: + name: Reviews Platform + email: reviews-platform@acme.test +servers: + - url: https://api.acme.test + description: Production +tags: + - name: Reviews + description: Submit, read and interact with product reviews. +paths: + /products/{productId}/reviews: + get: + operationId: getProductReviews + summary: Get product reviews + description: Returns the published reviews and aggregate rating for a product. + tags: + - Reviews + parameters: + - name: productId + in: path + required: true + schema: + type: string + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: pageSize + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: sort + in: query + schema: + type: string + enum: [newest, highest, lowest, most_helpful] + default: newest + responses: + '200': + description: The published reviews and aggregate rating for the product. + content: + application/json: + schema: + $ref: '#/components/schemas/ProductReviews' + post: + operationId: submitReview + summary: Submit a review + description: Submits a review for a product. On success a `ReviewSubmitted` event is published and the review awaits moderation. + tags: + - Reviews + parameters: + - name: productId + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SubmitReviewRequest' + responses: + '202': + description: The review was accepted and is awaiting moderation. + content: + application/json: + schema: + $ref: '#/components/schemas/Review' + '400': + description: The request was invalid. + /reviews/{reviewId}/flags: + post: + operationId: flagReview + summary: Flag a review + description: Flags a published review for re-moderation. On success a `ReviewFlagged` event is published. + tags: + - Reviews + parameters: + - name: reviewId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FlagReviewRequest' + responses: + '202': + description: The flag was recorded. + /reviews/{reviewId}/helpful: + post: + operationId: voteReviewHelpful + summary: Vote a review helpful + description: Marks a review as helpful (or removes the vote). On success a `ReviewHelpfulVoted` event is published. + tags: + - Reviews + parameters: + - name: reviewId + in: path + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VoteReviewHelpfulRequest' + responses: + '200': + description: The helpful count was updated. +components: + schemas: + Review: + type: object + properties: + reviewId: + type: string + format: uuid + productId: + type: string + customerId: + type: string + rating: + type: integer + minimum: 1 + maximum: 5 + title: + type: string + body: + type: string + status: + type: string + enum: [submitted, published, rejected] + helpfulCount: + type: integer + minimum: 0 + submittedAt: + type: string + format: date-time + ProductReviews: + type: object + properties: + productId: + type: string + averageRating: + type: number + minimum: 0 + maximum: 5 + reviewCount: + type: integer + minimum: 0 + reviews: + type: array + items: + $ref: '#/components/schemas/Review' + SubmitReviewRequest: + type: object + required: [customerId, rating, body] + properties: + customerId: + type: string + rating: + type: integer + minimum: 1 + maximum: 5 + title: + type: string + body: + type: string + FlagReviewRequest: + type: object + required: [flaggedBy, reason] + properties: + flaggedBy: + type: string + reason: + type: string + enum: [spam, abuse, off_topic, inappropriate, other] + notes: + type: string + VoteReviewHelpfulRequest: + type: object + required: [customerId, vote] + properties: + customerId: + type: string + vote: + type: string + enum: [helpful, remove] diff --git a/examples/default/domains/Reviews/services/RatingAggregator/events/rating-updated/index.mdx b/examples/default/domains/Reviews/services/RatingAggregator/events/rating-updated/index.mdx new file mode 100644 index 000000000..22feb3771 --- /dev/null +++ b/examples/default/domains/Reviews/services/RatingAggregator/events/rating-updated/index.mdx @@ -0,0 +1,29 @@ +--- +id: rating-updated +name: Rating Updated +version: 1.0.0 +summary: | + Published when a product's aggregate rating changes as a result of a newly published review. +owners: + - reviews-platform +schemaPath: schema.json +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`RatingUpdated` is published by the [[service|rating-aggregator]] whenever a product's aggregate rating changes. Other domains (such as the storefront and search) can consume it to keep displayed ratings fresh. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/RatingAggregator/events/rating-updated/schema.json b/examples/default/domains/Reviews/services/RatingAggregator/events/rating-updated/schema.json new file mode 100644 index 000000000..27ce8dfbf --- /dev/null +++ b/examples/default/domains/Reviews/services/RatingAggregator/events/rating-updated/schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RatingUpdated", + "type": "object", + "properties": { + "productId": { "type": "string" }, + "averageRating": { "type": "number", "minimum": 0, "maximum": 5 }, + "reviewCount": { "type": "integer", "minimum": 0 }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": ["productId", "averageRating", "reviewCount", "updatedAt"] +} diff --git a/examples/default/domains/Reviews/services/RatingAggregator/index.mdx b/examples/default/domains/Reviews/services/RatingAggregator/index.mdx new file mode 100644 index 000000000..652ca2d40 --- /dev/null +++ b/examples/default/domains/Reviews/services/RatingAggregator/index.mdx @@ -0,0 +1,47 @@ +--- +id: rating-aggregator +version: 1.0.0 +name: Rating Aggregator +summary: | + Keeps each product's aggregate star rating up to date as reviews are published, and serves it fast from a cache. +styles: + icon: /icons/languages/go.svg +owners: + - reviews-platform +receives: + - id: review-published + version: 1.0.0 +sends: + - id: rating-updated + version: 1.0.0 +writesTo: + - id: rating-cache +readsFrom: + - id: review-database + - id: rating-cache +repository: + language: Go + url: 'https://github.com/acme/rating-aggregator' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Rating Aggregator** consumes [[event|review-published]] and recomputes the affected product's aggregate rating (average score and review count). It writes the result to the [[container|rating-cache]] for fast reads and publishes [[event|rating-updated]] so other domains can stay in sync. + +### Responsibilities + +| Area | Description | +|------|-------------| +| Aggregation | Recomputes average rating and review count per product. | +| Caching | Writes aggregate ratings to the [[container\|rating-cache]]. | +| Eventing | Publishes [[event\|rating-updated]] on every change. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewAPI/commands/flag-review/index.mdx b/examples/default/domains/Reviews/services/ReviewAPI/commands/flag-review/index.mdx new file mode 100644 index 000000000..74d1fbcef --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/commands/flag-review/index.mdx @@ -0,0 +1,29 @@ +--- +id: flag-review +name: Flag Review +version: 1.0.0 +summary: | + Command issued by a customer or moderator to flag a published review for re-moderation. +owners: + - reviews-platform +schemaPath: schema.json +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`FlagReview` is sent to the [[service|review-api]] when someone reports a published review as inappropriate. The API records the flag and publishes [[event|review-flagged]] so the review can be screened again. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewAPI/commands/flag-review/schema.json b/examples/default/domains/Reviews/services/ReviewAPI/commands/flag-review/schema.json new file mode 100644 index 000000000..488407d6d --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/commands/flag-review/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FlagReview", + "type": "object", + "properties": { + "reviewId": { "type": "string", "format": "uuid" }, + "flaggedBy": { "type": "string", "description": "Customer or moderator id raising the flag." }, + "reason": { "type": "string", "enum": ["spam", "abuse", "off_topic", "inappropriate", "other"] }, + "notes": { "type": "string", "maxLength": 1000 }, + "flaggedAt": { "type": "string", "format": "date-time" } + }, + "required": ["reviewId", "flaggedBy", "reason", "flaggedAt"] +} diff --git a/examples/default/domains/Reviews/services/ReviewAPI/commands/submit-review/index.mdx b/examples/default/domains/Reviews/services/ReviewAPI/commands/submit-review/index.mdx new file mode 100644 index 000000000..965b0f4f6 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/commands/submit-review/index.mdx @@ -0,0 +1,29 @@ +--- +id: submit-review +name: Submit Review +version: 1.0.0 +summary: | + Command issued by a customer to submit a review and rating for a product. +owners: + - reviews-platform +schemaPath: schema.json +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`SubmitReview` is sent to the [[service|review-api]] when a customer submits a review for a product they have purchased. The API validates and stores the review, then publishes [[event|review-submitted]]. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewAPI/commands/submit-review/schema.json b/examples/default/domains/Reviews/services/ReviewAPI/commands/submit-review/schema.json new file mode 100644 index 000000000..8b360e06f --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/commands/submit-review/schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SubmitReview", + "type": "object", + "properties": { + "reviewId": { "type": "string", "format": "uuid", "description": "Client-generated id for the review." }, + "productId": { "type": "string", "description": "The product being reviewed." }, + "customerId": { "type": "string", "description": "The customer submitting the review." }, + "rating": { "type": "integer", "minimum": 1, "maximum": 5, "description": "Star rating from 1 to 5." }, + "title": { "type": "string", "maxLength": 120 }, + "body": { "type": "string", "maxLength": 5000 }, + "submittedAt": { "type": "string", "format": "date-time" } + }, + "required": ["reviewId", "productId", "customerId", "rating", "body", "submittedAt"] +} diff --git a/examples/default/domains/Reviews/services/ReviewAPI/commands/vote-review-helpful/index.mdx b/examples/default/domains/Reviews/services/ReviewAPI/commands/vote-review-helpful/index.mdx new file mode 100644 index 000000000..9ce8ca853 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/commands/vote-review-helpful/index.mdx @@ -0,0 +1,29 @@ +--- +id: vote-review-helpful +name: Vote Review Helpful +version: 1.0.0 +summary: | + Command issued by a customer to mark a published review as helpful (or remove their vote). +owners: + - reviews-platform +schemaPath: schema.json +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`VoteReviewHelpful` is sent to the [[service|review-api]] when a customer marks a review as helpful. The API updates the helpful count and publishes [[event|review-helpful-voted]]. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewAPI/commands/vote-review-helpful/schema.json b/examples/default/domains/Reviews/services/ReviewAPI/commands/vote-review-helpful/schema.json new file mode 100644 index 000000000..9707f3d6b --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/commands/vote-review-helpful/schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VoteReviewHelpful", + "type": "object", + "properties": { + "reviewId": { "type": "string", "format": "uuid" }, + "customerId": { "type": "string" }, + "vote": { "type": "string", "enum": ["helpful", "remove"], "description": "Add a helpful vote or remove an existing one." }, + "votedAt": { "type": "string", "format": "date-time" } + }, + "required": ["reviewId", "customerId", "vote", "votedAt"] +} diff --git a/examples/default/domains/Reviews/services/ReviewAPI/events/review-flagged/index.mdx b/examples/default/domains/Reviews/services/ReviewAPI/events/review-flagged/index.mdx new file mode 100644 index 000000000..e009c522d --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/events/review-flagged/index.mdx @@ -0,0 +1,29 @@ +--- +id: review-flagged +name: Review Flagged +version: 1.0.0 +summary: | + Published when a published review is flagged and needs to be screened again. +owners: + - reviews-platform +schemaPath: schema.json +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ReviewFlagged` is published by the [[service|review-api]] when a published review is flagged. The [[service|review-moderation-worker]] consumes it and re-screens the review, which may lead to it being rejected. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewAPI/events/review-flagged/schema.json b/examples/default/domains/Reviews/services/ReviewAPI/events/review-flagged/schema.json new file mode 100644 index 000000000..b38a182b0 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/events/review-flagged/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReviewFlagged", + "type": "object", + "properties": { + "reviewId": { "type": "string", "format": "uuid" }, + "productId": { "type": "string" }, + "reason": { "type": "string", "enum": ["spam", "abuse", "off_topic", "inappropriate", "other"] }, + "flagCount": { "type": "integer", "minimum": 1, "description": "Total flags this review has now received." }, + "flaggedAt": { "type": "string", "format": "date-time" } + }, + "required": ["reviewId", "productId", "reason", "flaggedAt"] +} diff --git a/examples/default/domains/Reviews/services/ReviewAPI/events/review-helpful-voted/index.mdx b/examples/default/domains/Reviews/services/ReviewAPI/events/review-helpful-voted/index.mdx new file mode 100644 index 000000000..7eef3fa54 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/events/review-helpful-voted/index.mdx @@ -0,0 +1,29 @@ +--- +id: review-helpful-voted +name: Review Helpful Voted +version: 1.0.0 +summary: | + Published when a review's helpful count changes as a result of a customer vote. +owners: + - reviews-platform +schemaPath: schema.json +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ReviewHelpfulVoted` is published by the [[service|review-api]] whenever a review's helpful count changes. It lets other surfaces (such as the storefront) keep "most helpful" ordering fresh. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewAPI/events/review-helpful-voted/schema.json b/examples/default/domains/Reviews/services/ReviewAPI/events/review-helpful-voted/schema.json new file mode 100644 index 000000000..b8e2413ce --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/events/review-helpful-voted/schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReviewHelpfulVoted", + "type": "object", + "properties": { + "reviewId": { "type": "string", "format": "uuid" }, + "productId": { "type": "string" }, + "helpfulCount": { "type": "integer", "minimum": 0 }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": ["reviewId", "productId", "helpfulCount", "updatedAt"] +} diff --git a/examples/default/domains/Reviews/services/ReviewAPI/events/review-submitted/index.mdx b/examples/default/domains/Reviews/services/ReviewAPI/events/review-submitted/index.mdx new file mode 100644 index 000000000..ac2dfabd5 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/events/review-submitted/index.mdx @@ -0,0 +1,29 @@ +--- +id: review-submitted +name: Review Submitted +version: 1.0.0 +summary: | + Published when a customer submits a review. The review is stored but not yet visible — it awaits moderation. +owners: + - reviews-platform +schemaPath: schema.json +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ReviewSubmitted` is published by the [[service|review-api]] once a review has been accepted and stored. The [[service|review-moderation-worker]] consumes it to screen the review before it can be published. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewAPI/events/review-submitted/schema.json b/examples/default/domains/Reviews/services/ReviewAPI/events/review-submitted/schema.json new file mode 100644 index 000000000..308f882b0 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/events/review-submitted/schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReviewSubmitted", + "type": "object", + "properties": { + "reviewId": { "type": "string", "format": "uuid" }, + "productId": { "type": "string" }, + "customerId": { "type": "string" }, + "rating": { "type": "integer", "minimum": 1, "maximum": 5 }, + "title": { "type": "string" }, + "body": { "type": "string" }, + "submittedAt": { "type": "string", "format": "date-time" } + }, + "required": ["reviewId", "productId", "customerId", "rating", "submittedAt"] +} diff --git a/examples/default/domains/Reviews/services/ReviewAPI/index.mdx b/examples/default/domains/Reviews/services/ReviewAPI/index.mdx new file mode 100644 index 000000000..0eccf6501 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/index.mdx @@ -0,0 +1,58 @@ +--- +id: review-api +version: 1.0.0 +name: Review API +summary: | + The public-facing API for product reviews. Accepts review submissions, serves published reviews, and is the entry point into the Reviews & Ratings domain. +styles: + icon: /icons/languages/nodejs.svg +owners: + - reviews-platform +receives: + - id: submit-review + version: 1.0.0 + - id: flag-review + version: 1.0.0 + - id: vote-review-helpful + version: 1.0.0 + - id: get-product-reviews + version: 1.0.0 +sends: + - id: review-submitted + version: 1.0.0 + - id: review-flagged + version: 1.0.0 + - id: review-helpful-voted + version: 1.0.0 +writesTo: + - id: review-database +readsFrom: + - id: review-database + - id: rating-cache +repository: + language: TypeScript + url: 'https://github.com/acme/review-api' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Review API** is the front door to the Reviews & Ratings domain. It validates incoming [[command|submit-review]] commands, persists reviews to the [[container|review-database]], and publishes [[event|review-submitted]]. It also serves [[query|get-product-reviews]], reading published reviews from the [[container|review-database]] and aggregate ratings from the [[container|rating-cache]]. + +### Responsibilities + +| Area | Description | +|------|-------------| +| Command handling | Validates and applies [[command\|submit-review]]. | +| Reads | Serves [[query\|get-product-reviews]] from the review database and rating cache. | +| Eventing | Publishes [[event\|review-submitted]] for moderation. | +| Persistence | Writes to the [[container\|review-database]]; reads from the [[container\|review-database]] and [[container\|rating-cache]]. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewAPI/queries/get-product-reviews/index.mdx b/examples/default/domains/Reviews/services/ReviewAPI/queries/get-product-reviews/index.mdx new file mode 100644 index 000000000..a218643d7 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/queries/get-product-reviews/index.mdx @@ -0,0 +1,24 @@ +--- +id: get-product-reviews +name: Get Product Reviews +version: 1.0.0 +summary: | + Query to fetch the published reviews and aggregate rating for a product. +owners: + - reviews-platform +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`GetProductReviews` is served by the [[service|review-api]]. It returns the published reviews for a product along with its aggregate rating, read from the [[container|review-database]] and the [[container|rating-cache]]. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewAPI/queries/get-product-reviews/schema.json b/examples/default/domains/Reviews/services/ReviewAPI/queries/get-product-reviews/schema.json new file mode 100644 index 000000000..a65eb3384 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewAPI/queries/get-product-reviews/schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetProductReviews", + "type": "object", + "properties": { + "productId": { "type": "string", "description": "The product to fetch reviews for." }, + "page": { "type": "integer", "minimum": 1, "default": 1 }, + "pageSize": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, + "sort": { "type": "string", "enum": ["newest", "highest", "lowest", "most_helpful"], "default": "newest" } + }, + "required": ["productId"] +} diff --git a/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-published/index.mdx b/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-published/index.mdx new file mode 100644 index 000000000..774093e22 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-published/index.mdx @@ -0,0 +1,29 @@ +--- +id: review-published +name: Review Published +version: 1.0.0 +summary: | + Published when a submitted review passes moderation and becomes visible on the storefront. +owners: + - reviews-platform +schemaPath: schema.json +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ReviewPublished` is published by the [[service|review-moderation-worker]] when a review passes moderation. The [[service|rating-aggregator]] consumes it to update the product's aggregate rating. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-published/schema.json b/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-published/schema.json new file mode 100644 index 000000000..6420fec00 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-published/schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReviewPublished", + "type": "object", + "properties": { + "reviewId": { "type": "string", "format": "uuid" }, + "productId": { "type": "string" }, + "customerId": { "type": "string" }, + "rating": { "type": "integer", "minimum": 1, "maximum": 5 }, + "publishedAt": { "type": "string", "format": "date-time" } + }, + "required": ["reviewId", "productId", "rating", "publishedAt"] +} diff --git a/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-rejected/index.mdx b/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-rejected/index.mdx new file mode 100644 index 000000000..1074977a6 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-rejected/index.mdx @@ -0,0 +1,29 @@ +--- +id: review-rejected +name: Review Rejected +version: 1.0.0 +summary: | + Published when a submitted review fails moderation (spam, abuse or policy violation) and will not be shown. +owners: + - reviews-platform +schemaPath: schema.json +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`ReviewRejected` is published by the [[service|review-moderation-worker]] when a review fails moderation. The review is kept for audit but never published to the storefront. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-rejected/schema.json b/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-rejected/schema.json new file mode 100644 index 000000000..aa777cc49 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewModerationWorker/events/review-rejected/schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ReviewRejected", + "type": "object", + "properties": { + "reviewId": { "type": "string", "format": "uuid" }, + "productId": { "type": "string" }, + "reason": { "type": "string", "enum": ["spam", "abuse", "off_topic", "policy_violation"] }, + "rejectedAt": { "type": "string", "format": "date-time" } + }, + "required": ["reviewId", "productId", "reason", "rejectedAt"] +} diff --git a/examples/default/domains/Reviews/services/ReviewModerationWorker/index.mdx b/examples/default/domains/Reviews/services/ReviewModerationWorker/index.mdx new file mode 100644 index 000000000..6b856a871 --- /dev/null +++ b/examples/default/domains/Reviews/services/ReviewModerationWorker/index.mdx @@ -0,0 +1,50 @@ +--- +id: review-moderation-worker +version: 1.0.0 +name: Review Moderation Worker +summary: | + Asynchronous worker that screens submitted reviews for spam, abuse and policy violations, then publishes or rejects them. +styles: + icon: /icons/languages/nodejs.svg +owners: + - reviews-platform +receives: + - id: review-submitted + version: 1.0.0 + - id: review-flagged + version: 1.0.0 +sends: + - id: review-published + version: 1.0.0 + - id: review-rejected + version: 1.0.0 +writesTo: + - id: review-database +readsFrom: + - id: review-database +repository: + language: TypeScript + url: 'https://github.com/acme/review-moderation-worker' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Review Moderation Worker** consumes [[event|review-submitted]] (and [[event|review-flagged]] for already-published reviews), screens each review (automated checks plus thresholds for manual review), and records the outcome on the [[container|review-database]]. It then publishes either [[event|review-published]] or [[event|review-rejected]]. + +### Responsibilities + +| Area | Description | +|------|-------------| +| Moderation | Screens reviews for spam, abuse and policy violations. | +| Outcome | Publishes [[event\|review-published]] or [[event\|review-rejected]]. | +| Persistence | Records the moderation decision on the [[container\|review-database]]. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Shopping/entities/cart-item/index.mdx b/examples/default/domains/Shopping/entities/cart-item/index.mdx new file mode 100644 index 000000000..0f6a7b38e --- /dev/null +++ b/examples/default/domains/Shopping/entities/cart-item/index.mdx @@ -0,0 +1,38 @@ +--- +id: cart-item +name: Cart Item +version: 1.0.0 +identifier: cartItemId +summary: A product variant and quantity inside a cart. +owners: + - shopping-platform +properties: + - name: cartItemId + type: UUID + required: true + description: Unique cart item identifier. + - name: cartId + type: UUID + required: true + description: Cart this item belongs to. + references: cart + relationType: belongsTo + referencesIdentifier: cartId + - name: productId + type: UUID + required: true + description: Product selected by the customer. + references: product + relationType: references + referencesIdentifier: productId + - name: quantity + type: integer + required: true + description: Quantity requested. +--- + +## Overview + +Cart Item records the customer's intent to buy a product variant before checkout. It is owned by [[entity|cart]]. + + diff --git a/examples/default/domains/Shopping/entities/cart/index.mdx b/examples/default/domains/Shopping/entities/cart/index.mdx new file mode 100644 index 000000000..bd298f675 --- /dev/null +++ b/examples/default/domains/Shopping/entities/cart/index.mdx @@ -0,0 +1,40 @@ +--- +id: cart +name: Cart +version: 1.0.0 +identifier: cartId +aggregateRoot: true +summary: The customer's active collection of items before an order exists. +owners: + - shopping-platform +properties: + - name: cartId + type: UUID + required: true + description: Unique cart identifier. + - name: customerId + type: UUID + required: false + description: Customer that owns the cart, if authenticated. + references: customer-profile + relationType: belongsTo + referencesIdentifier: customerId + - name: status + type: string + required: true + description: Current cart state. + enum: + - active + - checked-out + - abandoned + - name: updatedAt + type: datetime + required: true + description: Time the cart last changed. +--- + +## Overview + +Cart is the Shopping domain aggregate. It accepts item changes and emits [[event|cart-checked-out]] when the customer commits to buy. + + diff --git a/examples/default/domains/Shopping/entities/discount/index.mdx b/examples/default/domains/Shopping/entities/discount/index.mdx new file mode 100644 index 000000000..94f68325a --- /dev/null +++ b/examples/default/domains/Shopping/entities/discount/index.mdx @@ -0,0 +1,35 @@ +--- +id: discount +name: Discount +version: 1.0.0 +identifier: discountId +summary: The calculated benefit applied to a cart or cart item. +owners: + - shopping-platform +properties: + - name: discountId + type: UUID + required: true + description: Unique discount identifier. + - name: promotionId + type: UUID + required: false + description: Promotion that produced the discount. + references: promotion + relationType: derivedFrom + referencesIdentifier: promotionId + - name: amount + type: decimal + required: true + description: Monetary value of the discount. + - name: reason + type: string + required: true + description: Human-readable explanation for the discount. +--- + +## Overview + +Discount is a calculated result from [[command|calculate-discount]]. It is separate from cart persistence so promotion logic can evolve independently. + + diff --git a/examples/default/domains/Shopping/entities/promotion/index.mdx b/examples/default/domains/Shopping/entities/promotion/index.mdx new file mode 100644 index 000000000..052c9bf04 --- /dev/null +++ b/examples/default/domains/Shopping/entities/promotion/index.mdx @@ -0,0 +1,40 @@ +--- +id: promotion +name: Promotion +version: 1.0.0 +identifier: promotionId +aggregateRoot: true +summary: A commercial offer evaluated against a cart. +owners: + - shopping-platform +properties: + - name: promotionId + type: UUID + required: true + description: Unique promotion identifier. + - name: code + type: string + required: false + description: Optional customer-entered promotion code. + - name: categoryId + type: UUID + required: false + description: Category the promotion applies to when scoped. + references: category + relationType: appliesTo + referencesIdentifier: categoryId + - name: status + type: string + required: true + description: Promotion lifecycle state. + - name: startsAt + type: datetime + required: true + description: When the promotion becomes active. +--- + +## Overview + +Promotion is owned by the Promotion System and is evaluated when Shopping calculates discounts for a cart. + + diff --git a/examples/default/domains/Shopping/index.mdx b/examples/default/domains/Shopping/index.mdx new file mode 100644 index 000000000..83673c9b0 --- /dev/null +++ b/examples/default/domains/Shopping/index.mdx @@ -0,0 +1,67 @@ +--- +id: shopping +name: Shopping +version: 1.0.0 +summary: | + The Shopping domain owns the customer's path to purchase — building a cart, applying promotions, and checking out. It coordinates the cart and the discounts that apply to it. +owners: + - shopping-platform +systems: + - id: cart-system + version: 1.0.0 + - id: promotion-system + version: 1.0.0 +entities: + - id: cart + version: 1.0.0 + - id: cart-item + version: 1.0.0 + - id: promotion + version: 1.0.0 + - id: discount + version: 1.0.0 +badges: + - content: Core domain + backgroundColor: blue + textColor: blue + icon: RectangleGroupIcon + - content: Business Critical + backgroundColor: red + textColor: red + icon: ShieldCheckIcon +--- + +## Overview + +The **Shopping** domain is responsible for everything between browsing and buying — the shopping cart and the promotions applied to it. It is split into two systems: + + + + + + +### How the systems work together + +The **Cart System** owns the customer's cart and the checkout flow. When pricing a cart, it asks the **Promotion System** to calculate the discounts that apply. When the customer checks out, the Cart System publishes an event the rest of the business reacts to. + +## System Diagram + +The systems in this domain, how they relate, and the people who interact with them. + + + +## Resource Diagram + +The components that make up this domain. + + diff --git a/examples/default/domains/Shopping/systems/cart-system/containers/cart-database/index.mdx b/examples/default/domains/Shopping/systems/cart-system/containers/cart-database/index.mdx new file mode 100644 index 000000000..8e1e634f0 --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/containers/cart-database/index.mdx @@ -0,0 +1,39 @@ +--- +id: cart-database +name: Cart Database +version: 1.0.0 +summary: PostgreSQL database that is the system of record for shopping carts and their items. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for shopping carts +classification: internal +retention: 90d +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **Cart Database** is the authoritative store for shopping carts at Acme Inc. The [[service|cart-api]] reads from and writes to it as customers add, remove and check out items. + +### What does it store? + +- **Carts** — one row per cart: id, customer, status and timestamps. +- **Cart Items** — one row per item in a cart: product, quantity and unit price. + +### Schema + + + + + + + +### Retention + +Carts are transient. Abandoned carts are pruned after **90 days** (see frontmatter). A checked-out cart's contents live on in the [[event|cart-checked-out]] event and downstream order records. diff --git a/examples/default/domains/Shopping/systems/cart-system/containers/cart-database/schema.sql b/examples/default/domains/Shopping/systems/cart-system/containers/cart-database/schema.sql new file mode 100644 index 000000000..113fc17f4 --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/containers/cart-database/schema.sql @@ -0,0 +1,22 @@ +-- Cart Database — system of record for shopping carts + +CREATE TABLE carts ( + cart_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + customer_id UUID, + status TEXT NOT NULL DEFAULT 'OPEN' + CHECK (status IN ('OPEN', 'CHECKED_OUT', 'ABANDONED')), + currency CHAR(3) NOT NULL DEFAULT 'USD', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE cart_items ( + cart_id UUID NOT NULL REFERENCES carts (cart_id) ON DELETE CASCADE, + product_id UUID NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + unit_price_cents INTEGER NOT NULL CHECK (unit_price_cents >= 0), + PRIMARY KEY (cart_id, product_id) +); + +CREATE INDEX idx_carts_customer ON carts (customer_id); +CREATE INDEX idx_carts_status ON carts (status); diff --git a/examples/default/domains/Shopping/systems/cart-system/index.mdx b/examples/default/domains/Shopping/systems/cart-system/index.mdx new file mode 100644 index 000000000..d1b8c9108 --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/index.mdx @@ -0,0 +1,53 @@ +--- +id: cart-system +name: Cart System +version: 1.0.0 +summary: | + Internal system that owns the customer's shopping cart and the checkout flow. It is the source of truth for cart contents and publishes an event when a cart is checked out. +owners: + - shopping-platform +services: + - id: cart-api +containers: + - id: cart-database +relationships: + - id: promotion-system + label: calculates discounts via +actors: + - id: shopper + name: Shopper + label: adds items and checks out + direction: inbound +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Cart System** owns the shopping cart at Acme Inc. It accepts commands to add and remove items and to check out, persists cart contents in the [[container|cart-database]], and asks the [[system|promotion-system]] to calculate the discounts that apply. When a cart is checked out it publishes a [[event|cart-checked-out]] event. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|cart-api]] | Service | Public-facing API. Handles add/remove/checkout commands and publishes the checkout event. | +| [[container\|cart-database]] | Data store | PostgreSQL system of record for shopping carts and their items. | + +## Messages this system publishes + +- [[event|cart-checked-out]] — a customer has checked out their cart. diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/AddItemToCart/index.mdx b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/AddItemToCart/index.mdx new file mode 100644 index 000000000..0d310bef9 --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/AddItemToCart/index.mdx @@ -0,0 +1,32 @@ +--- +id: add-item-to-cart +name: Add Item To Cart +version: 1.0.0 +summary: | + Command to add an item to a shopping cart. +owners: + - shopping-platform +schemaPath: schema.json +operation: + method: POST + path: /carts/{cartId}/items + statusCodes: ['200', '400', '404'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`AddItemToCart` is handled by the [[service|cart-api]]. It adds an item (or increases its quantity) in the cart and persists the change to the [[container|cart-database]]. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/AddItemToCart/schema.json b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/AddItemToCart/schema.json new file mode 100644 index 000000000..2ff550e23 --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/AddItemToCart/schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AddItemToCart", + "description": "Command to add an item to a shopping cart", + "type": "object", + "properties": { + "cartId": { + "description": "Unique identifier of the cart", + "type": "string", + "format": "uuid" + }, + "productId": { + "description": "Identifier of the product to add", + "type": "string", + "format": "uuid" + }, + "quantity": { + "description": "Number of units to add", + "type": "integer", + "minimum": 1, + "default": 1 + } + }, + "required": ["cartId", "productId", "quantity"] +} diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/CheckoutCart/index.mdx b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/CheckoutCart/index.mdx new file mode 100644 index 000000000..8b2dde56c --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/CheckoutCart/index.mdx @@ -0,0 +1,32 @@ +--- +id: checkout-cart +name: Checkout Cart +version: 1.0.0 +summary: | + Command to check out a shopping cart. +owners: + - shopping-platform +schemaPath: schema.json +operation: + method: POST + path: /carts/{cartId}/checkout + statusCodes: ['200', '400', '404', '409'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CheckoutCart` is handled by the [[service|cart-api]]. It finalises the cart — pricing it (including discounts from the [[system|promotion-system]]) — and on success publishes a [[event|cart-checked-out]] event. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/CheckoutCart/schema.json b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/CheckoutCart/schema.json new file mode 100644 index 000000000..57c74aed5 --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/CheckoutCart/schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CheckoutCart", + "description": "Command to check out a shopping cart", + "type": "object", + "properties": { + "cartId": { + "description": "Unique identifier of the cart to check out", + "type": "string", + "format": "uuid" + }, + "customerId": { + "description": "Identifier of the customer checking out", + "type": "string", + "format": "uuid" + }, + "promotionCode": { + "description": "Optional promotion code to apply at checkout", + "type": "string" + } + }, + "required": ["cartId", "customerId"] +} diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/RemoveItemFromCart/index.mdx b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/RemoveItemFromCart/index.mdx new file mode 100644 index 000000000..6fd484071 --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/RemoveItemFromCart/index.mdx @@ -0,0 +1,32 @@ +--- +id: remove-item-from-cart +name: Remove Item From Cart +version: 1.0.0 +summary: | + Command to remove an item from a shopping cart. +owners: + - shopping-platform +schemaPath: schema.json +operation: + method: DELETE + path: /carts/{cartId}/items/{productId} + statusCodes: ['200', '404'] +sidebar: + badge: 'DELETE' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`RemoveItemFromCart` is handled by the [[service|cart-api]]. It removes an item from the cart (or decreases its quantity) and persists the change to the [[container|cart-database]]. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/RemoveItemFromCart/schema.json b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/RemoveItemFromCart/schema.json new file mode 100644 index 000000000..d3dcbfe60 --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/commands/RemoveItemFromCart/schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RemoveItemFromCart", + "description": "Command to remove an item from a shopping cart", + "type": "object", + "properties": { + "cartId": { + "description": "Unique identifier of the cart", + "type": "string", + "format": "uuid" + }, + "productId": { + "description": "Identifier of the product to remove", + "type": "string", + "format": "uuid" + }, + "quantity": { + "description": "Number of units to remove. If omitted, removes the item entirely.", + "type": "integer", + "minimum": 1 + } + }, + "required": ["cartId", "productId"] +} diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/events/CartCheckedOut/index.mdx b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/events/CartCheckedOut/index.mdx new file mode 100644 index 000000000..fc2b7875c --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/events/CartCheckedOut/index.mdx @@ -0,0 +1,29 @@ +--- +id: cart-checked-out +name: Cart Checked Out +version: 1.0.0 +summary: | + Published when a customer has checked out their cart. +owners: + - shopping-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CartCheckedOut` is published by the [[service|cart-api]] when a customer checks out. Downstream systems consume this event to create an order, take payment, and begin fulfilment. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/events/CartCheckedOut/schema.json b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/events/CartCheckedOut/schema.json new file mode 100644 index 000000000..3242b297e --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/events/CartCheckedOut/schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CartCheckedOut", + "description": "Emitted when a customer checks out their cart", + "type": "object", + "properties": { + "eventId": { + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "cartId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "productId": { + "type": "string", + "format": "uuid" + }, + "quantity": { + "type": "integer", + "minimum": 1 + }, + "unitPrice": { + "description": "Price per unit in minor units (e.g. cents)", + "type": "integer" + } + }, + "required": ["productId", "quantity", "unitPrice"] + } + }, + "subtotal": { + "description": "Total before discounts, in minor units (e.g. cents)", + "type": "integer" + }, + "discount": { + "description": "Total discount applied, in minor units (e.g. cents)", + "type": "integer" + }, + "total": { + "description": "Final total, in minor units (e.g. cents)", + "type": "integer" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + } + }, + "required": ["eventId", "occurredAt", "cartId", "customerId", "items", "total", "currency"] +} diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/index.mdx b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/index.mdx new file mode 100644 index 000000000..45fc15d95 --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/index.mdx @@ -0,0 +1,62 @@ +--- +id: cart-api +version: 1.0.0 +name: Cart API +summary: | + The public-facing API for shopping carts. Handles commands to add and remove items and to check out, and publishes an event when a cart is checked out. +styles: + icon: /icons/languages/nodejs.svg +owners: + - shopping-platform +receives: + - id: add-item-to-cart + version: 1.0.0 + - id: remove-item-from-cart + version: 1.0.0 + - id: checkout-cart + version: 1.0.0 +sends: + - id: cart-checked-out + version: 1.0.0 + - id: calculate-discount + version: 1.0.0 +writesTo: + - id: cart-database +readsFrom: + - id: cart-database +repository: + language: TypeScript + url: 'https://github.com/acme/cart-api' +specifications: + - type: openapi + path: openapi.yml + name: Cart API +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Cart API** is the front door to the Cart System. It validates incoming commands, persists cart contents to the [[container|cart-database]], asks the [[system|promotion-system]] to calculate discounts via [[command|calculate-discount]], and publishes a [[event|cart-checked-out]] event when the customer checks out. + + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Cart management | Validates and applies [[command\|add-item-to-cart]], [[command\|remove-item-from-cart]] and [[command\|checkout-cart]]. | +| Discounts | Requests pricing from the Promotion System via [[command\|calculate-discount]]. | +| Event publishing | Emits [[event\|cart-checked-out]] when a cart is checked out. | +| Persistence | Reads from and writes to the [[container\|cart-database]] (system of record). | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/openapi.yml b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/openapi.yml new file mode 100644 index 000000000..3671bc55c --- /dev/null +++ b/examples/default/domains/Shopping/systems/cart-system/services/CartAPI/openapi.yml @@ -0,0 +1,200 @@ +openapi: 3.0.3 +info: + title: Cart API + version: 1.0.0 + description: | + Public-facing API for the Cart System. Customers use it to build a cart — + adding and removing items — and to check out. At checkout the cart is priced + (including discounts from the Promotion System) and a `CartCheckedOut` event + is published. + contact: + name: Shopping Platform + email: shopping-platform@acme.test +servers: + - url: https://api.acme.test + description: Production +tags: + - name: Cart + description: Build and check out a shopping cart. +paths: + /carts/{cartId}/items: + parameters: + - $ref: '#/components/parameters/CartId' + post: + operationId: addItemToCart + summary: Add an item to the cart + tags: + - Cart + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddItemRequest' + responses: + '200': + description: The item was added; the updated cart is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/Cart' + '400': + description: The request was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: The cart was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /carts/{cartId}/items/{productId}: + parameters: + - $ref: '#/components/parameters/CartId' + - name: productId + in: path + required: true + description: Identifier of the product to remove. + schema: + type: string + format: uuid + delete: + operationId: removeItemFromCart + summary: Remove an item from the cart + tags: + - Cart + responses: + '200': + description: The item was removed; the updated cart is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/Cart' + '404': + description: The cart or item was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /carts/{cartId}/checkout: + parameters: + - $ref: '#/components/parameters/CartId' + post: + operationId: checkoutCart + summary: Check out the cart + description: Prices the cart (including discounts) and, on success, publishes a `CartCheckedOut` event. + tags: + - Cart + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CheckoutRequest' + responses: + '200': + description: The cart was checked out. + content: + application/json: + schema: + $ref: '#/components/schemas/Cart' + '400': + description: The request was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: The cart was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: The cart is empty or already checked out. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + parameters: + CartId: + name: cartId + in: path + required: true + description: Unique identifier of the cart. + schema: + type: string + format: uuid + schemas: + Cart: + type: object + required: [cartId, status, items, currency] + properties: + cartId: + type: string + format: uuid + customerId: + type: string + format: uuid + status: + type: string + enum: [OPEN, CHECKED_OUT, ABANDONED] + items: + type: array + items: + $ref: '#/components/schemas/CartItem' + subtotal: + type: integer + description: Total before discounts, in minor units (e.g. cents). + discount: + type: integer + description: Total discount applied, in minor units (e.g. cents). + total: + type: integer + description: Final total, in minor units (e.g. cents). + currency: + type: string + pattern: '^[A-Z]{3}$' + CartItem: + type: object + required: [productId, quantity, unitPrice] + properties: + productId: + type: string + format: uuid + quantity: + type: integer + minimum: 1 + unitPrice: + type: integer + description: Price per unit in minor units (e.g. cents). + AddItemRequest: + type: object + required: [productId, quantity] + properties: + productId: + type: string + format: uuid + quantity: + type: integer + minimum: 1 + CheckoutRequest: + type: object + required: [customerId] + properties: + customerId: + type: string + format: uuid + promotionCode: + type: string + Error: + type: object + required: [code, message] + properties: + code: + type: string + message: + type: string diff --git a/examples/default/domains/Shopping/systems/promotion-system/containers/promotion-database/index.mdx b/examples/default/domains/Shopping/systems/promotion-system/containers/promotion-database/index.mdx new file mode 100644 index 000000000..f552037eb --- /dev/null +++ b/examples/default/domains/Shopping/systems/promotion-system/containers/promotion-database/index.mdx @@ -0,0 +1,40 @@ +--- +id: promotion-database +name: Promotion Database +version: 1.0.0 +summary: PostgreSQL store of promotion and discount rules. +container_type: database +technology: postgres@16 +authoritative: true +access_mode: readWrite +purpose: System of record for promotion and discount rules +classification: internal +retention: indefinite +residency: eu-west-1 +styles: + icon: /icons/database/postgresql.svg +--- + + + +### What is this? + +The **Promotion Database** holds the promotion and discount rules that the [[service|promotion-service]] evaluates when pricing a cart. It is the source of truth for what promotions exist, who they apply to, and when they are valid. + +### What does it store? + +- **Promotions** — one row per promotion: code, type (percentage / fixed amount), value, and validity window. +- **Eligibility rules** — the conditions under which a promotion applies (minimum spend, customer segment, etc.). + +### Schema + + + + + + + +### Access patterns + +- The [[service|promotion-service]] reads rules when handling [[command|calculate-discount]]. +- Rules are managed by the promotions team and change infrequently relative to read volume. diff --git a/examples/default/domains/Shopping/systems/promotion-system/containers/promotion-database/schema.sql b/examples/default/domains/Shopping/systems/promotion-system/containers/promotion-database/schema.sql new file mode 100644 index 000000000..125d58a1b --- /dev/null +++ b/examples/default/domains/Shopping/systems/promotion-system/containers/promotion-database/schema.sql @@ -0,0 +1,21 @@ +-- Promotion Database — system of record for promotion and discount rules + +CREATE TABLE promotions ( + promotion_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code TEXT NOT NULL UNIQUE, + description TEXT, + type TEXT NOT NULL CHECK (type IN ('PERCENTAGE', 'FIXED_AMOUNT')), + value INTEGER NOT NULL CHECK (value >= 0), + starts_at TIMESTAMPTZ NOT NULL, + ends_at TIMESTAMPTZ, + active BOOLEAN NOT NULL DEFAULT true +); + +CREATE TABLE eligibility_rules ( + promotion_id UUID NOT NULL REFERENCES promotions (promotion_id) ON DELETE CASCADE, + min_spend_cents INTEGER CHECK (min_spend_cents >= 0), + customer_segment TEXT, + PRIMARY KEY (promotion_id) +); + +CREATE INDEX idx_promotions_active ON promotions (active) WHERE active = true; diff --git a/examples/default/domains/Shopping/systems/promotion-system/index.mdx b/examples/default/domains/Shopping/systems/promotion-system/index.mdx new file mode 100644 index 000000000..658f2f878 --- /dev/null +++ b/examples/default/domains/Shopping/systems/promotion-system/index.mdx @@ -0,0 +1,48 @@ +--- +id: promotion-system +name: Promotion System +version: 1.0.0 +summary: | + Internal system that calculates the discounts and promotions that apply to a cart. It owns promotion rules and returns calculated discounts to the Cart System. +owners: + - shopping-platform +services: + - id: promotion-service +containers: + - id: promotion-database +relationships: + - id: cart-system + label: provides discounts to +badges: + - content: Internal + backgroundColor: gray + textColor: gray + icon: LockClosedIcon +--- + +## Overview + +The **Promotion System** owns the discount and promotion rules at Acme Inc. It receives requests to calculate the discount for a cart, evaluates the applicable rules from the [[container|promotion-database]], and publishes a [[event|discount-calculated]] event with the result. It provides discounts to the [[system|cart-system]]. + +## Context Diagram + +How this system relates to the other systems around it. + + + +## Resource Diagram + +The services, data stores and messages that make up this system. + + + +## What's inside + +| Component | Type | Responsibility | +|-----------|------|----------------| +| [[service\|promotion-service]] | Service | Evaluates promotion rules and calculates discounts for a cart. | +| [[container\|promotion-database]] | Data store | PostgreSQL store of promotion and discount rules. | + +## Messages this system publishes + +- [[event|discount-calculated]] — a discount has been calculated for a cart. diff --git a/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/commands/CalculateDiscount/index.mdx b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/commands/CalculateDiscount/index.mdx new file mode 100644 index 000000000..70fc6186f --- /dev/null +++ b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/commands/CalculateDiscount/index.mdx @@ -0,0 +1,32 @@ +--- +id: calculate-discount +name: Calculate Discount +version: 1.0.0 +summary: | + Command to calculate the discount that applies to a cart. +owners: + - shopping-platform +schemaPath: schema.json +operation: + method: POST + path: /discounts/calculate + statusCodes: ['200', '400'] +sidebar: + badge: 'POST' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`CalculateDiscount` is handled by the [[service|promotion-service]]. It is sent by the [[service|cart-api]] when pricing a cart. The service evaluates the applicable promotion rules and returns the discount, also publishing a [[event|discount-calculated]] event. + +## Architecture diagram + + + +## Schema + + + +
    diff --git a/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/commands/CalculateDiscount/schema.json b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/commands/CalculateDiscount/schema.json new file mode 100644 index 000000000..8fb6c243c --- /dev/null +++ b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/commands/CalculateDiscount/schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CalculateDiscount", + "description": "Command to calculate the discount for a cart, and the discount returned", + "type": "object", + "properties": { + "request": { + "type": "object", + "properties": { + "cartId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "subtotal": { + "description": "Cart subtotal before discounts, in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "promotionCode": { + "description": "Optional promotion code supplied by the customer", + "type": "string" + } + }, + "required": ["cartId", "subtotal", "currency"] + }, + "response": { + "type": "object", + "properties": { + "discount": { + "description": "Total discount to apply, in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "appliedPromotions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["discount"] + } + }, + "required": ["request", "response"] +} diff --git a/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/events/DiscountCalculated/index.mdx b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/events/DiscountCalculated/index.mdx new file mode 100644 index 000000000..eb2dc0182 --- /dev/null +++ b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/events/DiscountCalculated/index.mdx @@ -0,0 +1,29 @@ +--- +id: discount-calculated +name: Discount Calculated +version: 1.0.0 +summary: | + Published when a discount has been calculated for a cart. +owners: + - shopping-platform +badges: + - content: 'Broker:Kafka' + backgroundColor: blue + textColor: blue + icon: BoltIcon +schemaPath: schema.json +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +`DiscountCalculated` is published by the [[service|promotion-service]] after evaluating the promotion rules for a cart. The [[system|cart-system]] uses the result to price the cart, and other systems can consume it for analytics. + + + +## Architecture diagram + + + +
    diff --git a/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/events/DiscountCalculated/schema.json b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/events/DiscountCalculated/schema.json new file mode 100644 index 000000000..a4f22b48a --- /dev/null +++ b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/events/DiscountCalculated/schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DiscountCalculated", + "description": "Emitted when a discount has been calculated for a cart", + "type": "object", + "properties": { + "eventId": { + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "cartId": { + "type": "string", + "format": "uuid" + }, + "discount": { + "description": "Total discount applied, in minor units (e.g. cents)", + "type": "integer", + "minimum": 0 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "appliedPromotions": { + "description": "Identifiers of the promotions that were applied", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["eventId", "occurredAt", "cartId", "discount", "currency"] +} diff --git a/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/index.mdx b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/index.mdx new file mode 100644 index 000000000..4ea87ec2b --- /dev/null +++ b/examples/default/domains/Shopping/systems/promotion-system/services/PromotionService/index.mdx @@ -0,0 +1,49 @@ +--- +id: promotion-service +version: 1.0.0 +name: Promotion Service +summary: | + Evaluates promotion rules and calculates the discount that applies to a cart. Receives discount requests and publishes the calculated result. +styles: + icon: /icons/languages/go.svg +owners: + - shopping-platform +receives: + - id: calculate-discount + version: 1.0.0 +sends: + - id: discount-calculated + version: 1.0.0 +readsFrom: + - id: promotion-database +repository: + language: Go + url: 'https://github.com/acme/promotion-service' +--- + +import Footer from '@catalog/components/footer.astro'; + +## Overview + +The **Promotion Service** is the heart of the Promotion System. It receives [[command|calculate-discount]] requests from the [[system|cart-system]], evaluates the applicable rules from the [[container|promotion-database]], and publishes a [[event|discount-calculated]] event with the result. + + + + + + +### Responsibilities + +| Area | Description | +|------|-------------| +| Discount calculation | Handles [[command\|calculate-discount]] and evaluates promotion rules. | +| Rules | Reads promotion and discount rules from the [[container\|promotion-database]]. | +| Event publishing | Emits [[event\|discount-calculated]] with the calculated result. | + +## Architecture diagram + + + + + +
    diff --git a/examples/default/domains/Shopping/ubiquitous-language.mdx b/examples/default/domains/Shopping/ubiquitous-language.mdx new file mode 100644 index 000000000..60676a0fb --- /dev/null +++ b/examples/default/domains/Shopping/ubiquitous-language.mdx @@ -0,0 +1,48 @@ +--- +dictionary: + - id: Cart + name: Cart + summary: "A customer's in-progress collection of items they intend to buy." + description: | + The cart is the central concept of the Shopping domain. It is owned by the Cart System, which + is the source of truth for cart contents. A cart has a status (OPEN, CHECKED_OUT or ABANDONED), + a customer, a currency, and a set of cart items. Carts are transient — abandoned carts are + pruned after a retention window. + icon: ShoppingCart + - id: Cart Item + name: Cart Item + summary: "A single product line within a cart: a product, a quantity and a unit price." + icon: ListOrdered + - id: Checkout + name: Checkout + summary: "The act of finalising a cart — pricing it and committing to purchase." + description: | + At checkout the Cart System prices the cart (including any discounts from the Promotion System) + and, on success, publishes a CartCheckedOut event that the rest of the business reacts to — + creating an order, taking payment and beginning fulfilment. + icon: CreditCard + - id: Promotion + name: Promotion + summary: "A rule that reduces the price of a cart, such as a percentage off or a fixed amount." + description: | + Promotions are owned by the Promotion System. Each promotion has a code, a type (PERCENTAGE or + FIXED_AMOUNT), a value, a validity window, and eligibility rules (e.g. minimum spend or customer + segment) that determine when it applies. + icon: Tag + - id: Discount + name: Discount + summary: "The actual amount taken off a specific cart after evaluating the applicable promotions." + description: | + A discount is the calculated result of applying promotions to a cart. The Cart System asks the + Promotion System to calculate it; the Promotion System evaluates the rules and returns the + discount, also publishing a DiscountCalculated event. + icon: Percent + - id: Subtotal + name: Subtotal + summary: "The total value of a cart's items before any discounts are applied." + icon: Calculator + - id: Promotion Code + name: Promotion Code + summary: "A code a customer enters at checkout to apply a specific promotion." + icon: Ticket +--- diff --git a/examples/default/eventcatalog.auth.js b/examples/default/eventcatalog.auth.js deleted file mode 100644 index 472a1df28..000000000 --- a/examples/default/eventcatalog.auth.js +++ /dev/null @@ -1,24 +0,0 @@ -export default { - debug: false, - providers: { - github: { - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET - }, - okta: { - clientId: process.env.AUTH_OKTA_ID, - clientSecret: process.env.AUTH_OKTA_SECRET, - issuer: process.env.AUTH_OKTA_ISSUER, - }, - entra: { - clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID, - clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET, - issuer: process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER, - }, - auth0: { - clientId: process.env.AUTH_AUTH0_ID, - clientSecret: process.env.AUTH_AUTH0_SECRET, - issuer: process.env.AUTH_AUTH0_ISSUER, - }, - }, -} \ No newline at end of file diff --git a/examples/default/eventcatalog.config.js b/examples/default/eventcatalog.config.js index add492eb2..1a38aabe3 100644 --- a/examples/default/eventcatalog.config.js +++ b/examples/default/eventcatalog.config.js @@ -5,109 +5,102 @@ const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); /** @type {import('../../bin/eventcatalog.config').Config} */ export default { - cId: '8027010c-f3d6-417a-8234-e2f46087fc56', - title: 'FlowMart', - tagline: 'Welcome to FlowMart EventCatalog. Here you can find all the information you need to know about our events and services (demo catalog).', - organizationName: 'FlowMart', - homepageLink: 'https://eventcatalog.dev', - editUrl: 'https://github.com/event-catalog/eventcatalog/edit/main', + cId: "8027010c-f3d6-417a-8234-e2f46087fc56", + title: "Acme Inc", + tagline: "Discover, Explore and Document your Event Driven Architectures.", + organizationName: "Acme Inc", + homepageLink: "https://eventcatalog.dev", + editUrl: "https://github.com/event-catalog/eventcatalog/edit/main", port: 3000, - outDir: 'dist', - // output: 'server', + outDir: "dist", logo: { - alt: 'FlowMart', - src: '/logo.png', - text: 'FlowMart', + src: "/logo.png", + text: "Acme Inc", }, - base: '/', + + base: "/", trailingSlash: false, + + // Theme: 'default', 'ocean', 'sapphire', 'sunset', 'forest', or custom (defined in eventcatalog.styles.css) + theme: "sunset", + mermaid: { enableSupportForElkLayout: true, - iconPacks: ['logos'], + iconPacks: ["logos"], }, + rss: { enabled: true, limit: 15, }, - llmsTxt: { - enabled: true, - }, - // chat: { - // enabled: true, - // provider: 'openai', - // model: 'gpt-4.1', + + // navigation: { + // pages: [ + // { + // type: "group", + // title: "Domains", + // icon: "Boxes", + // pages: [ + // "domain:catalog", + // "domain:customer", + // "domain:shopping", + // "domain:ordering", + // "domain:payments", + // "domain:fulfilment", + // "domain:reviews", + // ], + // }, + // { + // type: "group", + // title: "Top level diagrams", + // icon: "Workflow", + // pages: [ + // { + // type: "item", + // title: "System Context Map", + // href: "/visualiser/system-context-map", + // }, + // ], + // }, + // ], // }, + customDocs: { sidebar: [ { - label: 'Guides', - badge: { - text: 'New', color: 'green' - }, + label: "Platform", + collapsed: false, + autogenerated: { directory: "/platform" }, + }, + { + label: "Domains", collapsed: false, - items: [ - { - label: 'Creating new microservices', autogenerated: { directory: 'guides/creating-new-microservices' } - }, - { - label: 'Event Storming', autogenerated: { directory: 'guides/event-storming', collapsed: false } - } - ] + autogenerated: { directory: "/domains" }, }, { - label: 'Technical Architecture & Design', - badge: { - text: 'New', color: 'green' - }, + label: "Flows", collapsed: false, - items: [ - { - label: 'Architecture Decision Records', autogenerated: { directory: 'technical-architecture-design/architecture-decision-records', collapsed: false } - }, - { label: 'System Architecture Diagrams', autogenerated: { directory: 'technical-architecture-design/system-architecture-diagrams', collapsed: false } }, - { label: 'Infrastructure as Code', autogenerated: { directory: 'technical-architecture-design/infrastructure-as-code', collapsed: false } }, - { - label: 'Read more on GitHub', - link: 'https://github.com/event-catalog/eventcatalog', - attrs: { target: '_blank', style: 'font-style: italic;' }, - }, - ] + autogenerated: { directory: "/flows" }, }, { - label: 'Operations & Support', - badge: { - text: 'New', color: 'green' - }, + label: "Operations", collapsed: false, - items: [ - { label: 'Runbooks', autogenerated: { directory: 'operations-and-support/runbooks', collapsed: false } }, - ] - } - ] + autogenerated: { directory: "/operations" }, + }, + ], + }, + + search: { + type: "resource", }, + visualiser: { channels: { - renderMode: 'flat' - } - }, - environments: [ - { - name: 'Development', - url: 'https://demo.eventcatalog.dev', - description: 'Local development environment', - shortName: 'Dev' - }, - { - name: 'Test', - url: 'https://demo.eventcatalog.dev', - description: 'Test environment for QA', - shortName: 'Test' + renderMode: "flat", }, - { - name: 'Production', - url: 'https://demo.eventcatalog.dev', - description: 'Production environment', - shortName: 'Prod' - }, - ] + }, + + llmsTxt: { + enabled: true, + }, }; diff --git a/examples/default/eventcatalog.styles.css b/examples/default/eventcatalog.styles.css index 0bbd3620e..e69de29bb 100644 --- a/examples/default/eventcatalog.styles.css +++ b/examples/default/eventcatalog.styles.css @@ -1,15 +0,0 @@ - -/** - -Stylesheet overrides for EventCatalog. -There is currently a limit on what can be changed. - -If you want to override some styles of particular elements, raise an issue for discussion. (https://github.com/event-catalog/eventcatalog) - -Note: Override based on custom selectors / hacks at your own risk. EventCatalog selectors will change over time, which may break your styles. - use the styles marked with `ec-` for non breaking styling changes. - -*/ -.ec-homepage{ - background:black; -} \ No newline at end of file diff --git a/examples/default/middleware.ts b/examples/default/middleware.ts deleted file mode 100644 index 057a12da1..000000000 --- a/examples/default/middleware.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { MiddlewareHandler } from 'astro'; -interface Locals { - hasRole: (role: string) => boolean; - hasGroup: (group: string) => boolean; - findMatchingRule: (rules: Record boolean>, pathname: string) => (() => boolean) | null; -} - -export const rbacMiddleware: MiddlewareHandler = async (context, next) => { - const { locals, url } = context; - const pathname = url.pathname; - - const { hasRole, hasGroup, findMatchingRule } = locals as Locals; - - const accessRules = { - '/docs/domains/E-Commerce/*': () => !hasGroup('Viewer'), - '/visualiser/domains/E-Commerce/*': () => !hasGroup('Viewer'), - }; - - if (findMatchingRule) { - // Use the utility from locals - no import needed! - const rule = findMatchingRule(accessRules, pathname); - - if (rule && !rule()) { - return new Response('Forbidden', { status: 403 }); - } - } - - return next(); -}; \ No newline at end of file diff --git a/examples/default/package.json b/examples/default/package.json index b32cb9372..3740abbb9 100644 --- a/examples/default/package.json +++ b/examples/default/package.json @@ -1,5 +1,9 @@ { "name": "default-example-catalog", "version": "0.1.0", - "private": true + "private": true, + "dependencies": { + "@eventcatalog/connectors": "workspace:*", + "@eventcatalog/core": "workspace:*" + } } diff --git a/examples/default/pages/homepage.astro b/examples/default/pages/homepage.astro index fbf277f79..c29cc0bef 100644 --- a/examples/default/pages/homepage.astro +++ b/examples/default/pages/homepage.astro @@ -1,95 +1,109 @@ --- -const { Tile, Tiles, Flow, NodeGraph, Admonition, EntityMap } = Astro.props.components; ---- +import { getCollection } from 'astro:content'; -
    -

    Welcome to FlowMart's EventCatalog

    -

    Explore the events, services, and domains that power the FlowMart ecosystem. This catalog provides a centralized place to discover and understand our asynchronous architecture.

    +// EventCatalog injects its components via Astro.props.components. Destructure the ones +// this landing page uses (including the SystemContextMap embed). +const { Tile, Tiles, SystemContextMap } = Astro.props.components; - - -

    This is a demo of the EventCatalog and what it can do. The company is called FlowMart and they are an e-commerce company.

    -

    Using EventCatalog, we documented their systems (domains, services, events, commands, flows) and how they fit together.

    -
    +// Count the catalog's top-level resources. We filter out versioned/ entries so each +// resource is counted once (by its latest version). +const isCurrent = (entry) => !entry.filePath?.includes('/versioned/'); -
    -
    -

    E-Commerce Domain

    -

    The core domain of FlowMart, responsible for all e-commerce operations.

    - -
    -
    -

    Orders Domain

    -

    The sub-domain responsible for all orders.

    - -
    -
    -

    Payment Domain

    -

    The sub-domain responsible for all payments.

    - -
    -
    -

    Payment Entity Map

    -

    The entity map for the payment domain.

    - -
    -
    -

    Subscription Domain

    -

    The sub-domain responsible for all subscriptions.

    - -
    -
    -

    Subscription Entity Map

    -

    The entity map for the subscription domain.

    - -
    -
    +const [domains, systems, services] = await Promise.all([ + getCollection('domains'), + getCollection('systems'), + getCollection('services'), +]); -
    -

    Discover Our Architecture

    -

    - Navigate through our Domains to understand the different business capabilities, explore Services to see the microservices involved, and dive into Events and Commands to see how they communicate. -

    -

    - Use the search bar above or browse the sections in the sidebar to get started. +const stats = [ + { label: 'Domains', value: domains.filter(isCurrent).length, href: '/discover/domains' }, + { label: 'Systems', value: systems.filter(isCurrent).length, href: '/visualiser/system-context-map' }, + { label: 'Services', value: services.filter(isCurrent).length, href: '/discover/services' }, +]; +--- + +

    + {/* Hero */} +
    +

    Acme Inc Catalog

    +

    + Discover, explore and document the event-driven architecture that powers Acme Inc — our + domains, the systems inside them, and the services and messages that connect everything.

    -
    -
    -

    Cancel Subscription Flow

    -

    This flow is triggered when a user cancels their subscription.

    - -
    -
    -

    Payment Flow

    -

    This flow documents how a payment is processed at FlowMart.

    - -
    + {/* Stat cards */} +
    + { + stats.map((stat) => ( + +
    {stat.value}
    +
    + {stat.label} +
    +
    + )) + }
    -
    -

    Quick Links

    -

    Learn how to get started with EventCatalog, create domains, services, events, and commands.

    - - - - - - - - + {/* System context map */} +

    System context map

    +

    + How every system in the catalog relates — and the people who interact with them. +

    -
    -

    Join the community

    - -

    Our project and community is growing fast. We have over 1000+ members in our Discord community.

    - - - - - -
    + +
    + {/* Domains */} +

    Explore the domains

    + + + + + + + + + + diff --git a/examples/default/public/icons/analytics/clickhouse.svg b/examples/default/public/icons/analytics/clickhouse.svg new file mode 100644 index 000000000..95c0bef80 --- /dev/null +++ b/examples/default/public/icons/analytics/clickhouse.svg @@ -0,0 +1 @@ +ClickHouse \ No newline at end of file diff --git a/examples/default/public/icons/cache/redis.svg b/examples/default/public/icons/cache/redis.svg new file mode 100644 index 000000000..61480c931 --- /dev/null +++ b/examples/default/public/icons/cache/redis.svg @@ -0,0 +1 @@ +Redis \ No newline at end of file diff --git a/examples/default/public/icons/database/postgresql.svg b/examples/default/public/icons/database/postgresql.svg new file mode 100644 index 000000000..931bdae18 --- /dev/null +++ b/examples/default/public/icons/database/postgresql.svg @@ -0,0 +1 @@ +PostgreSQL \ No newline at end of file diff --git a/examples/default/public/icons/languages/go.svg b/examples/default/public/icons/languages/go.svg new file mode 100644 index 000000000..51990bd01 --- /dev/null +++ b/examples/default/public/icons/languages/go.svg @@ -0,0 +1 @@ +Go \ No newline at end of file diff --git a/examples/default/public/icons/languages/java.svg b/examples/default/public/icons/languages/java.svg new file mode 100644 index 000000000..c72c27432 --- /dev/null +++ b/examples/default/public/icons/languages/java.svg @@ -0,0 +1 @@ +Java diff --git a/examples/default/public/icons/languages/nodejs.svg b/examples/default/public/icons/languages/nodejs.svg new file mode 100644 index 000000000..3cc5893e0 --- /dev/null +++ b/examples/default/public/icons/languages/nodejs.svg @@ -0,0 +1 @@ +Node.js \ No newline at end of file diff --git a/examples/default/public/icons/payments/stripe.svg b/examples/default/public/icons/payments/stripe.svg new file mode 100644 index 000000000..77e678c71 --- /dev/null +++ b/examples/default/public/icons/payments/stripe.svg @@ -0,0 +1 @@ +Stripe diff --git a/examples/default/public/logo.png b/examples/default/public/logo.png index d65c29da2..670057fbb 100644 Binary files a/examples/default/public/logo.png and b/examples/default/public/logo.png differ diff --git a/examples/default/public/logo.svg b/examples/default/public/logo.svg deleted file mode 100644 index a3b326372..000000000 --- a/examples/default/public/logo.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/default/reports/eventcatalog-assessment.pdf b/examples/default/reports/eventcatalog-assessment.pdf deleted file mode 100644 index 5d81752d6..000000000 Binary files a/examples/default/reports/eventcatalog-assessment.pdf and /dev/null differ diff --git a/examples/default/reports/eventcatalog-full-report.pdf b/examples/default/reports/eventcatalog-full-report.pdf deleted file mode 100644 index 87927f0e2..000000000 Binary files a/examples/default/reports/eventcatalog-full-report.pdf and /dev/null differ diff --git a/examples/default/teams/customer-platform.mdx b/examples/default/teams/customer-platform.mdx new file mode 100644 index 000000000..d7902b036 --- /dev/null +++ b/examples/default/teams/customer-platform.mdx @@ -0,0 +1,18 @@ +--- +id: customer-platform +name: Customer Platform +summary: Owns the Customer domain — customer profiles and authentication across Acme Inc. +members: + - dboyne +email: customer-platform@acme.test +slackDirectMessageUrl: https://acme.slack.com/channels/customer-platform +--- + +## Overview + +The **Customer Platform** team owns the [[domain|customer]] domain. They are responsible for how customers register, how their profiles are maintained, and how they authenticate. + +## Responsibilities + +- **Customer Management System** — registering, updating and reading customer profiles, and publishing customer change events. +- **Identity Provider** — authenticating customers and owning credentials in the user directory. diff --git a/examples/default/teams/fulfilment-platform.mdx b/examples/default/teams/fulfilment-platform.mdx new file mode 100644 index 000000000..d13738aac --- /dev/null +++ b/examples/default/teams/fulfilment-platform.mdx @@ -0,0 +1,20 @@ +--- +id: fulfilment-platform +name: Fulfilment Platform +summary: Owns the Fulfilment domain — reserving stock, picking and packing orders, and shipping them to customers. +members: + - dboyne +email: fulfilment-platform@acme.test +slackDirectMessageUrl: https://acme.slack.com/channels/fulfilment-platform +--- + +## Overview + +The **Fulfilment Platform** team owns the [[domain|fulfilment]] domain. They are responsible for inventory, warehousing and shipping — everything that gets a confirmed order physically to the customer. + +## Responsibilities + +- **Inventory System** — tracking and reserving stock. +- **Warehouse System** — picking and packing orders. +- **Shipping System** — handing packed orders to carriers. +- Integration with external **Carriers** for delivery. diff --git a/examples/default/teams/order-management.mdx b/examples/default/teams/order-management.mdx deleted file mode 100644 index 80700dbbb..000000000 --- a/examples/default/teams/order-management.mdx +++ /dev/null @@ -1,24 +0,0 @@ ---- -id: order-management -name: The order management team -members: - - asmith - - mchen - - nshah - - slee - - spatel ---- - -The order management team is responsible for overseeing and optimizing the entire order lifecycle within our organization. Their key responsibilities include: - -- Processing and validating incoming customer orders -- Managing order fulfillment workflows and inventory allocation -- Coordinating with warehouse and shipping teams -- Handling order modifications and cancellations -- Resolving order-related customer issues and disputes -- Monitoring order processing metrics and KPIs -- Implementing and maintaining order management systems -- Developing strategies to improve order accuracy and efficiency - -The team works closely with sales, customer service, and logistics departments to ensure smooth order processing and customer satisfaction. - diff --git a/examples/default/teams/ordering-platform.mdx b/examples/default/teams/ordering-platform.mdx new file mode 100644 index 000000000..ee58d62ab --- /dev/null +++ b/examples/default/teams/ordering-platform.mdx @@ -0,0 +1,18 @@ +--- +id: ordering-platform +name: Ordering Platform +summary: Owns the Ordering domain — turning a checked-out cart into a confirmed order. +members: + - dboyne +email: ordering-platform@acme.test +slackDirectMessageUrl: https://acme.slack.com/channels/ordering-platform +--- + +## Overview + +The **Ordering Platform** team owns the [[domain|ordering]] domain. They are responsible for the checkout flow that turns a checked-out cart into a confirmed order, and for the lifecycle of orders once they exist. + +## Responsibilities + +- **Checkout System** — orchestrating checkout: reserving inventory, authorizing payment and creating the order. +- **Order Management System** — the system of record for orders and their lifecycle. diff --git a/examples/default/teams/payments-platform.mdx b/examples/default/teams/payments-platform.mdx new file mode 100644 index 000000000..3a9d135d1 --- /dev/null +++ b/examples/default/teams/payments-platform.mdx @@ -0,0 +1,18 @@ +--- +id: payments-platform +name: Payments Platform +summary: Owns the Payments domain — authorizing payments, processing refunds, and integrating with external payment and fraud providers. +members: + - dboyne +email: payments-platform@acme.test +slackDirectMessageUrl: https://acme.slack.com/channels/payments-platform +--- + +## Overview + +The **Payments Platform** team owns the [[domain|payments]] domain. They are responsible for taking payment for orders, processing refunds, and integrating with the external payment processor and fraud provider. + +## Responsibilities + +- **Payment Processing System** — orchestrating authorization, capture and refunds. +- Integrations with **Stripe** (payment processing) and **Fraud Detection** (fraud screening). diff --git a/examples/default/teams/product-platform.mdx b/examples/default/teams/product-platform.mdx new file mode 100644 index 000000000..43846b74d --- /dev/null +++ b/examples/default/teams/product-platform.mdx @@ -0,0 +1,20 @@ +--- +id: product-platform +name: Product Platform +summary: Owns the Product Catalog System — the source of truth for product data at Acme Inc. +members: + - dboyne +email: product-platform@acme.test +slackDirectMessageUrl: https://acme.slack.com/channels/product-platform +--- + +## Overview + +The **Product Platform** team owns the [[system|product-catalog-system]]. They are responsible for how products are created, maintained and stored, and for reliably publishing product change events to the rest of the business. + +## Responsibilities + +- **Product API** — the public-facing API for creating, updating, deleting and reading products. +- **Product Worker** — asynchronous catalog processing (enrichment, media, bulk imports). +- **Product Search Publisher** — reliable, ordered delivery of product change events. +- **Product Database** — the PostgreSQL system of record for all product data. diff --git a/examples/default/teams/reviews-platform.mdx b/examples/default/teams/reviews-platform.mdx new file mode 100644 index 000000000..19a5bb07d --- /dev/null +++ b/examples/default/teams/reviews-platform.mdx @@ -0,0 +1,20 @@ +--- +id: reviews-platform +name: Reviews Platform +summary: Owns the Reviews & Ratings domain — how customers review products and how those reviews are moderated, published and aggregated into ratings. +members: + - dboyne +email: reviews-platform@acme.test +slackDirectMessageUrl: https://acme.slack.com/channels/reviews-platform +--- + +## Overview + +The **Reviews Platform** team owns the [[domain|reviews]] domain. They are responsible for collecting customer reviews, moderating them, publishing the approved ones, and turning ratings into the aggregate scores shown across the storefront. + +## Responsibilities + +- **Review API** — accepts review submissions and serves product reviews. +- **Review Moderation Worker** — screens submitted reviews and decides whether to publish or reject them. +- **Rating Aggregator** — keeps each product's aggregate rating up to date as reviews are published. +- **Review Database** — the system of record for reviews and their moderation state. diff --git a/examples/default/teams/search-platform.mdx b/examples/default/teams/search-platform.mdx new file mode 100644 index 000000000..9b10a2bfb --- /dev/null +++ b/examples/default/teams/search-platform.mdx @@ -0,0 +1,19 @@ +--- +id: search-platform +name: Search Platform +summary: Owns the Search System — keeps the catalog discoverable through fast, relevant product search. +members: + - dboyne +email: search-platform@acme.test +slackDirectMessageUrl: https://acme.slack.com/channels/search-platform +--- + +## Overview + +The **Search Platform** team owns the [[system|search-system]]. They consume product change events, keep the search index in sync, and serve product search to the rest of Acme Inc. + +## Responsibilities + +- **Search API** — the public-facing API that serves product search queries. +- **Search Indexer** — consumes product change events and keeps the index current. +- **Search Index** — the search-optimised, rebuildable read model of the catalog. diff --git a/examples/default/teams/shopping-platform.mdx b/examples/default/teams/shopping-platform.mdx new file mode 100644 index 000000000..bd972fa05 --- /dev/null +++ b/examples/default/teams/shopping-platform.mdx @@ -0,0 +1,18 @@ +--- +id: shopping-platform +name: Shopping Platform +summary: Owns the Shopping domain — the cart and the promotions applied to it. +members: + - dboyne +email: shopping-platform@acme.test +slackDirectMessageUrl: https://acme.slack.com/channels/shopping-platform +--- + +## Overview + +The **Shopping Platform** team owns the [[domain|shopping]] domain. They are responsible for the shopping cart, the checkout flow, and the promotions and discounts applied to a cart. + +## Responsibilities + +- **Cart System** — building, pricing and checking out shopping carts. +- **Promotion System** — evaluating promotion rules and calculating discounts. diff --git a/examples/default/tsconfig.json b/examples/default/tsconfig.json new file mode 100644 index 000000000..1da4b3189 --- /dev/null +++ b/examples/default/tsconfig.json @@ -0,0 +1,26 @@ +{ + // Editor support (IntelliSense, go-to-definition) for custom pages. + // These aliases mirror the ones the EventCatalog build provides at runtime. + // In a real catalog the paths point into .eventcatalog-core instead. + "compilerOptions": { + "baseUrl": ".", + "allowJs": true, + "jsx": "preserve", + "paths": { + "@catalog/components/*": ["components/*"], + "@catalog/layouts/*": ["../../packages/core/eventcatalog/src/toolkit/layouts/*"], + "@catalog/utils": ["../../packages/core/eventcatalog/src/toolkit/utils/index.ts"], + "@catalog/utils/*": ["../../packages/core/eventcatalog/src/toolkit/utils/*"], + "@components/*": ["../../packages/core/eventcatalog/src/components/*"], + "@layouts/*": ["../../packages/core/eventcatalog/src/layouts/*"], + "@utils/*": ["../../packages/core/eventcatalog/src/utils/*"], + "@enterprise/*": ["../../packages/core/eventcatalog/src/enterprise/*"], + "@icons/*": ["../../packages/core/eventcatalog/src/icons/*"], + "@stores/*": ["../../packages/core/eventcatalog/src/stores/*"], + "@types": ["../../packages/core/eventcatalog/src/types/index.ts"], + "@config": ["eventcatalog.config.js"], + "@eventcatalog": ["../../packages/core/eventcatalog/src/utils/eventcatalog-config/catalog.ts"] + } + }, + "include": ["pages/**/*", "components/**/*"] +} diff --git a/examples/default/users/aSmith.mdx b/examples/default/users/aSmith.mdx deleted file mode 100644 index 26d703845..000000000 --- a/examples/default/users/aSmith.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -id: asmith -name: Amy Smith -avatarUrl: https://randomuser.me/api/portraits/women/48.jpg -role: Product owner ---- - -Hello! I'm Amy Smith, the Product Owner of the innovative Full Stackers team. With a strong focus on delivering exceptional value, I specialize in connecting business objectives with technical solutions to create products that users love. - -### About Me - -With a comprehensive background in product management and a solid understanding of software development, I bring a unique perspective to the table. My career has been driven by a passion for understanding user needs, defining clear product visions, and leading teams to successful product deliveries. - -### What I Do - -As the Product Owner for Full Stackers, my role involves a wide range of responsibilities aimed at ensuring our products are both high-quality and user-centric. Key aspects of my role include: - -- **Product Vision & Strategy**: Defining and communicating the long-term vision and strategy for our products, ensuring alignment with the company's objectives and market demands. -- **Roadmap Planning**: Developing and maintaining a product roadmap that highlights key features and milestones, prioritizing tasks based on their business value and user feedback. -- **Stakeholder Management**: Engaging with stakeholders across the organization to gather requirements, provide updates, and ensure everyone is aligned on the product's direction. -- **User-Centric Design**: Championing the end-users by conducting user research, analyzing feedback, and ensuring our products effectively solve their problems. -- **Agile Leadership**: Leading the development process using Agile methodologies, facilitating sprint planning, and ensuring the team has clear priorities and objectives. - -My mission is to deliver products that not only meet but exceed customer expectations. I thrive on the challenge of translating complex requirements into simple, intuitive solutions. - -If you’re interested in product management, user experience, or discussing the latest trends in technology, feel free to reach out! - diff --git a/examples/default/users/alee.mdx b/examples/default/users/alee.mdx deleted file mode 100644 index d9c3e1aa8..000000000 --- a/examples/default/users/alee.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -id: alee -name: Alice Lee -avatarUrl: https://randomuser.me/api/portraits/women/91.jpg -role: Technical Writer ---- - -As a Technical Writer on the Documentation team, I create clear, comprehensive documentation for our products and APIs. My focus is on making complex technical concepts accessible to developers and end-users alike. I collaborate with engineering teams to ensure our documentation stays current and accurate. \ No newline at end of file diff --git a/examples/default/users/azhang.mdx b/examples/default/users/azhang.mdx deleted file mode 100644 index 1d86cf90f..000000000 --- a/examples/default/users/azhang.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: azhang -name: Alice Zhang -avatarUrl: https://randomuser.me/api/portraits/women/97.jpg -role: Data Engineer -email: azhang@company.com -slackDirectMessageUrl: https://yourteam.slack.com/channels/azhang -msTeamsDirectMessageUrl: https://teams.microsoft.com/l/chat/0/0?users=azhang@company.com ---- - -Building and maintaining data pipelines and infrastructure... \ No newline at end of file diff --git a/examples/default/users/dkim.mdx b/examples/default/users/dkim.mdx deleted file mode 100644 index 96fd60ede..000000000 --- a/examples/default/users/dkim.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: dkim -name: David Kim -avatarUrl: https://randomuser.me/api/portraits/men/96.jpg -role: Performance Engineer -email: dkim@company.com -slackDirectMessageUrl: https://yourteam.slack.com/channels/dkim -msTeamsDirectMessageUrl: https://teams.microsoft.com/l/chat/0/0?users=dkim@company.com ---- - -Optimizing application performance and user experience... \ No newline at end of file diff --git a/examples/default/users/jbrown.mdx b/examples/default/users/jbrown.mdx deleted file mode 100644 index 226bf6a5d..000000000 --- a/examples/default/users/jbrown.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: jbrown -name: Jessica Brown -avatarUrl: https://randomuser.me/api/portraits/women/95.jpg -role: UI/UX Designer -email: jbrown@company.com -slackDirectMessageUrl: https://yourteam.slack.com/channels/jbrown -msTeamsDirectMessageUrl: https://teams.microsoft.com/l/chat/0/0?users=jbrown@company.com ---- - -Creating intuitive and engaging user interfaces... \ No newline at end of file diff --git a/examples/default/users/mSmith.mdx b/examples/default/users/mSmith.mdx deleted file mode 100644 index 1311cd656..000000000 --- a/examples/default/users/mSmith.mdx +++ /dev/null @@ -1,8 +0,0 @@ ---- -id: msmith -name: Martin Smith -avatarUrl: "https://randomuser.me/api/portraits/men/51.jpg" -role: Senior software engineer ---- - -As a Senior Mobile Developer on The Mobile Devs team, I play a key role in designing, developing, and maintaining our company’s mobile applications. My focus is on creating a seamless and intuitive user experience for our customers on both iOS and Android platforms. I work closely with cross-functional teams, including backend developers, UX/UI designers, and product managers, to deliver high-quality mobile solutions that meet business objectives and exceed user expectations. \ No newline at end of file diff --git a/images/ai-demo.gif b/images/ai-demo.gif new file mode 100644 index 000000000..4241e31bf Binary files /dev/null and b/images/ai-demo.gif differ diff --git a/images/banner.png b/images/banner.png new file mode 100644 index 000000000..e3d2cca1d Binary files /dev/null and b/images/banner.png differ diff --git a/images/bring-your-own-docs.gif b/images/bring-your-own-docs.gif new file mode 100644 index 000000000..88c09a6b3 Binary files /dev/null and b/images/bring-your-own-docs.gif differ diff --git a/images/example.png b/images/example.png index 2a341bb80..6b949e453 100644 Binary files a/images/example.png and b/images/example.png differ diff --git a/images/flow-demo.gif b/images/flow-demo.gif new file mode 100644 index 000000000..2b4469727 Binary files /dev/null and b/images/flow-demo.gif differ diff --git a/images/overview-demo.gif b/images/overview-demo.gif new file mode 100644 index 000000000..fcf0cf24c Binary files /dev/null and b/images/overview-demo.gif differ diff --git a/images/schema-explorer-demo.gif b/images/schema-explorer-demo.gif new file mode 100644 index 000000000..96f994fba Binary files /dev/null and b/images/schema-explorer-demo.gif differ diff --git a/images/schema-fields-demo.gif b/images/schema-fields-demo.gif new file mode 100644 index 000000000..8f3ca3959 Binary files /dev/null and b/images/schema-fields-demo.gif differ diff --git a/images/sponsors/gravitee-logo-black.svg b/images/sponsors/gravitee-logo-black.svg deleted file mode 100644 index 17d1740fa..000000000 --- a/images/sponsors/gravitee-logo-black.svg +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/images/sponsors/gravitee-logo-white.webp b/images/sponsors/gravitee-logo-white.webp deleted file mode 100644 index 9397b59fa..000000000 Binary files a/images/sponsors/gravitee-logo-white.webp and /dev/null differ diff --git a/images/sponsors/hookdeck.svg b/images/sponsors/hookdeck.svg deleted file mode 100644 index 962b07dcc..000000000 --- a/images/sponsors/hookdeck.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/images/sponsors/oso-logo-green.png b/images/sponsors/oso-logo-green.png deleted file mode 100644 index 61a8e8185..000000000 Binary files a/images/sponsors/oso-logo-green.png and /dev/null differ diff --git a/images/visualizer-demo.gif b/images/visualizer-demo.gif new file mode 100644 index 000000000..74a5c92c0 Binary files /dev/null and b/images/visualizer-demo.gif differ diff --git a/package.json b/package.json index 5f6dd269c..f3cbddac3 100644 --- a/package.json +++ b/package.json @@ -1,159 +1,35 @@ { - "name": "@eventcatalog/core", - "homepage": "https://github.com/event-catalog/eventcatalog", - "repository": { - "type": "git", - "url": "https://github.com/event-catalog/eventcatalog.git" - }, - "type": "module", - "version": "2.65.1", - "publishConfig": { - "access": "public" - }, - "bin": { - "eventcatalog": "bin/eventcatalog.js" - }, - "files": [ - "eventcatalog/", - "!eventcatalog/**/__tests__/", - "bin/", - "dist/", - "default-files-for-collections/" - ], + "name": "eventcatalog-monorepo", + "private": true, + "license": "MIT", "scripts": { - "dev": "astro dev", - "build:bin": "tsup", - "prepublishOnly": "pnpm run build:bin", - "test": "vitest", - "test:ci": "node scripts/ci/test.js", - "test:e2e": "playwright test", - "start": "astro dev", - "build": "astro build", - "build:cd": "node scripts/build-ci.js", - "preview": "astro preview", - "astro": "astro", - "start:catalog": "node scripts/start-catalog-locally.js", - "pagefind": "node scripts/pagefind.js", - "preview:catalog": "node scripts/preview-catalog-locally.js", - "generate:catalog": "node scripts/generate-catalog-locally.js", - "verify-build:catalog": "rimraf dist && pnpm run build:cd", + "build:bin": "turbo run build:bin", + "build": "turbo run build", + "test": "turbo run test", + "test:ci": "turbo run test:ci", + "format": "turbo run format", + "format:diff": "turbo run format:diff", + "start:playground": "pnpm --filter @eventcatalog/dsl-playground run dev", + "start:dsl-docs": "pnpm --filter @eventcatalog/language-server run docs:dev", + "build:dsl-docs": "pnpm --filter @eventcatalog/sdk run build && pnpm --filter @eventcatalog/language-server run build && pnpm --filter @eventcatalog/language-server run docs:build", + "start:catalog": "pnpm --filter @eventcatalog/core run start:catalog", + "export:catalog": "pnpm --filter @eventcatalog/core run export:catalog", + "preview:catalog": "pnpm --filter @eventcatalog/core run preview:catalog", + "start:catalog:server": "pnpm --filter @eventcatalog/core run start:catalog:server", + "verify-build:catalog": "pnpm --filter @eventcatalog/core run verify-build:catalog", "changeset": "changeset", - "generate": "pnpm run build:bin && npx . generate", - "release": "changeset publish", - "format": "prettier --config .prettierrc --write \"**/*.{js,jsx,ts,tsx,json,astro}\"", - "format:diff": "prettier --config .prettierrc --list-different \"**/*.{js,jsx,ts,tsx,json,astro}\"", - "lint:catalog": "pnpm dlx @eventcatalog/linter examples/default" - }, - "dependencies": { - "@ai-sdk/anthropic": "^2.0.23", - "@ai-sdk/google": "^2.0.17", - "@ai-sdk/openai": "^2.0.42", - "@ai-sdk/react": "^2.0.60", - "@astrojs/markdown-remark": "^6.3.9", - "@astrojs/mdx": "^4.3.12", - "@astrojs/node": "^9.5.1", - "@astrojs/react": "^4.4.2", - "@astrojs/rss": "^4.0.14", - "@astrojs/tailwind": "^6.0.2", - "@asyncapi/avro-schema-parser": "3.0.24", - "@asyncapi/parser": "3.4.0", - "@asyncapi/react-component": "2.4.3", - "@auth/core": "^0.37.4", - "@eventcatalog/generator-ai": "^1.1.0", - "@eventcatalog/license": "^0.0.7", - "@eventcatalog/linter": "^0.0.2", - "@eventcatalog/sdk": "^2.9.2", - "@eventcatalog/visualizer": "^0.0.6", - "@fontsource/inter": "^5.2.5", - "@headlessui/react": "^2.0.3", - "@heroicons/react": "^2.1.3", - "@iconify-json/logos": "^1.2.4", - "@mermaid-js/layout-elk": "^0.2.0", - "@parcel/watcher": "^2.4.1", - "@radix-ui/react-context-menu": "^2.2.6", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.12", - "@radix-ui/react-tooltip": "^1.1.8", - "@scalar/api-reference-react": "^0.4.37", - "@tailwindcss/typography": "^0.5.13", - "@tanstack/react-query": "^5.74.3", - "@tanstack/react-table": "^8.17.3", - "@xyflow/react": "^12.3.6", - "ai": "^5.0.60", - "astro": "^5.16.0", - "astro-compress": "^2.3.8", - "astro-expressive-code": "^0.41.3", - "astro-seo": "^0.8.4", - "auth-astro": "^4.2.0", - "axios": "^1.7.7", - "boxen": "^8.0.1", - "commander": "^12.1.0", - "concurrently": "^8.2.2", - "cross-env": "^7.0.3", - "dagre": "^0.8.5", - "date-fns": "^4.1.0", - "diff": "^7.0.0", - "diff2html": "^3.4.48", - "dotenv": "^16.5.0", - "elkjs": "^0.10.0", - "glob": "^10.5.0", - "gray-matter": "^4.0.3", - "html-to-image": "^1.11.11", - "js-yaml": "^4.1.0", - "jsonpath": "^1.1.1", - "jsonwebtoken": "^9.0.2", - "lodash.debounce": "^4.0.8", - "lodash.merge": "4.6.2", - "lucide-react": "^0.453.0", - "marked": "^15.0.6", - "mermaid": "^11.4.1", - "pagefind": "^1.3.0", - "pako": "^2.1.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-markdown": "^10.1.0", - "react-syntax-highlighter": "^15.6.1", - "rehype-autolink-headings": "^7.1.0", - "rehype-expressive-code": "^0.41.3", - "rehype-slug": "^6.0.0", - "remark-comment": "^1.0.0", - "remark-directive": "^3.0.0", - "remark-gfm": "^3.0.1", - "rimraf": "^5.0.7", - "semver": "7.6.3", - "shelljs": "^0.8.5", - "tailwindcss": "^3.4.3", - "typescript": "^5.4.5", - "unist-util-visit": "^5.0.0", - "update-notifier": "^7.3.1", - "uuid": "^10.0.0" + "release": "changeset publish" }, "devDependencies": { - "@astrojs/check": "^0.9.5", "@changesets/cli": "^2.27.5", - "@playwright/test": "^1.48.1", - "@types/dagre": "^0.7.52", - "@types/diff": "^5.2.2", - "@types/js-yaml": "^4.0.9", - "@types/jsonpath": "^0.2.4", - "@types/jsonwebtoken": "^9.0.10", - "@types/lodash.debounce": "^4.0.9", - "@types/lodash.merge": "4.6.9", - "@types/node": "^20.14.2", - "@types/pako": "^2.0.3", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@types/react-syntax-highlighter": "^15.5.13", - "@types/semver": "^7.5.8", - "@types/shelljs": "^0.8.15", - "@types/update-notifier": "^6.0.8", - "@types/uuid": "^10.0.0", - "prettier": "^3.3.3", "prettier-plugin-astro": "^0.14.1", - "tsup": "^8.1.0", - "vite": "^7.1.7", - "vite-tsconfig-paths": "^4.3.2", - "vitest": "2.1.9" + "turbo": "^2.8.4" + }, + "pnpm": { + "overrides": { + "prismjs": ">=1.30.0" + } }, - "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b" + "packageManager": "pnpm@10.23.0+sha512.21c4e5698002ade97e4efe8b8b4a89a8de3c85a37919f957e7a0f30f38fbc5bbdd05980ffe29179b2fb6e6e691242e098d945d1601772cad0fef5fb6411e2a4b", + "dependencies": {} } diff --git a/packages/breaking-changes/CHANGELOG.md b/packages/breaking-changes/CHANGELOG.md new file mode 100644 index 000000000..16a91fb3f --- /dev/null +++ b/packages/breaking-changes/CHANGELOG.md @@ -0,0 +1,9 @@ +# @eventcatalog/breaking-changes + +## 0.2.0 + +### Minor Changes + +- a3313f6: feat: add breaking schema change detection with governance webhooks + + New `schema_breaking_change` governance trigger that detects breaking JSON Schema changes per compatibility strategy (BACKWARD, FORWARD, FULL, NONE). Users configure `compatibility.strategy` in governance.yaml and subscribe to breaking change webhooks with detailed diff information. diff --git a/packages/breaking-changes/package.json b/packages/breaking-changes/package.json new file mode 100644 index 000000000..974eb1531 --- /dev/null +++ b/packages/breaking-changes/package.json @@ -0,0 +1,41 @@ +{ + "name": "@eventcatalog/breaking-changes", + "version": "0.2.0", + "description": "Breaking schema change detection for EventCatalog", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs" + } + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsup", + "build:bin": "tsup", + "test": "vitest --run", + "test:ci": "vitest --run", + "format": "prettier --write .", + "format:diff": "prettier --list-different ." + }, + "files": [ + "dist" + ], + "license": "ISC", + "devDependencies": { + "@types/node": "^20.14.10", + "prettier": "^3.3.3", + "tsup": "^8.1.0", + "typescript": "^5.5.3", + "vitest": "^3.2.4" + }, + "repository": { + "type": "git", + "url": "https://github.com/event-catalog/eventcatalog" + } +} diff --git a/packages/breaking-changes/src/index.ts b/packages/breaking-changes/src/index.ts new file mode 100644 index 000000000..a95c46b28 --- /dev/null +++ b/packages/breaking-changes/src/index.ts @@ -0,0 +1,9 @@ +export type { + CompatibilityStrategy, + SchemaChange, + BreakingChange, + SchemaChangeType, +} from "./types"; + +export { detectBreakingChanges } from "./json-schema/rules"; +export { diffJsonSchemas } from "./json-schema/differ"; diff --git a/packages/breaking-changes/src/json-schema/differ.ts b/packages/breaking-changes/src/json-schema/differ.ts new file mode 100644 index 000000000..5b7d848e5 --- /dev/null +++ b/packages/breaking-changes/src/json-schema/differ.ts @@ -0,0 +1,122 @@ +import type { SchemaChange } from "../types"; + +type JsonSchemaObject = { + type?: string; + properties?: Record; + required?: string[]; + [key: string]: any; +}; + +const hasProperties = (schema: JsonSchemaObject): boolean => + !!(schema.properties || schema.required); + +export const diffJsonSchemas = ( + before: JsonSchemaObject, + after: JsonSchemaObject, +): SchemaChange[] => { + const changes: SchemaChange[] = []; + + // Detect root-level type changes + if (before.type && after.type && before.type !== after.type) { + changes.push({ + type: "TYPE_CHANGED", + field: "(root)", + message: `Root type changed from '${before.type}' to '${after.type}'`, + previousType: before.type, + currentType: after.type, + }); + } + + compareProperties(before, after, "", changes); + return changes; +}; + +const compareProperties = ( + before: JsonSchemaObject, + after: JsonSchemaObject, + prefix: string, + changes: SchemaChange[], +): void => { + const beforeProps = before.properties || {}; + const afterProps = after.properties || {}; + const beforeRequired = new Set(before.required || []); + const afterRequired = new Set(after.required || []); + + const allFields = new Set([ + ...Object.keys(beforeProps), + ...Object.keys(afterProps), + ]); + + for (const field of allFields) { + const fullPath = prefix ? `${prefix}.${field}` : field; + const inBefore = field in beforeProps; + const inAfter = field in afterProps; + + if (!inBefore && inAfter) { + const isRequired = afterRequired.has(field); + changes.push({ + type: isRequired ? "FIELD_ADDED_REQUIRED" : "FIELD_ADDED_OPTIONAL", + field: fullPath, + message: `${isRequired ? "Required" : "Optional"} field '${fullPath}' was added`, + }); + continue; + } + + if (inBefore && !inAfter) { + const wasRequired = beforeRequired.has(field); + changes.push({ + type: wasRequired ? "FIELD_REMOVED_REQUIRED" : "FIELD_REMOVED_OPTIONAL", + field: fullPath, + message: `${wasRequired ? "Required" : "Optional"} field '${fullPath}' was removed`, + }); + continue; + } + + // Field exists in both — check type + const beforeType = beforeProps[field].type; + const afterType = afterProps[field].type; + + if (beforeType && afterType && beforeType !== afterType) { + changes.push({ + type: "TYPE_CHANGED", + field: fullPath, + message: `Field '${fullPath}' type changed from '${beforeType}' to '${afterType}'`, + previousType: beforeType, + currentType: afterType, + }); + } + + // Check required status change + const wasPreviouslyRequired = beforeRequired.has(field); + const isNowRequired = afterRequired.has(field); + + if (!wasPreviouslyRequired && isNowRequired) { + changes.push({ + type: "REQUIRED_ADDED", + field: fullPath, + message: `Field '${fullPath}' was made required`, + }); + } else if (wasPreviouslyRequired && !isNowRequired) { + changes.push({ + type: "REQUIRED_REMOVED", + field: fullPath, + message: `Field '${fullPath}' was made optional`, + }); + } + + // Recurse into nested objects (explicit type or implicit via properties/required) + if ( + beforeProps[field].type === "object" || + afterProps[field].type === "object" || + hasProperties(beforeProps[field]) || + hasProperties(afterProps[field]) + ) { + compareProperties( + beforeProps[field], + afterProps[field], + fullPath, + changes, + ); + } + } +}; diff --git a/packages/breaking-changes/src/json-schema/rules.ts b/packages/breaking-changes/src/json-schema/rules.ts new file mode 100644 index 000000000..c25184292 --- /dev/null +++ b/packages/breaking-changes/src/json-schema/rules.ts @@ -0,0 +1,44 @@ +import type { + CompatibilityStrategy, + SchemaChange, + BreakingChange, + SchemaChangeType, +} from "../types"; +import { diffJsonSchemas } from "./differ"; + +const BREAKING_RULES: Record> = { + BACKWARD: new Set([ + "FIELD_ADDED_REQUIRED", + "FIELD_REMOVED_REQUIRED", + "TYPE_CHANGED", + "REQUIRED_ADDED", + ]), + FORWARD: new Set([ + "FIELD_ADDED_REQUIRED", + "FIELD_REMOVED_REQUIRED", + "TYPE_CHANGED", + "REQUIRED_ADDED", + ]), + FULL: new Set([ + "FIELD_ADDED_REQUIRED", + "FIELD_REMOVED_REQUIRED", + "TYPE_CHANGED", + "REQUIRED_ADDED", + ]), + NONE: new Set(), +}; + +export const detectBreakingChanges = ( + before: object, + after: object, + strategy: CompatibilityStrategy, +): BreakingChange[] => { + if (strategy === "NONE") return []; + + const changes = diffJsonSchemas(before, after); + const breakingTypes = BREAKING_RULES[strategy]; + + return changes + .filter((change) => breakingTypes.has(change.type)) + .map((change) => ({ ...change, breaking: true as const })); +}; diff --git a/packages/breaking-changes/src/test/index.test.ts b/packages/breaking-changes/src/test/index.test.ts new file mode 100644 index 000000000..5312ebb73 --- /dev/null +++ b/packages/breaking-changes/src/test/index.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import { detectBreakingChanges, diffJsonSchemas } from "../index"; + +describe("public API", () => { + it("detectBreakingChanges returns breaking changes for a real schema evolution", () => { + const v1 = { + type: "object", + properties: { + orderId: { type: "string" }, + amount: { type: "string" }, + currency: { type: "string" }, + }, + required: ["orderId", "amount"], + }; + + const v2 = { + type: "object", + properties: { + orderId: { type: "string" }, + amount: { type: "number" }, + currency: { type: "string" }, + region: { type: "string" }, + }, + required: ["orderId", "amount", "region"], + }; + + const result = detectBreakingChanges(v1, v2, "BACKWARD"); + + expect(result).toContainEqual( + expect.objectContaining({ + type: "TYPE_CHANGED", + field: "amount", + breaking: true, + }), + ); + expect(result).toContainEqual( + expect.objectContaining({ + type: "FIELD_ADDED_REQUIRED", + field: "region", + breaking: true, + }), + ); + expect(result).toHaveLength(2); + }); + + it("diffJsonSchemas is exported and returns all changes", () => { + const before = { + type: "object", + properties: { name: { type: "string" } }, + }; + const after = { + type: "object", + properties: { name: { type: "number" } }, + }; + + const changes = diffJsonSchemas(before, after); + expect(changes).toHaveLength(1); + expect(changes[0].type).toBe("TYPE_CHANGED"); + }); + + it("NONE strategy always returns empty", () => { + const result = detectBreakingChanges( + { + type: "object", + properties: { a: { type: "string" } }, + required: ["a"], + }, + { type: "object" }, + "NONE", + ); + expect(result).toEqual([]); + }); +}); diff --git a/packages/breaking-changes/src/test/json-schema-differ.test.ts b/packages/breaking-changes/src/test/json-schema-differ.test.ts new file mode 100644 index 000000000..e3f573b4a --- /dev/null +++ b/packages/breaking-changes/src/test/json-schema-differ.test.ts @@ -0,0 +1,375 @@ +import { describe, it, expect } from "vitest"; +import { diffJsonSchemas } from "../json-schema/differ"; + +describe("diffJsonSchemas", () => { + describe("field additions", () => { + it("detects an added optional field", () => { + const before = { + type: "object", + properties: { name: { type: "string" } }, + }; + const after = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "FIELD_ADDED_OPTIONAL", + field: "email", + message: "Optional field 'email' was added", + }, + ]); + }); + + it("detects an added required field", () => { + const before = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const after = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "FIELD_ADDED_REQUIRED", + field: "email", + message: "Required field 'email' was added", + }, + ]); + }); + }); + + describe("field removals", () => { + it("detects a removed optional field", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name"], + }; + const after = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "FIELD_REMOVED_OPTIONAL", + field: "email", + message: "Optional field 'email' was removed", + }, + ]); + }); + + it("detects a removed required field", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const after = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "FIELD_REMOVED_REQUIRED", + field: "email", + message: "Required field 'email' was removed", + }, + ]); + }); + }); + + describe("type changes", () => { + it("detects a field type change", () => { + const before = { + type: "object", + properties: { amount: { type: "string" } }, + }; + const after = { + type: "object", + properties: { amount: { type: "number" } }, + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "TYPE_CHANGED", + field: "amount", + message: "Field 'amount' type changed from 'string' to 'number'", + previousType: "string", + currentType: "number", + }, + ]); + }); + }); + + describe("required changes (without add/remove)", () => { + it("detects a field becoming required", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name"], + }; + const after = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "REQUIRED_ADDED", + field: "email", + message: "Field 'email' was made required", + }, + ]); + }); + + it("detects a field becoming optional", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const after = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name"], + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "REQUIRED_REMOVED", + field: "email", + message: "Field 'email' was made optional", + }, + ]); + }); + }); + + describe("nested objects", () => { + it("detects changes in nested properties using dot-path notation", () => { + const before = { + type: "object", + properties: { + address: { + type: "object", + properties: { + street: { type: "string" }, + zipCode: { type: "string" }, + }, + required: ["street"], + }, + }, + }; + const after = { + type: "object", + properties: { + address: { + type: "object", + properties: { street: { type: "string" } }, + required: ["street"], + }, + }, + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "FIELD_REMOVED_OPTIONAL", + field: "address.zipCode", + message: "Optional field 'address.zipCode' was removed", + }, + ]); + }); + + it("detects type changes in deeply nested fields", () => { + const before = { + type: "object", + properties: { + order: { + type: "object", + properties: { + item: { + type: "object", + properties: { price: { type: "string" } }, + }, + }, + }, + }, + }; + const after = { + type: "object", + properties: { + order: { + type: "object", + properties: { + item: { + type: "object", + properties: { price: { type: "number" } }, + }, + }, + }, + }, + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "TYPE_CHANGED", + field: "order.item.price", + message: + "Field 'order.item.price' type changed from 'string' to 'number'", + previousType: "string", + currentType: "number", + }, + ]); + }); + }); + + describe("no changes", () => { + it("returns empty array when schemas are identical", () => { + const schema = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + expect(diffJsonSchemas(schema, schema)).toEqual([]); + }); + }); + + describe("edge cases", () => { + it("handles empty schemas", () => { + expect(diffJsonSchemas({}, {})).toEqual([]); + }); + + it("handles schema with no properties going to one with properties", () => { + const before = { type: "object" }; + const after = { + type: "object", + properties: { name: { type: "string" } }, + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "FIELD_ADDED_OPTIONAL", + field: "name", + message: "Optional field 'name' was added", + }, + ]); + }); + + it("handles schema with properties going to one with no properties", () => { + const before = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const after = { type: "object" }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "FIELD_REMOVED_REQUIRED", + field: "name", + message: "Required field 'name' was removed", + }, + ]); + }); + + it("handles multiple simultaneous changes", () => { + const before = { + type: "object", + properties: { + name: { type: "string" }, + age: { type: "string" }, + email: { type: "string" }, + }, + required: ["name", "email"], + }; + const after = { + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" }, + phone: { type: "string" }, + }, + required: ["name"], + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toContainEqual({ + type: "TYPE_CHANGED", + field: "age", + message: "Field 'age' type changed from 'string' to 'number'", + previousType: "string", + currentType: "number", + }); + expect(changes).toContainEqual({ + type: "FIELD_REMOVED_REQUIRED", + field: "email", + message: "Required field 'email' was removed", + }); + expect(changes).toContainEqual({ + type: "FIELD_ADDED_OPTIONAL", + field: "phone", + message: "Optional field 'phone' was added", + }); + expect(changes).toHaveLength(3); + }); + + it("detects root-level type changes", () => { + const before = { + type: "object", + properties: { name: { type: "string" } }, + }; + const after = { type: "array" }; + const changes = diffJsonSchemas(before, after); + expect(changes).toContainEqual({ + type: "TYPE_CHANGED", + field: "(root)", + message: "Root type changed from 'object' to 'array'", + previousType: "object", + currentType: "array", + }); + }); + + it("recurses into implicit object nodes (no explicit type)", () => { + const before = { + type: "object", + properties: { + address: { + properties: { + street: { type: "string" }, + city: { type: "string" }, + }, + required: ["street"], + }, + }, + }; + const after = { + type: "object", + properties: { + address: { + properties: { + street: { type: "string" }, + }, + required: ["street"], + }, + }, + }; + const changes = diffJsonSchemas(before, after); + expect(changes).toEqual([ + { + type: "FIELD_REMOVED_OPTIONAL", + field: "address.city", + message: "Optional field 'address.city' was removed", + }, + ]); + }); + }); +}); diff --git a/packages/breaking-changes/src/test/json-schema-rules.test.ts b/packages/breaking-changes/src/test/json-schema-rules.test.ts new file mode 100644 index 000000000..cbd6f7cdb --- /dev/null +++ b/packages/breaking-changes/src/test/json-schema-rules.test.ts @@ -0,0 +1,324 @@ +import { describe, it, expect } from "vitest"; +import { detectBreakingChanges } from "../json-schema/rules"; +import type { CompatibilityStrategy } from "../types"; + +describe("detectBreakingChanges", () => { + const beforeSchema = { + type: "object", + properties: { + name: { type: "string" }, + email: { type: "string" }, + }, + required: ["name", "email"], + }; + + describe("NONE strategy", () => { + it("never returns breaking changes regardless of schema diff", () => { + const after = { type: "object" }; + const result = detectBreakingChanges(beforeSchema, after, "NONE"); + expect(result).toEqual([]); + }); + }); + + describe("BACKWARD strategy", () => { + it("adding an optional field is NOT breaking", () => { + const after = { + ...beforeSchema, + properties: { ...beforeSchema.properties, phone: { type: "string" } }, + }; + const result = detectBreakingChanges(beforeSchema, after, "BACKWARD"); + expect(result).toEqual([]); + }); + + it("adding a required field IS breaking", () => { + const after = { + type: "object", + properties: { ...beforeSchema.properties, phone: { type: "string" } }, + required: ["name", "email", "phone"], + }; + const result = detectBreakingChanges(beforeSchema, after, "BACKWARD"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("FIELD_ADDED_REQUIRED"); + expect(result[0].breaking).toBe(true); + }); + + it("removing a required field IS breaking", () => { + const after = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const result = detectBreakingChanges(beforeSchema, after, "BACKWARD"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("FIELD_REMOVED_REQUIRED"); + }); + + it("removing an optional field is NOT breaking", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, nickname: { type: "string" } }, + required: ["name"], + }; + const after = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const result = detectBreakingChanges(before, after, "BACKWARD"); + expect(result).toEqual([]); + }); + + it("changing a field type IS breaking", () => { + const after = { + type: "object", + properties: { name: { type: "number" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const result = detectBreakingChanges(beforeSchema, after, "BACKWARD"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("TYPE_CHANGED"); + }); + + it("making an optional field required IS breaking", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name"], + }; + const after = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const result = detectBreakingChanges(before, after, "BACKWARD"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("REQUIRED_ADDED"); + }); + + it("making a required field optional is NOT breaking", () => { + const after = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name"], + }; + const result = detectBreakingChanges(beforeSchema, after, "BACKWARD"); + expect(result).toEqual([]); + }); + }); + + describe("FORWARD strategy", () => { + it("adding a required field IS breaking", () => { + const after = { + type: "object", + properties: { ...beforeSchema.properties, phone: { type: "string" } }, + required: ["name", "email", "phone"], + }; + const result = detectBreakingChanges(beforeSchema, after, "FORWARD"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("FIELD_ADDED_REQUIRED"); + }); + + it("removing a required field IS breaking", () => { + const after = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const result = detectBreakingChanges(beforeSchema, after, "FORWARD"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("FIELD_REMOVED_REQUIRED"); + }); + + it("changing a field type IS breaking", () => { + const after = { + type: "object", + properties: { name: { type: "number" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const result = detectBreakingChanges(beforeSchema, after, "FORWARD"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("TYPE_CHANGED"); + }); + + it("adding an optional field is NOT breaking", () => { + const after = { + ...beforeSchema, + properties: { ...beforeSchema.properties, phone: { type: "string" } }, + }; + const result = detectBreakingChanges(beforeSchema, after, "FORWARD"); + expect(result).toEqual([]); + }); + + it("removing an optional field is NOT breaking", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, nickname: { type: "string" } }, + required: ["name"], + }; + const after = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const result = detectBreakingChanges(before, after, "FORWARD"); + expect(result).toEqual([]); + }); + + it("making an optional field required IS breaking", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name"], + }; + const after = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const result = detectBreakingChanges(before, after, "FORWARD"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("REQUIRED_ADDED"); + }); + + it("making a required field optional is NOT breaking", () => { + const after = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name"], + }; + const result = detectBreakingChanges(beforeSchema, after, "FORWARD"); + expect(result).toEqual([]); + }); + }); + + describe("FULL strategy", () => { + it("adding a required field IS breaking", () => { + const after = { + type: "object", + properties: { ...beforeSchema.properties, phone: { type: "string" } }, + required: ["name", "email", "phone"], + }; + const result = detectBreakingChanges(beforeSchema, after, "FULL"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("FIELD_ADDED_REQUIRED"); + }); + + it("removing a required field IS breaking", () => { + const after = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const result = detectBreakingChanges(beforeSchema, after, "FULL"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("FIELD_REMOVED_REQUIRED"); + }); + + it("changing a field type IS breaking", () => { + const after = { + type: "object", + properties: { name: { type: "number" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const result = detectBreakingChanges(beforeSchema, after, "FULL"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("TYPE_CHANGED"); + }); + + it("adding an optional field is NOT breaking", () => { + const after = { + ...beforeSchema, + properties: { ...beforeSchema.properties, phone: { type: "string" } }, + }; + const result = detectBreakingChanges(beforeSchema, after, "FULL"); + expect(result).toEqual([]); + }); + + it("removing an optional field is NOT breaking", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, nickname: { type: "string" } }, + required: ["name"], + }; + const after = { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }; + const result = detectBreakingChanges(before, after, "FULL"); + expect(result).toEqual([]); + }); + + it("making an optional field required IS breaking", () => { + const before = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name"], + }; + const after = { + type: "object", + properties: { name: { type: "string" }, email: { type: "string" } }, + required: ["name", "email"], + }; + const result = detectBreakingChanges(before, after, "FULL"); + expect(result).toHaveLength(1); + expect(result[0].type).toBe("REQUIRED_ADDED"); + }); + }); + + describe("multiple breaking changes", () => { + it("returns all breaking changes in a single diff", () => { + const after = { + type: "object", + properties: { name: { type: "number" }, phone: { type: "string" } }, + required: ["name", "phone"], + }; + const result = detectBreakingChanges(beforeSchema, after, "BACKWARD"); + expect(result.length).toBeGreaterThanOrEqual(2); + expect(result.every((c) => c.breaking === true)).toBe(true); + }); + }); + + describe("nested schemas", () => { + it("detects breaking changes in nested fields", () => { + const before = { + type: "object", + properties: { + address: { + type: "object", + properties: { + street: { type: "string" }, + city: { type: "string" }, + }, + required: ["street", "city"], + }, + }, + }; + const after = { + type: "object", + properties: { + address: { + type: "object", + properties: { street: { type: "number" } }, + required: ["street"], + }, + }, + }; + const result = detectBreakingChanges(before, after, "BACKWARD"); + expect(result).toContainEqual( + expect.objectContaining({ + type: "TYPE_CHANGED", + field: "address.street", + breaking: true, + }), + ); + expect(result).toContainEqual( + expect.objectContaining({ + type: "FIELD_REMOVED_REQUIRED", + field: "address.city", + breaking: true, + }), + ); + }); + }); +}); diff --git a/packages/breaking-changes/src/types.ts b/packages/breaking-changes/src/types.ts new file mode 100644 index 000000000..9814e157c --- /dev/null +++ b/packages/breaking-changes/src/types.ts @@ -0,0 +1,22 @@ +export type CompatibilityStrategy = "BACKWARD" | "FORWARD" | "FULL" | "NONE"; + +export type SchemaChangeType = + | "FIELD_ADDED_REQUIRED" + | "FIELD_ADDED_OPTIONAL" + | "FIELD_REMOVED_REQUIRED" + | "FIELD_REMOVED_OPTIONAL" + | "TYPE_CHANGED" + | "REQUIRED_ADDED" + | "REQUIRED_REMOVED"; + +export type SchemaChange = { + type: SchemaChangeType; + field: string; + message: string; + previousType?: string; + currentType?: string; +}; + +export type BreakingChange = SchemaChange & { + breaking: true; +}; diff --git a/packages/breaking-changes/tsconfig.json b/packages/breaking-changes/tsconfig.json new file mode 100644 index 000000000..2e6fb2b7a --- /dev/null +++ b/packages/breaking-changes/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "bundler" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/breaking-changes/tsup.config.ts b/packages/breaking-changes/tsup.config.ts new file mode 100644 index 000000000..94d39e344 --- /dev/null +++ b/packages/breaking-changes/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + shims: true, +}); diff --git a/packages/breaking-changes/vitest.config.ts b/packages/breaking-changes/vitest.config.ts new file mode 100644 index 000000000..7057c8f3c --- /dev/null +++ b/packages/breaking-changes/vitest.config.ts @@ -0,0 +1,8 @@ +/// +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + testTimeout: 15_000, + }, +}); diff --git a/packages/cli/.claude/rules/sdk-cli-docs-sync.md b/packages/cli/.claude/rules/sdk-cli-docs-sync.md new file mode 100644 index 000000000..4e9d15bed --- /dev/null +++ b/packages/cli/.claude/rules/sdk-cli-docs-sync.md @@ -0,0 +1,37 @@ +# SDK and CLI Documentation Sync + +When any SDK function is **added**, **edited**, or **deleted** in this project, you must check and update the `src/cli-docs.ts` file to keep CLI documentation in sync. + +## What to check + +1. **New function added** → Add a corresponding entry to the `cliFunctions` array in `src/cli-docs.ts` +2. **Function signature changed** → Update the `args` array for that function in `cli-docs.ts` +3. **Function deleted** → Remove the corresponding entry from `cli-docs.ts` +4. **Function renamed** → Update the `name` field in `cli-docs.ts` + +## Files to watch + +- `src/index.ts` - Main SDK exports +- `src/events.ts`, `src/commands.ts`, `src/queries.ts`, etc. - Individual resource modules +- `src/services.ts`, `src/domains.ts`, `src/channels.ts` +- `src/teams.ts`, `src/users.ts`, `src/custom-docs.ts` +- `src/entities.ts`, `src/data-stores.ts`, `src/data-products.ts` +- `src/messages.ts`, `src/eventcatalog.ts` + +## CLI docs entry format + +Each function in `cli-docs.ts` should have: + +```typescript +{ + name: 'functionName', + description: 'What the function does', + category: 'Events' | 'Commands' | 'Services' | etc., + args: [ + { name: 'argName', type: 'string' | 'json' | 'boolean' | 'number', required: true/false, description: '...' } + ], + examples: [ + { description: 'Example description', command: 'npx @eventcatalog/sdk functionName "arg1"' } + ] +} +``` diff --git a/packages/cli/.prettierrc b/packages/cli/.prettierrc new file mode 100644 index 000000000..2fbe55cde --- /dev/null +++ b/packages/cli/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 130, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "es5", + "bracketSpacing": true +} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md new file mode 100644 index 000000000..33279a997 --- /dev/null +++ b/packages/cli/CHANGELOG.md @@ -0,0 +1,380 @@ +# @eventcatalog/cli + +## 0.6.12 + +### Patch Changes + +- Updated dependencies [9949f9f] +- Updated dependencies [d64cfab] + - @eventcatalog/sdk@2.25.0 + - @eventcatalog/language-server@0.8.23 + +## 0.6.12-beta.1 + +### Patch Changes + +- Updated dependencies [9949f9f] + - @eventcatalog/sdk@2.25.0-beta.1 + - @eventcatalog/language-server@0.8.23-beta.1 + +## 0.6.12-beta.0 + +### Patch Changes + +- Updated dependencies [d64cfab] + - @eventcatalog/sdk@2.25.0-beta.0 + - @eventcatalog/language-server@0.8.23-beta.0 + +## 0.6.11 + +### Patch Changes + +- Updated dependencies [29dc055] + - @eventcatalog/sdk@2.24.2 + - @eventcatalog/language-server@0.8.22 + +## 0.6.10 + +### Patch Changes + +- Updated dependencies [3334ab1] + - @eventcatalog/sdk@2.24.1 + - @eventcatalog/language-server@0.8.21 + +## 0.6.9 + +### Patch Changes + +- Updated dependencies [6b7fc3c] + - @eventcatalog/sdk@2.24.0 + - @eventcatalog/language-server@0.8.20 + +## 0.6.8 + +### Patch Changes + +- Updated dependencies [2801b3e] + - @eventcatalog/sdk@2.23.1 + - @eventcatalog/language-server@0.8.19 + +## 0.6.7 + +### Patch Changes + +- Updated dependencies [3ad00ad] + - @eventcatalog/sdk@2.23.0 + - @eventcatalog/language-server@0.8.18 + +## 0.6.6 + +### Patch Changes + +- Updated dependencies [50b38f6] + - @eventcatalog/sdk@2.22.0 + - @eventcatalog/language-server@0.8.17 + +## 0.6.5 + +### Patch Changes + +- Updated dependencies [67940c4] + - @eventcatalog/sdk@2.21.2 + - @eventcatalog/language-server@0.8.16 + +## 0.6.4 + +### Patch Changes + +- Updated dependencies [1c9c217] + - @eventcatalog/sdk@2.21.1 + - @eventcatalog/language-server@0.8.15 + +## 0.6.3 + +### Patch Changes + +- Updated dependencies [3a7b096] + - @eventcatalog/sdk@2.21.0 + - @eventcatalog/language-server@0.8.14 + +## 0.6.2 + +### Patch Changes + +- Updated dependencies [8f724a7] + - @eventcatalog/sdk@2.20.0 + - @eventcatalog/language-server@0.8.13 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies [52b4f24] + - @eventcatalog/sdk@2.19.0 + - @eventcatalog/language-server@0.8.12 + +## 0.6.0 + +### Minor Changes + +- a3313f6: feat: add breaking schema change detection with governance webhooks + + New `schema_breaking_change` governance trigger that detects breaking JSON Schema changes per compatibility strategy (BACKWARD, FORWARD, FULL, NONE). Users configure `compatibility.strategy` in governance.yaml and subscribe to breaking change webhooks with detailed diff information. + +### Patch Changes + +- Updated dependencies [a3313f6] + - @eventcatalog/breaking-changes@0.2.0 + +## 0.5.11 + +### Patch Changes + +- Updated dependencies [ee142e4] + - @eventcatalog/sdk@2.18.4 + - @eventcatalog/language-server@0.8.11 + +## 0.5.10 + +### Patch Changes + +- Updated dependencies [88795d0] + - @eventcatalog/sdk@2.18.3 + - @eventcatalog/language-server@0.8.10 + +## 0.5.9 + +### Patch Changes + +- Updated dependencies [4639ec3] + - @eventcatalog/sdk@2.18.2 + - @eventcatalog/language-server@0.8.9 + +## 0.5.8 + +### Patch Changes + +- Updated dependencies [e2b0460] + - @eventcatalog/sdk@2.18.1 + - @eventcatalog/language-server@0.8.8 + +## 0.5.7 + +### Patch Changes + +- Updated dependencies [913ca20] + - @eventcatalog/sdk@2.18.0 + - @eventcatalog/language-server@0.8.7 + +## 0.5.6 + +### Patch Changes + +- Updated dependencies [83aca74] + - @eventcatalog/sdk@2.17.4 + - @eventcatalog/language-server@0.8.6 + +## 0.5.5 + +### Patch Changes + +- 21c8095: Add `type: fail` governance action to block CI/CD pipelines when rules trigger. Supports optional `message` field with `$ENV_VAR` interpolation. Rules without a fail action continue to exit 0 (backward compatible). + +## 0.5.4 + +### Patch Changes + +- Updated dependencies [46a2292] + - @eventcatalog/sdk@2.17.3 + - @eventcatalog/language-server@0.8.5 + +## 0.5.3 + +### Patch Changes + +- Updated dependencies [0514bb1] + - @eventcatalog/sdk@2.17.2 + - @eventcatalog/language-server@0.8.4 + +## 0.5.2 + +### Patch Changes + +- Updated dependencies [6456054] + - @eventcatalog/sdk@2.17.1 + - @eventcatalog/language-server@0.8.3 + +## 0.5.1 + +### Patch Changes + +- 78bf2d4: Support governance.yml in addition to governance.yaml +- bcef8cc: add message_deprecated governance trigger to notify teams when a producing service deprecates a message + +## 0.5.0 + +### Minor Changes + +- af2d4f4: add architecture change detection with governance rules, catalog snapshots, and webhook notifications + +### Patch Changes + +- Updated dependencies [af2d4f4] + - @eventcatalog/sdk@2.17.0 + - @eventcatalog/language-server@0.8.2 + +## 0.4.11 + +### Patch Changes + +- Updated dependencies [283c147] + - @eventcatalog/sdk@2.16.0 + - @eventcatalog/language-server@0.8.1 + +## 0.4.10 + +### Patch Changes + +- Updated dependencies [a461a9b] + - @eventcatalog/language-server@0.8.0 + +## 0.4.9 + +### Patch Changes + +- Updated dependencies [b86daae] + - @eventcatalog/language-server@0.7.0 + +## 0.4.8 + +### Patch Changes + +- cb0235a: Replace playground.eventcatalog.dev URLs with compass.eventcatalog.dev +- Updated dependencies [cb0235a] + - @eventcatalog/language-server@0.6.2 + +## 0.4.7 + +### Patch Changes + +- Updated dependencies [a43021e] + - @eventcatalog/language-server@0.6.1 + +## 0.4.6 + +### Patch Changes + +- Updated dependencies [b5bbb3d] + - @eventcatalog/sdk@2.15.1 + +## 0.4.5 + +### Patch Changes + +- Updated dependencies [2663850] + - @eventcatalog/language-server@0.6.0 + +## 0.4.4 + +### Patch Changes + +- Updated dependencies [a85a4d7] + - @eventcatalog/language-server@0.5.0 + +## 0.4.3 + +### Patch Changes + +- Updated dependencies [6122f93] + - @eventcatalog/language-server@0.4.1 + +## 0.4.2 + +### Patch Changes + +- e6a759e: Default container_type to 'database' when not specified in .ec file imports + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [7531be8] +- Updated dependencies [a1c5012] +- Updated dependencies [25acfdd] + - @eventcatalog/sdk@2.15.0 + - @eventcatalog/language-server@0.4.0 + +## 0.4.0 + +### Minor Changes + +- cf7fcac: add DSL-managed key merging for import to preserve non-DSL frontmatter, fix deprecated/draft falsy value handling in compiler + +### Patch Changes + +- Updated dependencies [cf7fcac] +- Updated dependencies [c4104a1] + - @eventcatalog/language-server@0.3.0 + +## 0.3.4 + +### Patch Changes + +- Updated dependencies [f3685b6] + - @eventcatalog/sdk@2.14.3 + +## 0.3.3 + +### Patch Changes + +- 6a4e582: Improve DSL import with container ref support, resource stubs, and cache upsert; add writes-to and reads-from compilation in the language server. +- Updated dependencies [6a4e582] + - @eventcatalog/language-server@0.2.3 + - @eventcatalog/sdk@2.14.2 + +## 0.3.2 + +### Patch Changes + +- d703923: Replace base64 logo blob with actual logo.png asset, add import DSL command, and improve compiler with nested output support and domain/subdomain service pointer generation +- Updated dependencies [d703923] + - @eventcatalog/language-server@0.2.2 + +## 0.3.1 + +### Patch Changes + +- 69cb966: add in-memory file index cache to SDK for faster lookups, fix playground URL in CLI export +- Updated dependencies [69cb966] + - @eventcatalog/sdk@2.14.1 + +## 0.3.0 + +### Minor Changes + +- c782129: Add CLI export command for exporting catalog resources to EventCatalog DSL (.ec) format. Supports single resource, bulk, and full catalog export with hydration, section grouping, and visualizer blocks. Fix getServices to ignore nested channels, containers, data-products, data-stores, and flows. +- c782129: feat(cli): add export command for DSL (.ec) format + +### Patch Changes + +- Updated dependencies [c782129] +- Updated dependencies [c782129] + - @eventcatalog/sdk@2.14.0 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies [b466daf] + - @eventcatalog/sdk@2.13.2 + +## 0.2.0 + +### Minor Changes + +- 0b35904: Extract CLI into new @eventcatalog/cli package for cleaner separation of concerns. The SDK no longer bundles the CLI binary. + +### Patch Changes + +- Updated dependencies [0b35904] + - @eventcatalog/sdk@2.13.1 diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 000000000..ca97528c4 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,199 @@ +## @eventcatalog/cli + +Command-line interface for [EventCatalog](https://eventcatalog.dev). Import and export catalogs using the [EventCatalog DSL](https://www.eventcatalog.dev/docs/development/dsl/introduction), run SDK functions directly from your terminal, and automate your EventCatalog workflows. + +### Installation + +```sh +npm i @eventcatalog/cli +``` + +### Running with npx (no installation required) + +```bash +npx @eventcatalog/cli --dir [args...] +``` + +### Running after installation + +If you've installed the package, the `eventcatalog` command is available: + +```bash +eventcatalog --dir [args...] +``` + +### Global Options + +| Option | Description | Default | +| ------------------ | ------------------------------ | ----------------------- | +| `-d, --dir ` | Path to your catalog directory | `.` (current directory) | + +--- + +### Commands + +#### `import` — Import DSL files into your catalog + +Parse `.ec` (EventCatalog DSL) files and write them as catalog resources (markdown + frontmatter). + +```bash +eventcatalog import [files...] [options] +``` + +**Options:** + +| Option | Description | +| ----------- | ------------------------------------------------------------ | +| `--stdin` | Read DSL from stdin instead of files | +| `--dry-run` | Preview changes without writing to disk | +| `--flat` | Write all resources at the top level (no nested directories) | +| `--no-init` | Skip the interactive catalog scaffolding prompt | + +**Examples:** + +```bash +# Import a single DSL file +eventcatalog import architecture.ec + +# Import multiple files +eventcatalog import services.ec events.ec domains.ec + +# Pipe DSL from another tool +cat architecture.ec | eventcatalog import --stdin + +# Preview what would change +eventcatalog import architecture.ec --dry-run + +# Import without nesting services inside domains +eventcatalog import architecture.ec --flat +``` + +**Behaviors:** + +- If no `eventcatalog.config.js` exists, you'll be prompted to scaffold a new catalog (skip with `--no-init`). +- Importing a newer version of an existing resource automatically versions the old one. +- Re-importing the same version overwrites the existing resource. +- Referenced resources that aren't defined in the DSL (e.g., `sends event OrderCreated` without an inline body) are created as stubs at version `0.0.1`. +- Existing resource locations are preserved — updates go to where the resource already lives. + +--- + +#### `export` — Export catalog resources to DSL + +Convert catalog resources back into EventCatalog DSL (`.ec`) format. + +```bash +eventcatalog export [options] +``` + +**Options:** + +| Option | Description | +| --------------------- | ----------------------------------------------------------------------------------------- | +| `--all` | Export the entire catalog | +| `--resource ` | Resource type to export (`event`, `command`, `query`, `service`, `domain`, `channel`) | +| `--id ` | Export a specific resource by ID (requires `--resource`) | +| `--version ` | Export a specific version (requires `--resource` and `--id`) | +| `--hydrate` | Include referenced resources (e.g., messages referenced by a service) | +| `--stdout` | Print to stdout instead of writing a file | +| `--playground` | Open the exported DSL in the [EventCatalog Playground](https://compass.eventcatalog.dev/) | +| `--output ` | Custom output file path | + +**Examples:** + +```bash +# Export a single event +eventcatalog export --resource event --id OrderCreated --stdout + +# Export all services with their referenced messages +eventcatalog export --resource service --hydrate --stdout + +# Export the entire catalog to a file +eventcatalog export --all --output catalog.ec + +# Export and open in the playground +eventcatalog export --all --playground +``` + +--- + +#### `list` — List available SDK functions + +Display all SDK functions organized by category (Events, Commands, Queries, Services, Domains, etc.). + +```bash +eventcatalog list +``` + +--- + +#### `` — Run any SDK function + +Any unrecognized command is treated as an SDK function call. Output is JSON. + +```bash +eventcatalog [args...] +``` + +**Examples:** + +```bash +# Get an event (latest version) +eventcatalog --dir ./my-catalog getEvent "OrderCreated" + +# Get a specific version +eventcatalog --dir ./my-catalog getEvent "OrderCreated" "1.0.0" + +# Get all events with options +eventcatalog --dir ./my-catalog getEvents '{"latestOnly":true}' + +# Write an event +eventcatalog --dir ./my-catalog writeEvent '{"id":"OrderCreated","name":"Order Created","version":"1.0.0","markdown":"# Order Created"}' + +# Get a service +eventcatalog --dir ./my-catalog getService "InventoryService" + +# Add an event to a service +eventcatalog --dir ./my-catalog addEventToService "InventoryService" "sends" '{"id":"OrderCreated","version":"1.0.0"}' +``` + +Run `eventcatalog list` to see all available functions. + +--- + +### Piping and Composing + +Output from SDK functions is JSON, making it easy to pipe to tools like `jq`: + +```bash +# Filter events by version +eventcatalog --dir ./my-catalog getEvents | jq '.[] | select(.version == "1.0.0")' + +# Count total events +eventcatalog --dir ./my-catalog getEvents | jq 'length' + +# Extract event IDs +eventcatalog --dir ./my-catalog getEvents | jq '.[].id' +``` + +### Arguments Format + +Arguments are automatically parsed: + +- **JSON objects:** `'{"key":"value"}'` — parsed as object +- **JSON arrays:** `'["item1","item2"]'` — parsed as array +- **Booleans:** `true` or `false` — parsed as boolean +- **Numbers:** `42` or `3.14` — parsed as number +- **Strings:** anything else — kept as string + +### Documentation + +- [EventCatalog DSL](https://www.eventcatalog.dev/docs/development/dsl/introduction) +- [SDK reference](https://www.eventcatalog.dev/docs/sdk) +- [CLI documentation](https://www.eventcatalog.dev/docs/development/cli) + +## Enterprise support + +Interested in collaborating with us? Our offerings include dedicated support, priority assistance, feature development, custom integrations, and more. + +Find more details on our [services page](https://eventcatalog.dev/services). diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000..8657fa0a7 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,59 @@ +{ + "name": "@eventcatalog/cli", + "version": "0.6.12", + "description": "CLI for EventCatalog", + "scripts": { + "build": "tsup", + "build:bin": "tsup", + "test": "vitest --run", + "test:ci": "vitest --run", + "bench:import-export": "node src/test/bench/import-export.benchmark.js", + "format": "prettier --write .", + "format:diff": "prettier --list-different ." + }, + "bin": { + "eventcatalog": "./dist/cli/index.js" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [], + "author": "", + "license": "MIT", + "files": [ + "dist", + "package.json" + ], + "exports": { + "./cli-docs": { + "types": "./dist/cli-docs.d.ts", + "require": "./dist/cli-docs.js", + "import": "./dist/cli-docs.mjs" + } + }, + "dependencies": { + "@eventcatalog/breaking-changes": "workspace:*", + "@eventcatalog/language-server": "workspace:*", + "@eventcatalog/sdk": "workspace:*", + "@eventcatalog/license": "^0.0.7", + "commander": "^12.0.0", + "dotenv": "^16.5.0", + "gray-matter": "^4.0.3", + "js-yaml": "^4.1.1", + "langium": "^3.3.1", + "open": "^11.0.0", + "semver": "^7.7.2" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.14.10", + "prettier": "^3.3.3", + "tsup": "^8.1.0", + "typescript": "^5.5.3", + "vitest": "^3.2.4" + }, + "repository": { + "type": "git", + "url": "https://github.com/event-catalog/eventcatalog" + } +} diff --git a/packages/cli/src/cli-docs.ts b/packages/cli/src/cli-docs.ts new file mode 100644 index 000000000..9d124685c --- /dev/null +++ b/packages/cli/src/cli-docs.ts @@ -0,0 +1,1885 @@ +/** + * CLI Documentation Registry + * + * This file contains metadata for all SDK functions that can be called via the CLI. + * It is used to generate documentation for the EventCatalog website. + * + * @module cli-docs + */ + +export interface CLIFunctionArg { + name: string; + type: 'string' | 'json' | 'boolean' | 'number'; + required: boolean; + description: string; +} + +export interface CLIFunctionExample { + description: string; + command: string; +} + +export interface CLIFunctionDoc { + name: string; + description: string; + category: string; + args: CLIFunctionArg[]; + examples: CLIFunctionExample[]; +} + +/** + * All CLI-callable functions with their documentation + */ +export const cliFunctions: CLIFunctionDoc[] = [ + // ================================ + // Events + // ================================ + { + name: 'getEvent', + description: 'Returns an event from EventCatalog by its ID', + category: 'Events', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the event to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve (supports semver)' }, + { name: 'options', type: 'json', required: false, description: 'Options object, e.g. {"attachSchema": true}' }, + ], + examples: [ + { description: 'Get the latest version of an event', command: 'npx @eventcatalog/cli getEvent "OrderCreated"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getEvent "OrderCreated" "1.0.0"' }, + { + description: 'Get event with schema attached', + command: 'npx @eventcatalog/cli getEvent "OrderCreated" "1.0.0" \'{"attachSchema":true}\'', + }, + ], + }, + { + name: 'getEvents', + description: 'Returns all events from EventCatalog', + category: 'Events', + args: [ + { + name: 'options', + type: 'json', + required: false, + description: 'Options object, e.g. {"latestOnly": true, "attachSchema": true}', + }, + ], + examples: [ + { description: 'Get all events', command: 'npx @eventcatalog/cli getEvents' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getEvents \'{"latestOnly":true}\'' }, + { + description: 'Get all events with schemas', + command: 'npx @eventcatalog/cli getEvents \'{"latestOnly":true,"attachSchema":true}\'', + }, + ], + }, + { + name: 'writeEvent', + description: 'Writes an event to EventCatalog', + category: 'Events', + args: [ + { name: 'event', type: 'json', required: true, description: 'Event object with id, name, version, and markdown' }, + { + name: 'options', + type: 'json', + required: false, + description: 'Options: {path?, override?, versionExistingContent?, format?}', + }, + ], + examples: [ + { + description: 'Write a new event', + command: + 'npx @eventcatalog/cli writeEvent \'{"id":"OrderCreated","name":"Order Created","version":"1.0.0","markdown":"# Order Created"}\'', + }, + { + description: 'Write and version existing content', + command: + 'npx @eventcatalog/cli writeEvent \'{"id":"OrderCreated","name":"Order Created","version":"2.0.0","markdown":"# Order Created v2"}\' \'{"versionExistingContent":true}\'', + }, + ], + }, + { + name: 'writeEventToService', + description: 'Writes an event to a specific service in EventCatalog', + category: 'Events', + args: [ + { name: 'event', type: 'json', required: true, description: 'Event object with id, name, version, and markdown' }, + { name: 'service', type: 'json', required: true, description: 'Service reference: {id, version?}' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, format?, override?}' }, + ], + examples: [ + { + description: 'Write event to a service', + command: + 'npx @eventcatalog/cli writeEventToService \'{"id":"InventoryUpdated","name":"Inventory Updated","version":"1.0.0","markdown":"# Inventory Updated"}\' \'{"id":"InventoryService"}\'', + }, + ], + }, + { + name: 'rmEvent', + description: 'Removes an event by its path', + category: 'Events', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the event, e.g. /InventoryAdjusted' }], + examples: [{ description: 'Remove an event by path', command: 'npx @eventcatalog/cli rmEvent "/InventoryAdjusted"' }], + }, + { + name: 'rmEventById', + description: 'Removes an event by its ID', + category: 'Events', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the event to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [ + { description: 'Remove latest version', command: 'npx @eventcatalog/cli rmEventById "OrderCreated"' }, + { description: 'Remove specific version', command: 'npx @eventcatalog/cli rmEventById "OrderCreated" "1.0.0"' }, + ], + }, + { + name: 'versionEvent', + description: 'Moves the current event to a versioned directory', + category: 'Events', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the event to version' }], + examples: [{ description: 'Version an event', command: 'npx @eventcatalog/cli versionEvent "OrderCreated"' }], + }, + { + name: 'addFileToEvent', + description: 'Adds a file to an event', + category: 'Events', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the event' }, + { name: 'file', type: 'json', required: true, description: 'File object: {content, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to add file to' }, + ], + examples: [ + { + description: 'Add a file to an event', + command: 'npx @eventcatalog/cli addFileToEvent "OrderCreated" \'{"content":"# Schema","fileName":"schema.md"}\'', + }, + ], + }, + { + name: 'addSchemaToEvent', + description: 'Adds a schema file to an event', + category: 'Events', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the event' }, + { name: 'schema', type: 'json', required: true, description: 'Schema object: {schema, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to add schema to' }, + ], + examples: [ + { + description: 'Add a JSON schema to an event', + command: + 'npx @eventcatalog/cli addSchemaToEvent "OrderCreated" \'{"schema":"{\\"type\\":\\"object\\"}","fileName":"schema.json"}\'', + }, + ], + }, + { + name: 'eventHasVersion', + description: 'Checks if a specific version of an event exists', + category: 'Events', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the event' }, + { name: 'version', type: 'string', required: true, description: 'Version to check (supports semver)' }, + ], + examples: [ + { description: 'Check if version exists', command: 'npx @eventcatalog/cli eventHasVersion "OrderCreated" "1.0.0"' }, + { description: 'Check with semver range', command: 'npx @eventcatalog/cli eventHasVersion "OrderCreated" "1.0.x"' }, + ], + }, + + // ================================ + // Commands + // ================================ + { + name: 'getCommand', + description: 'Returns a command from EventCatalog by its ID', + category: 'Commands', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the command to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve (supports semver)' }, + ], + examples: [ + { description: 'Get the latest command', command: 'npx @eventcatalog/cli getCommand "CreateOrder"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getCommand "CreateOrder" "1.0.0"' }, + ], + }, + { + name: 'getCommands', + description: 'Returns all commands from EventCatalog', + category: 'Commands', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {latestOnly?, attachSchema?}' }], + examples: [ + { description: 'Get all commands', command: 'npx @eventcatalog/cli getCommands' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getCommands \'{"latestOnly":true}\'' }, + ], + }, + { + name: 'writeCommand', + description: 'Writes a command to EventCatalog', + category: 'Commands', + args: [ + { name: 'command', type: 'json', required: true, description: 'Command object with id, name, version, and markdown' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?, versionExistingContent?}' }, + ], + examples: [ + { + description: 'Write a new command', + command: + 'npx @eventcatalog/cli writeCommand \'{"id":"CreateOrder","name":"Create Order","version":"1.0.0","markdown":"# Create Order"}\'', + }, + ], + }, + { + name: 'writeCommandToService', + description: 'Writes a command to a specific service', + category: 'Commands', + args: [ + { name: 'command', type: 'json', required: true, description: 'Command object' }, + { name: 'service', type: 'json', required: true, description: 'Service reference: {id, version?}' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, format?, override?}' }, + ], + examples: [ + { + description: 'Write command to a service', + command: + 'npx @eventcatalog/cli writeCommandToService \'{"id":"UpdateInventory","name":"Update Inventory","version":"1.0.0","markdown":"# Update Inventory"}\' \'{"id":"InventoryService"}\'', + }, + ], + }, + { + name: 'rmCommand', + description: 'Removes a command by its path', + category: 'Commands', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the command' }], + examples: [{ description: 'Remove a command', command: 'npx @eventcatalog/cli rmCommand "/CreateOrder"' }], + }, + { + name: 'rmCommandById', + description: 'Removes a command by its ID', + category: 'Commands', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the command to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [{ description: 'Remove a command', command: 'npx @eventcatalog/cli rmCommandById "CreateOrder"' }], + }, + { + name: 'versionCommand', + description: 'Moves the current command to a versioned directory', + category: 'Commands', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the command to version' }], + examples: [{ description: 'Version a command', command: 'npx @eventcatalog/cli versionCommand "CreateOrder"' }], + }, + { + name: 'addFileToCommand', + description: 'Adds a file to a command', + category: 'Commands', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the command' }, + { name: 'file', type: 'json', required: true, description: 'File object: {content, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Add a file to a command', + command: 'npx @eventcatalog/cli addFileToCommand "CreateOrder" \'{"content":"# Notes","fileName":"notes.md"}\'', + }, + ], + }, + { + name: 'addSchemaToCommand', + description: 'Adds a schema to a command', + category: 'Commands', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the command' }, + { name: 'schema', type: 'json', required: true, description: 'Schema object: {schema, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Add a schema to a command', + command: + 'npx @eventcatalog/cli addSchemaToCommand "CreateOrder" \'{"schema":"{\\"type\\":\\"object\\"}","fileName":"schema.json"}\'', + }, + ], + }, + { + name: 'commandHasVersion', + description: 'Checks if a specific version of a command exists', + category: 'Commands', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the command' }, + { name: 'version', type: 'string', required: true, description: 'Version to check' }, + ], + examples: [ + { description: 'Check if version exists', command: 'npx @eventcatalog/cli commandHasVersion "CreateOrder" "1.0.0"' }, + ], + }, + + // ================================ + // Queries + // ================================ + { + name: 'getQuery', + description: 'Returns a query from EventCatalog by its ID', + category: 'Queries', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the query to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve' }, + ], + examples: [ + { description: 'Get the latest query', command: 'npx @eventcatalog/cli getQuery "GetOrder"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getQuery "GetOrder" "1.0.0"' }, + ], + }, + { + name: 'getQueries', + description: 'Returns all queries from EventCatalog', + category: 'Queries', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {latestOnly?, attachSchema?}' }], + examples: [ + { description: 'Get all queries', command: 'npx @eventcatalog/cli getQueries' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getQueries \'{"latestOnly":true}\'' }, + ], + }, + { + name: 'writeQuery', + description: 'Writes a query to EventCatalog', + category: 'Queries', + args: [ + { name: 'query', type: 'json', required: true, description: 'Query object with id, name, version, and markdown' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?, versionExistingContent?}' }, + ], + examples: [ + { + description: 'Write a new query', + command: + 'npx @eventcatalog/cli writeQuery \'{"id":"GetOrder","name":"Get Order","version":"1.0.0","markdown":"# Get Order"}\'', + }, + ], + }, + { + name: 'writeQueryToService', + description: 'Writes a query to a specific service', + category: 'Queries', + args: [ + { name: 'query', type: 'json', required: true, description: 'Query object' }, + { name: 'service', type: 'json', required: true, description: 'Service reference: {id, version?}' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, format?, override?}' }, + ], + examples: [ + { + description: 'Write query to a service', + command: + 'npx @eventcatalog/cli writeQueryToService \'{"id":"GetInventory","name":"Get Inventory","version":"1.0.0","markdown":"# Get Inventory"}\' \'{"id":"InventoryService"}\'', + }, + ], + }, + { + name: 'rmQuery', + description: 'Removes a query by its path', + category: 'Queries', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the query' }], + examples: [{ description: 'Remove a query', command: 'npx @eventcatalog/cli rmQuery "/GetOrder"' }], + }, + { + name: 'rmQueryById', + description: 'Removes a query by its ID', + category: 'Queries', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the query to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [{ description: 'Remove a query', command: 'npx @eventcatalog/cli rmQueryById "GetOrder"' }], + }, + { + name: 'versionQuery', + description: 'Moves the current query to a versioned directory', + category: 'Queries', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the query to version' }], + examples: [{ description: 'Version a query', command: 'npx @eventcatalog/cli versionQuery "GetOrder"' }], + }, + { + name: 'addFileToQuery', + description: 'Adds a file to a query', + category: 'Queries', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the query' }, + { name: 'file', type: 'json', required: true, description: 'File object: {content, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Add a file to a query', + command: 'npx @eventcatalog/cli addFileToQuery "GetOrder" \'{"content":"# Notes","fileName":"notes.md"}\'', + }, + ], + }, + { + name: 'addSchemaToQuery', + description: 'Adds a schema to a query', + category: 'Queries', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the query' }, + { name: 'schema', type: 'json', required: true, description: 'Schema object: {schema, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Add a schema to a query', + command: + 'npx @eventcatalog/cli addSchemaToQuery "GetOrder" \'{"schema":"{\\"type\\":\\"object\\"}","fileName":"schema.json"}\'', + }, + ], + }, + { + name: 'queryHasVersion', + description: 'Checks if a specific version of a query exists', + category: 'Queries', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the query' }, + { name: 'version', type: 'string', required: true, description: 'Version to check' }, + ], + examples: [{ description: 'Check if version exists', command: 'npx @eventcatalog/cli queryHasVersion "GetOrder" "1.0.0"' }], + }, + + // ================================ + // Services + // ================================ + { + name: 'getService', + description: 'Returns a service from EventCatalog by its ID', + category: 'Services', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the service to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve' }, + ], + examples: [ + { description: 'Get the latest service', command: 'npx @eventcatalog/cli getService "OrderService"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getService "OrderService" "1.0.0"' }, + ], + }, + { + name: 'getServices', + description: 'Returns all services from EventCatalog', + category: 'Services', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {latestOnly?}' }], + examples: [ + { description: 'Get all services', command: 'npx @eventcatalog/cli getServices' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getServices \'{"latestOnly":true}\'' }, + ], + }, + { + name: 'writeService', + description: 'Writes a service to EventCatalog', + category: 'Services', + args: [ + { name: 'service', type: 'json', required: true, description: 'Service object with id, name, version, and markdown' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?, versionExistingContent?}' }, + ], + examples: [ + { + description: 'Write a new service', + command: + 'npx @eventcatalog/cli writeService \'{"id":"OrderService","name":"Order Service","version":"1.0.0","markdown":"# Order Service"}\'', + }, + ], + }, + { + name: 'writeServiceToDomain', + description: 'Writes a service to a specific domain', + category: 'Services', + args: [ + { name: 'service', type: 'json', required: true, description: 'Service object' }, + { name: 'domain', type: 'json', required: true, description: 'Domain reference: {id, version?}' }, + { name: 'options', type: 'json', required: false, description: 'Options' }, + ], + examples: [ + { + description: 'Write service to a domain', + command: + 'npx @eventcatalog/cli writeServiceToDomain \'{"id":"PaymentService","name":"Payment Service","version":"1.0.0","markdown":"# Payment Service"}\' \'{"id":"Payments"}\'', + }, + ], + }, + { + name: 'rmService', + description: 'Removes a service by its path', + category: 'Services', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the service' }], + examples: [{ description: 'Remove a service', command: 'npx @eventcatalog/cli rmService "/OrderService"' }], + }, + { + name: 'rmServiceById', + description: 'Removes a service by its ID', + category: 'Services', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the service to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [{ description: 'Remove a service', command: 'npx @eventcatalog/cli rmServiceById "OrderService"' }], + }, + { + name: 'versionService', + description: 'Moves the current service to a versioned directory', + category: 'Services', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the service to version' }], + examples: [{ description: 'Version a service', command: 'npx @eventcatalog/cli versionService "OrderService"' }], + }, + { + name: 'addFileToService', + description: 'Adds a file to a service', + category: 'Services', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the service' }, + { name: 'file', type: 'json', required: true, description: 'File object: {content, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Add a file to a service', + command: 'npx @eventcatalog/cli addFileToService "OrderService" \'{"content":"# API Docs","fileName":"api.md"}\'', + }, + ], + }, + { + name: 'addEventToService', + description: 'Adds an event relationship to a service', + category: 'Services', + args: [ + { name: 'serviceId', type: 'string', required: true, description: 'The ID of the service' }, + { name: 'direction', type: 'string', required: true, description: 'Direction: "sends" or "receives"' }, + { name: 'event', type: 'json', required: true, description: 'Event reference: {id, version}' }, + { name: 'serviceVersion', type: 'string', required: false, description: 'Specific service version' }, + ], + examples: [ + { + description: 'Add event that service sends', + command: 'npx @eventcatalog/cli addEventToService "OrderService" "sends" \'{"id":"OrderCreated","version":"1.0.0"}\'', + }, + { + description: 'Add event that service receives', + command: + 'npx @eventcatalog/cli addEventToService "OrderService" "receives" \'{"id":"PaymentCompleted","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addCommandToService', + description: 'Adds a command relationship to a service', + category: 'Services', + args: [ + { name: 'serviceId', type: 'string', required: true, description: 'The ID of the service' }, + { name: 'direction', type: 'string', required: true, description: 'Direction: "sends" or "receives"' }, + { name: 'command', type: 'json', required: true, description: 'Command reference: {id, version}' }, + { name: 'serviceVersion', type: 'string', required: false, description: 'Specific service version' }, + ], + examples: [ + { + description: 'Add command that service sends', + command: 'npx @eventcatalog/cli addCommandToService "OrderService" "sends" \'{"id":"ProcessPayment","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addQueryToService', + description: 'Adds a query relationship to a service', + category: 'Services', + args: [ + { name: 'serviceId', type: 'string', required: true, description: 'The ID of the service' }, + { name: 'direction', type: 'string', required: true, description: 'Direction: "sends" or "receives"' }, + { name: 'query', type: 'json', required: true, description: 'Query reference: {id, version}' }, + { name: 'serviceVersion', type: 'string', required: false, description: 'Specific service version' }, + ], + examples: [ + { + description: 'Add query that service sends', + command: 'npx @eventcatalog/cli addQueryToService "OrderService" "sends" \'{"id":"GetInventory","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addEntityToService', + description: 'Adds an entity to a service', + category: 'Services', + args: [ + { name: 'serviceId', type: 'string', required: true, description: 'The ID of the service' }, + { name: 'entity', type: 'json', required: true, description: 'Entity reference: {id, version}' }, + { name: 'serviceVersion', type: 'string', required: false, description: 'Specific service version' }, + ], + examples: [ + { + description: 'Add entity to a service', + command: 'npx @eventcatalog/cli addEntityToService "OrderService" \'{"id":"Order","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addDataStoreToService', + description: 'Adds a data store relationship to a service', + category: 'Services', + args: [ + { name: 'serviceId', type: 'string', required: true, description: 'The ID of the service' }, + { name: 'relationship', type: 'string', required: true, description: 'Relationship: "writesTo" or "readsFrom"' }, + { name: 'dataStore', type: 'json', required: true, description: 'Data store reference: {id, version}' }, + { name: 'serviceVersion', type: 'string', required: false, description: 'Specific service version' }, + ], + examples: [ + { + description: 'Add data store that service writes to', + command: 'npx @eventcatalog/cli addDataStoreToService "OrderService" "writesTo" \'{"id":"orders-db","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'serviceHasVersion', + description: 'Checks if a specific version of a service exists', + category: 'Services', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the service' }, + { name: 'version', type: 'string', required: true, description: 'Version to check' }, + ], + examples: [ + { description: 'Check if version exists', command: 'npx @eventcatalog/cli serviceHasVersion "OrderService" "1.0.0"' }, + ], + }, + { + name: 'getSpecificationFilesForService', + description: 'Returns specification files (OpenAPI, AsyncAPI) for a service', + category: 'Services', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the service' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { description: 'Get spec files', command: 'npx @eventcatalog/cli getSpecificationFilesForService "OrderService"' }, + ], + }, + + // ================================ + // Domains + // ================================ + { + name: 'getDomain', + description: 'Returns a domain from EventCatalog by its ID', + category: 'Domains', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the domain to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve' }, + ], + examples: [ + { description: 'Get the latest domain', command: 'npx @eventcatalog/cli getDomain "Orders"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getDomain "Orders" "1.0.0"' }, + ], + }, + { + name: 'getDomains', + description: 'Returns all domains from EventCatalog', + category: 'Domains', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {latestOnly?}' }], + examples: [ + { description: 'Get all domains', command: 'npx @eventcatalog/cli getDomains' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getDomains \'{"latestOnly":true}\'' }, + ], + }, + { + name: 'writeDomain', + description: 'Writes a domain to EventCatalog', + category: 'Domains', + args: [ + { name: 'domain', type: 'json', required: true, description: 'Domain object with id, name, version, and markdown' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?, versionExistingContent?}' }, + ], + examples: [ + { + description: 'Write a new domain', + command: + 'npx @eventcatalog/cli writeDomain \'{"id":"Orders","name":"Orders Domain","version":"1.0.0","markdown":"# Orders Domain"}\'', + }, + ], + }, + { + name: 'rmDomain', + description: 'Removes a domain by its path', + category: 'Domains', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the domain' }], + examples: [{ description: 'Remove a domain', command: 'npx @eventcatalog/cli rmDomain "/Orders"' }], + }, + { + name: 'rmDomainById', + description: 'Removes a domain by its ID', + category: 'Domains', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the domain to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [{ description: 'Remove a domain', command: 'npx @eventcatalog/cli rmDomainById "Orders"' }], + }, + { + name: 'versionDomain', + description: 'Moves the current domain to a versioned directory', + category: 'Domains', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the domain to version' }], + examples: [{ description: 'Version a domain', command: 'npx @eventcatalog/cli versionDomain "Orders"' }], + }, + { + name: 'addFileToDomain', + description: 'Adds a file to a domain', + category: 'Domains', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'file', type: 'json', required: true, description: 'File object: {content, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Add a file to a domain', + command: 'npx @eventcatalog/cli addFileToDomain "Orders" \'{"content":"# Overview","fileName":"overview.md"}\'', + }, + ], + }, + { + name: 'addServiceToDomain', + description: 'Adds a service to a domain', + category: 'Domains', + args: [ + { name: 'domainId', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'service', type: 'json', required: true, description: 'Service reference: {id, version}' }, + { name: 'domainVersion', type: 'string', required: false, description: 'Specific domain version' }, + ], + examples: [ + { + description: 'Add service to domain', + command: 'npx @eventcatalog/cli addServiceToDomain "Orders" \'{"id":"OrderService","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addSubDomainToDomain', + description: 'Adds a subdomain to a domain', + category: 'Domains', + args: [ + { name: 'domainId', type: 'string', required: true, description: 'The ID of the parent domain' }, + { name: 'subDomain', type: 'json', required: true, description: 'Subdomain reference: {id, version}' }, + { name: 'domainVersion', type: 'string', required: false, description: 'Specific domain version' }, + ], + examples: [ + { + description: 'Add subdomain', + command: 'npx @eventcatalog/cli addSubDomainToDomain "Orders" \'{"id":"Fulfillment","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addEntityToDomain', + description: 'Adds an entity to a domain', + category: 'Domains', + args: [ + { name: 'domainId', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'entity', type: 'json', required: true, description: 'Entity reference: {id, version}' }, + { name: 'domainVersion', type: 'string', required: false, description: 'Specific domain version' }, + ], + examples: [ + { + description: 'Add entity to domain', + command: 'npx @eventcatalog/cli addEntityToDomain "Orders" \'{"id":"Order","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addEventToDomain', + description: 'Adds an event relationship to a domain', + category: 'Domains', + args: [ + { name: 'domainId', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'direction', type: 'string', required: true, description: 'Direction: "sends" or "receives"' }, + { name: 'event', type: 'json', required: true, description: 'Event reference: {id, version}' }, + { name: 'domainVersion', type: 'string', required: false, description: 'Specific domain version' }, + ], + examples: [ + { + description: 'Add event that domain sends', + command: 'npx @eventcatalog/cli addEventToDomain "Orders" "sends" \'{"id":"OrderCreated","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addCommandToDomain', + description: 'Adds a command relationship to a domain', + category: 'Domains', + args: [ + { name: 'domainId', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'direction', type: 'string', required: true, description: 'Direction: "sends" or "receives"' }, + { name: 'command', type: 'json', required: true, description: 'Command reference: {id, version}' }, + { name: 'domainVersion', type: 'string', required: false, description: 'Specific domain version' }, + ], + examples: [ + { + description: 'Add command that domain sends', + command: 'npx @eventcatalog/cli addCommandToDomain "Orders" "sends" \'{"id":"ProcessOrder","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addQueryToDomain', + description: 'Adds a query relationship to a domain', + category: 'Domains', + args: [ + { name: 'domainId', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'direction', type: 'string', required: true, description: 'Direction: "sends" or "receives"' }, + { name: 'query', type: 'json', required: true, description: 'Query reference: {id, version}' }, + { name: 'domainVersion', type: 'string', required: false, description: 'Specific domain version' }, + ], + examples: [ + { + description: 'Add query that domain sends', + command: 'npx @eventcatalog/cli addQueryToDomain "Orders" "sends" \'{"id":"GetOrderStatus","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addUbiquitousLanguageToDomain', + description: 'Adds ubiquitous language definitions to a domain', + category: 'Domains', + args: [ + { name: 'domainId', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'dictionary', type: 'json', required: true, description: 'Array of {term, definition} objects' }, + { name: 'domainVersion', type: 'string', required: false, description: 'Specific domain version' }, + ], + examples: [ + { + description: 'Add ubiquitous language', + command: + 'npx @eventcatalog/cli addUbiquitousLanguageToDomain "Orders" \'[{"term":"Order","definition":"A customer purchase request"}]\'', + }, + ], + }, + { + name: 'getUbiquitousLanguageFromDomain', + description: 'Gets ubiquitous language definitions from a domain', + category: 'Domains', + args: [ + { name: 'domainId', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'domainVersion', type: 'string', required: false, description: 'Specific domain version' }, + ], + examples: [ + { description: 'Get ubiquitous language', command: 'npx @eventcatalog/cli getUbiquitousLanguageFromDomain "Orders"' }, + ], + }, + { + name: 'domainHasVersion', + description: 'Checks if a specific version of a domain exists', + category: 'Domains', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'version', type: 'string', required: true, description: 'Version to check' }, + ], + examples: [{ description: 'Check if version exists', command: 'npx @eventcatalog/cli domainHasVersion "Orders" "1.0.0"' }], + }, + + // ================================ + // Channels + // ================================ + { + name: 'getChannel', + description: 'Returns a channel from EventCatalog by its ID', + category: 'Channels', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the channel to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve' }, + ], + examples: [ + { description: 'Get the latest channel', command: 'npx @eventcatalog/cli getChannel "orders.events"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getChannel "orders.events" "1.0.0"' }, + ], + }, + { + name: 'getChannels', + description: 'Returns all channels from EventCatalog', + category: 'Channels', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {latestOnly?}' }], + examples: [ + { description: 'Get all channels', command: 'npx @eventcatalog/cli getChannels' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getChannels \'{"latestOnly":true}\'' }, + ], + }, + { + name: 'writeChannel', + description: 'Writes a channel to EventCatalog', + category: 'Channels', + args: [ + { name: 'channel', type: 'json', required: true, description: 'Channel object with id, name, version, and markdown' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?}' }, + ], + examples: [ + { + description: 'Write a new channel', + command: + 'npx @eventcatalog/cli writeChannel \'{"id":"orders.events","name":"Orders Events","version":"1.0.0","markdown":"# Orders Events Channel"}\'', + }, + ], + }, + { + name: 'rmChannel', + description: 'Removes a channel by its path', + category: 'Channels', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the channel' }], + examples: [{ description: 'Remove a channel', command: 'npx @eventcatalog/cli rmChannel "/orders.events"' }], + }, + { + name: 'rmChannelById', + description: 'Removes a channel by its ID', + category: 'Channels', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the channel to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [{ description: 'Remove a channel', command: 'npx @eventcatalog/cli rmChannelById "orders.events"' }], + }, + { + name: 'versionChannel', + description: 'Moves the current channel to a versioned directory', + category: 'Channels', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the channel to version' }], + examples: [{ description: 'Version a channel', command: 'npx @eventcatalog/cli versionChannel "orders.events"' }], + }, + { + name: 'addEventToChannel', + description: 'Adds an event to a channel', + category: 'Channels', + args: [ + { name: 'channelId', type: 'string', required: true, description: 'The ID of the channel' }, + { name: 'event', type: 'json', required: true, description: 'Event reference: {id, version, parameters?}' }, + ], + examples: [ + { + description: 'Add event to channel', + command: 'npx @eventcatalog/cli addEventToChannel "orders.events" \'{"id":"OrderCreated","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addCommandToChannel', + description: 'Adds a command to a channel', + category: 'Channels', + args: [ + { name: 'channelId', type: 'string', required: true, description: 'The ID of the channel' }, + { name: 'command', type: 'json', required: true, description: 'Command reference: {id, version, parameters?}' }, + ], + examples: [ + { + description: 'Add command to channel', + command: 'npx @eventcatalog/cli addCommandToChannel "orders.commands" \'{"id":"CreateOrder","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'addQueryToChannel', + description: 'Adds a query to a channel', + category: 'Channels', + args: [ + { name: 'channelId', type: 'string', required: true, description: 'The ID of the channel' }, + { name: 'query', type: 'json', required: true, description: 'Query reference: {id, version, parameters?}' }, + ], + examples: [ + { + description: 'Add query to channel', + command: 'npx @eventcatalog/cli addQueryToChannel "orders.queries" \'{"id":"GetOrder","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'channelHasVersion', + description: 'Checks if a specific version of a channel exists', + category: 'Channels', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the channel' }, + { name: 'version', type: 'string', required: true, description: 'Version to check' }, + ], + examples: [ + { description: 'Check if version exists', command: 'npx @eventcatalog/cli channelHasVersion "orders.events" "1.0.0"' }, + ], + }, + + // ================================ + // Teams + // ================================ + { + name: 'getTeam', + description: 'Returns a team from EventCatalog by its ID', + category: 'Teams', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the team to retrieve' }], + examples: [{ description: 'Get a team', command: 'npx @eventcatalog/cli getTeam "platform-team"' }], + }, + { + name: 'getTeams', + description: 'Returns all teams from EventCatalog', + category: 'Teams', + args: [], + examples: [{ description: 'Get all teams', command: 'npx @eventcatalog/cli getTeams' }], + }, + { + name: 'writeTeam', + description: 'Writes a team to EventCatalog', + category: 'Teams', + args: [ + { name: 'team', type: 'json', required: true, description: 'Team object with id, name, and markdown' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?}' }, + ], + examples: [ + { + description: 'Write a new team', + command: 'npx @eventcatalog/cli writeTeam \'{"id":"platform-team","name":"Platform Team","markdown":"# Platform Team"}\'', + }, + ], + }, + { + name: 'rmTeamById', + description: 'Removes a team by its ID', + category: 'Teams', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the team to remove' }], + examples: [{ description: 'Remove a team', command: 'npx @eventcatalog/cli rmTeamById "platform-team"' }], + }, + + // ================================ + // Users + // ================================ + { + name: 'getUser', + description: 'Returns a user from EventCatalog by their ID', + category: 'Users', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the user to retrieve' }], + examples: [{ description: 'Get a user', command: 'npx @eventcatalog/cli getUser "jsmith"' }], + }, + { + name: 'getUsers', + description: 'Returns all users from EventCatalog', + category: 'Users', + args: [], + examples: [{ description: 'Get all users', command: 'npx @eventcatalog/cli getUsers' }], + }, + { + name: 'writeUser', + description: 'Writes a user to EventCatalog', + category: 'Users', + args: [ + { name: 'user', type: 'json', required: true, description: 'User object with id, name, and markdown' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?}' }, + ], + examples: [ + { + description: 'Write a new user', + command: 'npx @eventcatalog/cli writeUser \'{"id":"jsmith","name":"John Smith","markdown":"# John Smith"}\'', + }, + ], + }, + { + name: 'rmUserById', + description: 'Removes a user by their ID', + category: 'Users', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the user to remove' }], + examples: [{ description: 'Remove a user', command: 'npx @eventcatalog/cli rmUserById "jsmith"' }], + }, + + // ================================ + // Custom Docs + // ================================ + { + name: 'getCustomDoc', + description: 'Returns a custom doc from EventCatalog by its path', + category: 'Custom Docs', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the custom doc' }], + examples: [{ description: 'Get a custom doc', command: 'npx @eventcatalog/cli getCustomDoc "/getting-started"' }], + }, + { + name: 'getCustomDocs', + description: 'Returns all custom docs from EventCatalog', + category: 'Custom Docs', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {path?}' }], + examples: [ + { description: 'Get all custom docs', command: 'npx @eventcatalog/cli getCustomDocs' }, + { description: 'Get docs from a path', command: 'npx @eventcatalog/cli getCustomDocs \'{"path":"/guides"}\'' }, + ], + }, + { + name: 'writeCustomDoc', + description: 'Writes a custom doc to EventCatalog', + category: 'Custom Docs', + args: [ + { name: 'customDoc', type: 'json', required: true, description: 'Custom doc object with id, title, and markdown' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?}' }, + ], + examples: [ + { + description: 'Write a custom doc', + command: + 'npx @eventcatalog/cli writeCustomDoc \'{"id":"getting-started","title":"Getting Started","markdown":"# Getting Started"}\'', + }, + ], + }, + { + name: 'rmCustomDoc', + description: 'Removes a custom doc by its path', + category: 'Custom Docs', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the custom doc to remove' }], + examples: [{ description: 'Remove a custom doc', command: 'npx @eventcatalog/cli rmCustomDoc "/getting-started"' }], + }, + + // ================================ + // Entities + // ================================ + { + name: 'getEntity', + description: 'Returns an entity from EventCatalog by its ID', + category: 'Entities', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the entity to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve' }, + ], + examples: [ + { description: 'Get the latest entity', command: 'npx @eventcatalog/cli getEntity "Order"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getEntity "Order" "1.0.0"' }, + ], + }, + { + name: 'getEntities', + description: 'Returns all entities from EventCatalog', + category: 'Entities', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {latestOnly?}' }], + examples: [ + { description: 'Get all entities', command: 'npx @eventcatalog/cli getEntities' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getEntities \'{"latestOnly":true}\'' }, + ], + }, + { + name: 'writeEntity', + description: 'Writes an entity to EventCatalog', + category: 'Entities', + args: [ + { name: 'entity', type: 'json', required: true, description: 'Entity object with id, name, version, and markdown' }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?, versionExistingContent?}' }, + ], + examples: [ + { + description: 'Write a new entity', + command: + 'npx @eventcatalog/cli writeEntity \'{"id":"Order","name":"Order","version":"1.0.0","markdown":"# Order Entity"}\'', + }, + ], + }, + { + name: 'rmEntity', + description: 'Removes an entity by its path', + category: 'Entities', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the entity' }], + examples: [{ description: 'Remove an entity', command: 'npx @eventcatalog/cli rmEntity "/Order"' }], + }, + { + name: 'rmEntityById', + description: 'Removes an entity by its ID', + category: 'Entities', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the entity to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [{ description: 'Remove an entity', command: 'npx @eventcatalog/cli rmEntityById "Order"' }], + }, + { + name: 'versionEntity', + description: 'Moves the current entity to a versioned directory', + category: 'Entities', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the entity to version' }], + examples: [{ description: 'Version an entity', command: 'npx @eventcatalog/cli versionEntity "Order"' }], + }, + { + name: 'entityHasVersion', + description: 'Checks if a specific version of an entity exists', + category: 'Entities', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the entity' }, + { name: 'version', type: 'string', required: true, description: 'Version to check' }, + ], + examples: [{ description: 'Check if version exists', command: 'npx @eventcatalog/cli entityHasVersion "Order" "1.0.0"' }], + }, + + // ================================ + // Data Stores + // ================================ + { + name: 'getDataStore', + description: 'Returns a data store from EventCatalog by its ID', + category: 'Data Stores', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the data store to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve' }, + ], + examples: [ + { description: 'Get the latest data store', command: 'npx @eventcatalog/cli getDataStore "orders-db"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getDataStore "orders-db" "1.0.0"' }, + ], + }, + { + name: 'getDataStores', + description: 'Returns all data stores from EventCatalog', + category: 'Data Stores', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {latestOnly?}' }], + examples: [ + { description: 'Get all data stores', command: 'npx @eventcatalog/cli getDataStores' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getDataStores \'{"latestOnly":true}\'' }, + ], + }, + { + name: 'writeDataStore', + description: 'Writes a data store to EventCatalog', + category: 'Data Stores', + args: [ + { + name: 'dataStore', + type: 'json', + required: true, + description: 'Data store object with id, name, version, and markdown', + }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?, versionExistingContent?}' }, + ], + examples: [ + { + description: 'Write a new data store', + command: + 'npx @eventcatalog/cli writeDataStore \'{"id":"orders-db","name":"Orders Database","version":"1.0.0","markdown":"# Orders Database"}\'', + }, + ], + }, + { + name: 'writeDataStoreToService', + description: 'Writes a data store to a specific service', + category: 'Data Stores', + args: [ + { name: 'dataStore', type: 'json', required: true, description: 'Data store object' }, + { name: 'service', type: 'json', required: true, description: 'Service reference: {id, version?}' }, + ], + examples: [ + { + description: 'Write data store to a service', + command: + 'npx @eventcatalog/cli writeDataStoreToService \'{"id":"orders-db","name":"Orders Database","version":"1.0.0","markdown":"# Orders DB"}\' \'{"id":"OrderService"}\'', + }, + ], + }, + { + name: 'rmDataStore', + description: 'Removes a data store by its path', + category: 'Data Stores', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the data store' }], + examples: [{ description: 'Remove a data store', command: 'npx @eventcatalog/cli rmDataStore "/orders-db"' }], + }, + { + name: 'rmDataStoreById', + description: 'Removes a data store by its ID', + category: 'Data Stores', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the data store to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [{ description: 'Remove a data store', command: 'npx @eventcatalog/cli rmDataStoreById "orders-db"' }], + }, + { + name: 'versionDataStore', + description: 'Moves the current data store to a versioned directory', + category: 'Data Stores', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the data store to version' }], + examples: [{ description: 'Version a data store', command: 'npx @eventcatalog/cli versionDataStore "orders-db"' }], + }, + { + name: 'addFileToDataStore', + description: 'Adds a file to a data store', + category: 'Data Stores', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the data store' }, + { name: 'file', type: 'json', required: true, description: 'File object: {content, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Add a file to a data store', + command: 'npx @eventcatalog/cli addFileToDataStore "orders-db" \'{"content":"# Schema","fileName":"schema.md"}\'', + }, + ], + }, + { + name: 'dataStoreHasVersion', + description: 'Checks if a specific version of a data store exists', + category: 'Data Stores', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the data store' }, + { name: 'version', type: 'string', required: true, description: 'Version to check' }, + ], + examples: [ + { description: 'Check if version exists', command: 'npx @eventcatalog/cli dataStoreHasVersion "orders-db" "1.0.0"' }, + ], + }, + + // ================================ + // Data Products + // ================================ + { + name: 'getDataProduct', + description: 'Returns a data product from EventCatalog by its ID', + category: 'Data Products', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the data product to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve' }, + ], + examples: [ + { description: 'Get the latest data product', command: 'npx @eventcatalog/cli getDataProduct "customer-360"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getDataProduct "customer-360" "1.0.0"' }, + ], + }, + { + name: 'getDataProducts', + description: 'Returns all data products from EventCatalog', + category: 'Data Products', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {latestOnly?}' }], + examples: [ + { description: 'Get all data products', command: 'npx @eventcatalog/cli getDataProducts' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getDataProducts \'{"latestOnly":true}\'' }, + ], + }, + { + name: 'writeDataProduct', + description: 'Writes a data product to EventCatalog', + category: 'Data Products', + args: [ + { + name: 'dataProduct', + type: 'json', + required: true, + description: 'Data product object with id, name, version, and markdown', + }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?, versionExistingContent?}' }, + ], + examples: [ + { + description: 'Write a new data product', + command: + 'npx @eventcatalog/cli writeDataProduct \'{"id":"customer-360","name":"Customer 360","version":"1.0.0","markdown":"# Customer 360"}\'', + }, + ], + }, + { + name: 'writeDataProductToDomain', + description: 'Writes a data product to a specific domain', + category: 'Data Products', + args: [ + { name: 'dataProduct', type: 'json', required: true, description: 'Data product object' }, + { name: 'domain', type: 'json', required: true, description: 'Domain reference: {id, version?}' }, + { name: 'options', type: 'json', required: false, description: 'Options' }, + ], + examples: [ + { + description: 'Write data product to a domain', + command: + 'npx @eventcatalog/cli writeDataProductToDomain \'{"id":"customer-360","name":"Customer 360","version":"1.0.0","markdown":"# Customer 360"}\' \'{"id":"Analytics"}\'', + }, + ], + }, + { + name: 'rmDataProduct', + description: 'Removes a data product by its path', + category: 'Data Products', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the data product' }], + examples: [{ description: 'Remove a data product', command: 'npx @eventcatalog/cli rmDataProduct "/customer-360"' }], + }, + { + name: 'rmDataProductById', + description: 'Removes a data product by its ID', + category: 'Data Products', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the data product to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [{ description: 'Remove a data product', command: 'npx @eventcatalog/cli rmDataProductById "customer-360"' }], + }, + { + name: 'versionDataProduct', + description: 'Moves the current data product to a versioned directory', + category: 'Data Products', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the data product to version' }], + examples: [{ description: 'Version a data product', command: 'npx @eventcatalog/cli versionDataProduct "customer-360"' }], + }, + { + name: 'addFileToDataProduct', + description: 'Adds a file to a data product', + category: 'Data Products', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the data product' }, + { name: 'file', type: 'json', required: true, description: 'File object: {content, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Add a file to a data product', + command: 'npx @eventcatalog/cli addFileToDataProduct "customer-360" \'{"content":"# Schema","fileName":"schema.md"}\'', + }, + ], + }, + { + name: 'addDataProductToDomain', + description: 'Adds a data product reference to a domain', + category: 'Data Products', + args: [ + { name: 'domainId', type: 'string', required: true, description: 'The ID of the domain' }, + { name: 'dataProduct', type: 'json', required: true, description: 'Data product reference: {id, version}' }, + { name: 'domainVersion', type: 'string', required: false, description: 'Specific domain version' }, + ], + examples: [ + { + description: 'Add data product to domain', + command: 'npx @eventcatalog/cli addDataProductToDomain "Analytics" \'{"id":"customer-360","version":"1.0.0"}\'', + }, + ], + }, + { + name: 'dataProductHasVersion', + description: 'Checks if a specific version of a data product exists', + category: 'Data Products', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the data product' }, + { name: 'version', type: 'string', required: true, description: 'Version to check' }, + ], + examples: [ + { + description: 'Check if version exists', + command: 'npx @eventcatalog/cli dataProductHasVersion "customer-360" "1.0.0"', + }, + ], + }, + + // ================================ + // Diagrams + // ================================ + { + name: 'getDiagram', + description: 'Returns a diagram from EventCatalog by its ID', + category: 'Diagrams', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the diagram to retrieve' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to retrieve' }, + ], + examples: [ + { description: 'Get the latest diagram', command: 'npx @eventcatalog/cli getDiagram "ArchitectureDiagram"' }, + { description: 'Get a specific version', command: 'npx @eventcatalog/cli getDiagram "ArchitectureDiagram" "1.0.0"' }, + ], + }, + { + name: 'getDiagrams', + description: 'Returns all diagrams from EventCatalog', + category: 'Diagrams', + args: [{ name: 'options', type: 'json', required: false, description: 'Options: {latestOnly?}' }], + examples: [ + { description: 'Get all diagrams', command: 'npx @eventcatalog/cli getDiagrams' }, + { description: 'Get only latest versions', command: 'npx @eventcatalog/cli getDiagrams \'{"latestOnly":true}\'' }, + ], + }, + { + name: 'writeDiagram', + description: 'Writes a diagram to EventCatalog', + category: 'Diagrams', + args: [ + { + name: 'diagram', + type: 'json', + required: true, + description: 'Diagram object with id, name, version, and markdown', + }, + { name: 'options', type: 'json', required: false, description: 'Options: {path?, override?, versionExistingContent?}' }, + ], + examples: [ + { + description: 'Write a new diagram', + command: + 'npx @eventcatalog/cli writeDiagram \'{"id":"ArchitectureDiagram","name":"Architecture Diagram","version":"1.0.0","markdown":"# Architecture Diagram"}\'', + }, + ], + }, + { + name: 'rmDiagram', + description: 'Removes a diagram by its path', + category: 'Diagrams', + args: [{ name: 'path', type: 'string', required: true, description: 'Path to the diagram' }], + examples: [{ description: 'Remove a diagram', command: 'npx @eventcatalog/cli rmDiagram "/ArchitectureDiagram"' }], + }, + { + name: 'rmDiagramById', + description: 'Removes a diagram by its ID', + category: 'Diagrams', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the diagram to remove' }, + { name: 'version', type: 'string', required: false, description: 'Specific version to remove' }, + ], + examples: [{ description: 'Remove a diagram', command: 'npx @eventcatalog/cli rmDiagramById "ArchitectureDiagram"' }], + }, + { + name: 'versionDiagram', + description: 'Moves the current diagram to a versioned directory', + category: 'Diagrams', + args: [{ name: 'id', type: 'string', required: true, description: 'The ID of the diagram to version' }], + examples: [{ description: 'Version a diagram', command: 'npx @eventcatalog/cli versionDiagram "ArchitectureDiagram"' }], + }, + { + name: 'addFileToDiagram', + description: 'Adds a file to a diagram', + category: 'Diagrams', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the diagram' }, + { name: 'file', type: 'json', required: true, description: 'File object: {content, fileName}' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Add a file to a diagram', + command: 'npx @eventcatalog/cli addFileToDiagram "ArchitectureDiagram" \'{"content":"...","fileName":"diagram.png"}\'', + }, + ], + }, + { + name: 'diagramHasVersion', + description: 'Checks if a specific version of a diagram exists', + category: 'Diagrams', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the diagram' }, + { name: 'version', type: 'string', required: true, description: 'Version to check' }, + ], + examples: [ + { + description: 'Check if version exists', + command: 'npx @eventcatalog/cli diagramHasVersion "ArchitectureDiagram" "1.0.0"', + }, + ], + }, + + // ================================ + // Messages + // ================================ + { + name: 'getProducersAndConsumersForMessage', + description: 'Returns the producers and consumers (services) for a given message', + category: 'Messages', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the message' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { + description: 'Get producers and consumers', + command: 'npx @eventcatalog/cli getProducersAndConsumersForMessage "OrderCreated"', + }, + ], + }, + { + name: 'getConsumersOfSchema', + description: 'Returns services that consume a given schema', + category: 'Messages', + args: [{ name: 'schemaPath', type: 'string', required: true, description: 'Path to the schema file' }], + examples: [ + { + description: 'Get consumers of a schema', + command: 'npx @eventcatalog/cli getConsumersOfSchema "events/OrderCreated/schema.json"', + }, + ], + }, + { + name: 'getProducersOfSchema', + description: 'Returns services that produce a given schema', + category: 'Messages', + args: [{ name: 'schemaPath', type: 'string', required: true, description: 'Path to the schema file' }], + examples: [ + { + description: 'Get producers of a schema', + command: 'npx @eventcatalog/cli getProducersOfSchema "events/OrderCreated/schema.json"', + }, + ], + }, + { + name: 'getOwnersForResource', + description: 'Returns the owners (users/teams) for a given resource', + category: 'Messages', + args: [ + { name: 'id', type: 'string', required: true, description: 'The ID of the resource' }, + { name: 'version', type: 'string', required: false, description: 'Specific version' }, + ], + examples: [ + { description: 'Get owners for a resource', command: 'npx @eventcatalog/cli getOwnersForResource "OrderService"' }, + ], + }, + + // ================================ + // Utilities + // ================================ + { + name: 'dumpCatalog', + description: 'Dumps the entire catalog to a JSON structure', + category: 'Utilities', + args: [], + examples: [ + { description: 'Dump entire catalog', command: 'npx @eventcatalog/cli dumpCatalog' }, + { description: 'Dump and save to file', command: 'npx @eventcatalog/cli dumpCatalog > catalog.json' }, + ], + }, + { + name: 'getEventCatalogConfigurationFile', + description: 'Returns the EventCatalog configuration file', + category: 'Utilities', + args: [], + examples: [{ description: 'Get config file', command: 'npx @eventcatalog/cli getEventCatalogConfigurationFile' }], + }, + + // ================================ + // Export (DSL) + // ================================ + { + name: 'export', + description: 'Export catalog resources to EventCatalog DSL (.ec) format', + category: 'Export', + args: [ + { name: 'all', type: 'boolean', required: false, description: 'Export the entire catalog (all resource types)' }, + { + name: 'resource', + type: 'string', + required: false, + description: 'Resource type: event, command, query, service, domain (or plural forms)', + }, + { name: 'id', type: 'string', required: false, description: 'Resource ID (omit to export all of the given type)' }, + { name: 'version', type: 'string', required: false, description: 'Resource version (defaults to latest)' }, + { + name: 'hydrate', + type: 'boolean', + required: false, + description: 'Include referenced resources (messages, channels, owners)', + }, + { name: 'stdout', type: 'boolean', required: false, description: 'Print to stdout instead of writing a file' }, + { + name: 'playground', + type: 'boolean', + required: false, + description: 'Open the exported DSL in EventCatalog Compass', + }, + { name: 'output', type: 'string', required: false, description: 'Output file path (defaults to .ec or catalog.ec)' }, + ], + examples: [ + { + description: 'Export a single service', + command: 'npx @eventcatalog/cli export --resource service --id OrderService', + }, + { + description: 'Export a service with all dependencies', + command: 'npx @eventcatalog/cli export --resource service --id OrderService --hydrate', + }, + { + description: 'Export all services', + command: 'npx @eventcatalog/cli export --resource services', + }, + { + description: 'Export all services with hydration to stdout', + command: 'npx @eventcatalog/cli export --resource services --hydrate --stdout', + }, + { + description: 'Export the entire catalog', + command: 'npx @eventcatalog/cli export --all --hydrate', + }, + { + description: 'Export entire catalog to a custom file', + command: 'npx @eventcatalog/cli export --all --hydrate -o my-catalog.ec', + }, + { + description: 'Export and open in EventCatalog Compass', + command: 'npx @eventcatalog/cli export --resource services --hydrate --playground', + }, + ], + }, + + // ================================ + // Import + // ================================ + { + name: 'import', + description: + 'Import EventCatalog DSL (.ec) files into catalog markdown files. Existing resources with the same version are overridden. Importing a newer version automatically moves the old version into the versioned/ folder.', + category: 'Import', + args: [ + { name: 'files', type: 'string', required: false, description: 'One or more .ec file paths to import' }, + { name: 'stdin', type: 'boolean', required: false, description: 'Read DSL from stdin' }, + { name: 'dry-run', type: 'boolean', required: false, description: 'Preview resources without writing' }, + { + name: 'flat', + type: 'boolean', + required: false, + description: 'Write resources in a flat structure (no nesting under domains/services)', + }, + { name: 'no-init', type: 'boolean', required: false, description: 'Skip catalog initialization prompt' }, + ], + examples: [ + { + description: 'Import a single .ec file', + command: 'npx @eventcatalog/cli import catalog.ec', + }, + { + description: 'Import multiple .ec files', + command: 'npx @eventcatalog/cli import services.ec events.ec', + }, + { + description: 'Import from stdin (pipe from export)', + command: 'npx @eventcatalog/cli export --all --stdout | npx @eventcatalog/cli import --stdin -d ./new-catalog', + }, + { + description: 'Preview what would be written or merged', + command: 'npx @eventcatalog/cli import catalog.ec --dry-run', + }, + { + description: 'Import without nested folders', + command: 'npx @eventcatalog/cli import catalog.ec --flat', + }, + { + description: 'Import into an existing catalog directory without init prompts', + command: 'npx @eventcatalog/cli import catalog.ec --no-init -d ./existing-catalog', + }, + ], + }, + + // Snapshots + { + name: 'createSnapshot', + description: 'Take a point-in-time snapshot of the entire catalog, capturing all resources and their metadata as a JSON file', + category: 'Snapshots', + args: [ + { + name: 'label', + type: 'string', + required: false, + description: 'Human-readable label for the snapshot (defaults to ISO timestamp)', + }, + { + name: 'outputDir', + type: 'string', + required: false, + description: 'Output directory for the snapshot file (defaults to .snapshots/)', + }, + ], + examples: [ + { + description: 'Create a snapshot with default settings', + command: 'npx @eventcatalog/cli snapshot create', + }, + { + description: 'Create a snapshot with a custom label', + command: 'npx @eventcatalog/cli snapshot create --label pre-release-v2', + }, + { + description: 'Create a snapshot and print JSON to stdout', + command: 'npx @eventcatalog/cli snapshot create --stdout', + }, + ], + }, + { + name: 'diffSnapshots', + description: + 'Compare two snapshot files and output a structured diff showing added, removed, modified, and versioned resources plus relationship changes', + category: 'Snapshots', + args: [ + { name: 'fileA', type: 'string', required: true, description: 'Path to the first (older) snapshot file' }, + { name: 'fileB', type: 'string', required: true, description: 'Path to the second (newer) snapshot file' }, + ], + examples: [ + { + description: 'Diff two snapshot files as text', + command: 'npx @eventcatalog/cli snapshot diff .snapshots/before.snapshot.json .snapshots/after.snapshot.json', + }, + { + description: 'Diff two snapshot files as JSON', + command: + 'npx @eventcatalog/cli snapshot diff .snapshots/before.snapshot.json .snapshots/after.snapshot.json --format json', + }, + ], + }, + { + name: 'listSnapshots', + description: 'List all snapshots in the catalog .snapshots directory with their labels, timestamps, and git info', + category: 'Snapshots', + args: [], + examples: [ + { + description: 'List all snapshots', + command: 'npx @eventcatalog/cli snapshot list', + }, + { + description: 'List all snapshots as JSON', + command: 'npx @eventcatalog/cli snapshot list --format json', + }, + ], + }, + + // Governance + { + name: 'governanceCheck', + description: + 'Compare the current catalog (or a target branch) against a base branch and evaluate governance rules defined in governance.yaml (or governance.yml)', + category: 'Governance', + args: [ + { name: '--base', type: 'string', required: false, description: 'Base branch to compare against (default: main)' }, + { + name: '--target', + type: 'string', + required: false, + description: 'Target branch to compare (default: current working directory)', + }, + { name: '--format', type: 'string', required: false, description: 'Output format: text or json (default: text)' }, + { + name: '--status', + type: 'string', + required: false, + description: 'Status label to include in webhook payloads (e.g. proposed, approved)', + }, + ], + examples: [ + { + description: 'Check governance rules against main branch', + command: 'npx @eventcatalog/cli governance check', + }, + { + description: 'Check against a specific base branch', + command: 'npx @eventcatalog/cli governance check --base develop', + }, + { + description: 'Compare two branches directly', + command: 'npx @eventcatalog/cli governance check --base main --target feat/new-service', + }, + { + description: 'Output results as JSON', + command: 'npx @eventcatalog/cli governance check --format json', + }, + ], + }, +]; + +/** + * Get all unique categories + */ +export function getCategories(): string[] { + return [...new Set(cliFunctions.map((fn) => fn.category))]; +} + +/** + * Get functions by category + */ +export function getFunctionsByCategory(category: string): CLIFunctionDoc[] { + return cliFunctions.filter((fn) => fn.category === category); +} + +/** + * Get a function by name + */ +export function getFunction(name: string): CLIFunctionDoc | undefined { + return cliFunctions.find((fn) => fn.name === name); +} diff --git a/packages/cli/src/cli/dsl-managed-keys.ts b/packages/cli/src/cli/dsl-managed-keys.ts new file mode 100644 index 000000000..05b7aa8c9 --- /dev/null +++ b/packages/cli/src/cli/dsl-managed-keys.ts @@ -0,0 +1,14 @@ +/** + * DSL-managed frontmatter keys by resource type. + * + * Keys listed here are controlled by DSL import and should not be preserved + * from existing frontmatter when omitted by the incoming DSL. + */ +const MESSAGE_MANAGED_KEYS: ReadonlySet = new Set(['id', 'name', 'version', 'owners', 'deprecated', 'draft', 'summary']); + +export const DSL_MANAGED_KEYS_BY_TYPE: Record> = { + domain: new Set(['id', 'name', 'version', 'owners', 'deprecated', 'draft', 'summary', 'services', 'domains']), + event: MESSAGE_MANAGED_KEYS, + command: MESSAGE_MANAGED_KEYS, + query: MESSAGE_MANAGED_KEYS, +}; diff --git a/packages/cli/src/cli/executor.ts b/packages/cli/src/cli/executor.ts new file mode 100644 index 000000000..6a7bb5e36 --- /dev/null +++ b/packages/cli/src/cli/executor.ts @@ -0,0 +1,38 @@ +import { existsSync } from 'node:fs'; +import { parseArguments } from './parser'; +import createSDK from '@eventcatalog/sdk'; + +/** + * Execute a SDK function with the given arguments + */ +export async function executeFunction(catalogDir: string, functionName: string, rawArgs: string[]): Promise { + // Validate catalog directory exists + if (!existsSync(catalogDir)) { + throw new Error(`Catalog directory not found: ${catalogDir}`); + } + + // Initialize the SDK + const sdk = createSDK(catalogDir); + + // Validate function exists + if (!(functionName in sdk)) { + throw new Error(`Function '${functionName}' not found. Use 'eventcatalog list' to see available functions.`); + } + + const fn = (sdk as any)[functionName]; + + // Validate it's callable + if (typeof fn !== 'function') { + throw new Error(`'${functionName}' is not a callable function.`); + } + + // Parse arguments + const parsedArgs = parseArguments(rawArgs); + + // Execute function + try { + return await fn(...parsedArgs); + } catch (error) { + throw new Error(`Error executing '${functionName}': ${error instanceof Error ? error.message : String(error)}`); + } +} diff --git a/packages/cli/src/cli/export.ts b/packages/cli/src/cli/export.ts new file mode 100644 index 000000000..5113c9006 --- /dev/null +++ b/packages/cli/src/cli/export.ts @@ -0,0 +1,366 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import open from 'open'; +import createSDK from '@eventcatalog/sdk'; + +const RESOURCE_TYPES = ['event', 'command', 'query', 'service', 'domain'] as const; +type ResourceType = (typeof RESOURCE_TYPES)[number]; +const SUPPORTED_RESOURCE_TYPES = RESOURCE_TYPES.join(', '); + +const PLURAL_MAP: Record = { + events: 'event', + commands: 'command', + queries: 'query', + services: 'service', + domains: 'domain', +}; + +const KNOWN_UNSUPPORTED_EXPORT_TYPES = new Set([ + 'channel', + 'channels', + 'team', + 'teams', + 'user', + 'users', + 'container', + 'containers', + 'data-product', + 'data-products', + 'dataproduct', + 'dataproducts', + 'diagram', + 'diagrams', + 'flow', + 'flows', +]); + +interface ExportOptions { + resource: string; + id?: string; + version?: string; + hydrate?: boolean; + stdout?: boolean; + playground?: boolean; + output?: string; + dir: string; +} + +function normalizeResourceType(resource: string): ResourceType { + const lower = resource.toLowerCase(); + if (PLURAL_MAP[lower]) return PLURAL_MAP[lower]; + return lower as ResourceType; +} + +function assertSupportedExportType(resource: string, type: ResourceType): void { + const lower = resource.toLowerCase(); + + if (KNOWN_UNSUPPORTED_EXPORT_TYPES.has(lower)) { + throw new Error( + `Resource type '${resource}' is not yet supported for DSL export. Supported types: ${SUPPORTED_RESOURCE_TYPES}` + ); + } + + if (!RESOURCE_TYPES.includes(type)) { + throw new Error(`Invalid resource type '${resource}'. Must be one of: ${SUPPORTED_RESOURCE_TYPES}`); + } +} + +function getResourceFetcher(sdk: ReturnType, type: ResourceType) { + switch (type) { + case 'event': + return sdk.getEvent; + case 'command': + return sdk.getCommand; + case 'query': + return sdk.getQuery; + case 'service': + return sdk.getService; + case 'domain': + return sdk.getDomain; + } +} + +function getCollectionFetcher(sdk: ReturnType, type: ResourceType) { + switch (type) { + case 'event': + return sdk.getEvents; + case 'command': + return sdk.getCommands; + case 'query': + return sdk.getQueries; + case 'service': + return sdk.getServices; + case 'domain': + return sdk.getDomains; + } +} + +function pluralize(type: ResourceType): string { + if (type === 'query') return 'queries'; + return `${type}s`; +} + +const SECTION_ORDER = ['team', 'user', 'channel', 'event', 'command', 'query', 'service', 'domain'] as const; + +const SECTION_LABELS: Record = { + team: 'TEAMS', + user: 'USERS', + channel: 'CHANNELS', + event: 'EVENTS', + command: 'COMMANDS', + query: 'QUERIES', + service: 'SERVICES', + domain: 'DOMAINS', +}; + +/** + * Groups DSL blocks by resource type and orders them with section headers. + * + * Raw toDSL output may interleave different resource types (e.g. teams, channels, + * services, events). This splits on blank lines, buckets each block by its leading + * keyword, then reassembles in SECTION_ORDER with "// EVENTS" style headers. + */ +function groupDSLBlocks(dsl: string): string { + const blocks = dsl.split(/\n\n/).filter((b) => b.trim()); + const buckets: Record = {}; + + for (const block of blocks) { + const firstWord = block.trimStart().split(/\s/)[0]; + const key = firstWord === 'subdomain' ? 'domain' : firstWord; + if (!buckets[key]) buckets[key] = []; + buckets[key].push(block); + } + + const sections: string[] = []; + for (const type of SECTION_ORDER) { + if (!buckets[type] || buckets[type].length === 0) continue; + const label = SECTION_LABELS[type] || type.toUpperCase(); + sections.push(`// ${label}\n${buckets[type].join('\n\n')}`); + delete buckets[type]; + } + + // Any remaining types not in SECTION_ORDER + for (const [type, items] of Object.entries(buckets)) { + if (items.length === 0) continue; + const label = type.toUpperCase(); + sections.push(`// ${label}\n${items.join('\n\n')}`); + } + + return sections.join('\n\n'); +} + +interface ResourceDefinition { + keyword: string; + id: string; + version?: string; +} + +/** + * Extracts resource definitions from DSL text by scanning for top-level + * ` {` lines and reading the version from the block body. + */ +function extractResourceDefinitions(dsl: string, filterTypes: string[]): ResourceDefinition[] { + const results: ResourceDefinition[] = []; + const lines = dsl.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].trimStart(); + const parts = trimmed.split(/\s/); + const keyword = parts[0]; + const id = parts[1]; + if (!filterTypes.includes(keyword)) continue; + if (!id || !trimmed.endsWith('{')) continue; + + // Look ahead inside the block for a `version X.Y.Z` line + let version: string | undefined; + for (let j = i + 1; j < lines.length; j++) { + const inner = lines[j].trim(); + if (inner === '}') break; + const vMatch = inner.match(/^version\s+(.+)$/); + if (vMatch) { + version = vMatch[1]; + break; + } + } + results.push({ keyword, id, version }); + } + + return results; +} + +/** + * Builds a `visualizer main { ... }` block listing all resource definitions + * found in the DSL output. + * + * When multiple versions of the same resource exist (e.g. two versions of + * OrderService), references are version-qualified (`service OrderService@1.0.0`) + * to avoid ambiguity. Single-version resources use bare ids. + */ +function buildVisualizerBlock(dsl: string, name: string, filterTypes: ResourceType | ResourceType[]): string { + const types = Array.isArray(filterTypes) ? filterTypes : [filterTypes]; + const definitions = extractResourceDefinitions(dsl, types); + + if (definitions.length === 0) return ''; + + // Count occurrences of each keyword+id to detect duplicates needing version qualification + const counts = new Map(); + for (const def of definitions) { + const key = `${def.keyword}:${def.id}`; + counts.set(key, (counts.get(key) || 0) + 1); + } + + const entries = definitions.map((def) => { + const key = `${def.keyword}:${def.id}`; + const needsVersion = (counts.get(key) || 0) > 1 && def.version; + const ref = needsVersion ? `${def.id}@${def.version}` : def.id; + return ` ${def.keyword} ${ref}`; + }); + + return `\nvisualizer main {\n name "${name}"\n${entries.join('\n')}\n}`; +} + +export async function exportCatalog(options: Omit): Promise { + const { hydrate = false, stdout = false, playground = false, output, dir } = options; + + const sdk = createSDK(dir); + // Independent collection fetches/conversions can run in parallel. + // Promise.all preserves RESOURCE_TYPES order in the resulting array. + const dslParts = ( + await Promise.all( + RESOURCE_TYPES.map(async (type) => { + const fetcher = getCollectionFetcher(sdk, type); + const resources = await fetcher({ latestOnly: true }); + if (!resources || resources.length === 0) return ''; + return sdk.toDSL(resources, { type, hydrate }); + }) + ) + ).filter((dsl): dsl is string => Boolean(dsl)); + + if (dslParts.length === 0) { + throw new Error(`No resources found in catalog at '${dir}'`); + } + + const combined = dslParts.join('\n\n'); + const grouped = groupDSLBlocks(combined); + const vizBlock = buildVisualizerBlock(grouped, 'Full Catalog', [...RESOURCE_TYPES]); + const dsl = vizBlock ? `${grouped}\n${vizBlock}` : grouped; + + if (stdout) { + return dsl; + } + + const filename = output || 'catalog.ec'; + const filepath = resolve(filename); + writeFileSync(filepath, dsl + '\n', 'utf-8'); + + const lines = ['', ` Exported full catalog to ${filepath}`]; + + if (playground) { + const encoded = Buffer.from(dsl).toString('base64'); + const playgroundUrl = `https://compass.eventcatalog.dev/?code=${encoded}`; + await open(playgroundUrl); + lines.push('', ` Opening in EventCatalog Compass...`); + } else { + lines.push('', ` Tip: Use --playground to open in EventCatalog Compass`); + } + + lines.push(''); + return lines.join('\n'); +} + +export async function exportAll(options: ExportOptions): Promise { + const { resource, hydrate = false, stdout = false, playground = false, output, dir } = options; + + const type = normalizeResourceType(resource); + assertSupportedExportType(resource, type); + + const plural = pluralize(type); + const sdk = createSDK(dir); + + const fetcher = getCollectionFetcher(sdk, type); + const allResources = (await fetcher({ latestOnly: true })) || []; + + if (allResources.length === 0) { + throw new Error(`No ${plural} found in catalog at '${dir}'`); + } + + const rawDsl = await sdk.toDSL(allResources, { type, hydrate }); + const grouped = groupDSLBlocks(rawDsl); + const vizBlock = buildVisualizerBlock(grouped, `All ${plural}`, type); + const dsl = vizBlock ? `${grouped}\n${vizBlock}` : grouped; + + if (stdout) { + return dsl; + } + + const filename = output || `${plural}.ec`; + const filepath = resolve(filename); + writeFileSync(filepath, dsl + '\n', 'utf-8'); + + const lines = ['', ` Exported ${allResources.length} ${plural} to ${filepath}`]; + + if (playground) { + const encoded = Buffer.from(dsl).toString('base64'); + const playgroundUrl = `https://compass.eventcatalog.dev/?code=${encoded}`; + await open(playgroundUrl); + lines.push('', ` Opening in EventCatalog Compass...`); + } else { + lines.push('', ` Tip: Use --playground to open in EventCatalog Compass`); + } + + lines.push(''); + return lines.join('\n'); +} + +export async function exportResource(options: ExportOptions): Promise { + const { resource, id, version, hydrate = false, stdout = false, playground = false, output, dir } = options; + + if (!id) { + return exportAll(options); + } + + const type = normalizeResourceType(resource); + assertSupportedExportType(resource, type); + + const sdk = createSDK(dir); + + const fetcher = getResourceFetcher(sdk, type); + const data = await fetcher(id, version); + + if (!data) { + const versionStr = version ? `@${version}` : ' (latest)'; + throw new Error(`${resource} '${id}${versionStr}' not found in catalog at '${dir}'`); + } + + const rawDsl = await sdk.toDSL(data, { type, hydrate }); + + // When hydrating, toDSL pulls in related resources (services, channels, owners) + // so we group by type with section headers and widen the visualizer filter to + // include all resource types that may appear in the hydrated output. + const grouped = hydrate ? groupDSLBlocks(rawDsl) : rawDsl; + const vizTypes: ResourceType[] = hydrate ? [...RESOURCE_TYPES] : [type]; + const vizBlock = buildVisualizerBlock(grouped, `View of ${id}`, vizTypes); + const dsl = vizBlock ? `${grouped}\n${vizBlock}` : grouped; + + if (stdout) { + return dsl; + } + + const filename = output || `${id}.ec`; + const filepath = resolve(filename); + writeFileSync(filepath, dsl + '\n', 'utf-8'); + + const lines = ['', ` Exported ${type} '${id}' to ${filepath}`]; + + if (playground) { + const encoded = Buffer.from(dsl).toString('base64'); + const playgroundUrl = `https://compass.eventcatalog.dev/?code=${encoded}`; + await open(playgroundUrl); + lines.push('', ` Opening in EventCatalog Compass...`); + } else { + lines.push('', ` Tip: Use --playground to open in EventCatalog Compass`); + } + + lines.push(''); + return lines.join('\n'); +} diff --git a/packages/cli/src/cli/governance/actions.ts b/packages/cli/src/cli/governance/actions.ts new file mode 100644 index 000000000..fffe63dda --- /dev/null +++ b/packages/cli/src/cli/governance/actions.ts @@ -0,0 +1,249 @@ +import { randomUUID } from 'node:crypto'; +import type { CatalogSnapshot } from '@eventcatalog/sdk'; +import type { CompatibilityStrategy } from '@eventcatalog/breaking-changes'; +import type { GovernanceResult } from './types'; +import { resolveEnvVars, isProducerTrigger, getChangeVerb } from './rules'; + +export type MessageTypeMap = Map; +export type ServiceOwnersMap = Map; + +export type GovernanceActionOptions = { + messageTypes?: MessageTypeMap; + status?: string; + serviceOwners?: ServiceOwnersMap; + baseRef?: string; + targetRef?: string; + compatibilityStrategy?: CompatibilityStrategy; +}; + +export const buildMessageTypeMap = (snapshot: CatalogSnapshot): MessageTypeMap => { + const map: MessageTypeMap = new Map(); + for (const event of snapshot.resources.messages.events) { + map.set(event.id as string, 'event'); + } + for (const command of snapshot.resources.messages.commands) { + map.set(command.id as string, 'command'); + } + for (const query of snapshot.resources.messages.queries) { + map.set(query.id as string, 'query'); + } + return map; +}; + +export const buildServiceOwnersMap = (snapshot: CatalogSnapshot): ServiceOwnersMap => { + const map: ServiceOwnersMap = new Map(); + for (const service of snapshot.resources.services) { + if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) { + map.set(service.id as string, service.owners as string[]); + } + } + return map; +}; + +export const executeGovernanceActions = async ( + results: GovernanceResult[], + opts: GovernanceActionOptions = {} +): Promise => { + const { messageTypes, status, serviceOwners, baseRef, targetRef, compatibilityStrategy } = opts; + const webhookCalls: Array<{ urlTemplate: string; request: Promise }> = []; + const now = new Date().toISOString(); + + for (const result of results) { + for (const action of result.rule.actions) { + if (action.type !== 'webhook') continue; + + const url = resolveEnvVars(action.url); + const headers: Record = { 'Content-Type': 'application/json' }; + + if (action.headers) { + for (const [key, value] of Object.entries(action.headers)) { + headers[key] = resolveEnvVars(value); + } + } + + // Handle schema changes + if (result.schemaChanges && result.schemaChanges.length > 0) { + for (const sc of result.schemaChanges) { + const messageType = messageTypes?.get(sc.resourceChange.resourceId) || 'message'; + + const payload = { + specversion: '1.0', + type: 'eventcatalog.governance.schema_changed', + source: 'eventcatalog/governance', + id: randomUUID(), + time: now, + datacontenttype: 'application/json', + data: { + schemaVersion: 1, + ...(status && { status }), + summary: `Schema changed for ${messageType} ${sc.resourceChange.resourceId}`, + message: { + id: sc.resourceChange.resourceId, + version: sc.resourceChange.version, + type: messageType, + }, + schema: { + beforeHash: sc.beforeSchemaHash ?? null, + afterHash: sc.afterSchemaHash ?? null, + beforePath: sc.beforeSchemaPath ?? null, + afterPath: sc.afterSchemaPath ?? null, + }, + refs: { + base: baseRef ?? null, + target: targetRef ?? null, + }, + consumers: sc.consumerServices, + producers: sc.producerServices, + }, + }; + + webhookCalls.push({ + urlTemplate: action.url, + request: fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) }), + }); + } + continue; + } + + // Handle breaking schema changes + if (result.breakingSchemaChanges && result.breakingSchemaChanges.length > 0) { + for (const bsc of result.breakingSchemaChanges) { + const messageType = messageTypes?.get(bsc.resourceChange.resourceId) || 'message'; + + const payload = { + specversion: '1.0', + type: 'eventcatalog.governance.schema_breaking_change', + source: 'eventcatalog/governance', + id: randomUUID(), + time: now, + datacontenttype: 'application/json', + data: { + schemaVersion: 1, + ...(status && { status }), + ...(compatibilityStrategy && { compatibilityStrategy }), + summary: `Breaking schema change detected for ${messageType} ${bsc.resourceChange.resourceId}`, + message: { + id: bsc.resourceChange.resourceId, + version: bsc.resourceChange.version, + type: messageType, + }, + schema: { + beforeHash: bsc.beforeSchemaHash ?? null, + afterHash: bsc.afterSchemaHash ?? null, + beforePath: bsc.beforeSchemaPath ?? null, + afterPath: bsc.afterSchemaPath ?? null, + }, + breakingChanges: bsc.breakingChanges, + refs: { + base: baseRef ?? null, + target: targetRef ?? null, + }, + consumers: bsc.consumerServices, + producers: bsc.producerServices, + }, + }; + + webhookCalls.push({ + urlTemplate: action.url, + request: fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) }), + }); + } + continue; + } + + // Handle deprecation changes + if (result.deprecationChanges && result.deprecationChanges.length > 0) { + for (const dc of result.deprecationChanges) { + const messageType = messageTypes?.get(dc.resourceChange.resourceId) || 'message'; + + const producers = + dc.producerServices.length > 0 + ? dc.producerServices + : [{ id: 'unknown', version: 'unknown' } as { id: string; version: string; owners?: string[] }]; + + for (const producer of producers) { + const payload = { + specversion: '1.0', + type: `eventcatalog.governance.message_deprecated`, + source: 'eventcatalog/governance', + id: randomUUID(), + time: now, + datacontenttype: 'application/json', + data: { + schemaVersion: 1, + ...(status && { status }), + summary: `${dc.resourceChange.resourceId} (${messageType}) has been deprecated by ${producer.id}`, + producer: { + id: producer.id, + version: producer.version, + ...(producer.owners && { owners: producer.owners }), + }, + message: { + id: dc.resourceChange.resourceId, + version: dc.resourceChange.version, + type: messageType, + }, + }, + }; + + webhookCalls.push({ + urlTemplate: action.url, + request: fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) }), + }); + } + } + continue; + } + + // Handle relationship changes + for (const change of result.matchedChanges) { + const verb = getChangeVerb(result.trigger, change.changeType); + const messageType = messageTypes?.get(change.resourceId) || 'message'; + const serviceRole = isProducerTrigger(result.trigger) ? 'producer' : 'consumer'; + + const payload = { + specversion: '1.0', + type: `eventcatalog.governance.${result.trigger}`, + source: 'eventcatalog/governance', + id: randomUUID(), + time: now, + datacontenttype: 'application/json', + data: { + schemaVersion: 1, + ...(status && { status }), + summary: `${change.serviceId} is ${verb} the ${messageType} ${change.resourceId}`, + [serviceRole]: { + id: change.serviceId, + version: change.serviceVersion, + ...(serviceOwners?.get(change.serviceId) && { owners: serviceOwners.get(change.serviceId) }), + }, + message: { + id: change.resourceId, + version: change.resourceVersion, + type: messageType, + }, + }, + }; + + webhookCalls.push({ + urlTemplate: action.url, + request: fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) }), + }); + } + } + } + + const settled = await Promise.allSettled(webhookCalls.map((c) => c.request)); + + return settled.map((result, i) => { + const url = webhookCalls[i].urlTemplate; + if (result.status === 'fulfilled') { + const res = result.value; + if (!res.ok) { + return ` Webhook failed: ${url} ✗ (HTTP ${res.status})`; + } + return ` Webhook sent: ${url} ✓`; + } + return ` Webhook failed: ${url} ✗ (${result.reason instanceof Error ? result.reason.message : String(result.reason)})`; + }); +}; diff --git a/packages/cli/src/cli/governance/check.ts b/packages/cli/src/cli/governance/check.ts new file mode 100644 index 000000000..179d4f7cc --- /dev/null +++ b/packages/cli/src/cli/governance/check.ts @@ -0,0 +1,173 @@ +import path from 'node:path'; +import { execSync } from 'node:child_process'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import dotenv from 'dotenv'; +import createSDK from '@eventcatalog/sdk'; +import { isEventCatalogScaleEnabled } from '@eventcatalog/license'; +import { loadGovernanceConfig, evaluateGovernanceRules, enrichSchemaContent, resolveEnvVars } from './rules'; +import { formatGovernanceOutput, formatFailureOutput } from './format'; +import { executeGovernanceActions, buildMessageTypeMap, buildServiceOwnersMap } from './actions'; +import type { GovernanceCheckResult } from './types'; + +export type GovernanceCheckOptions = { + base?: string; + target?: string; + format?: string; + status?: string; + dir: string; +}; + +const BRANCH_NAME_RE = /^[a-zA-Z0-9._\-/]+$/; + +const extractBranchToTempDir = (branch: string, catalogDir: string, tempDirs: string[]): string => { + if (!BRANCH_NAME_RE.test(branch)) { + throw new Error(`Invalid branch name: "${branch}"`); + } + const tmpDir = mkdtempSync(path.join(tmpdir(), 'ec-governance-')); + tempDirs.push(tmpDir); + try { + execSync(`git archive ${branch} | tar -x -C ${tmpDir}`, { cwd: catalogDir, stdio: 'pipe' }); + } catch { + throw new Error(`Failed to extract branch "${branch}". Is it a valid git branch?`); + } + return tmpDir; +}; + +export const governanceCheck = async (opts: GovernanceCheckOptions): Promise => { + const dir = path.resolve(opts.dir); + + // Load .env file from catalog directory (contains license key, webhook secrets, etc.) + dotenv.config({ path: path.join(dir, '.env') }); + + const isScale = await isEventCatalogScaleEnabled(); + if (!isScale) { + throw new Error('Governance requires an EventCatalog Scale plan. Learn more at https://eventcatalog.dev/pricing'); + } + + const baseBranch = opts.base || 'main'; + + const tempDirs: string[] = []; + const trackTempDir = (prefix: string): string => { + const d = mkdtempSync(path.join(tmpdir(), prefix)); + tempDirs.push(d); + return d; + }; + + try { + const baseTmpDir = extractBranchToTempDir(baseBranch, dir, tempDirs); + + const baseSnapshotDir = trackTempDir('ec-snap-base-'); + const targetSnapshotDir = trackTempDir('ec-snap-target-'); + + const baseSDK = createSDK(baseTmpDir); + const baseResult = await baseSDK.createSnapshot({ label: `base-${baseBranch}`, outputDir: baseSnapshotDir }); + + let targetResult; + let targetCatalogDir: string; + if (opts.target) { + targetCatalogDir = extractBranchToTempDir(opts.target, dir, tempDirs); + const targetSDK = createSDK(targetCatalogDir); + targetResult = await targetSDK.createSnapshot({ label: `target-${opts.target}`, outputDir: targetSnapshotDir }); + } else { + targetCatalogDir = dir; + const targetSDK = createSDK(dir); + targetResult = await targetSDK.createSnapshot({ label: 'current', outputDir: targetSnapshotDir }); + } + + const diff = await baseSDK.diffSnapshots(baseResult.filePath, targetResult.filePath); + + const config = loadGovernanceConfig(dir); + + if (config.rules.length === 0) { + return { output: 'No governance.yaml (or governance.yml) found or no rules defined.', exitCode: 0, failures: [] }; + } + + const results = evaluateGovernanceRules(diff, config, targetResult.snapshot, baseResult.snapshot); + + // Populate before/after schema content for any schema_changed results + await enrichSchemaContent(results, baseTmpDir, targetCatalogDir, config.compatibility?.strategy); + + // Remove schema_breaking_change results where no actual breaking changes were found + const filteredResults = results.filter((r) => { + if (r.trigger !== 'schema_breaking_change') return true; + if (!r.breakingSchemaChanges) return false; + r.breakingSchemaChanges = r.breakingSchemaChanges.filter((bsc) => bsc.breakingChanges.length > 0); + return r.breakingSchemaChanges.length > 0; + }); + + // Mark results that have fail actions and collect their messages + for (const result of filteredResults) { + const failActions = result.rule.actions.filter((a) => a.type === 'fail'); + if (failActions.length > 0) { + result.failed = true; + result.failMessages = failActions + .map((a) => ('message' in a && a.message ? resolveEnvVars(a.message) : undefined)) + .filter((m): m is string => m !== undefined); + } + } + + // Always execute actions (webhooks) regardless of output format + const messageTypes = buildMessageTypeMap(targetResult.snapshot); + const serviceOwners = buildServiceOwnersMap(targetResult.snapshot); + const actionOutput = await executeGovernanceActions(filteredResults, { + messageTypes, + status: opts.status, + serviceOwners, + baseRef: baseBranch, + targetRef: opts.target || 'working-directory', + compatibilityStrategy: config.compatibility?.strategy, + }); + + // Collect failures + const failures = filteredResults + .filter((r) => r.failed) + .map((r) => ({ ruleName: r.rule.name, messages: r.failMessages || [] })); + + if (opts.format === 'json') { + const jsonOutput = { + baseBranch, + target: opts.target || 'working directory', + results: filteredResults, + summary: { + rulesTriggered: filteredResults.length, + failures: failures.length, + passed: failures.length === 0, + }, + diff: diff.summary, + }; + return { output: JSON.stringify(jsonOutput, null, 2), exitCode: failures.length > 0 ? 1 : 0, failures }; + } + + const targetLabel = opts.target || 'working directory'; + const lines: string[] = [`Governance check: comparing ${targetLabel} against ${baseBranch}`, '']; + + lines.push(formatGovernanceOutput(filteredResults)); + + if (actionOutput.length > 0) { + lines.push(''); + lines.push(...actionOutput); + } + + if (filteredResults.length > 0) { + const webhookCount = actionOutput.filter((l) => l.includes('Webhook sent')).length; + const parts = [`${filteredResults.length} rule${filteredResults.length === 1 ? '' : 's'} triggered`]; + if (webhookCount > 0) parts.push(`${webhookCount} webhook${webhookCount === 1 ? '' : 's'} sent`); + lines.push(''); + lines.push(parts.join(', ') + '.'); + } + + // Append failure output + const failureOutput = formatFailureOutput(failures); + if (failureOutput) { + lines.push(''); + lines.push(failureOutput); + } + + return { output: lines.join('\n'), exitCode: failures.length > 0 ? 1 : 0, failures }; + } finally { + for (const d of tempDirs) { + rmSync(d, { recursive: true, force: true }); + } + } +}; diff --git a/packages/cli/src/cli/governance/format.ts b/packages/cli/src/cli/governance/format.ts new file mode 100644 index 000000000..fff50b87d --- /dev/null +++ b/packages/cli/src/cli/governance/format.ts @@ -0,0 +1,53 @@ +import type { GovernanceResult } from './types'; +import { getChangeVerb } from './rules'; + +export const formatGovernanceOutput = (results: GovernanceResult[]): string => { + if (results.length === 0) { + return 'No governance rules triggered. Catalog is compliant.'; + } + + const lines: string[] = ['Governance:', '']; + + for (const result of results) { + lines.push(` Rule "${result.rule.name}" triggered (${result.trigger}):`); + + if (result.schemaChanges && result.schemaChanges.length > 0) { + for (const sc of result.schemaChanges) { + const consumers = sc.consumerServices.length > 0 ? sc.consumerServices.map((c) => c.id).join(', ') : 'no known consumers'; + lines.push( + ` ! Schema changed for ${sc.resourceChange.resourceId} (${sc.resourceChange.type}) — consumers: ${consumers}` + ); + } + } else if (result.deprecationChanges && result.deprecationChanges.length > 0) { + for (const dc of result.deprecationChanges) { + const producers = dc.producerServices.length > 0 ? dc.producerServices.map((p) => p.id).join(', ') : 'unknown producer'; + lines.push(` ! ${dc.resourceChange.resourceId} (${dc.resourceChange.type}) deprecated by ${producers}`); + } + } else { + for (const change of result.matchedChanges) { + const prefix = change.changeType === 'added' ? '+' : '-'; + const verb = getChangeVerb(result.trigger, change.changeType); + lines.push(` ${prefix} ${change.serviceId} is ${verb} ${change.resourceId}`); + } + } + + lines.push(''); + } + + return lines.join('\n'); +}; + +export const formatFailureOutput = (failures: Array<{ ruleName: string; messages: string[] }>): string => { + if (failures.length === 0) return ''; + + const lines: string[] = []; + + for (const f of failures) { + lines.push(`FAILED: ${f.ruleName}`); + for (const msg of f.messages) { + lines.push(` ${msg}`); + } + } + + return lines.join('\n'); +}; diff --git a/packages/cli/src/cli/governance/index.ts b/packages/cli/src/cli/governance/index.ts new file mode 100644 index 000000000..b3b19060c --- /dev/null +++ b/packages/cli/src/cli/governance/index.ts @@ -0,0 +1,25 @@ +export { + loadGovernanceConfig, + evaluateGovernanceRules, + resolveEnvVars, + isProducerTrigger, + getChangeVerb, + buildServiceMessageSets, + enrichSchemaContent, +} from './rules'; +export { executeGovernanceActions, buildMessageTypeMap, buildServiceOwnersMap } from './actions'; +export type { MessageTypeMap, ServiceOwnersMap, GovernanceActionOptions } from './actions'; +export { formatGovernanceOutput, formatFailureOutput } from './format'; +export { governanceCheck } from './check'; +export type { GovernanceCheckOptions } from './check'; +export type { + GovernanceTrigger, + GovernanceAction, + GovernanceRule, + GovernanceConfig, + GovernanceResult, + GovernanceCheckResult, + DeprecationChange, + SchemaChange, + BreakingSchemaChange, +} from './types'; diff --git a/packages/cli/src/cli/governance/rules.ts b/packages/cli/src/cli/governance/rules.ts new file mode 100644 index 000000000..b8ca08648 --- /dev/null +++ b/packages/cli/src/cli/governance/rules.ts @@ -0,0 +1,526 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createHash } from 'node:crypto'; +import yaml from 'js-yaml'; +import { satisfies, validRange } from 'semver'; +import createSDK from '@eventcatalog/sdk'; +import type { SnapshotDiff, RelationshipChange, CatalogSnapshot, ResourceChange } from '@eventcatalog/sdk'; +import type { + GovernanceConfig, + GovernanceTrigger, + GovernanceResult, + DeprecationChange, + SchemaChange, + BreakingSchemaChange, +} from './types'; +import { detectBreakingChanges } from '@eventcatalog/breaking-changes'; +import type { CompatibilityStrategy } from '@eventcatalog/breaking-changes'; + +export const loadGovernanceConfig = (catalogDir: string): GovernanceConfig => { + const yamlPath = path.join(catalogDir, 'governance.yaml'); + const ymlPath = path.join(catalogDir, 'governance.yml'); + + const configPath = fs.existsSync(yamlPath) ? yamlPath : fs.existsSync(ymlPath) ? ymlPath : null; + + if (!configPath) { + return { rules: [] }; + } + + const content = fs.readFileSync(configPath, 'utf-8'); + const parsed = yaml.load(content) as GovernanceConfig; + const rules = parsed?.rules || []; + + for (const rule of rules) { + for (const action of rule.actions) { + if (action.type === 'fail' && action.message !== undefined && typeof action.message !== 'string') { + throw new Error(`Invalid "message" in fail action for rule "${rule.name}". Must be a string.`); + } + } + } + + if (parsed?.compatibility?.strategy) { + const validStrategies = new Set(['BACKWARD', 'FORWARD', 'FULL', 'NONE']); + if (!validStrategies.has(parsed.compatibility.strategy)) { + throw new Error( + `Invalid compatibility strategy "${parsed.compatibility.strategy}". Must be one of: BACKWARD, FORWARD, FULL, NONE.` + ); + } + } + + return { + ...(parsed?.compatibility && { compatibility: parsed.compatibility }), + rules, + }; +}; + +const TRIGGER_FILTERS: Partial boolean>> = { + consumer_added: (c) => c.direction === 'receives' && c.changeType === 'added', + consumer_removed: (c) => c.direction === 'receives' && c.changeType === 'removed', + producer_added: (c) => c.direction === 'sends' && c.changeType === 'added', + producer_removed: (c) => c.direction === 'sends' && c.changeType === 'removed', +}; + +export const buildServiceMessageSets = ( + snapshot: CatalogSnapshot +): { produces: Map>; consumes: Map> } => { + const produces = new Map>(); + const consumes = new Map>(); + + for (const service of snapshot.resources.services) { + const serviceId = service.id as string; + if (service.sends) { + const ids = new Set(); + for (const s of service.sends) ids.add(s.id as string); + produces.set(serviceId, ids); + } + if (service.receives) { + const ids = new Set(); + for (const r of service.receives) ids.add(r.id as string); + consumes.set(serviceId, ids); + } + } + + return { produces, consumes }; +}; + +type ServiceMessageSets = ReturnType; + +const matchesResourceId = ( + resourceId: string, + serviceId: string | undefined, + resources: string[], + messageSets?: ServiceMessageSets +): boolean => { + return resources.some((r) => { + if (r === '*') return true; + if (r.startsWith('service:')) { + if (serviceId) return serviceId === r.slice(8); + // For deprecation: match if the specified service produces this message + return messageSets?.produces.get(r.slice(8))?.has(resourceId) ?? false; + } + if (r.startsWith('message:')) return resourceId === r.slice(8); + if (r.startsWith('produces:')) return messageSets?.produces.get(r.slice(9))?.has(resourceId) ?? false; + if (r.startsWith('consumes:')) return messageSets?.consumes.get(r.slice(9))?.has(resourceId) ?? false; + return false; + }); +}; + +const REMOVED_TRIGGERS: Set = new Set(['consumer_removed', 'producer_removed']); + +const MESSAGE_RESOURCE_TYPES = new Set(['event', 'command', 'query']); + +const buildMessageMap = (snapshot: CatalogSnapshot): Map> => { + const map = new Map>(); + for (const msg of snapshot.resources.messages.events) map.set(msg.id as string, msg); + for (const msg of snapshot.resources.messages.commands) map.set(msg.id as string, msg); + for (const msg of snapshot.resources.messages.queries) map.set(msg.id as string, msg); + return map; +}; + +type ServiceEntry = { id: string; version: string; owners?: string[] }; + +const buildServiceIndex = (snapshot: CatalogSnapshot, direction: 'sends' | 'receives'): Map => { + const index = new Map(); + for (const service of snapshot.resources.services) { + const pointers = service[direction] as Array<{ id: string }> | undefined; + if (!pointers) continue; + for (const pointer of pointers) { + let entries = index.get(pointer.id); + if (!entries) { + entries = []; + index.set(pointer.id, entries); + } + const entry: ServiceEntry = { + id: service.id as string, + version: service.version as string, + }; + if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) { + entry.owners = service.owners as string[]; + } + entries.push(entry); + } + } + return index; +}; + +const getMessageTypeKey = (resourceId: string, type: string): string => `${type}:${resourceId}`; + +const buildLatestMessageVersionMap = (snapshot: CatalogSnapshot): Map => { + const versions = new Map(); + + for (const event of snapshot.resources.messages.events) { + versions.set(getMessageTypeKey(event.id as string, 'event'), event.version as string); + } + for (const command of snapshot.resources.messages.commands) { + versions.set(getMessageTypeKey(command.id as string, 'command'), command.version as string); + } + for (const query of snapshot.resources.messages.queries) { + versions.set(getMessageTypeKey(query.id as string, 'query'), query.version as string); + } + + return versions; +}; + +const getTargetMessageVersion = (resourceChange: ResourceChange): string => { + if (resourceChange.changeType === 'versioned') { + return resourceChange.newVersion || resourceChange.version; + } + return resourceChange.version; +}; + +const pointerTargetsChangedVersion = ( + pointer: { id: string; version?: string }, + resourceChange: ResourceChange, + latestMessageVersions: Map +): boolean => { + if (pointer.id !== resourceChange.resourceId) return false; + + const targetVersion = getTargetMessageVersion(resourceChange); + const pointerVersion = pointer.version; + + if (!pointerVersion || pointerVersion === 'latest') { + const latestVersion = latestMessageVersions.get(getMessageTypeKey(resourceChange.resourceId, resourceChange.type)); + if (!latestVersion) return true; + return latestVersion === targetVersion; + } + + if (validRange(pointerVersion)) { + try { + return satisfies(targetVersion, pointerVersion); + } catch { + return false; + } + } + + return pointerVersion === targetVersion; +}; + +const getServicesForSchemaChange = ( + snapshot: CatalogSnapshot, + direction: 'sends' | 'receives', + resourceChange: ResourceChange, + latestMessageVersions: Map +): ServiceEntry[] => { + const matches: ServiceEntry[] = []; + + for (const service of snapshot.resources.services) { + const pointers = service[direction] as Array<{ id: string; version?: string }> | undefined; + if (!pointers) continue; + + const hasMatch = pointers.some((pointer) => pointerTargetsChangedVersion(pointer, resourceChange, latestMessageVersions)); + if (!hasMatch) continue; + + const entry: ServiceEntry = { + id: service.id as string, + version: service.version as string, + }; + + if (service.owners && Array.isArray(service.owners) && service.owners.length > 0) { + entry.owners = service.owners as string[]; + } + + matches.push(entry); + } + + return matches; +}; + +const matchesSchemaChangeResource = (schemaChange: SchemaChange, resources: string[]): boolean => { + return resources.some((resource) => { + if (resource === '*') return true; + if (resource.startsWith('message:')) return schemaChange.resourceChange.resourceId === resource.slice(8); + if (resource.startsWith('consumes:')) + return schemaChange.consumerServices.some((service) => service.id === resource.slice(9)); + if (resource.startsWith('produces:')) + return schemaChange.producerServices.some((service) => service.id === resource.slice(9)); + if (resource.startsWith('service:')) return schemaChange.producerServices.some((service) => service.id === resource.slice(8)); + return false; + }); +}; + +const evaluateDeprecationRules = ( + diff: SnapshotDiff, + config: GovernanceConfig, + targetSnapshot: CatalogSnapshot, + targetMessageSets: ServiceMessageSets, + baseSnapshot?: CatalogSnapshot +): GovernanceResult[] => { + const deprecationRules = config.rules.filter((rule) => rule.when.includes('message_deprecated')); + if (deprecationRules.length === 0) return []; + + const targetMessages = buildMessageMap(targetSnapshot); + const baseMessages = baseSnapshot ? buildMessageMap(baseSnapshot) : undefined; + const producerIndex = buildServiceIndex(targetSnapshot, 'sends'); + + // Find messages that were newly deprecated + const deprecatedResources = diff.resources.filter((rc) => { + if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false; + if (!rc.changedFields?.includes('deprecated')) return false; + + // Confirm the message is deprecated in the target (not un-deprecated) + const targetMessage = targetMessages.get(rc.resourceId); + if (!targetMessage || !targetMessage.deprecated) return false; + + // Confirm it was NOT deprecated in the base + if (baseMessages) { + const baseMessage = baseMessages.get(rc.resourceId); + if (baseMessage && baseMessage.deprecated) return false; + } + + return true; + }); + + if (deprecatedResources.length === 0) return []; + + const results: GovernanceResult[] = []; + + for (const rule of deprecationRules) { + const matched: DeprecationChange[] = []; + + for (const rc of deprecatedResources) { + if (!matchesResourceId(rc.resourceId, undefined, rule.resources, targetMessageSets)) continue; + + const producers = producerIndex.get(rc.resourceId) || []; + matched.push({ resourceChange: rc, producerServices: producers }); + } + + if (matched.length > 0) { + results.push({ rule, trigger: 'message_deprecated', matchedChanges: [], deprecationChanges: matched }); + } + } + + return results; +}; + +const evaluateSchemaChangeRules = ( + diff: SnapshotDiff, + config: GovernanceConfig, + targetSnapshot: CatalogSnapshot +): GovernanceResult[] => { + const schemaRules = config.rules.filter((rule) => rule.when.includes('schema_changed')); + if (schemaRules.length === 0) return []; + + const schemaChangedResources = diff.resources.filter((rc) => { + if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false; + return rc.changedFields?.includes('schemaHash'); + }); + + if (schemaChangedResources.length === 0) return []; + + const latestMessageVersions = buildLatestMessageVersionMap(targetSnapshot); + const schemaChanges = schemaChangedResources.map((resourceChange) => ({ + resourceChange, + producerServices: getServicesForSchemaChange(targetSnapshot, 'sends', resourceChange, latestMessageVersions), + consumerServices: getServicesForSchemaChange(targetSnapshot, 'receives', resourceChange, latestMessageVersions), + })); + + const results: GovernanceResult[] = []; + + for (const rule of schemaRules) { + const matched = schemaChanges.filter((schemaChange) => matchesSchemaChangeResource(schemaChange, rule.resources)); + + if (matched.length > 0) { + results.push({ rule, trigger: 'schema_changed', matchedChanges: [], schemaChanges: matched }); + } + } + + return results; +}; + +const evaluateBreakingSchemaChangeRules = ( + diff: SnapshotDiff, + config: GovernanceConfig, + targetSnapshot: CatalogSnapshot +): GovernanceResult[] => { + const breakingRules = config.rules.filter((rule) => rule.when.includes('schema_breaking_change')); + if (breakingRules.length === 0) return []; + + const strategy = config.compatibility?.strategy; + if (!strategy || strategy === 'NONE') return []; + + const schemaChangedResources = diff.resources.filter((rc) => { + if (!MESSAGE_RESOURCE_TYPES.has(rc.type)) return false; + return rc.changedFields?.includes('schemaHash'); + }); + + if (schemaChangedResources.length === 0) return []; + + const latestMessageVersions = buildLatestMessageVersionMap(targetSnapshot); + const breakingSchemaChanges: BreakingSchemaChange[] = schemaChangedResources.map((resourceChange) => ({ + resourceChange, + producerServices: getServicesForSchemaChange(targetSnapshot, 'sends', resourceChange, latestMessageVersions), + consumerServices: getServicesForSchemaChange(targetSnapshot, 'receives', resourceChange, latestMessageVersions), + breakingChanges: [], + })); + + const results: GovernanceResult[] = []; + + for (const rule of breakingRules) { + const matched = breakingSchemaChanges.filter((sc) => matchesSchemaChangeResource(sc, rule.resources)); + if (matched.length > 0) { + results.push({ rule, trigger: 'schema_breaking_change', matchedChanges: [], breakingSchemaChanges: matched }); + } + } + + return results; +}; + +export const evaluateGovernanceRules = ( + diff: SnapshotDiff, + config: GovernanceConfig, + targetSnapshot?: CatalogSnapshot, + baseSnapshot?: CatalogSnapshot +): GovernanceResult[] => { + const results: GovernanceResult[] = []; + const targetMessageSets = targetSnapshot ? buildServiceMessageSets(targetSnapshot) : undefined; + const baseMessageSets = baseSnapshot ? buildServiceMessageSets(baseSnapshot) : undefined; + + for (const rule of config.rules) { + for (const trigger of rule.when) { + const filter = TRIGGER_FILTERS[trigger]; + if (!filter) continue; + + // For removed triggers, resolve produces:/consumes: prefixes against + // the base snapshot where the relationship still existed. + const messageSets = REMOVED_TRIGGERS.has(trigger) && baseMessageSets ? baseMessageSets : targetMessageSets; + + const matchedChanges = diff.relationships.filter( + (c) => filter(c) && matchesResourceId(c.resourceId, c.serviceId, rule.resources, messageSets) + ); + + if (matchedChanges.length > 0) { + results.push({ rule, trigger, matchedChanges }); + } + } + } + + // Evaluate deprecation rules separately (they operate on resource changes, not relationship changes) + if (targetSnapshot && targetMessageSets) { + results.push(...evaluateDeprecationRules(diff, config, targetSnapshot, targetMessageSets, baseSnapshot)); + results.push(...evaluateSchemaChangeRules(diff, config, targetSnapshot)); + results.push(...evaluateBreakingSchemaChangeRules(diff, config, targetSnapshot)); + } + + return results; +}; + +const PRODUCER_TRIGGERS: Set = new Set(['producer_added', 'producer_removed']); + +export const isProducerTrigger = (trigger: GovernanceTrigger): boolean => PRODUCER_TRIGGERS.has(trigger); + +export const getChangeVerb = (trigger: GovernanceTrigger, changeType: 'added' | 'removed'): string => { + const producer = isProducerTrigger(trigger); + return changeType === 'added' + ? producer + ? 'now producing' + : 'now consuming' + : producer + ? 'no longer producing' + : 'no longer consuming'; +}; + +export const resolveEnvVars = (value: string): string => { + return value.replace(/\$([A-Z_][A-Z0-9_]*)/g, (match, varName) => { + const envValue = process.env[varName]; + if (envValue === undefined) { + throw new Error(`Environment variable ${varName} is not set`); + } + return envValue; + }); +}; + +type SDK = ReturnType; + +const readSchemaDetails = async ( + sdk: SDK, + resourceId: string, + version: string, + type: string +): Promise<{ content?: string; schemaPath?: string; schemaHash?: string }> => { + if (!MESSAGE_RESOURCE_TYPES.has(type)) return {}; + try { + const schema = await sdk.getSchemaForMessage(resourceId, version); + if (!schema) return {}; + return { + content: schema.schema, + schemaPath: schema.fileName, + schemaHash: createHash('sha256').update(schema.schema).digest('hex'), + }; + } catch { + return {}; + } +}; + +export const enrichSchemaContent = async ( + results: GovernanceResult[], + baseCatalogDir: string, + targetCatalogDir: string, + compatibilityStrategy?: CompatibilityStrategy +): Promise => { + const baseSDK = createSDK(baseCatalogDir); + const targetSDK = createSDK(targetCatalogDir); + + const promises: Promise[] = []; + + for (const result of results) { + if (!result.schemaChanges) continue; + for (const sc of result.schemaChanges) { + const { resourceId, version, type, changeType, previousVersion, newVersion } = sc.resourceChange; + const baseVersion = changeType === 'versioned' ? previousVersion || version : version; + const targetVersion = changeType === 'versioned' ? newVersion || version : version; + promises.push( + (async () => { + const [before, after] = await Promise.all([ + readSchemaDetails(baseSDK, resourceId, baseVersion, type), + readSchemaDetails(targetSDK, resourceId, targetVersion, type), + ]); + sc.before = before.content; + sc.after = after.content; + sc.beforeSchemaPath = before.schemaPath; + sc.afterSchemaPath = after.schemaPath; + sc.beforeSchemaHash = before.schemaHash; + sc.afterSchemaHash = after.schemaHash; + })() + ); + } + } + + for (const result of results) { + if (!result.breakingSchemaChanges || !compatibilityStrategy) continue; + for (const bsc of result.breakingSchemaChanges) { + const { resourceId, version, type, changeType, previousVersion, newVersion } = bsc.resourceChange; + const baseVersion = changeType === 'versioned' ? previousVersion || version : version; + const targetVersion = changeType === 'versioned' ? newVersion || version : version; + promises.push( + (async () => { + const [before, after] = await Promise.all([ + readSchemaDetails(baseSDK, resourceId, baseVersion, type), + readSchemaDetails(targetSDK, resourceId, targetVersion, type), + ]); + bsc.before = before.content; + bsc.after = after.content; + bsc.beforeSchemaPath = before.schemaPath; + bsc.afterSchemaPath = after.schemaPath; + bsc.beforeSchemaHash = before.schemaHash; + bsc.afterSchemaHash = after.schemaHash; + + if (before.content && after.content) { + let beforeSchema: object | undefined; + let afterSchema: object | undefined; + try { + beforeSchema = JSON.parse(before.content); + afterSchema = JSON.parse(after.content); + } catch { + // Schema is not valid JSON — skip breaking change detection + } + if (beforeSchema && afterSchema) { + bsc.breakingChanges = detectBreakingChanges(beforeSchema, afterSchema, compatibilityStrategy); + } + } + })() + ); + } + } + + await Promise.all(promises); +}; diff --git a/packages/cli/src/cli/governance/types.ts b/packages/cli/src/cli/governance/types.ts new file mode 100644 index 000000000..31b709c31 --- /dev/null +++ b/packages/cli/src/cli/governance/types.ts @@ -0,0 +1,67 @@ +import type { RelationshipChange, ResourceChange } from '@eventcatalog/sdk'; + +export type GovernanceTrigger = + | 'consumer_added' + | 'consumer_removed' + | 'producer_added' + | 'producer_removed' + | 'message_deprecated' + | 'schema_changed' + | 'schema_breaking_change'; + +export type GovernanceAction = + | { type: 'console' } + | { type: 'webhook'; url: string; headers?: Record } + | { type: 'fail'; message?: string }; + +export type GovernanceRule = { + name: string; + when: GovernanceTrigger[]; + resources: string[]; + actions: GovernanceAction[]; +}; + +export type GovernanceConfig = { + compatibility?: { + strategy: import('@eventcatalog/breaking-changes').CompatibilityStrategy; + }; + rules: GovernanceRule[]; +}; + +export type DeprecationChange = { + resourceChange: ResourceChange; + producerServices: Array<{ id: string; version: string; owners?: string[] }>; +}; + +export type SchemaChange = { + resourceChange: ResourceChange; + consumerServices: Array<{ id: string; version: string; owners?: string[] }>; + producerServices: Array<{ id: string; version: string; owners?: string[] }>; + before?: string; + after?: string; + beforeSchemaPath?: string; + afterSchemaPath?: string; + beforeSchemaHash?: string; + afterSchemaHash?: string; +}; + +export type BreakingSchemaChange = SchemaChange & { + breakingChanges: import('@eventcatalog/breaking-changes').BreakingChange[]; +}; + +export type GovernanceResult = { + rule: GovernanceRule; + trigger: GovernanceTrigger; + matchedChanges: RelationshipChange[]; + deprecationChanges?: DeprecationChange[]; + schemaChanges?: SchemaChange[]; + breakingSchemaChanges?: BreakingSchemaChange[]; + failed?: boolean; + failMessages?: string[]; +}; + +export type GovernanceCheckResult = { + output: string; + exitCode: number; + failures: Array<{ ruleName: string; messages: string[] }>; +}; diff --git a/packages/cli/src/cli/import.ts b/packages/cli/src/cli/import.ts new file mode 100644 index 000000000..1ea1089df --- /dev/null +++ b/packages/cli/src/cli/import.ts @@ -0,0 +1,982 @@ +import { readFileSync, existsSync, mkdirSync, writeFileSync, copyFileSync } from 'node:fs'; +import { resolve, join } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import { createInterface } from 'node:readline'; +import matter from 'gray-matter'; +import createSDK from '@eventcatalog/sdk'; +import { DSL_MANAGED_KEYS_BY_TYPE } from './dsl-managed-keys'; + +export interface ImportOptions { + files?: string[]; + stdin?: boolean; + dryRun?: boolean; + flat?: boolean; + noInit?: boolean; + dir: string; +} + +interface ParsedResource { + type: string; + id: string; + version?: string; + frontmatter: Record; + markdown: string; + path: string; +} + +interface ImportResult { + created: string[]; + updated: string[]; + versioned: string[]; + errors: string[]; +} + +function normalizeImportedFrontmatter(type: string, frontmatter: Record): Record { + const normalized = { ...frontmatter }; + + // SDK/container schema uses snake_case fields. + if (type === 'container') { + if (normalized.container_type === undefined && normalized.containerType !== undefined) { + normalized.container_type = normalized.containerType; + delete normalized.containerType; + } + if (normalized.access_mode === undefined && normalized.accessMode !== undefined) { + normalized.access_mode = normalized.accessMode; + delete normalized.accessMode; + } + // Default container_type to 'database' if not specified in the DSL + if (!normalized.container_type) { + normalized.container_type = 'database'; + } + } + + return normalized; +} + +function toFrontmatter(resource: any): Record { + if (!resource || typeof resource !== 'object') return {}; + const { markdown: _markdown, ...frontmatter } = resource; + return frontmatter; +} + +function mergeImportedFrontmatter( + type: string, + incomingFrontmatter: Record, + existing?: any, + latest?: any +): Record { + const normalizedIncoming = normalizeImportedFrontmatter(type, incomingFrontmatter); + const managedKeys = DSL_MANAGED_KEYS_BY_TYPE[type]; + + if (!managedKeys) { + return normalizedIncoming; + } + + const baseFrontmatter = toFrontmatter(existing ?? latest); + const preservedFrontmatter = { ...baseFrontmatter }; + + for (const key of managedKeys) { + delete preservedFrontmatter[key]; + } + + return { + ...preservedFrontmatter, + ...normalizedIncoming, + }; +} + +const RESOURCE_TYPE_FROM_FOLDER: Record = { + events: 'event', + commands: 'command', + queries: 'query', + services: 'service', + domains: 'domain', + channels: 'channel', + flows: 'flow', + containers: 'container', + 'data-products': 'dataProduct', + diagrams: 'diagram', + users: 'user', + teams: 'team', +}; + +async function parseDSL( + source: string, + options?: { nested?: boolean } +): Promise<{ outputs: { path: string; content: string }[]; program: any }> { + const { createEcServices, compile } = await import('@eventcatalog/language-server'); + const { EmptyFileSystem, URI } = await import('langium'); + + const services = createEcServices(EmptyFileSystem); + const uri = URI.parse(`file:///import-${Date.now()}.ec`); + const document = services.shared.workspace.LangiumDocumentFactory.fromString(source, uri); + services.shared.workspace.LangiumDocuments.addDocument(document); + await services.shared.workspace.DocumentBuilder.build([document]); + + const parserErrors = document.parseResult.parserErrors; + if (parserErrors.length > 0) { + const messages = parserErrors.map((e: any) => ` Line ${e.token?.startLine ?? '?'}: ${e.message}`); + throw new Error(`Parse errors:\n${messages.join('\n')}`); + } + + const program = document.parseResult.value; + const outputs = compile(program, { nested: options?.nested }); + + try { + services.shared.workspace.LangiumDocuments.deleteDocument(uri); + } catch { + // Ignore cleanup errors + } + + return { outputs, program }; +} + +/** + * Extract the resource type folder from a path that may be nested. + * e.g. "events/OrderCreated/..." → "events" + * e.g. "domains/Payment/services/OrderService/..." → "services" + * e.g. "domains/Payment/services/OrderService/events/X/..." → "events" + * e.g. "users/jdoe.md" → "users" + * + * Walk the segments and return the last known resource-type folder. + */ +function extractResourceTypeFolder(path: string): string { + const segments = path.split('/'); + let lastTypeFolder = segments[0]; + for (const seg of segments) { + if (RESOURCE_TYPE_FROM_FOLDER[seg]) { + lastTypeFolder = seg; + } + } + return lastTypeFolder; +} + +function parseCompiledOutput(output: { path: string; content: string }): ParsedResource { + const { data: frontmatter, content: markdown } = matter(output.content); + + const typeFolder = extractResourceTypeFolder(output.path); + const type = RESOURCE_TYPE_FROM_FOLDER[typeFolder] || typeFolder; + + return { + type, + id: frontmatter.id, + version: frontmatter.version, + frontmatter, + markdown: markdown.trim(), + path: output.path, + }; +} + +const MESSAGE_TYPE_FOLDER: Record = { + event: 'events', + command: 'commands', + query: 'queries', + channel: 'channels', +}; + +const DEFAULT_STUB_VERSION = '0.0.1'; +const NO_VERSION_KEY = '__no_version__'; + +function getResourceNameKey(type: string, id: string): string { + return `${type}:${id}`; +} + +function getResourceVersionKey(type: string, id: string, version?: string): string { + return `${type}:${id}@${version || NO_VERSION_KEY}`; +} + +function hasReferenceStatements(source: string): boolean { + return /\b(?:sends|receives|writes-to|reads-from)\b/.test(source); +} + +/** + * Extract resource stubs from service references that weren't compiled as standalone resources. + * Uses the DSL AST to determine message types accurately. + */ +async function extractMessageStubs(program: any, compiledIds: Set, nested: boolean = false): Promise { + const stubs: ParsedResource[] = []; + const stubIds = new Set(); + + function processDefinitions(definitions: any[], parentPath: string = '') { + for (const def of definitions) { + // Handle visualizer wrappers + if (def.$type === 'VisualizerDef' && def.body) { + processDefinitions(def.body, parentPath); + continue; + } + + // Handle domains — recurse into their services with domain parentPath + if (def.$type === 'DomainDef') { + const domainPath = nested ? `domains/${def.name}` : ''; + const domainBody = def.body || []; + + // Process services inside the domain + const domainServices = domainBody.filter((d: any) => d.$type === 'ServiceDef'); + processDefinitions(domainServices, domainPath); + + // Process subdomains + const subdomains = domainBody.filter((d: any) => d.$type === 'SubdomainDef'); + for (const sub of subdomains) { + const subPath = nested ? `domains/${def.name}/subdomains/${sub.name}` : ''; + const subServices = (sub.body || []).filter((d: any) => d.$type === 'ServiceDef'); + processDefinitions(subServices, subPath); + } + continue; + } + + if (def.$type !== 'ServiceDef') continue; + + const servicePath = nested ? (parentPath ? `${parentPath}/services/${def.name}` : `services/${def.name}`) : ''; + + const body = def.body || []; + for (const stmt of body) { + if (stmt.$type === 'SendsStmt' || stmt.$type === 'ReceivesStmt') { + const msgType = stmt.messageType; // 'event', 'command', or 'query' + const msgName = stmt.messageName; + const hasBody = stmt.body && stmt.body.length > 0; + const version = stmt.version || DEFAULT_STUB_VERSION; + + if (!hasBody) { + const folder = MESSAGE_TYPE_FOLDER[msgType]; + if (folder) { + const key = getResourceVersionKey(msgType, msgName, version); + const anyVersionKey = getResourceNameKey(msgType, msgName); + + // For explicit refs (e.g. Event@2.0.0), require exact version match. + // For unversioned refs, treat any compiled version as satisfying the reference. + if (!compiledIds.has(key) && !stubIds.has(key) && !(!stmt.version && compiledIds.has(anyVersionKey))) { + const stubFolder = nested && servicePath ? `${servicePath}/${folder}` : folder; + + stubIds.add(key); + stubs.push({ + type: msgType, + id: msgName, + version, + frontmatter: { + id: msgName, + name: msgName, + version, + }, + markdown: '', + path: `${stubFolder}/${msgName}/versioned/${version}/index.md`, + }); + } + } + } + + // Create channel stubs for to/from references + if (stmt.channelClause) { + const channels = stmt.channelClause.channels || []; + for (const ch of channels) { + const chName = ch.channelName; + const chVersion = ch.channelVersion || DEFAULT_STUB_VERSION; + const chKey = getResourceVersionKey('channel', chName, chVersion); + const chAnyVersionKey = getResourceNameKey('channel', chName); + if (compiledIds.has(chKey) || stubIds.has(chKey)) continue; + if (!ch.channelVersion && compiledIds.has(chAnyVersionKey)) continue; + + const chFolder = nested && parentPath ? `${parentPath}/channels` : 'channels'; + stubIds.add(chKey); + stubs.push({ + type: 'channel', + id: chName, + version: chVersion, + frontmatter: { + id: chName, + name: chName, + version: chVersion, + }, + markdown: '', + path: `${chFolder}/${chName}/versioned/${chVersion}/index.md`, + }); + } + } + continue; + } + + if (stmt.$type === 'WritesToStmt' || stmt.$type === 'ReadsFromStmt') { + const containerName = stmt.ref?.name; + if (!containerName) continue; + + const containerVersion = stmt.ref?.version || DEFAULT_STUB_VERSION; + const containerKey = getResourceVersionKey('container', containerName, containerVersion); + const containerAnyVersionKey = getResourceNameKey('container', containerName); + if (compiledIds.has(containerKey) || stubIds.has(containerKey)) continue; + if (!stmt.ref?.version && compiledIds.has(containerAnyVersionKey)) continue; + + const containerFolder = nested && parentPath ? `${parentPath}/containers` : 'containers'; + stubIds.add(containerKey); + stubs.push({ + type: 'container', + id: containerName, + version: containerVersion, + frontmatter: { + id: containerName, + name: containerName, + version: containerVersion, + }, + markdown: '', + path: `${containerFolder}/${containerName}/versioned/${containerVersion}/index.md`, + }); + } + } + } + } + + processDefinitions(program.definitions); + + return stubs; +} + +function getVersionFromBody(body: any[] | undefined): string | undefined { + if (!body) return undefined; + const versionStmt = body.find((stmt) => stmt?.$type === 'VersionStmt'); + return versionStmt?.value; +} + +function buildServiceOutputPath(serviceName: string, body: any[] | undefined, nested: boolean, parentPath: string): string { + const folder = nested && parentPath ? `${parentPath}/services` : 'services'; + const version = getVersionFromBody(body); + if (version) return `${folder}/${serviceName}/versioned/${version}/index.md`; + return `${folder}/${serviceName}/index.md`; +} + +function extractServiceContainerRefs( + program: any, + nested: boolean = false +): Map; readsFrom?: Array<{ id: string; version?: string }> }> { + const refsByPath = new Map< + string, + { writesTo?: Array<{ id: string; version?: string }>; readsFrom?: Array<{ id: string; version?: string }> } + >(); + + function processDefinitions(definitions: any[], parentPath: string = ''): void { + for (const def of definitions || []) { + if (def.$type === 'VisualizerDef' && def.body) { + processDefinitions(def.body, parentPath); + continue; + } + + if (def.$type === 'DomainDef') { + const domainPath = nested ? `domains/${def.name}` : ''; + const domainBody = def.body || []; + const domainServices = domainBody.filter((d: any) => d.$type === 'ServiceDef'); + processDefinitions(domainServices, domainPath); + + const subdomains = domainBody.filter((d: any) => d.$type === 'SubdomainDef'); + for (const sub of subdomains) { + const subPath = nested ? `domains/${def.name}/subdomains/${sub.name}` : ''; + const subServices = (sub.body || []).filter((d: any) => d.$type === 'ServiceDef'); + processDefinitions(subServices, subPath); + } + continue; + } + + if (def.$type !== 'ServiceDef') continue; + + const body = def.body || []; + const writesTo = body + .filter((stmt: any) => stmt.$type === 'WritesToStmt' && stmt.ref?.name) + .map((stmt: any) => ({ + id: stmt.ref.name, + ...(stmt.ref.version ? { version: stmt.ref.version } : {}), + })); + + const readsFrom = body + .filter((stmt: any) => stmt.$type === 'ReadsFromStmt' && stmt.ref?.name) + .map((stmt: any) => ({ + id: stmt.ref.name, + ...(stmt.ref.version ? { version: stmt.ref.version } : {}), + })); + + if (writesTo.length === 0 && readsFrom.length === 0) continue; + + const path = buildServiceOutputPath(def.name, body, nested, parentPath); + refsByPath.set(path, { + ...(writesTo.length > 0 ? { writesTo } : {}), + ...(readsFrom.length > 0 ? { readsFrom } : {}), + }); + } + } + + processDefinitions(program.definitions || []); + return refsByPath; +} + +/** + * Map resource type to the base folder used by the SDK writer. + * The SDK creates writers with type-prefixed directories, e.g. + * writeService → catalogDir/services/ + * writeEvent → catalogDir/events/ + * So any custom path must be relative to that base. + */ +const SDK_WRITER_BASE: Record = { + event: 'events', + command: 'commands', + query: 'queries', + service: 'services', + domain: 'domains', + channel: 'channels', + container: 'containers', + dataProduct: 'data-products', + diagram: 'diagrams', + team: 'teams', + user: 'users', +}; + +/** + * Extract the SDK-compatible path option from a compiler output path. + * The SDK writer already prefixes with the type folder, so we need to + * return a path relative to that base. + * + * e.g. for type "service": + * "services/OrderService/versioned/1.0.0/index.md" → "" (default behavior) + * "domains/Payment/services/OrderService/versioned/1.0.0/index.md" + * → "../domains/Payment/services/OrderService" + * + * Returns empty string when the resource is at its default location. + */ +function extractSdkPath(compiledPath: string, resourceType: string): string { + // Strip versioned/X/index.md or index.md or .md suffix + const dirPath = compiledPath + .replace(/\/versioned\/[^/]+\/index\.md$/, '') + .replace(/\/index\.md$/, '') + .replace(/\.md$/, ''); + + const baseFolder = SDK_WRITER_BASE[resourceType]; + if (!baseFolder) return ''; + + // If path starts with the type's default folder, strip it + // e.g. "services/OrderService" → "OrderService" (default SDK behavior handles this) + if (dirPath.startsWith(`${baseFolder}/`)) { + const relative = dirPath.slice(baseFolder.length + 1); + // If it's just the resource name (no nesting), return empty to use default + if (!relative.includes('/')) return ''; + return relative; + } + + // Path is nested under a different folder (e.g. domains/X/services/Y) + // Need to go up one level from the SDK writer's base directory + return `../${dirPath}`; +} + +const TYPES_WITH_NODE_GRAPH = new Set(['event', 'command', 'query', 'service', 'domain', 'channel', 'flow']); + +type SDK = ReturnType; + +function getWriter(sdk: SDK, type: string): ((resource: any, options?: any) => Promise) | null { + switch (type) { + case 'event': + return sdk.writeEvent; + case 'command': + return sdk.writeCommand; + case 'query': + return sdk.writeQuery; + case 'service': + return sdk.writeService; + case 'domain': + return sdk.writeDomain; + case 'channel': + return sdk.writeChannel; + case 'container': + return sdk.writeDataStore; + case 'dataProduct': + return sdk.writeDataProduct; + case 'diagram': + return sdk.writeDiagram; + case 'team': + return sdk.writeTeam; + case 'user': + return sdk.writeUser; + default: + return null; + } +} + +function getReader(sdk: SDK, type: string): ((id: string, version?: string) => Promise) | null { + switch (type) { + case 'event': + return sdk.getEvent; + case 'command': + return sdk.getCommand; + case 'query': + return sdk.getQuery; + case 'service': + return sdk.getService; + case 'domain': + return sdk.getDomain; + case 'channel': + return sdk.getChannel; + case 'container': + return sdk.getDataStore; + case 'dataProduct': + return sdk.getDataProduct; + case 'diagram': + return sdk.getDiagram; + case 'team': + return sdk.getTeam; + case 'user': + return sdk.getUser; + default: + return null; + } +} + +export function promptConfirm(message: string): Promise { + return new Promise((resolve) => { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + rl.question(`${message} `, (answer) => { + rl.close(); + const normalized = answer.trim().toLowerCase(); + resolve(normalized === '' || normalized === 'y' || normalized === 'yes'); + }); + }); +} + +export function promptInput(message: string, defaultValue: string): Promise { + return new Promise((resolve) => { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + rl.question(`${message} `, (answer) => { + rl.close(); + resolve(answer.trim() || defaultValue); + }); + }); +} + +export function initCatalog(dir: string, organizationName: string = 'My Organization'): void { + const catalogDir = resolve(dir); + mkdirSync(catalogDir, { recursive: true }); + + // package.json + const packageJson = { + name: 'my-catalog', + version: '0.1.0', + private: true, + scripts: { + dev: 'eventcatalog dev', + build: 'eventcatalog build', + start: 'eventcatalog start', + preview: 'eventcatalog preview', + generate: 'eventcatalog generate', + lint: 'eventcatalog-linter', + test: 'echo "Error: no test specified" && exit 1', + }, + dependencies: { + '@eventcatalog/core': 'latest', + '@eventcatalog/linter': 'latest', + }, + }; + writeFileSync(join(catalogDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n', 'utf-8'); + + // eventcatalog.config.js + const cId = randomUUID(); + const config = `/** @type {import('@eventcatalog/core/bin/eventcatalog.config').Config} */ +export default { + title: 'EventCatalog', + tagline: 'Discover, Explore and Document your Event Driven Architectures', + organizationName: '${organizationName}', + homepageLink: 'https://eventcatalog.dev/', + output: 'static', + trailingSlash: false, + base: '/', + logo: { + alt: 'EventCatalog Logo', + src: '/logo.png', + text: 'EventCatalog', + }, + cId: '${cId}', +}; +`; + writeFileSync(join(catalogDir, 'eventcatalog.config.js'), config, 'utf-8'); + + // .gitignore + const gitignore = `# Dependencies +/node_modules + +# Production +/build + +# Generated files +.astro +out +dist + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.eventcatalog-core + +.env +.env-* +`; + writeFileSync(join(catalogDir, '.gitignore'), gitignore, 'utf-8'); + + // .env + const envFile = `# EventCatalog Scale License Key, if you want to unlock the scale features +# You can get a 14 day trial license key from https://eventcatalog.cloud + +EVENTCATALOG_SCALE_LICENSE_KEY= + +# Optional key if you are using EventCatalog Chat with OpenAI Models. +# You need to set \`output\` to \`server\` in your eventcatalog.config.js file. +# See documentation for more details: https://www.eventcatalog.dev/features/ai-assistant +OPENAI_API_KEY= +`; + writeFileSync(join(catalogDir, '.env'), envFile, 'utf-8'); + + // .npmrc + writeFileSync(join(catalogDir, '.npmrc'), 'strict-peer-dependencies=false\n', 'utf-8'); + + // public/logo.png + mkdirSync(join(catalogDir, 'public'), { recursive: true }); + copyFileSync(join(__dirname, 'logo.png'), join(catalogDir, 'public', 'logo.png')); +} + +export async function importDSL(options: ImportOptions): Promise { + const { files, stdin = false, dryRun = false, flat = false, noInit = false, dir } = options; + const nested = !flat; + + let source: string; + + if (stdin) { + source = await readStdin(); + } else if (files && files.length > 0) { + const parts: string[] = []; + for (const file of files) { + const filepath = resolve(file); + if (!existsSync(filepath)) { + throw new Error(`File not found: ${filepath}`); + } + parts.push(readFileSync(filepath, 'utf-8')); + } + source = parts.join('\n\n'); + } else { + throw new Error('Either provide .ec file paths or use --stdin'); + } + + if (!source.trim()) { + throw new Error('No DSL content to import'); + } + + // Check if catalog needs initialization + const catalogDir = resolve(dir); + let didInit = false; + if (!noInit && !existsSync(join(catalogDir, 'eventcatalog.config.js')) && process.stdin.isTTY) { + const confirmed = await promptConfirm(`Initialize a new EventCatalog at ${catalogDir}? (Y/n)`); + if (confirmed) { + const organizationName = await promptInput('Organization name (My Organization):', 'My Organization'); + initCatalog(dir, organizationName); + didInit = true; + } + } + + const parsed = await parseDSL(source, { nested }); + const outputs = parsed.outputs; + + if (outputs.length === 0) { + throw new Error('No resources found in DSL content'); + } + + const sdk = createSDK(catalogDir); + const result: ImportResult = { created: [], updated: [], versioned: [], errors: [] }; + const readerCache = new Map>(); + + const readResourceCached = async ( + reader: ((id: string, version?: string) => Promise) | null, + type: string, + id: string, + version?: string + ): Promise => { + if (!reader) return undefined; + const cacheKey = getResourceVersionKey(type, id, version); + if (!readerCache.has(cacheKey)) { + readerCache.set( + cacheKey, + reader(id, version).catch(() => undefined) + ); + } + return await readerCache.get(cacheKey)!; + }; + + const invalidateReaderCache = (type: string, id: string, version?: string): void => { + // We only read "latest" (no version) and the current version in this import path, + // so targeted invalidation avoids scanning the full cache for every write. + readerCache.delete(getResourceVersionKey(type, id)); + readerCache.delete(getResourceVersionKey(type, id, version)); + }; + + const resources = outputs.map(parseCompiledOutput); + const serviceContainerRefsByPath = extractServiceContainerRefs(parsed.program, nested); + + for (const resource of resources) { + if (resource.type !== 'service') continue; + const refs = serviceContainerRefsByPath.get(resource.path); + if (!refs) continue; + resource.frontmatter = { + ...resource.frontmatter, + ...refs, + }; + } + + // Create stub resources for referenced messages that weren't compiled as standalone + const compiledIds = new Set(); + for (const resource of resources) { + compiledIds.add(getResourceNameKey(resource.type, resource.id)); + compiledIds.add(getResourceVersionKey(resource.type, resource.id, resource.version)); + } + const stubs = hasReferenceStatements(source) ? await extractMessageStubs(parsed.program, compiledIds, nested) : []; + resources.push(...stubs); + + for (const resource of resources) { + const label = resource.version ? `${resource.type} ${resource.id}@${resource.version}` : `${resource.type} ${resource.id}`; + + if (dryRun) { + const reader = getReader(sdk, resource.type); + if (reader) { + const existing = await readResourceCached(reader, resource.type, resource.id, resource.version); + if (existing) { + result.updated.push(label); + } else { + const latest = await readResourceCached(reader, resource.type, resource.id); + if (latest && latest.version && latest.version !== resource.version) { + result.versioned.push(`${resource.type} ${resource.id}@${latest.version}`); + } + result.created.push(label); + } + } else { + result.created.push(label); + } + continue; + } + + const writer = getWriter(sdk, resource.type); + if (!writer) { + result.errors.push(`${label}: unsupported resource type '${resource.type}'`); + continue; + } + + try { + const reader = getReader(sdk, resource.type); + const existing = await readResourceCached(reader, resource.type, resource.id, resource.version); + const latest = !existing && resource.version ? await readResourceCached(reader, resource.type, resource.id) : undefined; + const versionedFrom = + !existing && resource.version && latest?.version && latest.version !== resource.version ? latest.version : undefined; + + const incomingMarkdown = resource.markdown; + const hasIncomingMarkdown = incomingMarkdown.trim().length > 0; + let markdown = incomingMarkdown; + + // Preserve existing markdown when DSL import has no markdown body. + if (!hasIncomingMarkdown) { + if (existing?.markdown) { + markdown = existing.markdown; + } else if (!existing && latest?.markdown) { + // If importing a newer version without markdown, carry forward latest markdown. + markdown = latest.markdown; + } + } + + // Add to markdown for newly created resources (not updates) + if (!existing && TYPES_WITH_NODE_GRAPH.has(resource.type)) { + if (!markdown) { + markdown = ''; + } else if (!markdown.includes('')) { + markdown = `${markdown}\n\n`; + } + } + + const resourceData = { + ...mergeImportedFrontmatter(resource.type, resource.frontmatter, existing, latest), + markdown, + }; + + // If resource already exists, write with default SDK behavior (existing location). + // For new resources in nested mode, extract directory from compiler path. + const writeOptions: Record = { + override: true, + versionExistingContent: Boolean(versionedFrom), + }; + + if (!existing && nested) { + const sdkPath = extractSdkPath(resource.path, resource.type); + if (sdkPath) { + writeOptions.path = sdkPath; + } + } + + await writer(resourceData, writeOptions); + invalidateReaderCache(resource.type, resource.id, resource.version); + + if (existing) { + result.updated.push(label); + } else { + result.created.push(label); + if (versionedFrom) { + result.versioned.push(`${resource.type} ${resource.id}@${versionedFrom}`); + } + } + } catch (error) { + result.errors.push(`${label}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + let output = formatResult(result, dryRun); + if (didInit) { + output += ` Tip: Run 'npm install' in ${catalogDir} to install dependencies\n`; + } + return output; +} + +// ANSI color helpers +const c = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + red: '\x1b[31m', + white: '\x1b[37m', + gray: '\x1b[90m', +}; + +const TYPE_CONFIG: Record = { + domain: { color: c.magenta, label: 'domain', order: 0 }, + service: { color: c.blue, label: 'service', order: 1 }, + event: { color: c.green, label: 'event', order: 2 }, + command: { color: c.yellow, label: 'command', order: 3 }, + query: { color: c.cyan, label: 'query', order: 4 }, + channel: { color: c.gray, label: 'channel', order: 5 }, + flow: { color: c.white, label: 'flow', order: 6 }, + container: { color: c.white, label: 'container', order: 7 }, + dataProduct: { color: c.white, label: 'data product', order: 8 }, + diagram: { color: c.white, label: 'diagram', order: 9 }, + user: { color: c.blue, label: 'user', order: 10 }, + team: { color: c.blue, label: 'team', order: 11 }, +}; + +const DEFAULT_TYPE_CONFIG = { color: c.white, label: 'resource', order: 99 }; + +/** + * Parse a label like "event OrderCreated@1.0.0" into { type, name }. + */ +function parseLabel(label: string): { type: string; name: string } { + const spaceIdx = label.indexOf(' '); + if (spaceIdx === -1) return { type: '', name: label }; + return { type: label.slice(0, spaceIdx), name: label.slice(spaceIdx + 1) }; +} + +/** + * Group labels by resource type, sorted by type order then alphabetically. + */ +function groupByType(labels: string[]): Map { + const groups = new Map(); + for (const label of labels) { + const { type, name } = parseLabel(label); + if (!groups.has(type)) groups.set(type, []); + groups.get(type)!.push(name); + } + const sorted = new Map( + [...groups.entries()].sort((a, b) => { + const orderA = (TYPE_CONFIG[a[0]] || DEFAULT_TYPE_CONFIG).order; + const orderB = (TYPE_CONFIG[b[0]] || DEFAULT_TYPE_CONFIG).order; + return orderA - orderB; + }) + ); + for (const [, names] of sorted) { + names.sort(); + } + return sorted; +} + +/** + * Format a type badge like " event " with background color. + */ +function typeBadge(type: string): string { + const cfg = TYPE_CONFIG[type] || DEFAULT_TYPE_CONFIG; + const padded = ` ${cfg.label} `; + // Use reverse video for a "badge" effect: swaps fg/bg + return `${cfg.color}\x1b[7m${padded}${c.reset}`; +} + +function formatResourceList(labels: string[]): string[] { + const lines: string[] = []; + const groups = groupByType(labels); + + for (const [type, names] of groups) { + const cfg = TYPE_CONFIG[type] || DEFAULT_TYPE_CONFIG; + const plural = names.length === 1 ? cfg.label : `${cfg.label}s`; + lines.push(''); + lines.push(` ${typeBadge(type)} ${c.dim}${names.length} ${plural}${c.reset}`); + for (const name of names) { + lines.push(` ${cfg.color}│${c.reset} ${name}`); + } + } + return lines; +} + +function formatResult(result: ImportResult, dryRun: boolean): string { + const lines: string[] = ['']; + const prefix = dryRun ? `${c.yellow}${c.bold}DRY RUN${c.reset} ` : ''; + const total = result.created.length + result.updated.length + result.versioned.length; + + if (total > 0 || result.errors.length > 0) { + lines.push(` ${prefix}${c.bold}Import complete${c.reset}`); + lines.push(` ${c.dim}${'─'.repeat(40)}${c.reset}`); + } + + if (result.created.length > 0) { + const verb = dryRun ? 'Would create' : 'Created'; + lines.push(''); + lines.push(` ${c.green}${c.bold}+ ${verb} ${result.created.length} resource(s)${c.reset}`); + lines.push(...formatResourceList(result.created)); + } + + if (result.updated.length > 0) { + const verb = dryRun ? 'Would update' : 'Updated'; + lines.push(''); + lines.push(` ${c.blue}${c.bold}~ ${verb} ${result.updated.length} resource(s)${c.reset}`); + lines.push(...formatResourceList(result.updated)); + } + + if (result.versioned.length > 0) { + const verb = dryRun ? 'Would version' : 'Versioned'; + lines.push(''); + lines.push(` ${c.yellow}${c.bold}↑ ${verb} ${result.versioned.length} existing resource(s)${c.reset}`); + lines.push(...formatResourceList(result.versioned)); + } + + if (result.errors.length > 0) { + lines.push(''); + lines.push(` ${c.red}${c.bold}✘ ${result.errors.length} error(s)${c.reset}`); + for (const e of result.errors) { + lines.push(` ${c.red}│${c.reset} ${c.red}${e}${c.reset}`); + } + } + + if (result.created.length === 0 && result.updated.length === 0 && result.errors.length === 0) { + lines.push(` ${c.dim}No resources to write.${c.reset}`); + } + + lines.push(''); + return lines.join('\n'); +} + +function readStdin(): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + process.stdin.on('data', (chunk) => chunks.push(chunk)); + process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8'))); + process.stdin.on('error', reject); + }); +} diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts new file mode 100644 index 000000000..083c4fa5a --- /dev/null +++ b/packages/cli/src/cli/index.ts @@ -0,0 +1,223 @@ +#!/usr/bin/env node + +import { program } from 'commander'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { executeFunction } from './executor'; +import { listFunctions, formatListOutput } from './list'; +import { exportResource, exportCatalog } from './export'; +import { importDSL } from './import'; +import { snapshotCreate, snapshotDiff, snapshotList } from './snapshot'; +import { governanceCheck } from './governance'; + +// Read package.json to get version +let version = '1.0.0'; +try { + const packageJsonPath = resolve(__dirname, '../../package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + version = packageJson.version; +} catch { + // Fall back to default version if package.json not found +} + +program + .name('eventcatalog') + .description('EventCatalog Command-Line Interface') + .version(version) + .option('-d, --dir ', 'Path to the EventCatalog directory (default: current directory)', '.'); + +// List command - show all available functions +program + .command('list') + .description('List all available SDK functions') + .action(() => { + try { + const functions = listFunctions('.'); + const output = formatListOutput(functions); + console.log(output); + } catch (error) { + console.error('Error listing functions:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } + }); + +// Export command - export a resource to .ec DSL format +program + .command('export') + .description('Export a catalog resource to EventCatalog DSL (.ec) format') + .option('-a, --all', 'Export the entire catalog (all resource types)', false) + .option('-r, --resource ', 'Resource type (event, command, query, service, domain)') + .option('--id ', 'Resource ID (omit to export all resources of the given type)') + .option('-v, --version ', 'Resource version (defaults to latest)') + .option('--hydrate', 'Include referenced resources (messages, channels, owners)', false) + .option('--stdout', 'Print to stdout instead of writing a file', false) + .option('--playground', 'Open the exported DSL in EventCatalog Compass', false) + .option('-o, --output ', 'Output file path (defaults to .ec or catalog.ec)') + .action(async (opts) => { + try { + const globalOpts = program.opts() as any; + const dir = globalOpts.dir || '.'; + + if (opts.all) { + const result = await exportCatalog({ + hydrate: opts.hydrate, + stdout: opts.stdout, + playground: opts.playground, + output: opts.output, + dir, + }); + console.log(result); + return; + } + + if (!opts.resource) { + console.error('Either --all or --resource is required'); + process.exit(1); + } + + const result = await exportResource({ + resource: opts.resource, + id: opts.id, + version: opts.version, + hydrate: opts.hydrate, + stdout: opts.stdout, + playground: opts.playground, + output: opts.output, + dir, + }); + console.log(result); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + }); + +// Import command - import .ec DSL files into catalog markdown +program + .command('import [files...]') + .description('Import EventCatalog DSL (.ec) files into catalog markdown') + .option('--stdin', 'Read DSL from stdin', false) + .option('--dry-run', 'Preview resources without writing', false) + .option('--flat', 'Write resources in flat structure (no nesting under domains/services)', false) + .option('--no-init', 'Skip catalog initialization prompt') + .action(async (files: string[], opts) => { + try { + const globalOpts = program.opts() as any; + const dir = globalOpts.dir || '.'; + + const result = await importDSL({ + files: files.length > 0 ? files : undefined, + stdin: opts.stdin, + dryRun: opts.dryRun, + flat: opts.flat, + noInit: !opts.init, + dir, + }); + console.log(result); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + }); + +// Snapshot command group +const snapshot = program.command('snapshot').description('Create, diff, and list catalog snapshots'); + +snapshot + .command('create') + .description('Take a point-in-time snapshot of the catalog') + .option('--label
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    David Boyne
    David Boyne

    💻 🖋 🎨 💡 🤔 📖
    Benjamin Otto
    Benjamin Otto

    💻 🤔 📖 🐛
    Tiago Oliveira
    Tiago Oliveira

    📖 🐛
    Jay McGuinness
    Jay McGuinness

    📖
    David Khourshid
    David Khourshid

    📖
    thim81
    thim81

    🤔 🐛 💻
    Muthu
    Muthu

    🐛
    Dan Tavelli
    Dan Tavelli

    📖
    steppi91
    steppi91

    📖
    Donald Pipowitch
    Donald Pipowitch

    🐛 💻
    Ken
    Ken

    📖
    Rodolfo Toro
    Rodolfo Toro

    💻
    Drew Marsh
    Drew Marsh

    💻
    Dec Kolakowski
    Dec Kolakowski

    💻 📖
    Yevhenii Dytyniuk
    Yevhenii Dytyniuk

    💻
    lcsbltm
    lcsbltm

    💻
    Matt Martz
    Matt Martz

    💻
    Michel Grootjans
    Michel Grootjans

    💻
    Arturo Abruzzini
    Arturo Abruzzini

    💻
    Ad L'Ecluse
    Ad L'Ecluse

    💻
    Rafael Renan Pacheco
    Rafael Renan Pacheco

    💻 📖
    Luis Diego
    Luis Diego

    💻
    Daniel Ruf
    Daniel Ruf

    📖
    Fredrik Johansson
    Fredrik Johansson

    💻
    Naresh Kumar Reddy Gaddam
    Naresh Kumar Reddy Gaddam

    💻
    Andre Deutmeyer
    Andre Deutmeyer

    💻
    Pebbz
    Pebbz

    💻
    Alexander Holbreich
    Alexander Holbreich

    📖
    José Delgado
    José Delgado

    💻
    jlee-spt
    jlee-spt

    💻
    Kim Rejström
    Kim Rejström

    💻
    Christophe Gabard
    Christophe Gabard

    💻
    Carlo Bertini
    Carlo Bertini

    💻
    David Regla
    David Regla

    💻
    Marcio Vinicius
    Marcio Vinicius

    💻
    Daniel Andres Castillo Ardila
    Daniel Andres Castillo Ardila

    💻
    Baerten Dennis
    Baerten Dennis

    💻
    Ryan Cormack
    Ryan Cormack

    💻
    Nathan Birrell
    Nathan Birrell

    💻
    Jack Tomlinson
    Jack Tomlinson

    💻
    Carlos Rodrigues
    Carlos Rodrigues

    💻
    omid eidivandi
    omid eidivandi

    💻
    Simone Fumagalli
    Simone Fumagalli

    📖
    d-o-h
    d-o-h

    💻
    Cristian Pallarés
    Cristian Pallarés

    💻
    Sebastian Rendon
    Sebastian Rendon

    💻
    Craig Roberts
    Craig Roberts

    💻
    Ivan Milosavljevic
    Ivan Milosavljevic

    📖
    Martin Meredith
    Martin Meredith

    💻
    Ruud Welling
    Ruud Welling

    💻
    Kevin Pouget
    Kevin Pouget

    💻
    Vitalii Balash
    Vitalii Balash

    💻
    Arnaud Babilone
    Arnaud Babilone

    💻
    Alexander Horner
    Alexander Horner

    💻
    simonwfarrow
    simonwfarrow

    💻
    Augusto Romero Arango
    Augusto Romero Arango

    💻
    cc-stjm
    cc-stjm

    💻
    Lucian Lature
    Lucian Lature

    🐛 💻
    Vilas Chauvhan
    Vilas Chauvhan

    💻
    Eric Hoffman
    Eric Hoffman

    🐛
    wimhaesen-kine
    wimhaesen-kine

    💻
    Ondrej Musil
    Ondrej Musil

    🐛 🤔
    Anatoly Bolshakov
    Anatoly Bolshakov

    💻
    reisingerf
    reisingerf

    📖
    Jonathan Barette
    Jonathan Barette

    💻
    mumundum
    mumundum

    💻
    Piotr Rybarczyk
    Piotr Rybarczyk

    🐛
    ZakaryaCH
    ZakaryaCH

    🐛
    + + + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! + +# License + +MIT. diff --git a/packages/core/bin/eventcatalog.config.d.ts b/packages/core/bin/eventcatalog.config.d.ts new file mode 100644 index 000000000..6a6173f5c --- /dev/null +++ b/packages/core/bin/eventcatalog.config.d.ts @@ -0,0 +1 @@ +export type { Config } from '../dist/eventcatalog.config.js'; diff --git a/bin/eventcatalog.js b/packages/core/bin/eventcatalog.js similarity index 100% rename from bin/eventcatalog.js rename to packages/core/bin/eventcatalog.js diff --git a/packages/core/docs/api/01-overview.md b/packages/core/docs/api/01-overview.md new file mode 100644 index 000000000..f88ec88ea --- /dev/null +++ b/packages/core/docs/api/01-overview.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog CLI +sidebar_label: EventCatalog CLI +title: EventCatalog CLI +description: Understand the CLI of EventCatalog +--- + +EventCatalog provides a set of scripts to help you generate, serve, and deploy your catalog. + +Once your catalog is bootstrapped, the source will contain the EventCatalog scripts that you can invoke with your package manager: + +```json title="package.json" +{ + // ... + "scripts": { + // Runs the dev server locally + "dev": "eventcatalog dev", + // Builds the catalog + "build": "eventcatalog build", + + // start/preview both run the built catalog locally. + "start": "eventcatalog start", + "preview": "eventcatalog preview", + + // Export your catalog as JSON + "export": "eventcatalog export", + } +} +``` + +import TOCInline from "@theme/TOCInline" + + + +## EventCatalog CLI commands {#eventcatalog-cli-commands} + +Below is a list of EventCatalog CLI commands and their usages: + +### `eventcatalog dev` {#eventcatalog-dev-sitedir} + +Runs your catalog in development mode. + +This will start the dev server and watch for changes to your catalog. You will use this as you develop your catalog. + + +### `eventcatalog start/preview` {#eventcatalog-start-sitedir} + +Runs EventCatalog in production mode, requires a `build` first. This will serve the built catalog from the `dist` folder. + +Can be used to preview your catalog before deploying. + +### `eventcatalog build` {#eventcatalog-build-sitedir} + +Builds your catalog for production, and outputs the catalog to the `dist` folder. + +### `eventcatalog export` {#eventcatalog-export} + +Exports your entire catalog as a JSON file using the SDK's `dumpCatalog` function. The export is saved to the `exports/` directory in your project with a date stamp (e.g. `exports/catalog-2026-03-30.json`). + +**Options:** + +| Option | Description | +|--------|-------------| +| `--include-markdown` | Include markdown content in the export (default: `false`) | + +```bash +# Export catalog as JSON +eventcatalog export + +# Export with markdown content included +eventcatalog export --include-markdown +``` diff --git a/packages/core/docs/api/02-config.md b/packages/core/docs/api/02-config.md new file mode 100644 index 000000000..544e7b05e --- /dev/null +++ b/packages/core/docs/api/02-config.md @@ -0,0 +1,1136 @@ +--- +sidebar_position: 2 +keywords: +- EventCatalog api +sidebar_label: eventcatalog.config.js +title: eventcatalog.config.js +description: Understanding the configuration file for EventCatalog. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +## Overview {#overview} + +`eventcatalog.config.js` contains configurations for your site and is placed in the root directory of your site. + +## Required fields {#required-fields} + +### `cId` {#cId} + +- Type: `string` + +An automated generated ID for your catalog. EventCatalog will generate this for you. + +```js title="eventcatalog.config.js" +module.exports = { + cId: '107fdebb-7c68-42cc-975d-413b1d30d758', +}; +``` + +### `title` {#title} + +- Type: `string` + +Title for your website. + +```js title="eventcatalog.config.js" +module.exports = { + title: 'EventCatalog', +}; +``` + +### `organizationName` {#organizationName} + +- Type: `string` + +Your organization name. + +```js title="eventcatalog.config.js" +module.exports = { + organizationName: 'Your Company', +}; +``` + +## Optional fields {#optional-fields} + +### `base` {#base} + +- Type: `string` +- Default value: `/` + +The base path to deploy to. EventCatalog will use this path as the root for your pages and assets both in development and in production build. + +```js title="eventcatalog.config.js" +module.exports = { + base: '/', +}; +``` + +### `output` {#output} + + + +- Type: `string` +- Default value: `static` + +The output type for your EventCatalog, choose from `static` or `server`. + +:::info "What is the difference between static and server?" + +- `static` - The default output type for EventCatalog. This will output a static website that you can host anywhere. +- `server` - This will output a Node.js server that you can host anywhere. This is required for certain features like the [EventCatalog Chat](/features/ai-assistant) (bring your own keys). The easiest way to host this is with our [Docker image](/docs/development/deployment/hosting-options#hosting-a-server). + +::: + +```js title="eventcatalog.config.js" +module.exports = { + output: 'static', +}; +``` + +### `outDir` {#outDir} + + + +- Type: `string` +- Default value: `dist` + +The output path of your EventCatalog. By default it will output to the `dist` folder. + +```js title="eventcatalog.config.js" +module.exports = { + // Catalog would output to the /build folder + outDir: 'build', +}; +``` + +### `trailingSlash` {#trailingSlash} + +- Type: `boolean` +- Default: `false` + +Set the route matching behavior of the dev server. Choose from the following options: + +'true' - Only match URLs that include a trailing slash (ex: “/foo/“) +'false' - Match URLs regardless of whether a trailing ”/” exists + +Use this configuration option if your production host has strict handling of how trailing slashes work or do not work. + +```js title="eventcatalog.config.js" +module.exports = { + // Setting to true will add / onto all routes e.g http://website.com/visualiser/ + trailingSlash: true, +}; +``` + +### `port` {#port} + +- Type: `number` +- Default: 3000 + +Configure the port EventCatalog is running on. + +```js title="eventcatalog.config.js" +module.exports = { + // Changes the port from default 3000 to 5000 + port: 5000, +}; +``` + +### `host` {#host} + + + +- Type: `string` | `boolean` +- Default: `false` + +Set which network IP addresses the dev server should listen on (i.e. non-localhost IPs). + +- `false` - do not expose on a network IP address +- `true` - listen on all addresses, including LAN and public addresses +- `[custom-address]` - expose on a network IP address at `[custom-address]` + +```js title="eventcatalog.config.js" +module.exports = { + host: '0.0.0.0', +}; +``` + +### `server.allowedHosts` {#server.allowedHosts} + + + +- Type: `string[]` | `true` +- Default: `[]` + +A list of hostnames that Astro is allowed to respond to. When the value is set to true, any hostname is allowed. + +You can read more on the Astro documentation [here](https://docs.astro.build/en/reference/configuration-reference/#serverallowedhosts). + +```js title="eventcatalog.config.js" +module.exports = { + server: { + allowedHosts: ['example.com', 'subdomain.example.com'], + }, +}; +``` + +### `security` {#security} + + + +- Type: `Record<"checkOrigin", boolean> | undefined` +- Default: `{checkOrigin: true}` + +Enables security measures for an EventCatalog website. + +These features only exist for pages rendered on demand (SSR) using server mode or pages that opt out of prerendering in static mode. + +By default, EventCatalog will automatically check that the “origin” header matches the URL sent by each request in on-demand rendered pages. You can disable this behavior by setting checkOrigin to false: + +You can read more on the Astro documentation [here](https://docs.astro.build/en/reference/configuration-reference/#security). + +```js title="eventcatalog.config.js" +module.exports = { + output: "server", + security: { + checkOrigin: false + } +}; +``` + +### `generators` {#generators} + +- Type: `Generator[]` +- Default: `[]` + +Generators are the foundation of plugins with EventCatalog. EventCatalog will call your generators on build. + + +```js title="eventcatalog.config.js" +module.exports = { + generators: [ + [ + // This will load plugin.js in the root of your catalog + '/plugin.js', + // configuration for your generator + { + customValue: true, + test: "Add any configuration values you want" + }, + ], + ], +}; +``` + +### `environments` {#environment} + + + +- Type: `object` +- Default: `{}` + +Optional configuration for EventCatalog environments. + +When environments are set, a dropdown will be shown in the top right of the EventCatalog allowing your users to switch between environments. + + +```js title="eventcatalog.config.js" +module.exports = { + environments: [ + { + // Name of the environment + name: 'Development', + // URL of the environment + url: 'https://demo.eventcatalog.dev', + // Description of the environment + description: 'Local development environment', + // Short name of the environment (optional, used in the dropdown) + shortName: 'Dev' + }, + { + name: 'Test', + url: 'https://demo.eventcatalog.dev', + description: 'Test environment for QA', + shortName: 'Test' + }, + { + name: 'Production', + url: 'https://demo.eventcatalog.dev', + description: 'Production environment', + shortName: 'Prod' + }, + ] +}; +``` + +### `landingPage` {#landingPage} + +- Type: `string` +- Default: `'/docs'` + +Configure the landing page URL your EventCatalog loads. By default EventCatalog loads `/` (the default or [custom landing page](/docs/development/customization/customize-landing-page#how-to-customize-your-landing-page)). + +Clicking on the EventCatalog logo (or [your custom logo](/docs/api/config#logo)), will also go to this URL. + +If you set this value the `Home` icon in the Application sidebar will not be shown and your users will be redirected to this default URL. + +You can set this to any EventCatalog page URL. + +Examples: + +- `/visualiser` +- `/discover/events` +- `/docs/services/InventoryService/0.0.2` + +```js title="eventcatalog.config.js" +module.exports = { + landingPage: '/visualiser', +}; +``` + +### `navigation` {#navigation} + + + +- Type: `object` + +Configure navigation in EventCatalog. + +Use `navigation.groups` to configure the [Application sidebar](/docs/development/customization/application-sidebar). + +Use `navigation.pages` to configure the [Documentation sidebar](/docs/development/customization/documentation-sidebar) shown on `/docs/*` pages. + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + groups: [ + { + id: 'main', + items: [{ id: 'home' }, { id: 'docs' }], + }, + { + id: 'browse', + label: 'Browse', + items: [ + { id: 'catalog' }, + { id: 'schemas' }, + { id: 'schema-insights', visible: false }, + ], + }, + { + id: 'organization', + label: 'Organization', + items: [{ id: 'teams' }, { id: 'users' }], + }, + { + id: 'settings', + position: 'bottom', + items: [{ id: 'settings' }], + }, + ], + pages: ['list:top-level-domains', 'list:all'], + }, +}; +``` + +#### `navigation.groups` + +`navigation.groups` configures the Application sidebar that appears across EventCatalog. + +If you do not configure `navigation.groups`, EventCatalog renders the default Application sidebar. + +If you do configure `navigation.groups`, your groups replace the default Application sidebar. Add every item you want users to see. + +Built-in Application sidebar item IDs include: + +- `home` +- `docs` +- `catalog` +- `schemas` +- `schema-insights` +- `teams` +- `users` +- `settings` + +You can also reference built-in route IDs directly, such as `/schemas/explorer`, `/directory/teams`, or `/settings/general`. + +Groups support: + +| Property | Type | Description | +| --- | --- | --- | +| `id` | `string` | Unique group identifier. | +| `label` | `string` | Optional label shown above the group. | +| `visible` | `boolean` | Set to `false` to hide the group. | +| `position` | `'top' \| 'bottom'` | Use `bottom` for groups pinned below the main navigation. Defaults to `top`. | +| `items` | `array` | Navigation items in this group. | + +Items support: + +| Property | Type | Description | +| --- | --- | --- | +| `id` | `string` | Built-in item id, route id, or custom item id. | +| `label` | `string` | Custom label. Required for custom items. | +| `icon` | `string` | Any icon exported by [lucide-react](https://lucide.dev/icons/), such as `House`, `BookOpen`, or `LifeBuoy`. | +| `href` | `string` | Required for custom items. Built-in items provide their own `href`. | +| `visible` | `boolean` | Set to `false` to hide the item. | +| `match` | `string \| string[]` | Paths that should mark the item as active. Useful for custom navigation items. | + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + groups: [ + { + id: 'platform', + label: 'Platform', + items: [ + { + id: 'platform-docs', + label: 'Platform Docs', + icon: 'BookOpen', + href: '/docs/custom/platform/overview', + match: ['/docs/custom/platform'], + }, + { + id: 'support', + label: 'Support', + icon: 'LifeBuoy', + href: 'https://support.example.com', + }, + ], + }, + ], + }, +}; +``` + +External `href` values open in a new tab. + +#### `navigation.pages` + +`navigation.pages` configures the context-aware Documentation sidebar shown on `/docs/*` pages. + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + pages: ['list:top-level-domains', 'list:all'], + }, +}; +``` + +See [Documentation sidebar](/docs/development/customization/documentation-sidebar) for all supported list, resource, group, and custom link options. + +:::note Migrating from `sidebar` + +In v3 you could show or hide Application sidebar items using the top-level `sidebar` property. + +In v4 this has been replaced by `navigation.groups`. + +The `SideBarConfig` type is no longer exported. + +::: + +### `visualiser` {#visualiser} + + + +- Type: `object` + +Configuration for the [EventCatalog visualiser](/features/visualization). + +```js title="eventcatalog.config.js" +module.exports = { + visualiser: { + // visualizer is enabled by default + // you can turn it off by setting it to false + enabled: true, + + channels: { + // The render mode for channels in the visualiser + // Flat or single + renderMode: 'flat' + } + } +}; +``` + + +| Configuration | Option | Default | Description | +| ------------- | ----------- | ----------- | ----------- | +| `visualiser.enabled` | `true` or `false` | `true` | **Enabled or disables the visualiser**. Setting this to false will not render any visualiser pages in your catalog and also remove references to the visualiser features in your catalog. _(Added in 2.65.1)_ | +| `visualiser.channels.renderMode` | `flat` or `single` | `flat` | The render mode for the visualiser. `flat` means the channel node is duplicated for each message. `single` means the channel node is a single node for all messages. Depending on your use case/preferences you may want to use one or the other. | + + + + + + +### `docs` {#docs} + +- Type: `object` + +Configure documentation rendering options in EventCatalog. + +Use `docs.sidebar` for the Documentation sidebar render mode and orphaned message behavior. + +Use [`navigation.pages`](#navigationpages) when you want to choose which resources, groups, and custom links appear in the [Documentation sidebar](/docs/development/customization/documentation-sidebar). + +```js title="eventcatalog.config.js" +module.exports = { + docs: { + sidebar: { + type: 'LIST_VIEW' + } + } +}; +``` + +Configuration for the Documentation sidebar render mode. + +### `docs.sidebar` options + +#### Sidebar Type + +You can choose between `LIST_VIEW` or `TREE_VIEW` to render your documentation. + +- `LIST_VIEW` will render your docs as you see in the [demo](https://demo.eventcatalog.dev/docs/) +- `TREE_VIEW` will render the DOCS as a tree view and map your file system folder structure. + - Can be useful for large catalogs and navigation + + + + +```js title="eventcatalog.config.js" +docs: { + sidebar: { + type: 'LIST_VIEW' + }, + }, +``` + +### Show/Hide Orphaned Messages + + + +Any messages that do not belong to a service will be shown as orphaned messages in the sidebar (LIST_VIEW only). + +If you wish to hide orphaned messages you can set the `showOrphanedMessages` to `false`. + +```js title="eventcatalog.config.js" +docs: { + sidebar: { + type: 'LIST_VIEW', + // Default is true + showOrphanedMessages: false, + }, + }, +``` + +### `chat` {#chat} + +- Type: `object` + +Configuration for the EventCatalog AI chat feature. + +:::info +The `chat` property requires a Starter or Scale plan and `output: 'server'` to take effect. +::: + +```js title="eventcatalog.config.js" +module.exports = { + output: 'server', + chat: { + // Set to false to disable the AI chat feature entirely + enabled: false, + }, +}; +``` + +| Option | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| `chat.enabled` | `boolean` | `true` | Enables or disables the AI chat feature. Set to `false` to hide the chat UI and prevent chat requests even when all other prerequisites are met. | + +### `search` {#search} + + + +- Type: `object` + +Configure the search mode for your catalog. + +```js title="eventcatalog.config.js" +module.exports = { + search: { + type: 'resource', // default + }, +}; +``` + +| Option | Type | Default | Description | +| ------ | ---- | ------- | ----------- | +| `search.type` | `'resource'` \| `'indexed'` | `'resource'` | `'resource'` searches resource metadata only. `'indexed'` enables full-content search powered by [Pagefind](https://pagefind.app/) and builds a static index at build time. | + +See the [Search customization](/docs/development/customization/search) guide for details and trade-offs. + +### `changelog` {#changelog} + +- Type: `object` + +Configuration for EventCatalog Changelog. + +**Changelogs are disabled by default.** + +You can enable changelogs by setting `enabled` to `true`. + +```js title="eventcatalog.config.js" +module.exports = { + changelog: { + // Default is false + enabled: true, + }, +}; +``` + +### `editUrl` {#editUrl} + +- Type: `string` + +URL used when people want to edit the documentation. For example your GitHub repo and branch. + +```js title="eventcatalog.config.js" +module.exports = { + editUrl: 'https://github.com/boyney123/eventcatalog-demo/edit/master', +}; +``` + +### `repositoryUrl` {#repositoryUrl} + + + +- Type: `string` + +:::tip +For Stater or Scale plans. This gives you the ability to show your own GitHub repository in EventCatalog (in the header bar). +::: + +URL to your repository for EventCatalog. + +```js title="eventcatalog.config.js" +module.exports = { + repositoryUrl: 'https://github.com/boyney123/eventcatalog-demo', +}; +``` + +### `logo` {#logo} + +- Type: `object` + +Logo, alt and text for your company logo. + +:::tip Public directory +Add your logo to your `/public` directory. +::: + +```js title="eventcatalog.config.js" +module.exports = { + logo: { + // This logo is located at public/logo.svg + src: '/logo.svg', + alt: 'Company logo', + text: 'Urban Slice | EventCatalog', + }, +}; +``` + +**Example output** + +![./logo.png](./logo.png) + +### `homepageLink` {#homepageLink} + +- Type: `string` + +URL used when people want to link the logo & title in the top navigation to the homepage of a website. + +```js title="eventcatalog.config.js" +module.exports = { + homepageLink: 'https://eventcatalog.dev', +}; +``` + +### `mdxOptimize` {#mdxOptimize} + +- Type: `string` + +This is an optional configuration setting to optimize the MDX output for faster builds. This may be useful if you have many catalog files and notice slow builds. However, this option may generate some unescaped HTML, so make sure your catalog interactive parts still work correctly after enabling it. + +This is disabled by default. + +Read [Astro documentation on optimize for MDX](https://docs.astro.build/en/guides/integrations-guide/mdx/#optimize) for more information. + +```js title="eventcatalog.config.js" +module.exports = { + mdxOptimize: true +}; +``` + +### `compress` {#compress} + + + +- Type: `boolean` + +Setting this to true will automatically compress all your CSS, HTML, SVG, JavaScript, JSON and image files in the Astro outDir folder. + +This is disabled by default from EventCatalog v2.61.9. + +**This only works for static builds.** + +```js title="eventcatalog.config.js" +module.exports = { + compress: true +}; +``` + +### `asyncAPI.renderParsedSchemas` {#asyncAPI.renderParsedSchemas} + + + +- Type: `boolean` + +EventCatalog will render your AsyncAPI files into their own pages. By default EventCatalog will read your AsyncAPI files and parse your schemas to render them on the screen. Part of this process is validating your schemas and also adding metadata onto them (default). + +If you want to keep your schemas as they are then you can set the `asyncAPI.renderParsedSchemas` to false. + +:::tip Having issues rendering AsyncAPI files? +If you are having issues seeing or rendering your AsyncAPI file try setting the renderParsedSchemas to `false` +::: + +```js title="eventcatalog.config.js" +module.exports = { + asyncAPI: { + renderParsedSchemas: false // default is true + } +}; +``` + +### `mermaid` {#mermaid} + + + +- Type: `object` + +EventCatalog uses [mermaid](https://mermaid.js.org/) to render diagrams. + +Using mermaid you can render icons in your diagrams (e.g [AWS architecture icons](/docs/development/components/diagram-syntax/mermaid#architecture-diagrams-with-icons)). + +Example output of mermaid + +```js title="eventcatalog.config.js" +module.exports = { + mermaid: { + iconPacks: ['logos'], // will load https://icones.js.org/collection/logos into eventcatalog + maxTextSize: 100000 // maximum text size for Mermaid diagrams (default: 100000) + } +}; +``` + +You can choose from over **200,000 icons** from [icones.js.org](https://icones.js.org/collection/logos). + +#### `mermaid.iconPacks` + + + +- Type: `string[]` +- Default: `[]` + +Icon packs to load from [icones.js.org](https://icones.js.org/). Use icon pack names like `['logos', 'mdi']` to load AWS, Azure, and other service icons. + +[Learn more about using icons in mermaid diagrams](/docs/development/components/diagram-syntax/mermaid#architecture-diagrams-with-icons). + +#### `mermaid.maxTextSize` + + + +- Type: `number` +- Default: `100000` + +Maximum text size for Mermaid diagrams in characters. Increase this value if you have large diagrams that fail to render. + +```js title="eventcatalog.config.js" +module.exports = { + mermaid: { + maxTextSize: 200000 // Allow larger diagrams + } +}; +``` + +### `rss` {#rss} + + + +- Type: `object` +- Default: `{ enabled: false, limit: 15 }` + +Configure RSS feeds for catalog resources. RSS feeds are disabled by default. + +When enabled, EventCatalog creates feeds for messages, services, domains, flows, and an `all` feed that combines the latest changes across supported resource types. + +```js title="eventcatalog.config.js" +module.exports = { + rss: { + enabled: true, + limit: 20, + }, +}; +``` + +#### `rss.enabled` + +- Type: `boolean` +- Default: `false` + +Turns RSS feeds on or off. + +#### `rss.limit` + +- Type: `number` +- Default: `15` + +Controls the maximum number of items returned in each feed. + +Items are ordered by the last updated date of the resource file, with the most recently changed items first. + +#### Feed URLs + +After RSS is enabled, EventCatalog serves feeds at these paths: + +| Feed | Path | Demo | +| --- | --- | --- | +| Events | `/rss/events/rss.xml` | [View demo](https://demo.eventcatalog.dev/rss/events/rss.xml) | +| Commands | `/rss/commands/rss.xml` | [View demo](https://demo.eventcatalog.dev/rss/commands/rss.xml) | +| Queries | `/rss/queries/rss.xml` | [View demo](https://demo.eventcatalog.dev/rss/queries/rss.xml) | +| Services | `/rss/services/rss.xml` | [View demo](https://demo.eventcatalog.dev/rss/services/rss.xml) | +| Domains | `/rss/domains/rss.xml` | [View demo](https://demo.eventcatalog.dev/rss/domains/rss.xml) | +| Flows | `/rss/flows/rss.xml` | [View demo](https://demo.eventcatalog.dev/rss/flows/rss.xml) | +| Everything | `/rss/all/rss.xml` | [View demo](https://demo.eventcatalog.dev/rss/all/rss.xml) | + +### `llms.txt` {#llms.txt} + + + +Enable tools like Claude, ChatGPT, GitHub Copilot, and Cursor to quickly understand your EventCatalog. When enabled, your catalog is accessible at `/llms.txt` and `/llms-full.txt`. + +```js title="eventcatalog.config.js" +{ + llmsTxt: { + enabled: true, + } +} +``` + +See the [LLMs documentation](/docs/development/ask-your-architecture/llms.txt) for more information, how you can use it and examples. + + +### `fullCatalogAPIEnabled` {#fullCatalogAPIEnabled} + + + +- Type: `boolean` +- Default: `true` + +Enable or disable the full catalog API which allows you to get the full catalog dump in the `/api/catalog` endpoint. + +```js title="eventcatalog.config.js" +module.exports = { + api: { + fullCatalogAPIEnabled: false, // default is true + } +}; +``` +### `domains` {#domains} + + + +- Type: `object` + +Configuration for the domains table. + +```js title="eventcatalog.config.js" +module.exports = { + domains: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + services: { visible: true, label: 'Services' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +See the [Customize tables](/docs/development/customization/customize-tables) documentation for more information and examples. + +### `systems` {#systems} + + + +- Type: `object` + +Configuration for the systems table. + +```js title="eventcatalog.config.js" +module.exports = { + systems: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'System' }, + summary: { visible: true, label: 'Summary' }, + services: { visible: true, label: 'Services' }, + flows: { visible: true, label: 'Flows' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +See the [Customize tables](/docs/development/customization/customize-tables) documentation for more information and examples. + +### `events` {#events} + + + +- Type: `object` + +Configuration for the events table. + +```js title="eventcatalog.config.js" +module.exports = { + events: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + producers: { visible: true, label: 'Producers' }, + consumers: { visible: true, label: 'Consumers' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +See the [Customize tables](/docs/development/customization/customize-tables) documentation for more information and examples. + +### `queries` {#queries} + + + +- Type: `object` + +Configuration for the queries table. + +```js title="eventcatalog.config.js" +module.exports = { + queries: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + producers: { visible: true, label: 'Producers' }, + consumers: { visible: true, label: 'Consumers' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +### `commands` {#commands} + + + +- Type: `object` + +Configuration for the commands table. + +```js title="eventcatalog.config.js" +module.exports = { + commands: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + producers: { visible: true, label: 'Producers' }, + consumers: { visible: true, label: 'Consumers' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +See the [Customize tables](/docs/development/customization/customize-tables) documentation for more information and examples. + +### `services` {#services} + + + +- Type: `object` + +Configuration for the services table. + +```js title="eventcatalog.config.js" +module.exports = { + services: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + sends: { visible: true, label: 'Sends' }, + receives: { visible: true, label: 'Receives' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +See the [Customize tables](/docs/development/customization/customize-tables) documentation for more information and examples. + +### `containers` {#containers} + + + +- Type: `object` + +Configuration for the containers table. + +```js title="eventcatalog.config.js" +module.exports = { + containers: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + writes: { visible: true, label: 'Writes' }, + reads: { visible: true, label: 'Reads' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +See the [Customize tables](/docs/development/customization/customize-tables) documentation for more information and examples. + +### `flows` {#flows} + + + +- Type: `object` + +Configuration for the flows table. + +```js title="eventcatalog.config.js" +module.exports = { + flows: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + version: { visible: true, label: 'Version' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +See the [Customize tables](/docs/development/customization/customize-tables) documentation for more information and examples. + +### `users` {#users} + + + +- Type: `object` + +Configuration for the users table. + +```js title="eventcatalog.config.js" +module.exports = { + users: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + email: { visible: true, label: 'Email' }, + slackDirectMessageUrl: { visible: true, label: 'Slack URL' }, + summary: { visible: true, label: 'Summary' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +See the [Customize tables](/docs/development/customization/customize-tables) documentation for more information and examples. + +### `scalarConfiguration` {#scalarConfiguration} + +- Type: `object` + +Pass custom configuration directly to the [Scalar](https://scalar.com/) OpenAPI reference component. Any properties set here are spread into Scalar's configuration, allowing you to override or extend the defaults EventCatalog applies. + +This is useful when you need Scalar-specific behaviour such as routing API requests through a proxy, changing the theme, or disabling built-in UI controls. + +```js title="eventcatalog.config.js" +module.exports = { + scalarConfiguration: { + // Route OpenAPI requests through a proxy + proxy: 'https://proxy.example.com', + }, +}; +``` + +Refer to the [Scalar configuration reference](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) for the full list of supported options. + +:::info Note on defaults +EventCatalog sets several Scalar options by default (such as `theme`, `showSidebar`, and `hideDarkModeToggle`). Any matching keys in `scalarConfiguration` will override these defaults. +::: + +### `teams` {#teams} + + + +- Type: `object` + +Configuration for the teams table. + +```js title="eventcatalog.config.js" +module.exports = { + teams: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + } + } +}; +``` + +See the [Customize tables](/docs/development/customization/customize-tables) documentation for more information and examples. diff --git a/packages/core/docs/api/08-code-blocks.md b/packages/core/docs/api/08-code-blocks.md new file mode 100644 index 000000000..85d292005 --- /dev/null +++ b/packages/core/docs/api/08-code-blocks.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 6 +sidebar_label: Code blocks +title: Code blocks +description: Understanding what is possible with EventCatalog code blocks +--- + +EventCatalog is powered by markdown files, this allows you to add custom code blocks. + +EventCatalog is using [expressive-code](https://expressive-code.com/) which provides a range of additional features for your code blocks. + +**Features include:** + +- [Editor & Terminal Frames](https://expressive-code.com/key-features/frames/) +- [Text & Line Markers](https://expressive-code.com/key-features/text-markers/) + - [Adding labels to line markers](https://expressive-code.com/key-features/text-markers/#adding-labels-to-line-markers) - Great way to give context to your example code. + - [Using diff-like syntax](https://expressive-code.com/key-features/text-markers/#using-diff-like-syntax) - Add diffs to your examples (e.g Schemas) + - [Marking individual text inside lines](https://expressive-code.com/key-features/text-markers/#marking-individual-text-inside-lines) +- [Word wrapping](https://expressive-code.com/key-features/word-wrap/) +- [And more...](https://expressive-code.com/) + +## Examples of code blocks + +### Diff code blocks +Highlight diffs with your code, could be useful for your Schemas or any changes you want to show to your teams. + +![Example](/img/code-blocks/diff.png) +![Example](/img/code-blocks/diff2.png) + +See https://expressive-code.com/key-features/text-markers/#using-diff-like-syntax + +### Word highlighting +If you want to highlight words in your code blocks you can use this feature. + +![Example](/img/code-blocks/word-highlight.png) +![Example](/img/code-blocks/word-highlight2.png) + +See https://expressive-code.com/key-features/text-markers/#marking-individual-text-inside-lines + +### Labels to line markers +Great way to give context to changes in your code. For example if your Schema has changed, then maybe use this to give context with additional labels. + +![Example](/img/code-blocks/context-diff.png) + +See https://expressive-code.com/key-features/text-markers/#adding-labels-to-line-markers + +### Frames + +Nice UI for your code blocks. + +![Example](/img/code-blocks/title-blocks.png) + +See https://expressive-code.com/key-features/frames/ \ No newline at end of file diff --git a/packages/core/docs/api/_category_.json b/packages/core/docs/api/_category_.json new file mode 100644 index 000000000..a1e8dfb65 --- /dev/null +++ b/packages/core/docs/api/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Development", + "position": 2, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "api", + "title": "EventCatalog API Documentation", + "description": "EventCatalog API Documentation" + } +} \ No newline at end of file diff --git a/packages/core/docs/cli/channels.md b/packages/core/docs/cli/channels.md new file mode 100644 index 000000000..3b7381fa0 --- /dev/null +++ b/packages/core/docs/cli/channels.md @@ -0,0 +1,180 @@ +--- +id: cli-channels +title: Channels +sidebar_label: Channels +sidebar_position: 7 +--- + +# Channels CLI Commands + +Manage channels in your EventCatalog from the command line. + +## getChannel + +Returns a channel from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the channel to retrieve | +| version | string | No | Specific version to retrieve | + +**Examples:** + +```bash +# Get the latest channel +npx @eventcatalog/cli getChannel "orders.events" + +# Get a specific version +npx @eventcatalog/cli getChannel "orders.events" "1.0.0" +``` + +--- + +## getChannels + +Returns all channels from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {latestOnly?} | + +**Examples:** + +```bash +# Get all channels +npx @eventcatalog/cli getChannels +``` + +--- + +## writeChannel + +Writes a channel to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| channel | json | Yes | Channel object with id, name, version, and markdown | +| options | json | No | Options: {path?, override?} | + + + +--- + +## rmChannel + +Removes a channel by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the channel | + +**Examples:** + +```bash +# Remove a channel +npx @eventcatalog/cli rmChannel "/orders.events" +``` + +--- + +## rmChannelById + +Removes a channel by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the channel to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove a channel +npx @eventcatalog/cli rmChannelById "orders.events" +``` + +--- + +## versionChannel + +Moves the current channel to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the channel to version | + +**Examples:** + +```bash +# Version a channel +npx @eventcatalog/cli versionChannel "orders.events" +``` + +--- + +## addEventToChannel + +Adds an event to a channel + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| channelId | string | Yes | The ID of the channel | +| event | json | Yes | Event reference: {id, version, parameters?} | + + + +--- + +## addCommandToChannel + +Adds a command to a channel + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| channelId | string | Yes | The ID of the channel | +| command | json | Yes | Command reference: {id, version, parameters?} | + + + +--- + +## addQueryToChannel + +Adds a query to a channel + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| channelId | string | Yes | The ID of the channel | +| query | json | Yes | Query reference: {id, version, parameters?} | + + + +--- + +## channelHasVersion + +Checks if a specific version of a channel exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the channel | +| version | string | Yes | Version to check | + +**Examples:** + +```bash +# Check if version exists +npx @eventcatalog/cli channelHasVersion "orders.events" "1.0.0" +``` + +--- diff --git a/packages/core/docs/cli/commands.md b/packages/core/docs/cli/commands.md new file mode 100644 index 000000000..6c822fc75 --- /dev/null +++ b/packages/core/docs/cli/commands.md @@ -0,0 +1,183 @@ +--- +id: cli-commands +title: Commands +sidebar_label: Commands +sidebar_position: 3 +--- + +# Commands CLI Commands + +Manage commands in your EventCatalog from the command line. + +## getCommand + +Returns a command from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the command to retrieve | +| version | string | No | Specific version to retrieve (supports semver) | + +**Examples:** + +```bash +# Get the latest command +npx @eventcatalog/cli getCommand "CreateOrder" + +# Get a specific version +npx @eventcatalog/cli getCommand "CreateOrder" "1.0.0" +``` + +--- + +## getCommands + +Returns all commands from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {latestOnly?, attachSchema?} | + +**Examples:** + +```bash +# Get all commands +npx @eventcatalog/cli getCommands +``` + +--- + +## writeCommand + +Writes a command to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| command | json | Yes | Command object with id, name, version, and markdown | +| options | json | No | Options: {path?, override?, versionExistingContent?} | + + + +--- + +## writeCommandToService + +Writes a command to a specific service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| command | json | Yes | Command object | +| service | json | Yes | Service reference: {id, version?} | +| options | json | No | Options: {path?, format?, override?} | + + + +--- + +## rmCommand + +Removes a command by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the command | + +**Examples:** + +```bash +# Remove a command +npx @eventcatalog/cli rmCommand "/CreateOrder" +``` + +--- + +## rmCommandById + +Removes a command by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the command to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove a command +npx @eventcatalog/cli rmCommandById "CreateOrder" +``` + +--- + +## versionCommand + +Moves the current command to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the command to version | + +**Examples:** + +```bash +# Version a command +npx @eventcatalog/cli versionCommand "CreateOrder" +``` + +--- + +## addFileToCommand + +Adds a file to a command + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the command | +| file | json | Yes | File object: {content, fileName} | +| version | string | No | Specific version | + + + +--- + +## addSchemaToCommand + +Adds a schema to a command + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the command | +| schema | json | Yes | Schema object: {schema, fileName} | +| version | string | No | Specific version | + + + +--- + +## commandHasVersion + +Checks if a specific version of a command exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the command | +| version | string | Yes | Version to check | + +**Examples:** + +```bash +# Check if version exists +npx @eventcatalog/cli commandHasVersion "CreateOrder" "1.0.0" +``` + +--- diff --git a/packages/core/docs/cli/custom-docs.md b/packages/core/docs/cli/custom-docs.md new file mode 100644 index 000000000..c8913c728 --- /dev/null +++ b/packages/core/docs/cli/custom-docs.md @@ -0,0 +1,78 @@ +--- +id: cli-custom-docs +title: Custom Docs +sidebar_label: Custom Docs +sidebar_position: 10 +--- + +# Custom Docs CLI Commands + +Manage custom docs in your EventCatalog from the command line. + +## getCustomDoc + +Returns a custom doc from EventCatalog by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the custom doc | + +**Examples:** + +```bash +# Get a custom doc +npx @eventcatalog/cli getCustomDoc "/getting-started" +``` + +--- + +## getCustomDocs + +Returns all custom docs from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {path?} | + +**Examples:** + +```bash +# Get all custom docs +npx @eventcatalog/cli getCustomDocs +``` + +--- + +## writeCustomDoc + +Writes a custom doc to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| customDoc | json | Yes | Custom doc object with id, title, and markdown | +| options | json | No | Options: {path?, override?} | + + + +--- + +## rmCustomDoc + +Removes a custom doc by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the custom doc to remove | + +**Examples:** + +```bash +# Remove a custom doc +npx @eventcatalog/cli rmCustomDoc "/getting-started" +``` + +--- diff --git a/packages/core/docs/cli/data-products.md b/packages/core/docs/cli/data-products.md new file mode 100644 index 000000000..2c61f57a4 --- /dev/null +++ b/packages/core/docs/cli/data-products.md @@ -0,0 +1,177 @@ +--- +id: cli-data-products +title: Data Products +sidebar_label: Data Products +sidebar_position: 13 +--- + +# Data Products CLI Commands + +Manage data products in your EventCatalog from the command line. + +## getDataProduct + +Returns a data product from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data product to retrieve | +| version | string | No | Specific version to retrieve | + +**Examples:** + +```bash +# Get the latest data product +npx @eventcatalog/cli getDataProduct "customer-360" + +# Get a specific version +npx @eventcatalog/cli getDataProduct "customer-360" "1.0.0" +``` + +--- + +## getDataProducts + +Returns all data products from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {latestOnly?} | + +**Examples:** + +```bash +# Get all data products +npx @eventcatalog/cli getDataProducts +``` + +--- + +## writeDataProduct + +Writes a data product to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {path?, override?, versionExistingContent?} | + + + +--- + +## writeDataProductToDomain + +Writes a data product to a specific domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| dataProduct | json | Yes | Data product object | +| domain | json | Yes | Domain reference: {id, version?} | +| options | json | No | Options | + + + +--- + +## rmDataProduct + +Removes a data product by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the data product | + +**Examples:** + +```bash +# Remove a data product +npx @eventcatalog/cli rmDataProduct "/customer-360" +``` + +--- + +## rmDataProductById + +Removes a data product by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data product to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove a data product +npx @eventcatalog/cli rmDataProductById "customer-360" +``` + +--- + +## versionDataProduct + +Moves the current data product to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data product to version | + +**Examples:** + +```bash +# Version a data product +npx @eventcatalog/cli versionDataProduct "customer-360" +``` + +--- + +## addFileToDataProduct + +Adds a file to a data product + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data product | +| file | json | Yes | File object: {content, fileName} | +| version | string | No | Specific version | + + + +--- + +## addDataProductToDomain + +Adds a data product reference to a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domainId | string | Yes | The ID of the domain | +| dataProduct | json | Yes | Data product reference: {id, version} | +| domainVersion | string | No | Specific domain version | + + + +--- + +## dataProductHasVersion + +Checks if a specific version of a data product exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data product | +| version | string | Yes | Version to check | + + + +--- diff --git a/packages/core/docs/cli/data-stores.md b/packages/core/docs/cli/data-stores.md new file mode 100644 index 000000000..591851122 --- /dev/null +++ b/packages/core/docs/cli/data-stores.md @@ -0,0 +1,166 @@ +--- +id: cli-data-stores +title: Data Stores +sidebar_label: Data Stores +sidebar_position: 12 +--- + +# Data Stores CLI Commands + +Manage data stores in your EventCatalog from the command line. + +## getDataStore + +Returns a data store from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data store to retrieve | +| version | string | No | Specific version to retrieve | + +**Examples:** + +```bash +# Get the latest data store +npx @eventcatalog/cli getDataStore "orders-db" + +# Get a specific version +npx @eventcatalog/cli getDataStore "orders-db" "1.0.0" +``` + +--- + +## getDataStores + +Returns all data stores from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {latestOnly?} | + +**Examples:** + +```bash +# Get all data stores +npx @eventcatalog/cli getDataStores +``` + +--- + +## writeDataStore + +Writes a data store to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {path?, override?, versionExistingContent?} | + + + +--- + +## writeDataStoreToService + +Writes a data store to a specific service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| dataStore | json | Yes | Data store object | +| service | json | Yes | Service reference: {id, version?} | + + + +--- + +## rmDataStore + +Removes a data store by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the data store | + +**Examples:** + +```bash +# Remove a data store +npx @eventcatalog/cli rmDataStore "/orders-db" +``` + +--- + +## rmDataStoreById + +Removes a data store by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data store to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove a data store +npx @eventcatalog/cli rmDataStoreById "orders-db" +``` + +--- + +## versionDataStore + +Moves the current data store to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data store to version | + +**Examples:** + +```bash +# Version a data store +npx @eventcatalog/cli versionDataStore "orders-db" +``` + +--- + +## addFileToDataStore + +Adds a file to a data store + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data store | +| file | json | Yes | File object: {content, fileName} | +| version | string | No | Specific version | + + + +--- + +## dataStoreHasVersion + +Checks if a specific version of a data store exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the data store | +| version | string | Yes | Version to check | + +**Examples:** + +```bash +# Check if version exists +npx @eventcatalog/cli dataStoreHasVersion "orders-db" "1.0.0" +``` + +--- diff --git a/packages/core/docs/cli/diagrams.md b/packages/core/docs/cli/diagrams.md new file mode 100644 index 000000000..e8f4687d3 --- /dev/null +++ b/packages/core/docs/cli/diagrams.md @@ -0,0 +1,147 @@ +--- +id: cli-diagrams +title: Diagrams +sidebar_label: Diagrams +sidebar_position: 14 +--- + +# Diagrams CLI Commands + +Manage diagrams in your EventCatalog from the command line. + +## getDiagram + +Returns a diagram from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the diagram to retrieve | +| version | string | No | Specific version to retrieve | + +**Examples:** + +```bash +# Get the latest diagram +npx @eventcatalog/cli getDiagram "ArchitectureDiagram" + +# Get a specific version +npx @eventcatalog/cli getDiagram "ArchitectureDiagram" "1.0.0" +``` + +--- + +## getDiagrams + +Returns all diagrams from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {latestOnly?} | + +**Examples:** + +```bash +# Get all diagrams +npx @eventcatalog/cli getDiagrams +``` + +--- + +## writeDiagram + +Writes a diagram to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {path?, override?, versionExistingContent?} | + + + +--- + +## rmDiagram + +Removes a diagram by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the diagram | + +**Examples:** + +```bash +# Remove a diagram +npx @eventcatalog/cli rmDiagram "/ArchitectureDiagram" +``` + +--- + +## rmDiagramById + +Removes a diagram by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the diagram to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove a diagram +npx @eventcatalog/cli rmDiagramById "ArchitectureDiagram" +``` + +--- + +## versionDiagram + +Moves the current diagram to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the diagram to version | + +**Examples:** + +```bash +# Version a diagram +npx @eventcatalog/cli versionDiagram "ArchitectureDiagram" +``` + +--- + +## addFileToDiagram + +Adds a file to a diagram + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the diagram | +| file | json | Yes | File object: {content, fileName} | +| version | string | No | Specific version | + + + +--- + +## diagramHasVersion + +Checks if a specific version of a diagram exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the diagram | +| version | string | Yes | Version to check | + + + +--- diff --git a/packages/core/docs/cli/domains.md b/packages/core/docs/cli/domains.md new file mode 100644 index 000000000..f4592217b --- /dev/null +++ b/packages/core/docs/cli/domains.md @@ -0,0 +1,280 @@ +--- +id: cli-domains +title: Domains +sidebar_label: Domains +sidebar_position: 6 +--- + +# Domains CLI Commands + +Manage domains in your EventCatalog from the command line. + +## getDomain + +Returns a domain from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the domain to retrieve | +| version | string | No | Specific version to retrieve | + +**Examples:** + +```bash +# Get the latest domain +npx @eventcatalog/cli getDomain "Orders" + +# Get a specific version +npx @eventcatalog/cli getDomain "Orders" "1.0.0" +``` + +--- + +## getDomains + +Returns all domains from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {latestOnly?} | + +**Examples:** + +```bash +# Get all domains +npx @eventcatalog/cli getDomains +``` + +--- + +## writeDomain + +Writes a domain to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domain | json | Yes | Domain object with id, name, version, and markdown | +| options | json | No | Options: {path?, override?, versionExistingContent?} | + + + +--- + +## rmDomain + +Removes a domain by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the domain | + +**Examples:** + +```bash +# Remove a domain +npx @eventcatalog/cli rmDomain "/Orders" +``` + +--- + +## rmDomainById + +Removes a domain by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the domain to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove a domain +npx @eventcatalog/cli rmDomainById "Orders" +``` + +--- + +## versionDomain + +Moves the current domain to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the domain to version | + +**Examples:** + +```bash +# Version a domain +npx @eventcatalog/cli versionDomain "Orders" +``` + +--- + +## addFileToDomain + +Adds a file to a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the domain | +| file | json | Yes | File object: {content, fileName} | +| version | string | No | Specific version | + + + +--- + +## addServiceToDomain + +Adds a service to a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domainId | string | Yes | The ID of the domain | +| service | json | Yes | Service reference: {id, version} | +| domainVersion | string | No | Specific domain version | + + + +--- + +## addSubDomainToDomain + +Adds a subdomain to a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domainId | string | Yes | The ID of the parent domain | +| subDomain | json | Yes | Subdomain reference: {id, version} | +| domainVersion | string | No | Specific domain version | + + + +--- + +## addEntityToDomain + +Adds an entity to a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domainId | string | Yes | The ID of the domain | +| entity | json | Yes | Entity reference: {id, version} | +| domainVersion | string | No | Specific domain version | + + + +--- + +## addEventToDomain + +Adds an event relationship to a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domainId | string | Yes | The ID of the domain | +| direction | string | Yes | Direction: "sends" or "receives" | +| event | json | Yes | Event reference: {id, version} | +| domainVersion | string | No | Specific domain version | + + + +--- + +## addCommandToDomain + +Adds a command relationship to a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domainId | string | Yes | The ID of the domain | +| direction | string | Yes | Direction: "sends" or "receives" | +| command | json | Yes | Command reference: {id, version} | +| domainVersion | string | No | Specific domain version | + + + +--- + +## addQueryToDomain + +Adds a query relationship to a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domainId | string | Yes | The ID of the domain | +| direction | string | Yes | Direction: "sends" or "receives" | +| query | json | Yes | Query reference: {id, version} | +| domainVersion | string | No | Specific domain version | + + + +--- + +## addUbiquitousLanguageToDomain + +Adds ubiquitous language definitions to a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domainId | string | Yes | The ID of the domain | +| dictionary | json | Yes | Array of {term, definition} objects | +| domainVersion | string | No | Specific domain version | + + + +--- + +## getUbiquitousLanguageFromDomain + +Gets ubiquitous language definitions from a domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| domainId | string | Yes | The ID of the domain | +| domainVersion | string | No | Specific domain version | + +**Examples:** + +```bash +# Get ubiquitous language +npx @eventcatalog/cli getUbiquitousLanguageFromDomain "Orders" +``` + +--- + +## domainHasVersion + +Checks if a specific version of a domain exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the domain | +| version | string | Yes | Version to check | + +**Examples:** + +```bash +# Check if version exists +npx @eventcatalog/cli domainHasVersion "Orders" "1.0.0" +``` + +--- diff --git a/packages/core/docs/cli/entities.md b/packages/core/docs/cli/entities.md new file mode 100644 index 000000000..e6f4bf6aa --- /dev/null +++ b/packages/core/docs/cli/entities.md @@ -0,0 +1,138 @@ +--- +id: cli-entities +title: Entities +sidebar_label: Entities +sidebar_position: 11 +--- + +# Entities CLI Commands + +Manage entities in your EventCatalog from the command line. + +## getEntity + +Returns an entity from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the entity to retrieve | +| version | string | No | Specific version to retrieve | + +**Examples:** + +```bash +# Get the latest entity +npx @eventcatalog/cli getEntity "Order" + +# Get a specific version +npx @eventcatalog/cli getEntity "Order" "1.0.0" +``` + +--- + +## getEntities + +Returns all entities from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {latestOnly?} | + +**Examples:** + +```bash +# Get all entities +npx @eventcatalog/cli getEntities +``` + +--- + +## writeEntity + +Writes an entity to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| entity | json | Yes | Entity object with id, name, version, and markdown | +| options | json | No | Options: {path?, override?, versionExistingContent?} | + + + +--- + +## rmEntity + +Removes an entity by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the entity | + +**Examples:** + +```bash +# Remove an entity +npx @eventcatalog/cli rmEntity "/Order" +``` + +--- + +## rmEntityById + +Removes an entity by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the entity to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove an entity +npx @eventcatalog/cli rmEntityById "Order" +``` + +--- + +## versionEntity + +Moves the current entity to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the entity to version | + +**Examples:** + +```bash +# Version an entity +npx @eventcatalog/cli versionEntity "Order" +``` + +--- + +## entityHasVersion + +Checks if a specific version of an entity exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the entity | +| version | string | Yes | Version to check | + +**Examples:** + +```bash +# Check if version exists +npx @eventcatalog/cli entityHasVersion "Order" "1.0.0" +``` + +--- diff --git a/packages/core/docs/cli/events.md b/packages/core/docs/cli/events.md new file mode 100644 index 000000000..0c25530f9 --- /dev/null +++ b/packages/core/docs/cli/events.md @@ -0,0 +1,186 @@ +--- +id: cli-events +title: Events +sidebar_label: Events +sidebar_position: 2 +--- + +# Events CLI Commands + +Manage events in your EventCatalog from the command line. + +## getEvent + +Returns an event from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the event to retrieve | +| version | string | No | Specific version to retrieve (supports semver) | +| options | json | No | Options object, e.g. {"attachSchema": true} | + +**Examples:** + +```bash +# Get the latest version of an event +npx @eventcatalog/cli getEvent "OrderCreated" + +# Get a specific version +npx @eventcatalog/cli getEvent "OrderCreated" "1.0.0" +``` + +--- + +## getEvents + +Returns all events from EventCatalog + +**Arguments:** None + +**Examples:** + +```bash +# Get all events +npx @eventcatalog/cli getEvents +``` + +--- + +## writeEvent + +Writes an event to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| event | json | Yes | Event object with id, name, version, and markdown | + + + +--- + +## writeEventToService + +Writes an event to a specific service in EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| event | json | Yes | Event object with id, name, version, and markdown | +| service | json | Yes | Service reference: {id, version?} | +| options | json | No | Options: {path?, format?, override?} | + + + +--- + +## rmEvent + +Removes an event by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the event, e.g. /InventoryAdjusted | + +**Examples:** + +```bash +# Remove an event by path +npx @eventcatalog/cli rmEvent "/InventoryAdjusted" +``` + +--- + +## rmEventById + +Removes an event by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the event to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove latest version +npx @eventcatalog/cli rmEventById "OrderCreated" + +# Remove specific version +npx @eventcatalog/cli rmEventById "OrderCreated" "1.0.0" +``` + +--- + +## versionEvent + +Moves the current event to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the event to version | + +**Examples:** + +```bash +# Version an event +npx @eventcatalog/cli versionEvent "OrderCreated" +``` + +--- + +## addFileToEvent + +Adds a file to an event + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the event | +| file | json | Yes | File object: {content, fileName} | +| version | string | No | Specific version to add file to | + + + +--- + +## addSchemaToEvent + +Adds a schema file to an event + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the event | +| schema | json | Yes | Schema object: {schema, fileName} | +| version | string | No | Specific version to add schema to | + + + +--- + +## eventHasVersion + +Checks if a specific version of an event exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the event | +| version | string | Yes | Version to check (supports semver) | + +**Examples:** + +```bash +# Check if version exists +npx @eventcatalog/cli eventHasVersion "OrderCreated" "1.0.0" + +# Check with semver range +npx @eventcatalog/cli eventHasVersion "OrderCreated" "1.0.x" +``` + +--- diff --git a/packages/core/docs/cli/export.md b/packages/core/docs/cli/export.md new file mode 100644 index 000000000..379d1d3fd --- /dev/null +++ b/packages/core/docs/cli/export.md @@ -0,0 +1,27 @@ +--- +id: cli-export +title: Export +sidebar_label: Export +sidebar_position: 17 +--- + +# Export CLI Commands + +Manage export in your EventCatalog from the command line. + +## export + +Export catalog resources to EventCatalog DSL (.ec) format + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| all | boolean | No | Export the entire catalog (all resource types) | +| id | string | No | Resource ID (omit to export all of the given type) | +| version | string | No | Resource version (defaults to latest) | +| stdout | boolean | No | Print to stdout instead of writing a file | +| output | string | No | Output file path (defaults to <id>.ec or catalog.ec) | + + + +--- diff --git a/packages/core/docs/cli/governance.md b/packages/core/docs/cli/governance.md new file mode 100644 index 000000000..d385a373f --- /dev/null +++ b/packages/core/docs/cli/governance.md @@ -0,0 +1,24 @@ +--- +id: cli-governance +title: Governance +sidebar_label: Governance +sidebar_position: 20 +--- + +# Governance CLI Commands + +Manage governance in your EventCatalog from the command line. + +## governanceCheck + +Compare the current catalog (or a target branch) against a base branch and evaluate governance rules defined in governance.yaml (or governance.yml) + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| --base | string | No | Base branch to compare against (default: main) | +| --format | string | No | Output format: text or json (default: text) | + + + +--- diff --git a/packages/core/docs/cli/import.md b/packages/core/docs/cli/import.md new file mode 100644 index 000000000..277b4b8e6 --- /dev/null +++ b/packages/core/docs/cli/import.md @@ -0,0 +1,26 @@ +--- +id: cli-import +title: Import +sidebar_label: Import +sidebar_position: 18 +--- + +# Import CLI Commands + +Manage import in your EventCatalog from the command line. + +## import + +Import EventCatalog DSL (.ec) files into catalog markdown files. Existing resources with the same version are overridden. Importing a newer version automatically moves the old version into the versioned/ folder. + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| files | string | No | One or more .ec file paths to import | +| stdin | boolean | No | Read DSL from stdin | +| dry-run | boolean | No | Preview resources without writing | +| no-init | boolean | No | Skip catalog initialization prompt | + + + +--- diff --git a/packages/core/docs/cli/index.md b/packages/core/docs/cli/index.md new file mode 100644 index 000000000..4215c761a --- /dev/null +++ b/packages/core/docs/cli/index.md @@ -0,0 +1,121 @@ +--- +id: cli-overview +title: CLI Reference +sidebar_label: Overview +sidebar_position: 1 +--- + +# EventCatalog CLI + +The EventCatalog CLI allows you to interact with your EventCatalog directly from the command line. Execute any SDK function without writing code. + +## Installation + +You can run the CLI directly using `npx` without installing: + +```bash +npx @eventcatalog/cli [args...] +``` + +Or install globally: + +```bash +npm install -g @eventcatalog/cli +``` + +Then use the `eventcatalog` command: + +```bash +eventcatalog [args...] +``` + +## Basic Usage + +### Specifying the catalog directory + +By default, the CLI uses the current directory. Use `--dir` to specify a different path: + +```bash +npx @eventcatalog/cli --dir /path/to/catalog getEvents +``` + +### Listing available functions + +```bash +npx @eventcatalog/cli list +``` + +### Getting help + +```bash +npx @eventcatalog/cli --help +``` + +## Output Format + +All commands output JSON, making it easy to pipe to other tools like `jq`: + +```bash +# Get all events and extract IDs +npx @eventcatalog/cli getEvents | jq '.[].id' + +# Count total events +npx @eventcatalog/cli getEvents | jq 'length' + +# Filter events by version +npx @eventcatalog/cli getEvents | jq '.[] | select(.version == "1.0.0")' +``` + +## Argument Types + +Arguments are automatically parsed: + +| Type | Format | Example | +|------|--------|---------| +| String | Plain text or quoted | `"OrderCreated"` | +| Number | Numeric value | `42` or `3.14` | +| Boolean | `true` or `false` | `true` | +| JSON Object | `'{...}'` | `'{"id":"test","version":"1.0.0"}'` | +| JSON Array | `'[...]'` | `'["item1","item2"]'` | + +## Quick Examples + +### Read operations + +```bash +# Get a specific event +npx @eventcatalog/cli getEvent "OrderCreated" + +# Get all services (latest versions only) +npx @eventcatalog/cli getServices '{"latestOnly":true}' + +# Check if a version exists +npx @eventcatalog/cli eventHasVersion "OrderCreated" "1.0.0" +``` + +### Write operations + +```bash +# Create a new event +npx @eventcatalog/cli writeEvent '{"id":"OrderCreated","name":"Order Created","version":"1.0.0","markdown":"# Order Created Event"}' + +# Add a service to a domain +npx @eventcatalog/cli addServiceToDomain "Orders" '{"id":"OrderService","version":"1.0.0"}' +``` + +### Delete operations + +```bash +# Remove an event by ID +npx @eventcatalog/cli rmEventById "OrderCreated" + +# Remove a specific version +npx @eventcatalog/cli rmEventById "OrderCreated" "1.0.0" +``` + +### Version operations + +```bash +# Version an event (move current to versioned directory) +npx @eventcatalog/cli versionEvent "OrderCreated" +``` diff --git a/packages/core/docs/cli/messages.md b/packages/core/docs/cli/messages.md new file mode 100644 index 000000000..4787fe1e1 --- /dev/null +++ b/packages/core/docs/cli/messages.md @@ -0,0 +1,69 @@ +--- +id: cli-messages +title: Messages +sidebar_label: Messages +sidebar_position: 15 +--- + +# Messages CLI Commands + +Manage messages in your EventCatalog from the command line. + +## getProducersAndConsumersForMessage + +Returns the producers and consumers (services) for a given message + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the message | +| version | string | No | Specific version | + + + +--- + +## getConsumersOfSchema + +Returns services that consume a given schema + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| schemaPath | string | Yes | Path to the schema file | + + + +--- + +## getProducersOfSchema + +Returns services that produce a given schema + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| schemaPath | string | Yes | Path to the schema file | + + + +--- + +## getOwnersForResource + +Returns the owners (users/teams) for a given resource + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the resource | +| version | string | No | Specific version | + +**Examples:** + +```bash +# Get owners for a resource +npx @eventcatalog/cli getOwnersForResource "OrderService" +``` + +--- diff --git a/packages/core/docs/cli/queries.md b/packages/core/docs/cli/queries.md new file mode 100644 index 000000000..686a2d3ff --- /dev/null +++ b/packages/core/docs/cli/queries.md @@ -0,0 +1,183 @@ +--- +id: cli-queries +title: Queries +sidebar_label: Queries +sidebar_position: 4 +--- + +# Queries CLI Commands + +Manage queries in your EventCatalog from the command line. + +## getQuery + +Returns a query from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the query to retrieve | +| version | string | No | Specific version to retrieve | + +**Examples:** + +```bash +# Get the latest query +npx @eventcatalog/cli getQuery "GetOrder" + +# Get a specific version +npx @eventcatalog/cli getQuery "GetOrder" "1.0.0" +``` + +--- + +## getQueries + +Returns all queries from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {latestOnly?, attachSchema?} | + +**Examples:** + +```bash +# Get all queries +npx @eventcatalog/cli getQueries +``` + +--- + +## writeQuery + +Writes a query to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| query | json | Yes | Query object with id, name, version, and markdown | +| options | json | No | Options: {path?, override?, versionExistingContent?} | + + + +--- + +## writeQueryToService + +Writes a query to a specific service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| query | json | Yes | Query object | +| service | json | Yes | Service reference: {id, version?} | +| options | json | No | Options: {path?, format?, override?} | + + + +--- + +## rmQuery + +Removes a query by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the query | + +**Examples:** + +```bash +# Remove a query +npx @eventcatalog/cli rmQuery "/GetOrder" +``` + +--- + +## rmQueryById + +Removes a query by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the query to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove a query +npx @eventcatalog/cli rmQueryById "GetOrder" +``` + +--- + +## versionQuery + +Moves the current query to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the query to version | + +**Examples:** + +```bash +# Version a query +npx @eventcatalog/cli versionQuery "GetOrder" +``` + +--- + +## addFileToQuery + +Adds a file to a query + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the query | +| file | json | Yes | File object: {content, fileName} | +| version | string | No | Specific version | + + + +--- + +## addSchemaToQuery + +Adds a schema to a query + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the query | +| schema | json | Yes | Schema object: {schema, fileName} | +| version | string | No | Specific version | + + + +--- + +## queryHasVersion + +Checks if a specific version of a query exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the query | +| version | string | Yes | Version to check | + +**Examples:** + +```bash +# Check if version exists +npx @eventcatalog/cli queryHasVersion "GetOrder" "1.0.0" +``` + +--- diff --git a/packages/core/docs/cli/services.md b/packages/core/docs/cli/services.md new file mode 100644 index 000000000..c1d9ffeb0 --- /dev/null +++ b/packages/core/docs/cli/services.md @@ -0,0 +1,266 @@ +--- +id: cli-services +title: Services +sidebar_label: Services +sidebar_position: 5 +--- + +# Services CLI Commands + +Manage services in your EventCatalog from the command line. + +## getService + +Returns a service from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the service to retrieve | +| version | string | No | Specific version to retrieve | + +**Examples:** + +```bash +# Get the latest service +npx @eventcatalog/cli getService "OrderService" + +# Get a specific version +npx @eventcatalog/cli getService "OrderService" "1.0.0" +``` + +--- + +## getServices + +Returns all services from EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| options | json | No | Options: {latestOnly?} | + +**Examples:** + +```bash +# Get all services +npx @eventcatalog/cli getServices +``` + +--- + +## writeService + +Writes a service to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| service | json | Yes | Service object with id, name, version, and markdown | +| options | json | No | Options: {path?, override?, versionExistingContent?} | + + + +--- + +## writeServiceToDomain + +Writes a service to a specific domain + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| service | json | Yes | Service object | +| domain | json | Yes | Domain reference: {id, version?} | +| options | json | No | Options | + + + +--- + +## rmService + +Removes a service by its path + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| path | string | Yes | Path to the service | + +**Examples:** + +```bash +# Remove a service +npx @eventcatalog/cli rmService "/OrderService" +``` + +--- + +## rmServiceById + +Removes a service by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the service to remove | +| version | string | No | Specific version to remove | + +**Examples:** + +```bash +# Remove a service +npx @eventcatalog/cli rmServiceById "OrderService" +``` + +--- + +## versionService + +Moves the current service to a versioned directory + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the service to version | + +**Examples:** + +```bash +# Version a service +npx @eventcatalog/cli versionService "OrderService" +``` + +--- + +## addFileToService + +Adds a file to a service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the service | +| file | json | Yes | File object: {content, fileName} | +| version | string | No | Specific version | + + + +--- + +## addEventToService + +Adds an event relationship to a service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| serviceId | string | Yes | The ID of the service | +| direction | string | Yes | Direction: "sends" or "receives" | +| event | json | Yes | Event reference: {id, version} | +| serviceVersion | string | No | Specific service version | + + + +--- + +## addCommandToService + +Adds a command relationship to a service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| serviceId | string | Yes | The ID of the service | +| direction | string | Yes | Direction: "sends" or "receives" | +| command | json | Yes | Command reference: {id, version} | +| serviceVersion | string | No | Specific service version | + + + +--- + +## addQueryToService + +Adds a query relationship to a service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| serviceId | string | Yes | The ID of the service | +| direction | string | Yes | Direction: "sends" or "receives" | +| query | json | Yes | Query reference: {id, version} | +| serviceVersion | string | No | Specific service version | + + + +--- + +## addEntityToService + +Adds an entity to a service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| serviceId | string | Yes | The ID of the service | +| entity | json | Yes | Entity reference: {id, version} | +| serviceVersion | string | No | Specific service version | + + + +--- + +## addDataStoreToService + +Adds a data store relationship to a service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| serviceId | string | Yes | The ID of the service | +| relationship | string | Yes | Relationship: "writesTo" or "readsFrom" | +| dataStore | json | Yes | Data store reference: {id, version} | +| serviceVersion | string | No | Specific service version | + + + +--- + +## serviceHasVersion + +Checks if a specific version of a service exists + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the service | +| version | string | Yes | Version to check | + +**Examples:** + +```bash +# Check if version exists +npx @eventcatalog/cli serviceHasVersion "OrderService" "1.0.0" +``` + +--- + +## getSpecificationFilesForService + +Returns specification files (OpenAPI, AsyncAPI) for a service + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the service | +| version | string | No | Specific version | + +**Examples:** + +```bash +# Get spec files +npx @eventcatalog/cli getSpecificationFilesForService "OrderService" +``` + +--- diff --git a/packages/core/docs/cli/snapshots.md b/packages/core/docs/cli/snapshots.md new file mode 100644 index 000000000..cf8829138 --- /dev/null +++ b/packages/core/docs/cli/snapshots.md @@ -0,0 +1,44 @@ +--- +id: cli-snapshots +title: Snapshots +sidebar_label: Snapshots +sidebar_position: 19 +--- + +# Snapshots CLI Commands + +Manage snapshots in your EventCatalog from the command line. + +## createSnapshot + +Take a point-in-time snapshot of the entire catalog, capturing all resources and their metadata as a JSON file + +**Arguments:** None + + + +--- + +## diffSnapshots + +Compare two snapshot files and output a structured diff showing added, removed, modified, and versioned resources plus relationship changes + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| fileA | string | Yes | Path to the first (older) snapshot file | +| fileB | string | Yes | Path to the second (newer) snapshot file | + + + +--- + +## listSnapshots + +List all snapshots in the catalog .snapshots directory with their labels, timestamps, and git info + +**Arguments:** None + + + +--- diff --git a/packages/core/docs/cli/teams.md b/packages/core/docs/cli/teams.md new file mode 100644 index 000000000..01ba0e124 --- /dev/null +++ b/packages/core/docs/cli/teams.md @@ -0,0 +1,75 @@ +--- +id: cli-teams +title: Teams +sidebar_label: Teams +sidebar_position: 8 +--- + +# Teams CLI Commands + +Manage teams in your EventCatalog from the command line. + +## getTeam + +Returns a team from EventCatalog by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the team to retrieve | + +**Examples:** + +```bash +# Get a team +npx @eventcatalog/cli getTeam "platform-team" +``` + +--- + +## getTeams + +Returns all teams from EventCatalog + +**Arguments:** None + +**Examples:** + +```bash +# Get all teams +npx @eventcatalog/cli getTeams +``` + +--- + +## writeTeam + +Writes a team to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| team | json | Yes | Team object with id, name, and markdown | +| options | json | No | Options: {path?, override?} | + + + +--- + +## rmTeamById + +Removes a team by its ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the team to remove | + +**Examples:** + +```bash +# Remove a team +npx @eventcatalog/cli rmTeamById "platform-team" +``` + +--- diff --git a/packages/core/docs/cli/users.md b/packages/core/docs/cli/users.md new file mode 100644 index 000000000..22b06ab19 --- /dev/null +++ b/packages/core/docs/cli/users.md @@ -0,0 +1,75 @@ +--- +id: cli-users +title: Users +sidebar_label: Users +sidebar_position: 9 +--- + +# Users CLI Commands + +Manage users in your EventCatalog from the command line. + +## getUser + +Returns a user from EventCatalog by their ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the user to retrieve | + +**Examples:** + +```bash +# Get a user +npx @eventcatalog/cli getUser "jsmith" +``` + +--- + +## getUsers + +Returns all users from EventCatalog + +**Arguments:** None + +**Examples:** + +```bash +# Get all users +npx @eventcatalog/cli getUsers +``` + +--- + +## writeUser + +Writes a user to EventCatalog + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| user | json | Yes | User object with id, name, and markdown | +| options | json | No | Options: {path?, override?} | + + + +--- + +## rmUserById + +Removes a user by their ID + +**Arguments:** +| Name | Type | Required | Description | +|------|------|----------|-------------| +| id | string | Yes | The ID of the user to remove | + +**Examples:** + +```bash +# Remove a user +npx @eventcatalog/cli rmUserById "jsmith" +``` + +--- diff --git a/packages/core/docs/cli/utilities.md b/packages/core/docs/cli/utilities.md new file mode 100644 index 000000000..e5f8661dd --- /dev/null +++ b/packages/core/docs/cli/utilities.md @@ -0,0 +1,43 @@ +--- +id: cli-utilities +title: Utilities +sidebar_label: Utilities +sidebar_position: 16 +--- + +# Utilities CLI Commands + +Manage utilities in your EventCatalog from the command line. + +## dumpCatalog + +Dumps the entire catalog to a JSON structure + +**Arguments:** None + +**Examples:** + +```bash +# Dump entire catalog +npx @eventcatalog/cli dumpCatalog + +# Dump and save to file +npx @eventcatalog/cli dumpCatalog > catalog.json +``` + +--- + +## getEventCatalogConfigurationFile + +Returns the EventCatalog configuration file + +**Arguments:** None + +**Examples:** + +```bash +# Get config file +npx @eventcatalog/cli getEventCatalogConfigurationFile +``` + +--- diff --git a/packages/core/docs/contributing/01-overview.md b/packages/core/docs/contributing/01-overview.md new file mode 100644 index 000000000..ff171836c --- /dev/null +++ b/packages/core/docs/contributing/01-overview.md @@ -0,0 +1,186 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog Contributing +sidebar_label: Contributing to EventCatalog +title: Contributing +description: Understand how to contribute of EventCatalog +--- + +# Contributing to EventCatalog + +[EventCatalog](https://eventcatalog.dev) is designed to help teams document their document Event Driven Architectures. If you're interested in contributing to EventCatalog, hopefully, this document makes the process for contributing clear. + +The [Open Source Guides](https://opensource.guide/) website has a collection of resources for individuals, communities, and companies who want to learn how to run and contribute to an open source project. Contributors and people new to open source alike will find the following guides especially useful: + +- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) +- [Building Welcoming Communities](https://opensource.guide/building-community/) + +## Get Involved + +There are many ways to contribute to EventCatalog, and many of them do not involve writing any code. Here's a few ideas to get started: + +- Simply start using EventCatalog. Go through the [Getting Started](/docs/development/getting-started/installation) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues). +- Look through the [open issues](https://github.com/event-catalog/eventcatalog/issues). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests). +- If you find an issue you would like to fix, [open a pull request](#your-first-pull-request). Issues tagged as [_Good first issue_](https://github.com/event-catalog/eventcatalog/labels/Good%20first%20issue) are a good place to get started. +- Read through the [EventCatalog docs](/docs/development/getting-started/installation). If you find anything that is confusing or can be improved, you can click "Edit this page" at the bottom of most docs, which takes you to the GitHub interface to make and propose changes. +- Take a look at the [features requested](https://github.com/event-catalog/eventcatalog/labels/feature) by others in the community and consider opening a pull request if you see something you want to work on. + +Contributions are very welcome. If you think you need help planning your contribution, please ping on Twitter at [@boyney123](https://twitter.com/boyney123) and let us know you are looking for a bit of help. + +### Join our Discord Channel + +We have the [`#contributors`](https://eventcatalog.dev/discord) channel on [Discord](https://eventcatalog.dev/discord) to discuss all things about EventCatalog development. You can also be of great help by helping other users in the help channel. + +### Triaging Issues and Pull Requests + +One great way you can contribute to the project without writing any code is to help triage issues and pull requests as they come in. + +- Ask for more information if you believe the issue does not provide all the details required to solve it. +- Suggest [labels](https://github.com/event-catalog/eventcatalog/labels) that can help categorize issues. +- Flag issues that are stale or that should be closed. +- Ask for test plans and review code. + +## Our Development Process + +EventCatalog uses [GitHub](https://github.com/event-catalog/eventcatalog) as its source of truth. All changes will be public from the beginning. + +All pull requests will be checked by the continuous integration system, GitHub actions. + +### Running the project locally + +To run the project locally follow these steps: + +1. Clone the repo `git clone git@github.com:event-catalog/eventcatalog.git` +1. Install project dependencies `npm run i` +1. Run the project locally `npm run start:catalog` + - This will load EventCatalog locally using the catalog found in the [`/examples` folder](https://github.com/event-catalog/eventcatalog/tree/main/examples/default). + +### Branch Organization + +EventCatalog has one primary branch `main` and we use feature branches to deliver new features with pull requests. + +## Proposing a Change + +If you would like to request a new feature or enhancement but are not yet thinking about opening a pull request, you can also file an issue with the [feature template](https://github.com/event-catalog/eventcatalog/issues/new?assignees=&labels=feature%2Cneeds+triage&template=feature.yml). + +If you're only fixing a bug, it's fine to submit a pull request right away but we still recommend [filing an issue](https://github.com/event-catalog/eventcatalog/issues/new?assignees=&labels=bug%2Cneeds+triage&template=bug.yml) detailing what you're fixing. This is helpful in case we don't accept that specific fix but want to keep track of the issue. + +### Reporting New Issues + +When [opening a new issue](https://github.com/event-catalog/eventcatalog/issues/new/choose), always make sure to fill out the issue template. **This step is very important!** Not doing so may result in your issue not being managed in a timely fashion. Don't take this personally if this happens, and feel free to open a new issue once you've gathered all the information required by the template. + +- **One issue, one bug:** Please report a single bug per issue. +- **Provide reproduction steps:** List all the steps necessary to reproduce the issue. The person reading your bug report should be able to follow these steps to reproduce your issue with minimal effort. + +### Bugs + +We use [GitHub Issues](https://github.com/event-catalog/eventcatalog/issues) for our public bugs. If you would like to report a problem, take a look around and see if someone already opened an issue about it. If you are certain this is a new, unreported bug, you can submit a [bug report](#reporting-new-issues). + +### Feature requests + +You can also file issues as [feature requests or enhancements](https://github.com/event-catalog/eventcatalog/labels/feature%20request). If you see anything you'd like to be implemented, create an issue with [feature template](https://raw.githubusercontent.com/boyney123/eventcatalog/master/.github/ISSUE_TEMPLATE/feature.md) + +### Questions + +If you have questions about using EventCatalog, ask in [Discord](https://eventcatalog.dev/discord) or contact on [Twitter](https://twitter.com/boyney123) and we will do our best to answer your questions. + +## Pull Requests + +### Your First Pull Request + +So you have decided to contribute code back to upstream by opening a pull request. You've invested a good chunk of time, and we appreciate it. We will do our best to work with you and get the PR looked at. + +Working on your first Pull Request? You can learn how from this free video series: + +[**How to Contribute to an Open Source Project on GitHub**](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) + +We have a list of [beginner-friendly issues](https://github.com/event-catalog/eventcatalog/labels/good%20first%20issue) to help you get your feet wet in the EventCatalog codebase and familiar with our contribution process. This is a great place to get started. + +### Installation + +1. Ensure you have [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed. +1. After cloning the repository, run `npm i` in the root of the repository. +1. To start the catalog locally run `npm run start:catalog` + +### Sending a Pull Request + +Small pull requests are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it. It is recommended to follow this [commit message style](#semantic-commit-messages). + +Please make sure the following is done when submitting a pull request: + +1. Fork [the repository](https://github.com/event-catalog/eventcatalog) and create your branch from `main`. +1. Make sure your code lints (`yarn format && yarn lint`). +1. Make sure your Jest tests pass (`yarn test`). + +All pull requests should be opened against the `main` branch. + +#### Breaking Changes + +When adding a new breaking change, follow this template in your pull request: + +```md +### New breaking change here + +- **Who does this affect**: +- **How to migrate**: +- **Why make this breaking change**: +- **Severity (number of people affected x effort)**: +``` + +### What Happens Next? + +We will be monitoring for pull requests. Do help us by keeping pull requests consistent by following the guidelines above. + +## Style Guide + +[Prettier](https://prettier.io) will catch most styling issues that may exist in your code. You can check the status of your code styling by simply running `yarn prettier`. + +However, there are still some styles that Prettier cannot pick up. + +## Semantic Commit Messages + +See how a minor change to your commit message style can make you a better programmer. + +Format: `(): ` + +`` is optional. If your change is specific to one/two packages, consider adding the scope. Scopes should be brief but recognizable, e.g. `content-docs`, `theme-classic`, `core` + +The various types of commits: + +- `feat`: a new API or behavior **for the end user**. +- `fix`: a bug fix **for the end user**. +- `docs`: a change to the website or other Markdown documents in our repo. +- `refactor`: a change to production code that leads to no behavior difference, e.g. splitting files, renaming internal variables, improving code style... +- `test`: adding missing tests, refactoring tests; no production code change. +- `chore`: upgrading dependencies, releasing new versions... Chores that are **regularly done** for maintenance purposes. +- `misc`: anything else that doesn't change production code, yet is not `test` or `chore`. e.g. updating GitHub actions workflow. + +Do not get too stressed about PR titles, however. The maintainers will help you get them right, and we also have a PR label system that doesn't equate with the commit message types. Your code is more important than conventions! + +### Example + +``` +feat(core): allow overriding of webpack config +^--^^----^ ^------------^ +| | | +| | +-> Summary in present tense. +| | +| +-> The package(s) that this change affected. +| ++-------> Type: see below for the list we use. +``` + +Use lower case not title case! + +## Code Conventions + +### General + +- **Most important: Look around.** Match the style you see used in the rest of the project. This includes formatting, naming files, naming things in code, naming things in documentation, etc. +- "Attractive" +- We do have Prettier (a formatter) and ESLint (a syntax linter) to catch most stylistic problems. If you are working locally, they should automatically fix some issues during every git commit. + +## License + +By contributing to EventCatalog, you agree that your contributions will be licensed under its license. \ No newline at end of file diff --git a/packages/core/docs/contributing/_category_.json b/packages/core/docs/contributing/_category_.json new file mode 100644 index 000000000..824ba8ab1 --- /dev/null +++ b/packages/core/docs/contributing/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Contributing", + "position": 3, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "contributing", + "title": "EventCatalog Contributing Documentation", + "description": "EventCatalog Contributing Documentation" + } +} \ No newline at end of file diff --git a/packages/core/docs/development/00-why-eventcatalog.md b/packages/core/docs/development/00-why-eventcatalog.md new file mode 100644 index 000000000..cf1a2095d --- /dev/null +++ b/packages/core/docs/development/00-why-eventcatalog.md @@ -0,0 +1,76 @@ +--- +sidebar_position: 1 +slug: /development/getting-started/introduction +keywords: +- EventCatalog introduction +sidebar_label: Why EventCatalog? +title: Why EventCatalog? +description: EventCatalog is an open source project to help you bring discoverability to your event-driven architecture. +--- + +Software architecture consists of many different primitives including domains, systems, services, APIS, contracts, messages and many more. + +As systems grow, complexity grows. Documentation becomes important for teams but teams struggle to keep docs in sync, or relevant to what they are trying to model. + +**Many documentation tools are not designed for software architecture**; they let you create generic pages that get lost across the organization, they do not stay in sync with your code, specifications and teams making changes. + +**Our vision with EventCatalog is to change this.** + +### What is EventCatalog? + +EventCatalog is a documentation and governance tool that is built for architects. + + It lets you document and visualize your architecture with primitives you are already familiar with. You can document domains, systems, applications and schemas and visualize how they all relate to each other. + +EventCatalog creates an internal architecture graph of how your systems work. This makes EventCatalog accessible to your AI agents, LLMs and AI workflows and [creates context of your business and architecture](/docs/development/ask-your-architecture/intro). + +EventCatalog is technology agnostic, this means you can document and model any systems you have + + +#### What can EventCatalog do for you? + +- **Save time across the organization** + - Help teams find information they need across your business and organization + - Document high level information for stakeholders while providing lower level details to your teams. + - Connect to your AI Agents and AI Workflows. Give your LLMs the context they need. +- **Create documentation you can trust** + - Sync documentation from your code with our Agent + - Automate documentation from your OpenAPI, AsyncAPI, or schema registries + - Use EventCatalog Agent to sync code to docs from your CI/CD workflows. + - Write your own automation and plugins with the [EventCatalog SDK](/docs/sdk) +- **Detect breaking changes before they reach production** + - Use EventCatalog Agent to detect breaking schema changes + - Bring your own model and get notified when things break +- **Use natural langugage to query your architecture** + - Connect AI tools to your architecture using [EventCatalog's MCP server](/docs/development/ask-your-architecture/mcp-server/introduction) + - Ask architectural questions grounded in your documented domains, services, events, data products, and schemas +- **Show the bigger picture** + - Help business stakeholders understand the bigger picture + - Visualize your architecture and future ideas + - Document key business workflows + +**EventCatalog is self-hosted. You own your data and host it wherever you want.** + +--- + + + +## Join the community + +- [GitHub](https://github.com/event-catalog/eventcatalog) — Star, contribute, report issues +- [Discord](https://eventcatalog.dev/discord) — Ask questions, share feedback + +## Something missing? + +If you find issues with the documentation or have suggestions on how to improve the documentation or the project in general, please [file an issue](https://github.com/event-catalog/eventcatalog) for us. diff --git a/packages/core/docs/development/01-fundamentals.md b/packages/core/docs/development/01-fundamentals.md new file mode 100644 index 000000000..005a7bea7 --- /dev/null +++ b/packages/core/docs/development/01-fundamentals.md @@ -0,0 +1,92 @@ +--- +sidebar_position: 2 +slug: /development/getting-started/fundamentals +keywords: +- EventCatalog introduction +sidebar_label: Fundamentals +title: Fundamentals +description: Understanding the fundamentals of EventCatalog +--- + +EventCatalog is a architecture catalog built around software primitives and domain-driven design patterns, so you can model the systems you actually build instead of creating disconnected documentation pages. + +EventCatalog gives your teams and AI agents a shared understanding of how your systems work across your organization. + +EventCatalog is an open source project and is self-hosted and follows the [docs-as-code](https://www.writethedocs.org/guide/docs-as-code/) pattern. All pages in EventCatalog are powered by [markdown (MDX)](https://mdxjs.com/). + +Your catalog is private, stays in your own repository and infrastructure. + +At its core, EventCatalog gives you a small set of building blocks. + +- **Domains (Level 1)** - a domain describes a business boundary +- **Systems (Level 2)** - a system is a collection of resources that work together to perform a function. +- **Resources (Level 3)** - individual resources (e.g services, messages, data stores) that can be assigned to systems or domains. + +All building blocks are optional, it's up to you how you want to model your architecture in your catalog. + +![EventCatalog model showing domains, systems, resources, contracts, and ownership](./img/ec-types.png) + +## The EventCatalog model + +Domains give your catalog a business shape. They help users understand the boundaries in your architecture, such as `Shopping`, `Payments`, `Fulfilment`, or `Customer`. + +Systems give your catalog an operating shape. A system is a collection of resources that work together to perform a function. For example, a `Shopping` domain might contain a `Cart System` and a `Promotion System`. + +Resources are the documented building blocks inside your architecture. They can include: + +- [Services](/docs/development/guides/resources/services/introduction), such as APIs, workers, frontends, and backends. +- [Messages](/docs/development/guides/resources/messages/what-are-messages), such as events, commands, and queries. +- [Data stores](/docs/development/guides/resources/data/introduction), such as databases, caches, and file stores. +- [Entities](/docs/development/guides/resources/entities/introduction), such as `Order`, `Customer`, or `Payment`. +- [Data products](/docs/development/guides/resources/data-products/introduction), such as warehouse tables or analytical datasets. +- [Agents](/docs/development/guides/resources/agents/introduction), such as customer support AI agents. +- [Flows](/docs/development/guides/resources/flows/introduction), such as business workflows. + +Contracts are attached to resources. Schemas, OpenAPI specs, AsyncAPI specs, and GraphQL schemas help users understand the data and interfaces that resources produce, consume, or implement. + +Teams and users can own domains, systems, resources, and contracts. This makes ownership part of the architecture model, not a separate spreadsheet. + +## Levels of detail + +EventCatalog is useful to anyone in your organization. You can define high level primitives whilst giving lower level implementation details. + +| Level | Name | What it answers | Example | +|-------|------|-----------------|---------| +| Level 1 | Domains | High level, what business boundary are we looking at? | Shopping | +| Level 2 | Systems | What software capability exists inside that boundary? | Cart System | +| Level 3 | Resources | What makes up, connects to, or documents that system? | Cart API, cart database, checkout flow | + +Contracts and ownership sit across these levels. A service can implement an OpenAPI contract. A message can have a schema. A team can own a domain, system, or resource. + +This gives different users a way into the same catalog. Architects can start at domains, platform teams can reason about systems, and developers can drill into the resources and contracts they work with every day. + +## Docs-as-code + +EventCatalog is a [docs-as-code](https://www.writethedocs.org/guide/docs-as-code/) tool. This means you can store your documentation in your existing Git repository, version it, and use your existing workflows to review and merge changes. + +This also let's you define custom workflows and patterns in your organization for documentation and automation. + +It's up to you where you define your catalog (or catalogs). Here are some examples: + +| Pattern | Description | +|---------|-------------| +| **Standalone repo** | Keep documentation separate from code | +| **Next to your code** | Docs live alongside the services they describe | +| **Monorepo** | Documentation as part of your existing monorepo | +| **Federated** | Multiple EventCatalog instances connected into one view | + + +## Automation + +Your documentation can also be automated, keeping your implementation details close to your documentation. There are many [integrations](/integrations) or you can create your own automations with our [SDK](/docs/sdk). + +## Visual editing + +You can also use [EventCatalog Editor](/docs/editor/overview) to maintain your catalog through a local visual workflow. + +The editor runs on top of your EventCatalog project, writes changes back to the same local files, and gives you a Git-backed way to review and publish changes. This helps developers, architects, analysts, and product owners contribute to the catalog without needing to work directly in Markdown files. + + +## Ready to build? + +Now that you understand the fundamentals, [get started with EventCatalog](/docs/development/getting-started/installation). diff --git a/packages/core/docs/development/01-getting-started/_category_.json b/packages/core/docs/development/01-getting-started/_category_.json new file mode 100644 index 000000000..d81d6934f --- /dev/null +++ b/packages/core/docs/development/01-getting-started/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Getting Started", + "position": 3, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "development/getting-started", + "title": "Getting Started Documentation", + "description": "Learn how to install EventCatalog." + } +} diff --git a/packages/core/docs/development/01-getting-started/configuration-overview.md b/packages/core/docs/development/01-getting-started/configuration-overview.md new file mode 100644 index 000000000..606fa522e --- /dev/null +++ b/packages/core/docs/development/01-getting-started/configuration-overview.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 6 +keywords: +- EventCatalog configuration +sidebar_label: Configuration +title: Configuration +description: Understand how to configure EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +Most new EventCatalog projects are ready to run as soon as they are created. You only need to change configuration when you want to adjust how the catalog behaves, where it builds, or which features are enabled. + +Configuration lives in `eventcatalog.config.js` at the root of your project. You can read the [full reference documentation](/docs/api/config) to learn what can be changed. + +## Configuring environment variables {#configuring-environment-variables} + +Some features need values that should not live directly in `eventcatalog.config.js`, such as license keys or provider credentials. + +Put those values in a `.env` file in the root of your catalog. EventCatalog loads `.env` when it runs locally and when it builds. + +```bash title=".env (example)" +EVENTCATALOG_SCALE_LICENSE_KEY=your-api-key +``` + +Do not commit real secrets to source control. + +## Related configuration + +EventCatalog has a linter that can be used to validate your EventCatalog documentation. + +You can read more about the EventCatalog Linter in the [EventCatalog Linter documentation](/docs/development/developer-tools/eventcatalog-linter). diff --git a/packages/core/docs/development/01-getting-started/develop-and-build.md b/packages/core/docs/development/01-getting-started/develop-and-build.md new file mode 100644 index 000000000..763c7c9d2 --- /dev/null +++ b/packages/core/docs/development/01-getting-started/develop-and-build.md @@ -0,0 +1,72 @@ +--- +sidebar_position: 5 +keywords: +- EventCatalog Develop and build +sidebar_label: Develop and build +title: Develop and build +description: Understanding how to develop and build EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +## Edit your project + +To make changes to your project, open your project folder in your code editor. Working in development mode with the dev server running allows you to see updates to your site as you edit the code. + +## Starting the development server + +EventCatalog comes with a built-in development server that has everything you need for project development. The `eventcatalog dev` CLI command will start the local development server so that you can see your new website in action for the very first time. + +Every starter template comes with a pre-configured script that will run `eventcatalog dev` for you. After navigating into your project directory, run this command and start the EventCatalog development server: + +```bash +npm run dev +``` +If all goes well, EventCatalog will now be serving your project on http://localhost:3000/. Visit that link in your browser and see your new site! + +## Build and preview your catalog + +To check the version of your site that will be created at build time, quit the dev server (Ctrl + C) and run the appropriate build command in your terminal: + +```bash +npm run build +``` + +EventCatalog will build a deploy-ready version of your site in a separate folder (dist/ by default) and you can watch its progress in the terminal. This will alert you to any build errors in your project before you deploy to production. + +When the build is finished, run the appropriate preview command (e.g. npm run preview) in your terminal and you can view the built version of your site locally in the same browser preview window. + +Note that this previews your code as it existed when the build command was last run. This is meant to give you a preview of how your site will look when it is deployed to the web. Any later changes you make to your code after building will not be reflected while you preview your site until you run the build command again. + +Use (Ctrl + C) to quit the preview and run another terminal command, such as restarting the dev server to go back to working in development mode which does update as you edit to show a live preview of your code changes. + + +## EventCatalog static vs server output + +By default, EventCatalog will build a static website. This means you can [host this website](/docs/development/deployment/hosting-options) anywhere you like. + +Some features of EventCatalog (e.g SSO) require a to run EventCatalog as a server. + +You can opt into which build mode you want to use by setting the [`output` property in your `eventcatalog.config.js` file](/docs/api/config#output). + + + + +## Next Steps + +Success! You are now ready to start building with EventCatalog! 🥳 + +Here are a few things that we recommend exploring next. You can read them in any order. You can even leave our documentation for a bit and go play in your new EventCatalog project codebase, coming back here whenever you run into trouble or have a question. + +- [Create your first domain](/docs/development/guides/domains/create-domain) +- [Create your first system](/docs/development/guides/systems/create-system) +- [Create your first service](/docs/development/guides/resources/services/create-service) +- [Create your first message](/docs/development/guides/resources/messages/what-are-messages) diff --git a/packages/core/docs/development/01-getting-started/installation.md b/packages/core/docs/development/01-getting-started/installation.md new file mode 100644 index 000000000..f4e6ce797 --- /dev/null +++ b/packages/core/docs/development/01-getting-started/installation.md @@ -0,0 +1,111 @@ +--- +sidebar_position: 3 +keywords: +- EventCatalog installation install +sidebar_label: Installation +title: Installation +description: Install EventCatalog and create a new catalog +--- + +The `create-eventcatalog` CLI command is the fastest way to start a new EventCatalog project. It creates a catalog directory, installs what you need, and gives you scripts for local development and production builds. + +You can also start from a template, create an empty catalog, or use the EventCatalog AI skill to generate documentation from an existing codebase. + +## Prerequisites + +- [Node.js](https://nodejs.org/en/download/) v22 or higher. Check your version with `node -v`. +- A terminal to run the EventCatalog CLI. +- A text editor to edit your catalog files. + +## Install from the CLI wizard + +Run the following command in your terminal: + +```bash +npx @eventcatalog/create-eventcatalog@latest my-catalog +``` + +This creates a `my-catalog` directory with: + +- an EventCatalog project +- sample architecture resources +- an `eventcatalog.config.js` file +- a `package.json` file with scripts for local development and builds + +Now move into your new project directory: + +```bash +cd my-catalog +``` + +If the CLI asks whether to install dependencies and you skip that step, install them before continuing: + +```bash +npm install +``` + +## Start the development server + +Start EventCatalog locally: + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) to view your catalog. + +## CLI installation flags + +You can pass flags to `create-eventcatalog` to customize the catalog that gets created. + +### Create an empty catalog + +Use `--empty` when you want a clean catalog without sample resources. + +```bash +npx @eventcatalog/create-eventcatalog@latest my-catalog --empty +``` + +### Use a starter template + +Use `--template` to start from one of the EventCatalog integration templates. + +```bash +npx @eventcatalog/create-eventcatalog@latest my-catalog --template asyncapi +``` + +Templates are useful when you already know which integration you want to use, such as AsyncAPI, OpenAPI, EventBridge, GitHub, or a schema registry. + +## Generate a catalog from an existing codebase + +If you already have a codebase, you can use the [Catalog Documentation Creator](https://github.com/event-catalog/skills) AI skill to generate EventCatalog documentation from the information in your project. + +First, add the EventCatalog skill to your project: + +```bash +npx skills add event-catalog/skills --skill catalog-documentation-creator +``` + +Then ask your AI agent to inspect your codebase and generate EventCatalog documentation: + +```txt +Look at my directory and generate EventCatalog documentation from information you find. +``` + +The generated files can be edited like any other EventCatalog documentation. Learn more about [EventCatalog skills](/docs/development/ask-your-architecture/skills/introduction). + +## Optional: Use EventCatalog Editor + +You can edit your catalog files directly in Markdown, or use [EventCatalog Editor](/docs/editor/overview) for a local visual editing workflow. + +The editor runs on top of your EventCatalog project, writes changes back to your local files, and gives you a Git-backed way to review and publish changes. This is useful when architects, developers, analysts, or product owners need to help maintain the catalog without working directly in Markdown. + +To use the editor, you need Git installed and editor access through [EventCatalog Cloud](https://eventcatalog.cloud). Learn how to [run EventCatalog Editor locally](/docs/editor/how-to/run-locally). + +## Next steps + +- [Understand the project structure](/docs/development/getting-started/project-structure) +- [Develop and build your catalog](/docs/development/getting-started/develop-and-build) +- [Create your first domain](/docs/development/guides/domains/create-domain) +- [Create your first service](/docs/development/guides/resources/services/create-service) +- [Document your first message](/docs/development/guides/resources/messages/what-are-messages) diff --git a/packages/core/docs/development/01-getting-started/project-structure.md b/packages/core/docs/development/01-getting-started/project-structure.md new file mode 100644 index 000000000..56fb622b0 --- /dev/null +++ b/packages/core/docs/development/01-getting-started/project-structure.md @@ -0,0 +1,204 @@ +--- +sidebar_position: 4 +keywords: +- EventCatalog project structure +sidebar_label: Project structure +title: Project structure +description: Understanding how to structure your EventCatalog project +--- + +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + +An EventCatalog project is a folder of Markdown files that describe and model your architecture. + +You can edit these files directly, or use the optional [EventCatalog Editor](/docs/editor/overview) to create, preview, review, and commit catalog content through a visual workflow. + +Each resource type in your catalog (e.g domain, system, message) is represented by a file or folder in your project. EventCatalog reads those files and turns them into a catalog. + +You can also bring your own documentation into EventCatalog, such as guides, onboarding material, or runbooks. Learn more about [bringing your own documentation](/docs/development/bring-your-own-documentation/introduction). + +All resource types are optional. You decide what you want to document, and you can add more resource types later as your catalog becomes more useful to your teams. + +## Directories and files + +Every EventCatalog project has a few core files used to run and configure the catalog. + +Architecture resources exist in folders such as `domains/`, `systems/`, `services/`, and `events/`. All resources in EventCatalog are optional, it's up to you how you want to model and document your architecture. + +EventCatalog creates resources from folders that contain an `index.mdx` file. For example, `domains/Orders/index.mdx` creates an `Orders` domain, `domains/Orders/systems/order-management-system/index.mdx` creates an `order-management-system` system inside that domain, and `domains/Orders/systems/order-management-system/services/OrderService/index.mdx` creates an `OrderService` service inside that system. + +- `eventcatalog.config.js` - Configures your catalog. +- `package.json` - Defines scripts and dependencies. +- `public/` - Stores static assets that should be served directly. +- `domains/` - Documents business domains and bounded contexts. +- `systems/` - Documents groups of resources that work together to provide a capability. +- `services/` - Documents services, APIs, applications, external systems, jobs, and workers. +- `events/`, `commands/`, `queries/` - Documents messages. +- `channels/` - Documents topics, queues, streams, webhooks, or other communication paths. +- `diagrams/` - Documents architecture diagrams and flows. +- `teams/` and `users/` - Documents ownership metadata. + +### Example project tree + +A common EventCatalog project directory might look like this: + + + +You do not need every folder on day one. Start with the resources you know about, then add more as your catalog grows. + +## What should I create first? + +If you want to create your first few resources you can follow our [Learning EventCatalog Guide](/learn). + +## Next Steps + +Now you know some of the ways EventCatalog work, you can start to document some of your architecture resources. +Here are some links below to help depending on what you want to document. + +:::tip Using AI to help +AI is great at helping you document and get going with EventCatalog. You can install our [EventCatalog Skills](https://github.com/event-catalog/eventcatalog-skills) to teach your LLM about EventCatalog, and then ask your LLM to help you get going. +::: + +- [Start documenting a business domain](/docs/development/guides/domains/introduction) +- [Start documenting a system](/docs/development/guides/systems/create-system) +- [Start documenting producers and consumers of a service](/docs/development/guides/resources/services/introduction) diff --git a/packages/core/docs/development/_category_.json b/packages/core/docs/development/_category_.json new file mode 100644 index 000000000..6fa1f6fae --- /dev/null +++ b/packages/core/docs/development/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Development", + "position": 2, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "development", + "title": "EventCatalog documentation", + "description": "EventCatalog documentation. Learn about the fundamentals of EventCatalog." + } +} \ No newline at end of file diff --git a/packages/core/docs/development/_getting-started.mdx b/packages/core/docs/development/_getting-started.mdx new file mode 100644 index 000000000..306c5b246 --- /dev/null +++ b/packages/core/docs/development/_getting-started.mdx @@ -0,0 +1,15 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog introduction +sidebar_label: Getting Started +title: Getting Started +description: EventCatalog is an open source project to help you bring discoverability to your event-driven architecture. +hide_title: true +hide_table_of_contents: true +hide_breadcrumbs: true +--- + +import DocumentationLanding from '@site/src/components/MDX/DocumentationLanding'; + + diff --git a/packages/core/docs/development/agent-resources/_category_.json b/packages/core/docs/development/agent-resources/_category_.json new file mode 100644 index 000000000..00706688a --- /dev/null +++ b/packages/core/docs/development/agent-resources/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Agent Resources", + "position": 14, + "collapsible": true, + "collapsed": false +} diff --git a/packages/core/docs/development/agent-resources/eventcatalog-skills.md b/packages/core/docs/development/agent-resources/eventcatalog-skills.md new file mode 100644 index 000000000..f5f54f2ad --- /dev/null +++ b/packages/core/docs/development/agent-resources/eventcatalog-skills.md @@ -0,0 +1,17 @@ +--- +sidebar_position: 1 +sidebar_label: EventCatalog Skills +title: EventCatalog Skills +custom_edit_url: null +--- + +import BrowserOnly from '@docusaurus/BrowserOnly'; + + + {() => { + window.location.href = 'https://github.com/event-catalog/eventcatalog-skills'; + return null; + }} + + +EventCatalog Skills diff --git a/packages/core/docs/development/agent-resources/llms-full.md b/packages/core/docs/development/agent-resources/llms-full.md new file mode 100644 index 000000000..bad2dbfd5 --- /dev/null +++ b/packages/core/docs/development/agent-resources/llms-full.md @@ -0,0 +1,17 @@ +--- +sidebar_position: 3 +sidebar_label: EventCatalog Docs llms-full.txt +title: EventCatalog Docs llms-full.txt +custom_edit_url: null +--- + +import BrowserOnly from '@docusaurus/BrowserOnly'; + + + {() => { + window.location.href = 'https://www.eventcatalog.dev/llms-full.txt'; + return null; + }} + + +EventCatalog Docs llms-full.txt diff --git a/packages/core/docs/development/agent-resources/llms.md b/packages/core/docs/development/agent-resources/llms.md new file mode 100644 index 000000000..dfbe9e20f --- /dev/null +++ b/packages/core/docs/development/agent-resources/llms.md @@ -0,0 +1,17 @@ +--- +sidebar_position: 2 +sidebar_label: EventCatalog Docs llms.txt +title: EventCatalog Docs llms.txt +custom_edit_url: null +--- + +import BrowserOnly from '@docusaurus/BrowserOnly'; + + + {() => { + window.location.href = 'https://www.eventcatalog.dev/llms.txt'; + return null; + }} + + +EventCatalog Docs llms.txt diff --git a/packages/core/docs/development/ask-your-architecture/01-intro.md b/packages/core/docs/development/ask-your-architecture/01-intro.md new file mode 100644 index 000000000..8a3af3dac --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/01-intro.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 1 +keywords: +- components +sidebar_label: Using AI with EventCatalog +title: AI with EventCatalog +description: Architecture documentation for humans and AI +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +EventCatalog creates an internal graph of your architecture. Perfect for AI and LLMs. + +EventCatalog supports five ways to use AI with your catalog: + +- [llms.txt](/docs/development/ask-your-architecture/llms.txt) - Give AI tools a machine-readable index of your catalog. +- [schemas.txt](/docs/development/ask-your-architecture/schemas.txt) - Give AI tools access to your schemas and specifications. +- [EventCatalog Assistant](/docs/development/ask-your-architecture/eventcatalog-assistant/what-is-eventcatalog-assistant) - AI Chat in your Catalog. +- [EventCatalog MCP Server](/docs/development/ask-your-architecture/mcp-server/introduction) - Connect EventCatalog to Agents through MCP +- [EventCatalog Agent](/docs/development/ask-your-architecture/agents/overview) - CI/CD Agent for breaking changes and doc updates +- [AI Skills](/docs/development/ask-your-architecture/skills/introduction) - Install our Skills to help you manage your catalog diff --git a/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/01-what-is-eventcatalog-assistant.md b/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/01-what-is-eventcatalog-assistant.md new file mode 100644 index 000000000..c88908423 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/01-what-is-eventcatalog-assistant.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog Assistant +sidebar_label: What is EventCatalog Assistant? +title: What is EventCatalog Assistant? +description: Learn how EventCatalog Assistant helps users explore documentation using natural language. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +EventCatalog Assistant brings AI chat to your catalog. + +It is embedded directly into your documentation site, enabling users to quickly find answers, understand your system, and succeed without manually searching through pages. + +**You bring your own model to EventCatalog and your data is owned by you and never shared.** + +The assistant integrates with your preferred AI models using the [AI SDK](https://ai-sdk.dev/), giving you full control over which models power the experience. + +![EventCatalog Assistant](./images/example.png) diff --git a/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md b/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md new file mode 100644 index 000000000..48e02973a --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog Assistant +sidebar_label: Configuration +title: Configuration +description: Configure EventCatalog Assistant +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +**EventCatalog Assistant is turned off by default.** + +To enable the assistant feature, you need to set the following: + +1. Turn on the `chat` feature in your `eventcatalog.config.js` file and add +2. Add a `eventcatalog.chat.js` file to your catalog. + +### Enabling the feature + +To turn on the assistant feature, you need to set the following: + +```js title="eventcatalog.config.js" +module.exports = { + // Enable the chat feature in your catalog + chat: { + enabled: true, + }, + // AI integrations require you to run eventcatalog as as server + output: 'server' +}; +``` + +### Installing your model and configuring `eventcatalog.chat.js` file + +First you have to install your model of choice ([list of models](https://ai-sdk.dev/docs/foundations/providers-and-models#ai-sdk-providers)) and configure the relevant secrets in your `.env` file. + +_Example of installing the OpenAI model:_ + +```md + +npm install @ai-sdk/openai +``` + +#### Configuring `eventcatalog.chat.js` + +This file will provide the model and any model configuration to EventCatalog. + +In the example below we are using the OpenAI model `gpt-4.1-nano` and configuring the model with some additional parameters. + +```js title="eventcatalog.chat.js" +import { openai } from '@ai-sdk/openai'; + +// Export your model using the default export +export default async () => { + return openai('gpt-4.1-nano'); +} + +// Export the configuration for the model (optional) +export const configuration = { + topP: 0.9, + topK: 40, + frequencyPenalty: 0.0, + presencePenalty: 0.0, + temperature: 0.7, + maxTokens: 10000, +} +``` + +Once you have enabled the feature and configured your model, restart EventCatalog and you can start asking questions about your architecture. diff --git a/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/03-bring-your-own-tools.md b/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/03-bring-your-own-tools.md new file mode 100644 index 000000000..bf0169cd5 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/03-bring-your-own-tools.md @@ -0,0 +1,383 @@ +--- +sidebar_position: 3 +keywords: +- EventCatalog Assistant +- Custom Tools +- AI Tools +- Runtime Data +- Metrics +sidebar_label: Configure your own tools +title: Custom Tools +description: Extend EventCatalog Assistant with custom tools to bring real-time data, metrics, and integrations into your architecture conversations +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +EventCatalog Assistant comes with built-in tools that allow the AI to search and understand your architecture documentation. But what if you could go beyond static documentation and bring **real-time data** directly into your conversations? + +With custom tools, you can extend the assistant to query your production metrics, check service health, look up on-call engineers, fetch data from your databases, and much more. + +## Why custom tools? + +Your architecture documentation tells part of the story, but the real value often lies in the runtime data: + +- **Production metrics** - How many events per second? What's the error rate? +- **Service health** - Is OrderService healthy right now? +- **Queue depths** - Are there any event backlogs building up? +- **On-call information** - Who should I contact about PaymentService? +- **Database queries** - What's the current state of this entity? +- **External APIs** - Enrich answers with data from Datadog, PagerDuty, Jira, etc. + +Custom tools transform EventCatalog from a static documentation site into a **live knowledge hub** where developers can ask questions like: + +> "Is OrderService healthy and who should I contact if there's an issue?" + +And get real answers based on live data. + +## How it works + +Custom tools are defined in your `eventcatalog.chat.js` file alongside your model configuration. Each tool has: + +1. **A description** - Tells the AI when to use this tool +2. **An input schema** - Defines what parameters the tool accepts (using Zod) +3. **An execute function** - The code that runs when the tool is called + +The AI automatically decides when to use your tools based on the user's question and the tool descriptions. + +## Creating custom tools + +### Basic example + +Here's a simple tool that returns service health information: + +```js title="eventcatalog.chat.js" +import { anthropic } from '@ai-sdk/anthropic'; +import { tool } from 'ai'; +import { z } from 'zod'; + +// Export your model +export default async () => { + return anthropic('claude-haiku-4-5'); +} + +// Export custom tools +export const tools = { + getServiceHealth: tool({ + description: 'Get the current health status of a service including uptime and active instances. Use this when users ask if a service is up, healthy, or having issues.', + inputSchema: z.object({ + serviceName: z.string().describe('The name of the service to check health for'), + }), + execute: async ({ serviceName }) => { + // In production, query your monitoring system (Datadog, Prometheus, etc.) + const response = await fetch(`https://your-monitoring-api.com/health/${serviceName}`); + const health = await response.json(); + + return { + serviceName, + status: health.status, + uptime: health.uptime, + instances: health.activeInstances, + lastIncident: health.lastIncident, + }; + }, + }), +}; +``` + +### The AI uses tools automatically + +Once configured, the AI will automatically use your tools when relevant. If a user asks: + +> "Is the OrderService healthy?" + +The assistant will: +1. Recognize this is a health-related question +2. Call your `getServiceHealth` tool with `serviceName: "OrderService"` +3. Use the returned data to formulate a helpful response + +## Example tools + +### Production metrics + +Query real-time metrics from your observability platform: + +```js +getEventMetrics: tool({ + description: 'Get real-time production metrics for an event including throughput, latency, and error rates. Use this when users ask about event performance, traffic, or production health.', + inputSchema: z.object({ + eventId: z.string().describe('The event ID to get metrics for'), + timeRange: z.enum(['1h', '24h', '7d', '30d']).default('24h').describe('Time range for metrics'), + }), + execute: async ({ eventId, timeRange }) => { + // Query Datadog, Prometheus, CloudWatch, etc. + const metrics = await datadogClient.getMetrics(eventId, timeRange); + + return { + eventId, + timeRange, + throughput: `${metrics.eventsPerSecond.toLocaleString()} events/sec`, + latency: { + p50: `${metrics.p50}ms`, + p99: `${metrics.p99}ms`, + }, + errorRate: `${metrics.errorRate}%`, + status: metrics.errorRate > 0.1 ? 'degraded' : 'healthy', + }; + }, +}), +``` + +### On-call information + +Look up who's on-call for a service: + +```js +getOnCall: tool({ + description: 'Get the current on-call engineer and escalation contacts for a service. Use this when users ask who to contact, who owns a service, or who is on-call.', + inputSchema: z.object({ + serviceName: z.string().describe('The name of the service to get on-call info for'), + }), + execute: async ({ serviceName }) => { + // Query PagerDuty, OpsGenie, or your internal system + const oncall = await pagerdutyClient.getOnCall(serviceName); + + return { + serviceName, + team: oncall.team, + primary: { + name: oncall.primary.name, + email: oncall.primary.email, + slack: oncall.primary.slack, + }, + secondary: oncall.secondary, + slackChannel: oncall.slackChannel, + escalationPolicy: oncall.escalationUrl, + }; + }, +}), +``` + +### Queue depth and consumer lag + +Monitor your message brokers: + +```js +getQueueDepth: tool({ + description: 'Get the current queue depth, consumer lag, and processing rate for an event. Use this when users ask about event backlogs, processing delays, or queue health.', + inputSchema: z.object({ + eventId: z.string().describe('The event ID to check queue depth for'), + environment: z.enum(['production', 'staging', 'development']).default('production'), + }), + execute: async ({ eventId, environment }) => { + // Query Kafka, RabbitMQ, SQS, etc. + const queue = await kafkaClient.getConsumerLag(eventId, environment); + + return { + eventId, + environment, + status: queue.lag > 30 ? 'critical' : queue.lag > 5 ? 'warning' : 'healthy', + queue: { + depth: queue.depth.toLocaleString(), + oldestMessage: `${queue.lag.toFixed(1)} seconds ago`, + }, + consumers: { + active: queue.consumers, + processingRate: `${queue.rate.toLocaleString()} events/sec`, + }, + }; + }, +}), +``` + +### Database queries + +Look up entity state from your databases: + +```js +getEntityState: tool({ + description: 'Get the current state of an entity from the database. Use this when users ask about the current state of an order, user, or other business entity.', + inputSchema: z.object({ + entityType: z.enum(['order', 'user', 'product', 'inventory']), + entityId: z.string().describe('The ID of the entity to look up'), + }), + execute: async ({ entityType, entityId }) => { + // Query your database + const entity = await db.collection(entityType).findOne({ id: entityId }); + + return { + entityType, + entityId, + state: entity.state, + lastUpdated: entity.updatedAt, + history: entity.stateHistory?.slice(-5), // Last 5 state changes + }; + }, +}), +``` + +## Best practices + +### Write clear descriptions + +The AI uses tool descriptions to decide when to call them. Be specific about: +- What the tool does +- When it should be used +- What kind of questions it answers + +```js +// ❌ Vague description +description: 'Gets metrics', + +// ✅ Clear description +description: 'Get real-time production metrics for an event including throughput, latency, and error rates. Use this when users ask about event performance, traffic, or production health.', +``` + +### Return structured data + +Return well-structured objects that the AI can easily interpret: + +```js +// ❌ Raw data dump +return rawApiResponse; + +// ✅ Structured, meaningful data +return { + serviceName, + status: health.status, + statusEmoji: health.status === 'healthy' ? '✅' : '⚠️', + uptime: `${health.uptime}%`, + recommendation: health.status !== 'healthy' + ? 'Consider scaling up instances' + : 'No action needed', +}; +``` + +### Handle errors gracefully + +Always handle potential errors in your tools: + +```js +execute: async ({ serviceName }) => { + try { + const health = await monitoringApi.getHealth(serviceName); + return { serviceName, ...health }; + } catch (error) { + return { + serviceName, + error: `Unable to fetch health data: ${error.message}`, + suggestion: 'Check if the monitoring API is available', + }; + } +}, +``` + +### Secure your tools + +Remember that tools execute server-side. Keep security in mind: + +- **Validate inputs** - Don't trust user-provided data +- **Use least privilege** - Only grant tools the permissions they need +- **Protect secrets** - Store API keys in environment variables +- **Rate limit** - Consider adding rate limiting for expensive operations + +```js +execute: async ({ serviceName }) => { + // Validate the service name exists in your catalog + const validServices = await getServices(); + if (!validServices.includes(serviceName)) { + return { error: 'Service not found in catalog' }; + } + + // Proceed with the query... +}, +``` + +## Complete example + +Here's a complete `eventcatalog.chat.js` with multiple tools: + +```js title="eventcatalog.chat.js" +import { anthropic } from '@ai-sdk/anthropic'; +import { tool } from 'ai'; +import { z } from 'zod'; + +// Your model configuration +export default async () => { + return anthropic('claude-haiku-4-5'); +} + +export const configuration = { + temperature: 0.7, + maxTokens: 10000, +} + +// Your custom tools +export const tools = { + getEventMetrics: tool({ + description: 'Get real-time production metrics for an event including throughput, latency, and error rates.', + inputSchema: z.object({ + eventId: z.string(), + timeRange: z.enum(['1h', '24h', '7d', '30d']).default('24h'), + }), + execute: async ({ eventId, timeRange }) => { + // Your implementation + }, + }), + + getServiceHealth: tool({ + description: 'Get the current health status of a service including uptime and active instances.', + inputSchema: z.object({ + serviceName: z.string(), + }), + execute: async ({ serviceName }) => { + // Your implementation + }, + }), + + getOnCall: tool({ + description: 'Get the current on-call engineer and escalation contacts for a service.', + inputSchema: z.object({ + serviceName: z.string(), + }), + execute: async ({ serviceName }) => { + // Your implementation + }, + }), + + getQueueDepth: tool({ + description: 'Get the current queue depth and consumer lag for an event.', + inputSchema: z.object({ + eventId: z.string(), + environment: z.enum(['production', 'staging', 'development']).default('production'), + }), + execute: async ({ eventId, environment }) => { + // Your implementation + }, + }), +}; +``` + +## Viewing available tools + +Users can see all available tools (including custom ones) by clicking the **wrench icon** in the chat panel. Custom tools are labeled with a "Custom" badge to distinguish them from built-in tools. + +## What can you build? + +The possibilities are endless. Here are some ideas: + +- **Cost tracking** - "How much did the OrderCreated event cost to process last month?" +- **Compliance checks** - "Does the PaymentService meet our SLA requirements?" +- **Deployment info** - "When was InventoryService last deployed?" +- **Incident history** - "What incidents has NotificationService had this quarter?" +- **Schema validation** - "Would this schema change break any consumers?" +- **Test coverage** - "What's the test coverage for OrderService?" +- **Documentation gaps** - "Which events are missing descriptions?" + +Custom tools turn EventCatalog into your organization's single pane of glass for architecture knowledge—combining static documentation with live operational data. + +--- + +Have questions about custom tools? [Join our Discord community](https://eventcatalog.dev/discord) to share ideas and get help. diff --git a/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/_category_.json b/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/_category_.json new file mode 100644 index 000000000..a9c24dc4d --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/02-eventcatalog-assistant/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "EventCatalog Assistant", + "position": 4, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "/development/guides/eventcatalog-assistant", + "description": "EventCatalog Assistant" + } +} diff --git a/packages/core/docs/development/ask-your-architecture/02-llms.txt.md b/packages/core/docs/development/ask-your-architecture/02-llms.txt.md new file mode 100644 index 000000000..6b3c8b6f6 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/02-llms.txt.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 2 +keywords: +- llms.txt +- AI +- LLM +sidebar_label: llms.txt +title: LLMS.txt +description: Understanding how to use LLMS.txt with EventCatalog and your LLMs +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +Enable tools like Claude, ChatGPT, GitHub Copilot, and Cursor to quickly understand your EventCatalog. + +### What is LLMS.txt? + +[LLMS.txt](https://llmstxt.org/) is a proposed standard that helps AI-powered development tools better understand and interact with your documentation. Similar to how robots.txt guides web crawlers, LLMS.txt provides structured information that makes it easier for AI assistants like Claude, ChatGPT, and GitHub Copilot to process your EventCatalog documentation. + +The file is automatically generated and maintained as part of your documentation pipeline, requiring no manual configuration. It organizes your documentation's key concepts, structures, and relationships in a format optimized for machine reading. + +### llms.txt and llms-full.txt + +The `llms.txt` file includes your EventCatalog resources in a simple format, listing each resource with a short summary. + +The `llms-full.txt` file includes your EventCatalog resources in a more detailed format — all the contents of your catalog resources are included in the file. + +Both files cover events, commands, queries, services, domains, flows, channels, teams, users, entities, data products, and ubiquitous languages. + +### Enable in EventCatalog + +**`llms.txt` is enabled by default in EventCatalog.** + +You can disable it by turning it off in your `eventcatalog.config.js` file. + +```js title="eventcatalog.config.js" +llmsTxt: { + enabled: false, +}, +``` + +### Access the files + + + +The recommended URLs for AI tools are at the catalog root: + + - `https:///llms.txt` + - Demo: https://demo.eventcatalog.dev/llms.txt + - `https:///llms-full.txt` + - Demo: https://demo.eventcatalog.dev/llms-full.txt + +The legacy paths `/docs/llm/llms.txt` and `/docs/llm/llms-full.txt` still work for backward compatibility. + +### How to use LLMS.txt? + +Once you deploy your EventCatalog you can use your tools to ask questions about your Catalog. + +![LLMS.txt](./img/ai-example.png) + + + + diff --git a/packages/core/docs/development/ask-your-architecture/03-mcp-server/_category_.json b/packages/core/docs/development/ask-your-architecture/03-mcp-server/_category_.json new file mode 100644 index 000000000..a859825bc --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/03-mcp-server/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "EventCatalog MCP Server", + "position": 5, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "development/developer-tools/mcp-server", + "title": "MCP Server", + "description": "This section contains tutorials for the EventCatalog MCP Server." + } +} diff --git a/packages/core/docs/development/ask-your-architecture/03-mcp-server/getting-started.md b/packages/core/docs/development/ask-your-architecture/03-mcp-server/getting-started.md new file mode 100644 index 000000000..013405f6b --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/03-mcp-server/getting-started.md @@ -0,0 +1,288 @@ +--- +sidebar_position: 2 +keywords: +- MCP Server +sidebar_label: Installation +title: Getting started +description: Connect MCP clients to your EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +### Prerequisites + +1. **SSR mode** - EventCatalog must run in [server mode](/docs/development/deployment/build-ssr-mode#building-your-eventcatalog-ssr) (not static) +2. **Scale license** - You can get a 30-day free trial at [eventcatalog.cloud](https://eventcatalog.cloud) + +### Quick start + +Your MCP server is available at: + +``` +https://your-eventcatalog.com/docs/mcp/ +``` + +For local development: + +``` +http://localhost:3000/docs/mcp/ +``` + +### Verify the server + +Visit the endpoint in your browser to verify. It returns available tools and resources: + +```json +{ + "name": "EventCatalog MCP Server", + "version": "1.0.0", + "status": "running", + "tools": ["getResources", "getResource", ...], + "resources": ["eventcatalog://all", "eventcatalog://events", ...] +} +``` + +### Protect with OAuth + + + +The built-in MCP server can be protected with OAuth Bearer tokens, following the MCP authorization specification for HTTP transports. + +EventCatalog acts as the OAuth protected resource server for `/docs/mcp`. Your identity provider or authorization server remains responsible for user login, consent, client registration, `/authorize`, `/oauth/token`, and token refresh. + +Configure MCP authorization in `eventcatalog.config.js`: + +```js title="eventcatalog.config.js" +module.exports = { + output: 'server', + mcp: { + auth: { + enabled: true, + resource: 'https://your-eventcatalog.com/docs/mcp', + authorizationServers: ['https://auth.example.com'], + issuer: 'https://auth.example.com', + audience: 'https://your-eventcatalog.com/docs/mcp', + requiredScopes: ['catalog:read'], + jwksUri: 'https://auth.example.com/.well-known/jwks.json', + }, + }, +}; +``` + +When enabled, EventCatalog serves protected resource metadata at `/.well-known/oauth-protected-resource`. Unauthenticated MCP clients receive a `401 Unauthorized` response with a `WWW-Authenticate` header pointing at that document. MCP clients then obtain an access token from the advertised authorization server and call `/docs/mcp` with: + +```http +Authorization: Bearer +``` + +The access token must be valid, unexpired, issued by the configured issuer, intended for the configured audience, and include all required scopes. + +#### Key signing options + +Choose one of the following strategies for token validation: + +| Strategy | Config fields | +|---|---| +| JWKS endpoint (recommended) | `jwksUri` | +| Inline asymmetric public key | `publicKey` or `publicKeyEnvVar` | +| Symmetric shared secret | `sharedSecret` or `sharedSecretEnvVar` | + +Prefer `publicKeyEnvVar` or `sharedSecretEnvVar` over inline values to avoid committing secrets to source control. + +#### All options + +| Field | Required | Description | +|---|---|---| +| `enabled` | Yes | Enables OAuth Bearer token validation | +| `resource` | No | Absolute URL of the MCP resource. Set this explicitly when behind a proxy | +| `protectedResourceMetadataUrl` | No | URL for the protected resource metadata document. Defaults to `/.well-known/oauth-protected-resource` | +| `authorizationServers` | No | Authorization server URLs advertised to MCP clients | +| `issuer` | No | Expected token issuer (`iss` claim) | +| `audience` | No | Expected token audience (`aud` claim). Defaults to `resource` | +| `requiredScopes` | No | Scopes every token must include | +| `jwksUri` | No | JWKS endpoint for asymmetric JWT validation | +| `publicKey` | No | Inline public key for asymmetric JWT validation | +| `publicKeyEnvVar` | No | Environment variable containing the public key | +| `sharedSecret` | No | Inline shared secret for symmetric JWT validation | +| `sharedSecretEnvVar` | No | Environment variable containing the shared secret | + +:::note Existing website authentication +The `auth.enabled` and `eventcatalog.auth.js` settings protect the EventCatalog website with browser sessions. MCP authorization is separate because MCP clients authenticate with Bearer tokens, not browser cookies. +::: + +:::tip Authorization server discovery +EventCatalog serves `/.well-known/oauth-protected-resource` for MCP client discovery. It does not serve `/.well-known/oauth-authorization-server`, `/authorize`, or `/oauth/token` -- those endpoints must be provided by the authorization server listed in `authorizationServers`. If your MCP client expects those endpoints on the catalog host, proxy the authorization server behind that host with your load balancer or reverse proxy. +::: + +### Connect clients + +
    +Claude Desktop +1. Get your MCP URL `https://your-eventcatalog.com/docs/mcp/` +1. Navigate to the [Connectors](https://claude.ai/settings/connectors) page in Claude Settings. +1. Select **Add custom connector** +1. Select **Add** +5. When using Claude, select the attachments button (the plus icon). +6. Select your MCP server. +
    + +
    +Claude Code +1. Get your MCP URL `https://your-eventcatalog.com/docs/mcp/` +2. Run the command to connect claude code to your eventcatalog instance +```bash +claude mcp add --transport http +``` +
    + +
    +Cursor +1. Get your MCP URL `https://your-eventcatalog.com/docs/mcp/` +1. Use `Command` + `Shift` + `P` (`Ctrl` + `Shift` + `P` on Windows) to open the Command Palette. +1. Search for "Open MCP settings" +1. Select **Add custom MCP.** This opens the `mcp.json` file. +1. Add the following to the `mcp.json` file: +```json +{ + "servers": { + "": { + "url": "https://your-eventcatalog.com/docs/mcp/" + } + } +} +``` +
    + +
    +VS Code +1. Get your MCP URL `https://your-eventcatalog.com/docs/mcp/` +1. Create a `.vscode/mcp.json` file. +1. Inside the `mcp.json` file, add the following: +```json +{ + "servers": { + "": { + "type": "http", + "url": "https://your-eventcatalog.com/docs/mcp/" + } + } +} +``` +
    + +## Available tools + +### 15 built-in tools + +- `getResources` - Get events, services, commands, queries, flows, domains +- `getResource` - Get a specific resource by id and version +- `getMessagesProducedOrConsumedByResource` - Messages a resource sends/receives +- `getSchemaForResource` - Get OpenAPI, AsyncAPI, or other schemas +- `findResourcesByOwner` - Resources owned by a team or user +- `getProducersOfMessage` - Services that produce a message +- `getConsumersOfMessage` - Services that consume a message +- `analyzeChangeImpact` - Impact of changing a message +- `explainBusinessFlow` - Detailed flow information +- `getTeams` / `getTeam` - Query teams +- `getUsers` / `getUser` - Query users +- `findMessageBySchemaId` - Find messages by schema identifiers +- `explainUbiquitousLanguageTerms` - DDD ubiquitous language from domains + +[See full API documentation →](/docs/development/ask-your-architecture/mcp-server/getting-started) + +### 12 resources + +- `eventcatalog://all` - All resources +- `eventcatalog://events` - All events +- `eventcatalog://commands` - All commands +- `eventcatalog://queries` - All queries +- `eventcatalog://services` - All services +- `eventcatalog://channels` - All channels +- `eventcatalog://diagrams` - All diagrams +- `eventcatalog://containers` - All containers +- `eventcatalog://domains` - All domains +- `eventcatalog://flows` - All flows +- `eventcatalog://teams` - All teams +- `eventcatalog://users` - All users + +## Add custom tools + +Extend the MCP server with custom tools in `eventcatalog.chat.js`: + +```javascript +// eventcatalog.chat.js +export const tools = { + myCustomTool: { + description: 'My custom tool for EventCatalog', + parameters: z.object({ + query: z.string().describe('The query parameter'), + }), + execute: async ({ query }) => { + // Your custom logic here + return { result: 'Custom data' }; + }, + }, +}; +``` + +Custom tools appear alongside built-in tools automatically. + +## Use standalone server + +For catalogs without SSR mode, use the standalone `@eventcatalog/mcp-server` package. We plan to deprecate this in a future release, so we recommend migrating to the built-in server when possible. + +
    +Standalone server on stdio + +For local development and testing, you can use the MCP Server on stdio. This is useful for single-client, low-latency tools. + +**Prerequisites:** +- EventCatalog configured with the [`LLMS.txt` feature](/docs/development/ask-your-architecture/llms.txt) +- EventCatalog Scale license +- MCP client installed + +**Command:** + +```bash +npx -y @eventcatalog/mcp-server {URL_TO_YOUR_EVENTCATALOG_INSTANCE} {EVENTCATALOG_LICENSE_KEY} +``` + +
    + +
    +Standalone server over HTTP + +Run the MCP Server over HTTP for production deployments. + +**Prerequisites:** +- EventCatalog instance running +- EventCatalog Scale license +- MCP client installed + +**Run using npx:** + +```bash +npx -y @eventcatalog/mcp-server https://your-eventcatalog-instance.com {EVENTCATALOG_LICENSE_KEY} http {PORT} {ROOT_PATH} +``` + +**Example:** + +```bash +npx -y @eventcatalog/mcp-server https://demo.eventcatalog.dev {EVENTCATALOG_LICENSE_KEY} http 3000 /mcp +``` + +This starts the MCP Server over HTTP on port 3000 with root path `/mcp`. + +**Run using Docker:** + +See [instructions on the GitHub repository](https://github.com/event-catalog/mcp-server/blob/main/README.Docker.md). + +
    + + + + + diff --git a/packages/core/docs/development/ask-your-architecture/03-mcp-server/introduction.md b/packages/core/docs/development/ask-your-architecture/03-mcp-server/introduction.md new file mode 100644 index 000000000..69b91c348 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/03-mcp-server/introduction.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 1 +keywords: +- MCP Server +sidebar_label: Introduction +title: MCP server +description: Connect AI tools to your architecture catalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +EventCatalog exposes an MCP server. This allows you to connect any MCP client to your catalog to fetch the relevant information about your architecture when you need it. + +EventCatalog MCP server has out the box tools, that helps your AI agents and LLMs query the correct information based on the task it is trying to do. + + + + + + diff --git a/packages/core/docs/development/ask-your-architecture/03-schemas.txt.md b/packages/core/docs/development/ask-your-architecture/03-schemas.txt.md new file mode 100644 index 000000000..da723460f --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/03-schemas.txt.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 3 +keywords: +- mermaid +sidebar_label: schemas.txt +title: schemas.txt +description: Understanding how to use schemas.txt with EventCatalog and your schemas +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +Enable tools like Claude, ChatGPT, GitHub Copilot, and Cursor to quickly fetch and understand your EventCatalog schemas. + +Message schemas and service specifications can be fetched from your EventCatalog and used in your own applications. + +### What is schemas.txt? + +Schemas.txt is very similar to [LLMS.txt](/docs/development/ask-your-architecture/llms.txt), but it is specifically for schemas in your EventCatalog. + +Schemas.txt supports any schema format for your messages (e.g Avro, Protobuf, JSON Schema etc) and any specification for your services (e.g OpenAPI, AsyncAPI, GraphQL etc). + +The schemas.txt file is automatically generated and maintained as part of your documentation pipeline, requiring no manual configuration. It organizes your schemas in a format optimized for machine reading. + +### schemas.txt and schemas-full.txt + +The `schemas.txt` file includes your EventCatalog schemas in a simple format. Lists your schemas with a summary for each of them. + +### How to use schemas.txt? + +The [EventCatalog MCP](/docs/development/ask-your-architecture/mcp-server/introduction) already uses the schemas.txt file to provide access to your schemas in your MCP clients (e.g Cursor, Windsurf, Claude Desktop etc). + +If you want to use schemas.txt in your own application, you can query the urls: + + - `https:///docs/llm/schemas.txt` + - Demo: https://demo.eventcatalog.dev/docs/llm/schemas.txt + + + + diff --git a/packages/core/docs/development/ask-your-architecture/04-agents/01-overview.md b/packages/core/docs/development/ask-your-architecture/04-agents/01-overview.md new file mode 100644 index 000000000..2bc26b2af --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/04-agents/01-overview.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 1 +keywords: + - AI Agents + - documentation generation + - automation +sidebar_label: Overview +title: EventCatalog Agents +description: AI agents that help you manage and document your architecture with EventCatalog +--- + +[EventCatalog Agent](https://github.com/event-catalog/agents) is an agent that helps you manage your documentation through CI/CD and adds a governing layer to your pull requests. + +Our Agent understands EventCatalog conventions (domains, systems and resources) and uses that understanding to keep your architecture documented for you, without you having to hand-write and maintain every catalog file yourself. + +EventCatalog Agent lets you define and bring your own model. + +### Available workflows + +| Agent | What it does | +| --- | --- | +| [Code-to-Docs](/docs/development/ask-your-architecture/agents/code-to-docs) | The EventCatalog Agent will review your pull request code and update your catalog documentation for you. | +| [Breaking Changes](/docs/development/ask-your-architecture/agents/breaking-changes) | Detects breaking schema changes in a pull request and shows which catalog consumers could be affected. | + +### Have an idea for a workflow? + +More agents and AI workflows are on the way. If you have an idea for an agent or an AI workflow you'd like to see in EventCatalog, [open an issue on GitHub](https://github.com/event-catalog/agents/issues/new) and let us know. We use these ideas to decide what to build next. diff --git a/packages/core/docs/development/ask-your-architecture/04-agents/02-code-to-docs.md b/packages/core/docs/development/ask-your-architecture/04-agents/02-code-to-docs.md new file mode 100644 index 000000000..c831ad75b --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/04-agents/02-code-to-docs.md @@ -0,0 +1,124 @@ +--- +sidebar_position: 2 +keywords: + - Code to Docs + - AI Agents + - GitHub Action + - documentation generation +sidebar_label: Code-to-Docs Agent +title: Code-to-Docs Agent +description: Keep your EventCatalog documentation in sync with your code using the Code-to-Docs agent +--- + +The **Code-to-Docs** agent keeps your EventCatalog documentation in sync with your code. + +When you open a pull request, the agent reviews the diff, works out which documentation should change, updates it in your catalog repository, and opens (or updates) a documentation pull request. It then comments back on your source pull request with a summary and a link. + +## How it works + +```mermaid +flowchart LR + PR[Source pull request] --> Agent[Code-to-Docs agent] + Agent --> Plan{Docs need
    updating?} + Plan -- No --> Comment[Comment on source PR] + Plan -- Yes --> Docs[Update catalog docs] + Docs --> CatalogPR[Open catalog pull request] + CatalogPR --> Comment +``` + +When a pull request is opened, the agent: + +1. **Checks out your catalog** so it can see your existing documentation. +2. **Collects the changed source files** from the pull request. +3. **Plans the impact**. A read-only pass where the agent decides whether the diff requires any documentation changes, and if so, exactly which catalog resources should change. If nothing is needed, it stops here and says so. +4. **Applies the plan**. The agent updates the documentation, using EventCatalog conventions for frontmatter and folder structure, and a linter to validate its changes. It is only allowed to touch the resources approved in the plan. +5. **Opens a catalog pull request** with the changes for you to review. +6. **Comments on your source pull request** with a high-level summary and a link to the catalog pull request. + +You stay in control: the agent never edits your catalog silently. Every change arrives as a pull request you can review, tweak, and merge. + +## Getting started + +The Code-to-Docs agent runs as a GitHub Action. Add it to your repository in three steps. + +### 1. Add a new GitHub workflow + +Create a new workflow in your source directory. Typically this is where your source code lives (e.g a service that publishes/consumes messages). + +Create a `.github/workflows/eventcatalog.yml` file. + +```yaml +on: + pull_request: + +jobs: + eventcatalog: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: event-catalog/agents@main + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + agent: code-to-docs + catalog-repo: your-org/your-catalog + catalog-token: ${{ secrets.EVENTCATALOG_TOKEN }} +``` + +`fetch-depth: 0` is required so the agent can diff the pull request against its base. + +### 2. Add your model provider key + +Add the API key for your chosen model as a secret in your repository (**Settings → Secrets and variables → Actions**). Use the one that matches your model, for example `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY`. + +You can see the list of [available models here](https://pi.dev/models). + +### 3. Open a pull request + +When you next open a pull request on your project, the EventCatalog Agent will run. + +If your architecture changed, opens a documentation pull request in your catalog repository and comments back with a link. + +## Configuration + +### Inputs + +| Input | Required | Default | Description | +| --- | --- | --- | --- | +| `catalog-repo` | Yes | | The location of your hosted EventCatalog. EventCatalog repository to document into, in `owner/repo` format. | +| `catalog-ref` | No | `main` | Branch checked out from the catalog repository and targeted by documentation pull requests. | +| `catalog-token` | No | `github.token` | Token used to check out the catalog repository and open documentation pull requests. | +| `model` | No | `anthropic/claude-sonnet-4-6` | Model specifier for the agent. See [available models](https://pi.dev/models). | +| `ignore-paths` | No | common build/output paths | Comma-separated paths or glob patterns to ignore in pull request diffs. | +| `agent` | No | `code-to-docs` | Which EventCatalog agent to run. Use `code-to-docs` for this workflow. | + +### Provider API keys + +The model provider's API key is passed as a normal workflow environment variable. Set the one that matches your `model`: + +| Provider | Environment variable | +| --- | --- | +| Anthropic | `ANTHROPIC_API_KEY` | +| OpenAI | `OPENAI_API_KEY` | +| OpenRouter | `OPENROUTER_API_KEY` | + +The agent supports models from many providers. See the full list of model specifiers at [pi.dev/models](https://pi.dev/models). + +### Documenting into a separate catalog repository + +When your catalog lives in a **different** repository from your source code, provide a `catalog-token` with permission to push branches and open pull requests in that catalog repository (the default `github.token` only has access to the current repository). + +:::info Early access +The Code-to-Docs agent is in early access and free to evaluate. In the future, a license will be required to run EventCatalog Agents in production. +::: + +## Found an issue or have feedback? + +The Code-to-Docs agent is open on GitHub at [event-catalog/agents](https://github.com/event-catalog/agents). If you hit a problem, or the agent documents something in a way you didn't expect, [open an issue](https://github.com/event-catalog/agents/issues/new) and let us know. Your feedback during early access directly shapes how the agent works. diff --git a/packages/core/docs/development/ask-your-architecture/04-agents/03-breaking-changes.md b/packages/core/docs/development/ask-your-architecture/04-agents/03-breaking-changes.md new file mode 100644 index 000000000..e3b3000ce --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/04-agents/03-breaking-changes.md @@ -0,0 +1,170 @@ +--- +sidebar_position: 3 +keywords: + - Breaking Changes + - AI Agents + - GitHub Action + - schema governance + - schema compatibility +sidebar_label: Breaking Changes Agent +title: Breaking Changes Agent +description: Detect breaking schema changes and affected EventCatalog consumers from pull requests +--- + +The **Breaking Changes** agent reviews schema changes in pull requests and reports whether they are likely to break existing consumers. + +When a pull request changes a message schema, the agent checks whether the diff removes or renames fields, changes types, adds new required fields, narrows enums, or introduces similar compatibility risks. If it finds a breaking change, it traces the schema through your EventCatalog and comments on the source pull request with the breaking lines and affected consumers. + +The Breaking Changes agent is read-only. It does not edit your catalog or open documentation pull requests. + +![Breaking Changes agent pull request comment showing detected schema changes and affected consumers](./img/breaking-changes-agent.jpeg) + +## How it works + +```mermaid +flowchart LR + PR[Source pull request] --> Agent[Breaking Changes agent] + Agent --> Schema{Schema
    changed?} + Schema -- No --> Skip[Nothing to report] + Schema -- Yes --> Breaking{Breaking
    change?} + Breaking -- No --> Skip + Breaking -- Yes --> Consumers[Find catalog consumers] + Consumers --> Comment[Comment on source PR] +``` + +When a pull request is opened, the agent: + +1. **Checks out your catalog** so it can understand the existing producers, consumers, messages, schemas, and flows. +2. **Collects the changed source files** from the pull request. +3. **Filters to schema files** using the configured schema extensions. +4. **Scores each schema change** for breaking-change risk. Additive changes, such as adding an optional field, are skipped. +5. **Finds affected consumers** for each breaking schema change by tracing the message through EventCatalog. +6. **Comments on your source pull request** with the breaking change, the relevant diff lines, and the consumers that could be affected. + +If the pull request has no changed schema files, or only non-breaking schema changes, the workflow exits without creating a catalog pull request. + +## Getting started + +The Breaking Changes agent runs as a GitHub Action. + +### 1. Add a new GitHub workflow + +Create a `.github/workflows/eventcatalog-breaking-changes.yml` file in the repository where your source changes happen. + +```yaml +on: + pull_request: + +jobs: + eventcatalog-breaking-changes: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: event-catalog/agents@main + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + agent: breaking-changes + catalog-repo: your-org/your-catalog + catalog-token: ${{ secrets.EVENTCATALOG_TOKEN }} +``` + +`fetch-depth: 0` is required so the agent can diff the pull request against its base. + +### 2. Add your model provider key + +Add the API key for your chosen model as a secret in your repository (**Settings → Secrets and variables → Actions**). Use the one that matches your model, for example `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `OPENROUTER_API_KEY`. + +You can see the list of [available models here](https://pi.dev/models). + +### 3. Open a pull request that changes a schema + +When a pull request changes a schema, the agent reviews the schema diff. If it finds a breaking change, it comments on the pull request with the affected EventCatalog consumers. + +## Configuration + +### Inputs + +| Input | Required | Default | Description | +| --- | --- | --- | --- | +| `agent` | No | `code-to-docs` | Set this to `breaking-changes` to run the Breaking Changes agent. | +| `catalog-repo` | Yes | | The EventCatalog repository to inspect, in `owner/repo` format. | +| `catalog-ref` | No | `main` | Branch checked out from the catalog repository. | +| `catalog-token` | No | `github.token` | Token used to check out the catalog repository. Use a token with read access when the catalog is in another private repository. | +| `model` | No | `anthropic/claude-sonnet-4-6` | Model specifier for the agent. See [available models](https://pi.dev/models). | +| `ignore-paths` | No | common build/output paths | Comma-separated paths or glob patterns to ignore in pull request diffs. | +| `schema-extensions` | No | `.json,.yml,.yaml,.avro,.avsc,.proto,.graphql,.gql` | Comma-separated file extensions the agent treats as message schemas. | + +### Schema extensions + +By default, the agent checks common schema files: + +```yaml +schema-extensions: .json,.yml,.yaml,.avro,.avsc,.proto,.graphql,.gql +``` + +If your message contracts live in another file type, add that extension: + +```yaml +- uses: event-catalog/agents@main + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + agent: breaking-changes + catalog-repo: your-org/your-catalog + catalog-token: ${{ secrets.EVENTCATALOG_TOKEN }} + schema-extensions: .json,.yaml,.ts +``` + +Only add source-code extensions when those files contain message contracts. The agent ignores non-schema files for this workflow. + +### Provider API keys + +The model provider's API key is passed as a normal workflow environment variable. Set the one that matches your `model`: + +| Provider | Environment variable | +| --- | --- | +| Anthropic | `ANTHROPIC_API_KEY` | +| OpenAI | `OPENAI_API_KEY` | +| OpenRouter | `OPENROUTER_API_KEY` | + +The agent supports models from many providers. See the full list of model specifiers at [pi.dev/models](https://pi.dev/models). + +## Run with Code-to-Docs + +Each Action step runs one EventCatalog agent. To run Breaking Changes and [Code-to-Docs](/docs/development/ask-your-architecture/agents/code-to-docs) on the same pull request, add two steps: + +```yaml +- uses: event-catalog/agents@main + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + agent: breaking-changes + catalog-repo: your-org/your-catalog + catalog-token: ${{ secrets.EVENTCATALOG_TOKEN }} + +- uses: event-catalog/agents@main + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + agent: code-to-docs + catalog-repo: your-org/your-catalog + catalog-token: ${{ secrets.EVENTCATALOG_TOKEN }} +``` + +Use Breaking Changes when you want pull request feedback about schema compatibility. Use Code-to-Docs when you want catalog documentation updates proposed as a separate pull request. + +:::info Early access +The Breaking Changes agent is in early access and free to evaluate. In the future, a license will be required to run EventCatalog Agents in production. +::: + +## Found an issue or have feedback? + +The Breaking Changes agent is open on GitHub at [event-catalog/agents](https://github.com/event-catalog/agents). If you hit a problem, or the agent reports something in a way you didn't expect, [open an issue](https://github.com/event-catalog/agents/issues/new) and let us know. Your feedback during early access directly shapes how the agent works. diff --git a/packages/core/docs/development/ask-your-architecture/04-agents/_category_.json b/packages/core/docs/development/ask-your-architecture/04-agents/_category_.json new file mode 100644 index 000000000..e1b5e5679 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/04-agents/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Agents", + "position": 6, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "/development/ask-your-architecture/agents", + "title": "EventCatalog Agents", + "description": "AI agents that help you manage and document your architecture with EventCatalog." + } +} diff --git a/packages/core/docs/development/ask-your-architecture/05-skills/01-introduction.md b/packages/core/docs/development/ask-your-architecture/05-skills/01-introduction.md new file mode 100644 index 000000000..0e8021752 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/05-skills/01-introduction.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 1 +keywords: +- AI Skills +- Claude Code +- documentation generation +sidebar_label: Introduction +title: AI Skills +description: Pre-built AI skills to help you document your architecture with EventCatalog +--- + +EventCatalog Skills are pre-built instructions that teach AI agents how to work with your EventCatalog project. Install a skill and your AI agent gains the ability to generate correct documentation, following EventCatalog conventions and best practices. + +Skills work with any AI coding agent that supports the skills format (e.g. [Claude Code](https://claude.ai/code)). + +## Why use skills? + +When you ask an AI agent to generate EventCatalog documentation, it doesn't know about EventCatalog's frontmatter format, folder structure, or component conventions. Skills solve this by providing the agent with structured instructions and reference material. + +With skills installed, you can ask your agent things like: + +- "Document my OrderService that receives OrderCreated events and sends OrderConfirmed events" +- "Create a Payments domain with a PaymentService, PaymentProcessed event, and ProcessPayment command" +- "Look at my src/ directory and generate EventCatalog documentation for the services and events you find" +- "Document the checkout flow from cart submission through payment processing to order confirmation" + +The agent will generate properly formatted `index.mdx` files with correct frontmatter, schemas, and visualizations. + +## Available skills + +| Skill | Description | +|-------|-------------| +| [Catalog Documentation Creator](https://github.com/event-catalog/skills) | Generates EventCatalog documentation files (services, events, commands, queries, domains, flows, channels, containers) with correct frontmatter, folder structure, and best practices. | +| [Flow Wizard](https://github.com/event-catalog/skills) | Guides you through documenting a business flow step by step in a conversational session, cross-referencing your existing catalog resources to link services, events, and commands automatically. | + +## Getting started + +Head to the [installation guide](/docs/development/ask-your-architecture/skills/installation) to add skills to your project. + +**Source code:** [github.com/event-catalog/skills](https://github.com/event-catalog/skills) diff --git a/packages/core/docs/development/ask-your-architecture/05-skills/02-installation.md b/packages/core/docs/development/ask-your-architecture/05-skills/02-installation.md new file mode 100644 index 000000000..0ee1016e0 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/05-skills/02-installation.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 2 +keywords: +- AI Skills +- installation +sidebar_label: Installation +title: Installing AI Skills +description: How to install EventCatalog AI skills for your AI coding agent +--- + +## CLI install (recommended) + +Use [npx skills](https://github.com/vercel-labs/skills) to install skills directly into your project: + +```bash +# Install all EventCatalog skills +npx skills add event-catalog/skills + +# Install a specific skill +npx skills add event-catalog/skills --skill catalog-documentation-creator +``` + +This copies the skill files into your project's `.claude/skills/` directory where your AI agent can access them. + +## Clone and copy + +Clone the repository and copy the skills you need: + +```bash +git clone https://github.com/event-catalog/skills.git +cp -r skills/skills/catalog-documentation-creator .claude/skills/ +``` + +## Git submodule + +Add as a submodule for easy updates: + +```bash +git submodule add https://github.com/event-catalog/skills.git .claude/skills/eventcatalog +``` + +When new skills are released, pull updates with: + +```bash +git submodule update --remote +``` + +## Fork and customize + +Fork the repository and tailor skills to your team's conventions: + +1. Fork [event-catalog/skills](https://github.com/event-catalog/skills) on GitHub +2. Modify skills to match your naming conventions, ownership patterns, and schema formats +3. Install from your fork: + +```bash +npx add-skill https://github.com/YOUR_ORG/eventcatalog-skills +``` + +This is useful when your team has specific conventions for IDs, owners, or folder structures that differ from the defaults. diff --git a/packages/core/docs/development/ask-your-architecture/05-skills/_category_.json b/packages/core/docs/development/ask-your-architecture/05-skills/_category_.json new file mode 100644 index 000000000..a10d5fa0c --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/05-skills/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Skills", + "position": 7, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "/development/ask-your-architecture/skills", + "description": "Pre-built AI skills for generating EventCatalog documentation" + } +} diff --git a/packages/core/docs/development/ask-your-architecture/06-slack-integration/01-introduction.md b/packages/core/docs/development/ask-your-architecture/06-slack-integration/01-introduction.md new file mode 100644 index 000000000..b18559e25 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/06-slack-integration/01-introduction.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 1 +keywords: +- Slack Bot +- Slack Integration +sidebar_label: Introduction +title: Slack integration +description: Query architecture documentation directly from Slack +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +The EventCatalog Slack Bot connects to your EventCatalog MCP server, allowing teams to query architecture documentation directly from Slack. Ask questions about events, services, domains, and schemas without leaving your workspace. + +The slack bot is self hosted, you own your data and deployment. + +
    + EventCatalog Slack Bot + Example of the EventCatalog Slack Bot in action asking about the OrderCreated event +
    + +## How it works + +1. You configure the bot with your own AI provider and model (e.g. Anthropic, OpenAI, Google) +1. Users @mention the bot or post in dedicated channels +2. The bot queries your EventCatalog MCP server +3. AI-powered responses appear in threads to keep channels organized + +## Key features + +- **@Mention anywhere** - Add the bot to any channel and ask questions +- **Dedicated channels** - Create auto-reply channels for architecture questions +- **Multiple AI providers** - Works with Anthropic, OpenAI, or Google +- **Self-hosted** - Your data stays private, you control your data and deployment +- **Socket Mode** - No public URL or webhooks needed +- **Custom tools support** - Bring your own integrations for real-time data + +## Custom tools + +The Slack bot supports [custom tools](/docs/development/ask-your-architecture/eventcatalog-assistant/bring-your-own-tools) defined in your `eventcatalog.chat.js` file. This means you can bring real-time data into your Slack conversations: + +- Query production metrics from Datadog or Prometheus +- Check service health and uptime +- Look up on-call engineers from PagerDuty +- Fetch queue depths from Kafka +- Any custom integration your team needs + +Custom tools work automatically - the AI decides when to use them based on user questions. Ask "who is on-call for OrderService?" and get live data directly in Slack. + +## Prerequisites + +- EventCatalog instance (with configured MCP server) running in [SSR mode](/docs/development/deployment/build-ssr-mode) +- [EventCatalog Scale license](https://eventcatalog.cloud) +- Slack workspace with app creation permissions +- API key for your chosen AI provider + +## Repository + +The EventCatalog Slack Bot is open source: + +[github.com/event-catalog/eventcatalog-slack-bot](https://github.com/event-catalog/eventcatalog-slack-bot) diff --git a/packages/core/docs/development/ask-your-architecture/06-slack-integration/02-slack-app-setup.md b/packages/core/docs/development/ask-your-architecture/06-slack-integration/02-slack-app-setup.md new file mode 100644 index 000000000..55da58d09 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/06-slack-integration/02-slack-app-setup.md @@ -0,0 +1,154 @@ +--- +sidebar_position: 2 +keywords: +- Slack Bot +- Slack App +sidebar_label: Slack app setup +title: Slack app setup +description: Create and configure your Slack app +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +Create a Slack app to enable the EventCatalog bot in your workspace. You can create the app using a manifest (recommended) or manually configure all settings. + +## Create from manifest (recommended) + +The fastest way to get started. The manifest automatically configures all required permissions and settings. + +### Steps + +1. Navigate to [api.slack.com/apps](https://api.slack.com/apps) +2. Click **Create New App** → **From an app manifest** +3. Select your workspace and click **Next** +4. Choose **YAML** and paste this manifest: + +```yaml +display_information: + name: EventCatalog Bot + description: Query your EventCatalog directly from Slack + background_color: "#000000" + +features: + bot_user: + display_name: EventCatalog + always_online: true + +oauth_config: + scopes: + bot: + - app_mentions:read + - chat:write + - reactions:read + - reactions:write + - channels:history + +settings: + event_subscriptions: + bot_events: + - app_mention + - message.channels + interactivity: + is_enabled: false + org_deploy_enabled: false + socket_mode_enabled: true + token_rotation_enabled: false +``` + +5. Click **Next**, review the summary, and click **Create** + +### Get credentials + +After creating the app, collect three credentials needed to run the bot. + +#### App-Level Token + +1. Go to **Basic Information** → **App-Level Tokens** +2. Click **Generate Token and Scopes** +3. Name: `socket-mode-token` +4. Add scope: `connections:write` +5. Click **Generate** and copy the token (starts with `xapp-`) +6. Save this as `SLACK_APP_TOKEN` + +#### Bot Token + +1. Go to **OAuth & Permissions** +2. Click **Install to Workspace** +3. Review permissions and click **Allow** +4. Copy the **Bot User OAuth Token** (starts with `xoxb-`) +5. Save this as `SLACK_BOT_TOKEN` + +#### Signing Secret + +1. Go to **Basic Information** → **App Credentials** +2. Copy the **Signing Secret** +3. Save this as `SLACK_SIGNING_SECRET` + +
    +Setup Slack app manually + +If you prefer to configure settings yourself, follow these steps. + +### Create the app + +1. Navigate to [api.slack.com/apps](https://api.slack.com/apps) +2. Click **Create New App** → **From scratch** +3. Name your app (e.g., "EventCatalog Bot") and select your workspace +4. Click **Create App** + +### Enable Socket Mode + +1. Click **Socket Mode** in the left sidebar +2. Toggle **Enable Socket Mode** to On +3. Create an App-Level Token when prompted: + - Name: `socket-mode-token` + - Scope: `connections:write` +4. Click **Generate** +5. Copy the token (starts with `xapp-`) - this is `SLACK_APP_TOKEN` + +### Configure permissions + +1. Click **OAuth & Permissions** in the left sidebar +2. Scroll to **Scopes** → **Bot Token Scopes** +3. Add these scopes: + - `app_mentions:read` - Receive @mention events + - `chat:write` - Send messages + - `reactions:read` - Read reactions + - `reactions:write` - Add and remove reactions + - `channels:history` - Read public channel messages + +### Enable events + +1. Click **Event Subscriptions** in the left sidebar +2. Toggle **Enable Events** to On +3. Expand **Subscribe to bot events** +4. Add these events: + - `app_mention` + - `message.channels` + +### Install the app + +1. Click **OAuth & Permissions** in the left sidebar +2. Click **Install to Workspace** +3. Review permissions and click **Allow** +4. Copy the **Bot User OAuth Token** (starts with `xoxb-`) - this is `SLACK_BOT_TOKEN` + +### Get signing secret + +1. Click **Basic Information** in the left sidebar +2. Scroll to **App Credentials** +3. Copy the **Signing Secret** - this is `SLACK_SIGNING_SECRET` + +
    + +## Optional: Private channels and DMs + +By default, the bot only reads thread context in public channels. To enable thread context in private channels and DMs, add these additional scopes: + +- `groups:history` - Private channels +- `im:history` - Direct messages +- `mpim:history` - Group DMs + +After adding scopes, reinstall the app to your workspace. diff --git a/packages/core/docs/development/ask-your-architecture/06-slack-integration/03-installation.md b/packages/core/docs/development/ask-your-architecture/06-slack-integration/03-installation.md new file mode 100644 index 000000000..2d957395a --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/06-slack-integration/03-installation.md @@ -0,0 +1,169 @@ +--- +sidebar_position: 3 +keywords: +- Slack Bot +- Installation +sidebar_label: Installation +title: Installation +description: Install and configure the bot +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +Install the EventCatalog Slack Bot locally or deploy it to your infrastructure. The bot runs as a long-lived process that maintains a connection to Slack. + +## Install locally + +### Clone repository + +```bash +git clone https://github.com/event-catalog/eventcatalog-slack-bot.git +cd eventcatalog-slack-bot +``` + +### Install dependencies + + + + ```bash + npm install + ``` + + + ```bash + pnpm install + ``` + + + +### Configure environment + +Create a `.env` file with your credentials: + +```bash +# EventCatalog Scale License (Required) +EVENTCATALOG_SCALE_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX-XXXX-XXXX + +# Slack Credentials (Required) +SLACK_BOT_TOKEN=xoxb-... +SLACK_APP_TOKEN=xapp-... +SLACK_SIGNING_SECRET=... + +# AI Provider (at least one required) +ANTHROPIC_API_KEY=sk-ant-... +# OPENAI_API_KEY=sk-... +# GOOGLE_GENERATIVE_AI_API_KEY=... +``` + +### Create configuration + +Create `eventcatalog-bot.config.ts`: + +```typescript +export default { + eventCatalog: { + url: 'http://localhost:3000', + // Optional: Add authentication headers + // headers: { + // 'Authorization': 'Bearer your-token', + // }, + }, + ai: { + provider: 'anthropic', // 'anthropic' | 'openai' | 'google' + // model: 'claude-sonnet-4-20250514', // Optional: override default model + maxSteps: 5, + temperature: 0.4, + }, + slack: { + // Optional: Channel IDs for auto-reply (bot responds to all messages) + autoReplyChannels: [], + // Optional: Custom icon URL for bot messages + icon: 'https://www.eventcatalog.dev/img/logo.png', + // Optional: Custom username for bot messages + username: 'EventCatalog', + }, +}; +``` + +### Start the bot + + + + ```bash + npm run dev + ``` + + + ```bash + pnpm dev + ``` + + + +Or with a custom config path: + + + + ```bash + npm run dev -- --config ./my-config.ts + ``` + + + ```bash + pnpm dev --config ./my-config.ts + ``` + + + +## Configuration reference + +### eventCatalog + +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `url` | string | Yes | URL of your EventCatalog instance | +| `headers` | object | No | Authentication headers if needed | + +### ai + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `provider` | string | Required | AI provider: `anthropic`, `openai`, or `google` | +| `model` | string | Provider default | Model to use (see [supported models](#supported-ai-providers)) | +| `maxSteps` | number | 5 | Maximum tool-calling steps (1-20) | +| `temperature` | number | 0.4 | AI temperature (0-2) | + +### slack + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `autoReplyChannels` | string[] | [] | Channel IDs where bot auto-replies to all messages | +| `icon` | string | EventCatalog logo | Custom icon URL for bot messages | +| `username` | string | 'EventCatalog' | Custom display name for bot messages | + +## Supported AI providers + +| Provider | Environment Variable | Default Model | +|----------|---------------------|---------------| +| Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` | +| OpenAI | `OPENAI_API_KEY` | `gpt-4o` | +| Google | `GOOGLE_GENERATIVE_AI_API_KEY` | `gemini-2.0-flash` | + +You can override the default model in your configuration. For a full list of available models, see the [Vercel AI SDK Providers documentation](https://ai-sdk.dev/providers/ai-sdk-providers). + +```typescript +ai: { + provider: 'anthropic', + model: 'claude-opus-4-20250514', // Use a different model +} +``` + +## License key + +Get your EventCatalog Scale license key from [eventcatalog.cloud](https://eventcatalog.cloud). A 30-day free trial is available. + +The license key must be set as the `EVENTCATALOG_SCALE_LICENSE_KEY` environment variable. diff --git a/packages/core/docs/development/ask-your-architecture/06-slack-integration/04-deployment.md b/packages/core/docs/development/ask-your-architecture/06-slack-integration/04-deployment.md new file mode 100644 index 000000000..1a7862f14 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/06-slack-integration/04-deployment.md @@ -0,0 +1,236 @@ +--- +sidebar_position: 4 +keywords: +- Slack Bot +- Deployment +- Docker +sidebar_label: Deployment +title: Deployment +description: Deploy the bot to production +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +Deploy the EventCatalog Slack Bot to your infrastructure. The bot uses Socket Mode, so it maintains an outbound connection to Slack without requiring a public URL, load balancer, or SSL certificates. + +## Docker + +The simplest way to deploy is using Docker. The bot includes a Dockerfile and docker-compose configuration. + +### Build the image + +```bash +docker build -t eventcatalog-slack-bot . +``` + +### Run with docker-compose + +```bash +docker compose up -d +``` + +### View logs + +```bash +docker compose logs -f +``` + +The `docker-compose.yml` file mounts your config and reads environment variables from `.env`: + +```yaml +services: + eventcatalog-slack-bot: + build: . + container_name: eventcatalog-slack-bot + restart: unless-stopped + env_file: + - .env + volumes: + - ./eventcatalog-bot.config.ts:/app/eventcatalog-bot.config.ts:ro + environment: + - NODE_ENV=production +``` + +## Docker networking + +When running the bot in Docker, the container needs network access to your EventCatalog server. Configuration depends on where EventCatalog runs. + +### EventCatalog on localhost + +Docker containers cannot use `localhost` to reach the host machine. Use one of these approaches: + +**Option 1: Use host.docker.internal (recommended)** + +Update your config to use the special Docker hostname: + +```typescript +eventCatalog: { + url: 'http://host.docker.internal:3000', +} +``` + +:::info +Your EventCatalog server must bind to all interfaces (`0.0.0.0`), not just `localhost`. Check your EventCatalog startup logs. If it shows `localhost:3000`, you may need to start it with a `--host 0.0.0.0` flag. +::: + +**Option 2: Run the bot outside Docker** + +For local development, skip Docker and run the bot directly: + + + + ```bash + npm install + npm run dev + ``` + + + ```bash + pnpm install + pnpm dev + ``` + + + +This avoids networking complexity during development. + +### EventCatalog in Docker + +Put both containers on the same Docker network and use the container name as hostname: + +```yaml +services: + eventcatalog: + # your EventCatalog config + networks: + - app-network + + eventcatalog-slack-bot: + build: . + env_file: + - .env + volumes: + - ./eventcatalog-bot.config.ts:/app/eventcatalog-bot.config.ts:ro + networks: + - app-network + +networks: + app-network: +``` + +Configure the bot to use the container name: + +```typescript +eventCatalog: { + url: 'http://eventcatalog:3000', +} +``` + +### EventCatalog at public URL + +Use the public URL directly - no special configuration needed: + +```typescript +eventCatalog: { + url: 'https://your-catalog.example.com', +} +``` + +## Other deployment options + +
    +Railway + +Railway automatically detects the Dockerfile and deploys the bot. + +1. Create a new project and connect your repository +2. Add environment variables in the Railway dashboard: + - `EVENTCATALOG_SCALE_LICENSE_KEY` + - `SLACK_BOT_TOKEN` + - `SLACK_APP_TOKEN` + - `SLACK_SIGNING_SECRET` + - `ANTHROPIC_API_KEY` (or your chosen provider) +3. Add your `eventcatalog-bot.config.ts` to the repository +4. Deploy - Railway detects the Dockerfile automatically + +
    + +
    +Fly.io + +Deploy to Fly.io using their CLI. + +**Initialize:** + +```bash +fly launch --no-deploy +``` + +**Set secrets:** + +```bash +fly secrets set EVENTCATALOG_SCALE_LICENSE_KEY=your-key +fly secrets set SLACK_BOT_TOKEN=xoxb-... +fly secrets set SLACK_APP_TOKEN=xapp-... +fly secrets set SLACK_SIGNING_SECRET=... +fly secrets set ANTHROPIC_API_KEY=sk-ant-... +``` + +**Deploy:** + +```bash +fly deploy +``` + +
    + +
    +Render + +Deploy as a Background Worker on Render. + +1. Create a new **Background Worker** (not a Web Service) +2. Connect your repository +3. Set the build and start commands: + + + + - Build command: `npm install && npm run build` + - Start command: `npm start` + + + - Build command: `pnpm install && pnpm build` + - Start command: `pnpm start` + + + +4. Add environment variables in the Render dashboard +5. Deploy + +
    + +
    +AWS, GCP, or Azure + +Deploy as a container or long-running process. The bot requires: + +- Outbound HTTPS/WSS connections to Slack +- No inbound connections (Socket Mode handles communication) +- No load balancers or public endpoints needed + +Suitable services include: + +- **AWS:** ECS, EKS, EC2 +- **GCP:** Cloud Run (always running), GKE, Compute Engine +- **Azure:** Container Instances, AKS, Virtual Machines + +Ensure the process stays running and can make outbound connections to: +- `slack.com` (Socket Mode connection) +- Your EventCatalog instance +- Your AI provider API + +
    diff --git a/packages/core/docs/development/ask-your-architecture/06-slack-integration/05-usage.md b/packages/core/docs/development/ask-your-architecture/06-slack-integration/05-usage.md new file mode 100644 index 000000000..aa350f349 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/06-slack-integration/05-usage.md @@ -0,0 +1,140 @@ +--- +sidebar_position: 5 +keywords: +- Slack Bot +- Usage +sidebar_label: Using the bot +title: Using the bot +description: Query architecture documentation from Slack +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +Interact with the EventCatalog Slack Bot to query your architecture documentation. The bot responds to @mentions and can auto-reply in dedicated channels. + +## @Mention in any channel + +Invite the bot to any channel and @mention it to ask questions. The bot replies in a thread to keep channels organized. + +### Example questions + +``` +@eventcatalog Tell me about the OrderCreated event +``` + +``` +@eventcatalog What services consume the InventoryUpdated event? +``` + +``` +@eventcatalog List all events in the Orders domain +``` + +``` +@eventcatalog Show me the schema for the PaymentProcessed event +``` + +The bot reads your EventCatalog documentation and provides detailed answers grounded in your architecture. + +## Dedicated auto-reply channels + +Create a channel where the bot automatically responds to every message - no @mention needed. This gives your team a dedicated space for architecture questions. + +### Setup + +1. Create a new channel (e.g., `#ask-eventcatalog` or `#ask-eda`) +2. Invite the bot to the channel +3. Get the channel ID: + - Right-click the channel name + - Click **View channel details** + - Scroll to the bottom to find the Channel ID +4. Add the channel ID to your config: + +```typescript +slack: { + autoReplyChannels: ['C0123456789'], +} +``` + +5. Restart the bot + +Now anyone can post questions directly in the channel and receive instant answers. + +### Multiple channels + +Configure multiple auto-reply channels: + +```typescript +slack: { + autoReplyChannels: ['C0123456789', 'C9876543210'], +} +``` + +## Thread context + +The bot automatically reads conversation history in threads. This enables natural follow-up questions without repeating context. + +### Example conversation + +**User:** Tell me about the OrderCreated event + +**Bot:** The OrderCreated event is published when... + +**User:** What are its consumers? + +**Bot:** Based on our earlier discussion about OrderCreated, the consumers are... + +**User:** Show me the schema + +**Bot:** Here's the schema for OrderCreated... + +Thread context works automatically in public channels. For private channels and DMs, add optional scopes to your Slack app: + +- `groups:history` - Private channels +- `im:history` - Direct messages +- `mpim:history` - Group DMs + +After adding scopes, reinstall the app to your workspace. + +## Best practices + +### Ask specific questions + +The bot performs best with clear, specific questions: + +**Good:** +- "What services consume the OrderCreated event?" +- "Show me all events in the Payment domain" +- "What's the schema format for UserRegistered?" + +**Less effective:** +- "Tell me about everything" +- "What does this system do?" + +### Use thread conversations + +Keep related questions in the same thread. The bot uses thread history to provide better context-aware answers. + +### Name resources clearly + +Mention specific event names, service names, or domain names to get precise answers: + +- "Tell me about the **OrderCreated** event" +- "What events does the **PaymentService** produce?" +- "List all events in the **Orders** domain" + +## Invite the bot to channels + +The bot can only see messages in channels where it's invited. + +### Invite the bot + +1. Navigate to the channel +2. Click the channel name at the top +3. Click the **Integrations** tab +4. Click **Add an app** +5. Search for your bot and add it + +Alternatively, type `/invite @eventcatalog` in the channel. diff --git a/packages/core/docs/development/ask-your-architecture/06-slack-integration/06-troubleshooting.md b/packages/core/docs/development/ask-your-architecture/06-slack-integration/06-troubleshooting.md new file mode 100644 index 000000000..5860d2d36 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/06-slack-integration/06-troubleshooting.md @@ -0,0 +1,268 @@ +--- +sidebar_position: 6 +keywords: +- Slack Bot +- Troubleshooting +sidebar_label: Troubleshooting +title: Troubleshooting +description: Common issues and solutions +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +Common issues and solutions for the EventCatalog Slack Bot. + +## Bot doesn't respond + +### Check bot invitation + +The bot must be invited to the channel where you're trying to use it. + +**Solution:** +1. Navigate to the channel +2. Type `/invite @eventcatalog` (use your bot's actual name) +3. Try your question again + +### Verify Socket Mode + +Socket Mode must be enabled in your Slack app settings. + +**Solution:** +1. Go to [api.slack.com/apps](https://api.slack.com/apps) +2. Select your app +3. Click **Socket Mode** in the sidebar +4. Verify it's toggled to **On** +5. Restart the bot + +### Check bot logs + +Look for connection errors or startup issues. + +**Solution:** + +View logs based on how you're running the bot: + +```bash +# Local development +# Check terminal output + +# Docker +docker compose logs -f + +# Docker (specific container) +docker logs eventcatalog-slack-bot +``` + +Look for error messages about: +- Missing credentials +- Connection failures +- License validation issues + +## Missing API key error + +The bot cannot find the API key for your configured AI provider. + +**Solution:** + +1. Check your `.env` file contains the correct variable: + +```bash +# For Anthropic +ANTHROPIC_API_KEY=sk-ant-... + +# For OpenAI +OPENAI_API_KEY=sk-... + +# For Google +GOOGLE_GENERATIVE_AI_API_KEY=... +``` + +2. Verify your config matches the environment variable: + +```typescript +ai: { + provider: 'anthropic', // Must match your API key +} +``` + +3. Restart the bot after adding the key + +## MCP connection errors + +The bot cannot connect to your EventCatalog MCP server. + +### Verify EventCatalog URL + +**Solution:** + +1. Check your config has the correct URL: + +```typescript +eventCatalog: { + url: 'https://your-catalog.example.com', +} +``` + +2. Verify the URL is accessible from where the bot runs: + +```bash +curl https://your-catalog.example.com/docs/mcp/ +``` + +3. For Docker deployments, see [Docker networking](/docs/development/ask-your-architecture/slack-integration/deployment#docker-networking) + +### Check MCP server status + +**Solution:** + +1. Visit your MCP endpoint in a browser: + +``` +https://your-catalog.example.com/docs/mcp/ +``` + +2. You should see JSON output with available tools and resources +3. If you see an error, verify: + - EventCatalog is running in [SSR mode](/docs/development/deployment/build-ssr-mode) + - You have a valid [Scale license](https://eventcatalog.cloud) + +### Authentication required + +If your EventCatalog requires authentication, add headers to your config. + +**Solution:** + +```typescript +eventCatalog: { + url: 'https://your-catalog.example.com', + headers: { + 'Authorization': 'Bearer your-token', + }, +} +``` + +## Permission errors + +The bot is missing required Slack permissions. + +**Solution:** + +1. Go to [api.slack.com/apps](https://api.slack.com/apps) +2. Select your app +3. Click **OAuth & Permissions** +4. Verify these **Bot Token Scopes** are added: + - `app_mentions:read` + - `chat:write` + - `reactions:read` + - `reactions:write` + - `channels:history` +5. If you added new scopes, click **Reinstall to Workspace** +6. Restart the bot + +## Auto-reply channels not working + +The bot doesn't auto-reply in configured channels. + +### Verify channel ID + +**Solution:** + +1. Get the correct channel ID: + - Right-click channel name + - Click **View channel details** + - Scroll to bottom for Channel ID +2. Check your config uses the ID (not name): + +```typescript +slack: { + autoReplyChannels: ['C0123456789'], // Channel ID, not #channel-name +} +``` + +3. Restart the bot after changing config + +### Check event subscription + +**Solution:** + +1. Go to [api.slack.com/apps](https://api.slack.com/apps) +2. Select your app +3. Click **Event Subscriptions** +4. Verify **Enable Events** is On +5. Check `message.channels` is in **Subscribe to bot events** + +## Thread context not working + +The bot doesn't remember previous messages in a thread. + +### Public channels + +Thread context should work automatically in public channels. + +**Solution:** + +1. Verify `channels:history` scope is added +2. Reinstall the app if you just added the scope +3. Restart the bot + +### Private channels and DMs + +Thread context requires additional scopes. + +**Solution:** + +1. Go to [api.slack.com/apps](https://api.slack.com/apps) +2. Select your app +3. Click **OAuth & Permissions** +4. Add these scopes: + - `groups:history` - Private channels + - `im:history` - Direct messages + - `mpim:history` - Group DMs +5. Click **Reinstall to Workspace** +6. Restart the bot + +## Docker networking issues + +The bot cannot reach EventCatalog running on localhost. + +**Solution:** + +See the [Docker networking guide](/docs/development/ask-your-architecture/slack-integration/deployment#docker-networking) for detailed solutions based on your setup. + +Quick fix for local EventCatalog: + +```typescript +eventCatalog: { + url: 'http://host.docker.internal:3000', +} +``` + +## License validation errors + +The bot reports an invalid or expired license. + +**Solution:** + +1. Verify your license key is correct in `.env`: + +```bash +EVENTCATALOG_SCALE_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX-XXXX-XXXX +``` + +2. Check your license status at [eventcatalog.cloud](https://eventcatalog.cloud) +3. For expired trials, upgrade to a paid plan +4. Restart the bot after updating the key + +## Getting help + +If you're still experiencing issues: + +1. Check the bot logs for detailed error messages +2. Review the [GitHub repository](https://github.com/event-catalog/eventcatalog-slack-bot) for known issues +3. Join the [EventCatalog Discord](https://eventcatalog.dev/discord) for community support +4. File an issue on [GitHub](https://github.com/event-catalog/eventcatalog-slack-bot/issues) with: + - Bot version + - Error messages from logs + - Steps to reproduce the issue diff --git a/packages/core/docs/development/ask-your-architecture/06-slack-integration/_category_.json b/packages/core/docs/development/ask-your-architecture/06-slack-integration/_category_.json new file mode 100644 index 000000000..73bcd92c9 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/06-slack-integration/_category_.json @@ -0,0 +1,13 @@ +{ + "label": "Slack Integration", + "position": 8, + "className": "hidden", + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "development/ask-your-architecture/slack-integration", + "title": "Slack Integration", + "description": "Query your EventCatalog directly from Slack using the EventCatalog Slack Bot." + } +} diff --git a/packages/core/docs/development/ask-your-architecture/_category_.json b/packages/core/docs/development/ask-your-architecture/_category_.json new file mode 100644 index 000000000..5753d6240 --- /dev/null +++ b/packages/core/docs/development/ask-your-architecture/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "AI with EventCatalog", + "position": 6, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "ask-your-architecture", + "title": "Ask your architecture", + "description": "Learn how to integrate your LLM models with EventCatalog." + } +} diff --git a/packages/core/docs/development/authentication/01-introduction.md b/packages/core/docs/development/authentication/01-introduction.md new file mode 100644 index 000000000..c11d040ad --- /dev/null +++ b/packages/core/docs/development/authentication/01-introduction.md @@ -0,0 +1,78 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog components +sidebar_label: Introduction +title: Introduction +description: Introduction to EventCatalog Authentication +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + +# Authentication Guide + + + + + +EventCatalog provides secure authentication to control access to your event-driven architecture documentation. Whether you're a small team getting started or a large enterprise with complex identity requirements, EventCatalog's flexible authentication system grows with your needs. + + + + + + +## How it works + +EventCatalog uses industry-standard **OpenID Connect (OIDC)** and **OAuth 2.0** protocols to integrate with your identity provider. Here's the authentication flow: + +1. **User visits EventCatalog** and attempts to access protected documentation +2. **Redirected to your identity provider** (GitHub, Google, Auth0, etc.) +3. **User authenticates** with their existing credentials +4. **Provider confirms identity** and sends user back to EventCatalog +5. **User gains access** to your documentation and architecture + +![Authentication](./img/auth.png) + +EventCatalog runs in **SSR mode** to handle authentication sessions and uses [Auth.js](https://authjs.dev/) to manage the authentication flow securely. + +## Authentication by Plan + +EventCatalog Authentication is a **paid feature** available in Scale and Enterprise plans. + +#### Scale Plan +Perfect for growing teams that need secure collaboration with popular business providers: + +- **GitHub** - Ideal for development teams already using GitHub + + +#### Enterprise Plan +Designed for large organizations with dedicated identity management systems: + +- **Microsoft Azure AD (Entra ID)** - For organizations using Office 365 and Azure +- **Auth0** - Developer-friendly identity platform with advanced features +- **Okta** - Popular enterprise identity platform with custom claims +- **Custom OIDC** - Contact us to add your provider at [hello@eventcatalog.cloud](mailto:hello@eventcatalog.cloud) + +## Why EventCatalog Authentication? + +- ✅ **No new passwords** - Users authenticate with accounts they already have +- ✅ **Secure by default** - Leverage enterprise-grade security from major providers +- ✅ **Single sign-on experience** - Seamless access across your tools +- ✅ **Centralized management** - Control access through your existing identity systems +- ✅ **Team collaboration** - Secure access for distributed teams + +## Getting Started + +Ready to secure your EventCatalog with authentication? + +**New to EventCatalog?** Start your **30-day free trial** at [EventCatalog.cloud](https://eventcatalog.cloud) to explore all authentication features. + +## Next steps + +Ready to get started? Let's enable authentication in your EventCatalog project: + +→ [Enabling Authentication](/docs/development/authentication/enabling-authentication) + +**Questions?** Join our [Discord community](https://discord.gg/eventcatalog) for support and guidance. \ No newline at end of file diff --git a/packages/core/docs/development/authentication/02-enabling-authentication.md b/packages/core/docs/development/authentication/02-enabling-authentication.md new file mode 100644 index 000000000..5023b7c1b --- /dev/null +++ b/packages/core/docs/development/authentication/02-enabling-authentication.md @@ -0,0 +1,152 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog authentication +sidebar_label: Enabling authentication +title: Enabling authentication +description: Enabling authentication for EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; +import { Card, CardGrid, GitHubIcon, OktaIcon, AzureIcon, GoogleIcon, Auth0Icon, GitLabIcon } from '@site/src/components/MDX/Card'; + + + + +To enable authentication for your site, you will need to do three things: + +1. [Setup Environment](#setup-environment) +1. [Enable EventCatalog Server Side Rendering (SSR)](#enable-eventcatalog-server-side-rendering-ssr) +1. [Create your `eventcatalog.auth.js` file](#create-your-eventcatalogauthjs-file) + +:::info Authentication is a paid feature +Authentication is a paid feature, and is available on EventCatalog Scale and Enterprise plans. + +You can get a 30-day free trial of EventCatalog Scale and Enterprise [here](https://www.eventcatalog.dev/pricing). + +You will need to set your license key in your `.env` file. + +```env title=".env" +EVENTCATALOG_LICENSE_KEY=your-license-key +``` +::: + +### Setup Environment + +EventCatalog uses [Auth.js](https://authjs.dev/) to handle the authentication flow. + +Auth.js libraries require you to set an `AUTH_SECRET` environment variable. This is used to encrypt cookies and tokens. It should be a cryptographically secure random string of at least 32 characters: + +This is the only strictly required environment variable. It is the secret used to encode the JWT and encrypt things in transit. We recommend at least a 32 character random string. This can be generated via openssl with `openssl rand -base64 33`. + +```env title=".env" +AUTH_SECRET=your-secret +``` + +#### AUTH_TRUST_HOST {#auth_trust_host} + +When running EventCatalog behind a reverse proxy (Kubernetes/AKS, Nginx, Cloudflare, AWS ALB, etc.), you must set `AUTH_TRUST_HOST=true`. Without it, Auth.js falls back to the internal container URL (e.g. `http://localhost:3000`) instead of the real domain, which causes login and sign-out to fail with CSRF/cross-site errors such as "Cross-site POST form submissions are forbidden". + +```env title=".env" +AUTH_TRUST_HOST=true +``` + +Setting this tells Auth.js to trust the `x-forwarded-host` and `x-forwarded-proto` headers forwarded by your proxy so it can resolve the correct callback URL. + +:::tip Vercel and Cloudflare Pages +You do not need to set `AUTH_TRUST_HOST` when deploying to Vercel or Cloudflare Pages - it is inferred automatically. It is also not required in local development. +::: + +To learn more, refer to the [Auth.js deployment documentation](https://authjs.dev/getting-started/deployment#auth_trust_host). + + +### Enable EventCatalog Server Side Rendering (SSR) + +Authentication requires EventCatalog to be SSR enabled. This is because EventCatalog needs to be able to access the user's session to determine if they are authenticated. + +To enable SSR, you will need to add the following to your `eventcatalog.config.js` file: + +```js title="eventcatalog.config.js" +module.exports = { + // ... other config options + output: 'server', +}; +``` + +This will ensure that EventCatalog is rendered on the server side, and that the user's session is available to the client. + +:::info Deploying EventCatalog in SSR mode +You will be running EventCatalog in SSR mode when you deploy your site. This means the output of your site will require a server to be running. You can use EventCatalog Docker file to deploy your site or read our [deployment guide](https://www.eventcatalog.dev/docs/deployment/overview) for more information. +::: + +### Create your `eventcatalog.auth.js` file + +The `eventcatalog.auth.js` file is used to configure the authentication for your site, and is created in the root of your EventCatalog project. + +```js title="eventcatalog.auth.js" +module.exports = { + // Enable debug mode for development + debug: false, + // List of providers you want to enable + providers: { + github: { + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + }, + }, + // Optional session configuration + session?: { + // 30 days default + maxAge?: number; + }; +}; +``` + +Once you have these three things, you can start setting up your authentication providers. + +### Setting up your authentication providers + +EventCatalog supports a range of authentication providers, and you can find the documentation for each provider below. + + + } + badge="Scale" + href="/docs/development/authentication/providers/setting-up-github" + /> + } + badge="Scale" + href="/docs/development/authentication/providers/setting-up-google" + /> + } + badge="Enterprise" + href="/docs/development/authentication/providers/setting-up-azure-ad" + /> + + + } + href="/docs/development/authentication/providers/setting-up-okta" + badge="Enterprise" + /> + } + href="/docs/development/authentication/providers/setting-up-auth0" + badge="Enterprise" + /> + + +Missing a provider? [Let us know](https://github.com/event-catalog/eventcatalog/issues/new) and we'll add it to the list. + diff --git a/packages/core/docs/development/authentication/07-rbac-middleware.md b/packages/core/docs/development/authentication/07-rbac-middleware.md new file mode 100644 index 000000000..ce8e99c46 --- /dev/null +++ b/packages/core/docs/development/authentication/07-rbac-middleware.md @@ -0,0 +1,269 @@ +--- +sidebar_position: 7 +keywords: +- EventCatalog RBAC +- Role-based access control +- Middleware +- Custom authentication +- Access control +sidebar_label: Role-Based Access Control +title: RBAC Middleware +description: Implementing role-based access control with custom middleware in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + +# Role-Based Access Control (RBAC) Middleware + +EventCatalog supports **Role-Based Access Control (RBAC)** through custom middleware, allowing you to control user access to specific pages and sections based on their roles and groups. + +## How it works + +The RBAC middleware integrates with EventCatalog's authentication system to provide fine-grained access control: + +1. **User authenticates** through your configured identity provider +2. **Middleware evaluates** the user's roles and groups against defined access rules +3. **Access is granted or denied** based on the matching rules for the requested path +4. **User sees appropriate content** or receives a 403 Forbidden response + +## Prerequisites + +Before setting up RBAC middleware, ensure you have: + +- ✅ [Authentication enabled](/docs/development/authentication/enabling-authentication) in your EventCatalog +- ✅ An authentication provider configured (GitHub, Google, Azure AD, etc.) +- ✅ User roles and groups configured in your identity provider + +## Setting up RBAC Middleware + +### 1. Create the middleware file + +Create a `middleware.ts` file in the root of your EventCatalog project: + +```typescript title="middleware.ts" +import type { MiddlewareHandler } from 'astro'; + +interface Locals { + hasRole: (role: string) => boolean; + hasGroup: (group: string) => boolean; + findMatchingRule: (rules: Record boolean>, pathname: string) => (() => boolean) | null; +} + +export const rbacMiddleware: MiddlewareHandler = async (context, next) => { + const { locals, url } = context; + const pathname = url.pathname; + + // Utility functions are available in the locals object + const { hasRole, hasGroup, findMatchingRule } = locals as Locals; + + // Define your access rules + // Maps page routes to a function that returns true if the user has access, false otherwise + // You can use wildcards to match multiple paths + const accessRules = { + '/docs/domains/E-Commerce/*': () => !hasGroup('Viewer'), + '/visualiser/domains/E-Commerce/*': () => !hasGroup('Viewer'), + '/docs/services/payment/*': () => hasRole('Developer') || hasRole('Admin'), + '/admin/*': () => hasRole('Admin'), + }; + + if (findMatchingRule) { + // Find matching rule for the current path + const rule = findMatchingRule(accessRules, pathname); + + if (rule && !rule()) { + return new Response('Forbidden', { status: 403 }); + } + } + + return next(); +}; +``` + +### 2. Configure your access rules + +The `accessRules` object defines path-based access control: + +```typescript +const accessRules = { + // Block 'Viewer' group from E-Commerce domain docs + '/docs/domains/E-Commerce/*': () => !hasGroup('Viewer'), + + // Require 'Developer' or 'Admin' role for payment services + '/docs/services/payment/*': () => hasRole('Developer') || hasRole('Admin'), + + // Admin-only sections + '/admin/*': () => hasRole('Admin'), + + // Multiple conditions + '/docs/sensitive/*': () => hasRole('Admin') && !hasGroup('External'), +}; +``` + +### 3. Available helper functions + +The middleware provides several helper functions through `locals`: + +#### `hasRole(role: string)` +Checks if the user has a specific role: +```typescript +hasRole('Admin') // Returns true if user has Admin role +hasRole('Developer') // Returns true if user has Developer role +``` + +#### `hasGroup(group: string)` +Checks if the user belongs to a specific group: +```typescript +hasGroup('Viewer') // Returns true if user is in Viewer group +hasGroup('External') // Returns true if user is in External group +``` + +#### `findMatchingRule(rules, pathname)` +Finds the first matching rule for a given pathname using glob patterns: +```typescript +// Matches paths like: +// - /docs/domains/E-Commerce/orders +// - /docs/domains/E-Commerce/products/catalog +'/docs/domains/E-Commerce/*': () => hasRole('Developer') +``` + +## Access Rule Patterns + +### Path-based rules +Control access to specific pages or sections using exact paths or wildcard patterns. +```typescript +const accessRules = { + // Exact path match + '/admin/settings': () => hasRole('Admin'), + + // Wildcard matching + '/docs/domains/Banking/*': () => hasGroup('Banking-Team'), + + // Multiple level wildcards + '/api/*/internal/*': () => hasRole('Internal-Developer'), +}; +``` + +### Role-based rules +Define access permissions based on user roles with single or multiple role requirements. +```typescript +const accessRules = { + // Single role requirement + '/admin/*': () => hasRole('Admin'), + + // Multiple role options (OR) + '/docs/api/*': () => hasRole('Developer') || hasRole('Architect'), + + // Multiple role requirements (AND) + '/sensitive/*': () => hasRole('Admin') && hasRole('Security-Cleared'), +}; +``` + +### Group-based rules +Manage access using group membership with inclusion, exclusion, or complex group logic. +```typescript +const accessRules = { + // Exclude specific groups + '/public/*': () => !hasGroup('External'), + + // Include specific groups + '/team-docs/*': () => hasGroup('Internal-Team'), + + // Complex group logic + '/project-alpha/*': () => hasGroup('Alpha-Team') || hasGroup('Leadership'), +}; +``` + +## Common use cases + +### Department-based access +Organize access control around your organizational structure, ensuring teams only see documentation relevant to their department. +```typescript +const accessRules = { + '/docs/domains/HR/*': () => hasGroup('HR-Department'), + '/docs/domains/Finance/*': () => hasGroup('Finance-Department'), + '/docs/domains/Engineering/*': () => hasGroup('Engineering-Department'), +}; +``` + +### Hierarchical permissions +Create layered access levels where higher privilege users can access all lower-level content. +```typescript +const accessRules = { + '/docs/public/*': () => true, // Everyone can access + '/docs/internal/*': () => !hasGroup('External'), + '/docs/confidential/*': () => hasRole('Manager') || hasRole('Admin'), + '/docs/top-secret/*': () => hasRole('Admin'), +}; +``` + +### Feature-based access +Control access to specific EventCatalog features based on user roles and responsibilities. +```typescript +const accessRules = { + '/visualiser/*': () => hasRole('Architect') || hasRole('Developer'), + '/api-explorer/*': () => hasRole('Developer'), + '/admin/*': () => hasRole('Admin'), +}; +``` + +## Troubleshooting + +### Users getting 403 errors unexpectedly + +1. **Check role/group assignment** in your identity provider +2. **Debug user permissions** by logging the locals to see what roles and groups are available: + ```typescript + export const rbacMiddleware: MiddlewareHandler = async (context, next) => { + const { locals, url } = context; + + // Log the user's roles and groups for debugging + console.log('User locals:', { + pathname: url.pathname, + locals: locals + }); + + // Your existing middleware code... + }; + ``` +3. **Verify rule logic** - ensure your conditions are correct: + ```typescript + // Wrong: This blocks everyone except Viewers + '/docs/*': () => hasGroup('Viewer') + + // Correct: This blocks only Viewers + '/docs/*': () => !hasGroup('Viewer') + ``` + +### Rules not matching expected paths + +1. **Test your glob patterns** - ensure wildcards match your URL structure +2. **Check path casing** - paths are case-sensitive +3. **Verify rule order** - more specific rules should come before general ones + +### Session issues + +1. **Ensure user is authenticated** before middleware runs +2. **Check session expiration** - users may need to re-authenticate +3. **Verify locals are populated** - `hasRole` and `hasGroup` functions must be available + +## Security best practices + +- ✅ **Principle of least privilege** - Grant minimum required access +- ✅ **Regular access reviews** - Audit user roles and permissions +- ✅ **Test thoroughly** - Verify access rules with different user types +- ✅ **Monitor access attempts** - Log and review 403 responses +- ✅ **Use groups over individual users** - Easier to manage and scale + +## Next steps + +With RBAC middleware configured, you can: + +- Set up more complex access patterns based on your organization structure +- Integrate with external authorization systems +- Add custom logging for access attempts + +Need help? Join our [Discord community](https://eventcatalog.dev/discord) for support and best practices from other EventCatalog users. \ No newline at end of file diff --git a/packages/core/docs/development/authentication/_category_.json b/packages/core/docs/development/authentication/_category_.json new file mode 100644 index 000000000..08a992bf6 --- /dev/null +++ b/packages/core/docs/development/authentication/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Authentication", + "position": 10, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "/development/guides/auth", + "description": "Authentication for EventCatalog" + } +} diff --git a/packages/core/docs/development/authentication/providers/03-setting-up-github.md b/packages/core/docs/development/authentication/providers/03-setting-up-github.md new file mode 100644 index 000000000..e2fe7d482 --- /dev/null +++ b/packages/core/docs/development/authentication/providers/03-setting-up-github.md @@ -0,0 +1,107 @@ +--- +sidebar_position: 3 +keywords: +- EventCatalog GitHub Authentication +sidebar_label: GitHub +title: Setting up GitHub +description: Setting up GitHub authentication for EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + +:::info +This guide takes your through setting up a protected sign-in screen for your docs. Before going through this guide, make sure you’ve first gone through [Enabling authentication](/docs/development/authentication/enabling-authentication). +::: + +To setup your EventCatalog site with visitor authentication using [GitHub](https://github.com/), the process looks as follows: + +1. Create a new GitHub OAuth app +2. Configure the OAuth app in EventCatalog +3. Test the authentication + +## Create a new GitHub OAuth app + +First, you will need to create a new GitHub OAuth app. + +1. Go to [GitHub Developer Settings](https://github.com/settings/developers) +2. Click on "New OAuth App" +3. Fill in the details for your app + - **Application name:** `EventCatalog` + - **Homepage URL:** `{YOUR_EVENTCATALOG_SITE_URL}` + - Local development: `http://localhost:3000` + - **Authorization callback URL:** `{YOUR_EVENTCATALOG_SITE_URL}/api/auth/callback/github` + - Local development: `http://localhost:3000/api/auth/callback/github` +4. Click on "Register application" +5. Copy the Client ID and Client Secret + +## Configure the OAuth app in EventCatalog + +Add your GitHub Client ID and Client Secret to your `.env` file. + +```env title=".env" +AUTH_GITHUB_CLIENT_ID={YOUR_GITHUB_CLIENT_ID} +AUTH_GITHUB_CLIENT_SECRET={YOUR_GITHUB_CLIENT_SECRET} +``` + +In your `eventcatalog.auth.js` file, add the following: + +```js title="eventcatalog.auth.js" +export default { + providers: { + github: { + clientId: process.env.AUTH_GITHUB_CLIENT_ID, + clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET, + }, + }, +}; +``` + +## Test the authentication + +Restart your EventCatalog server and test the authentication. + +```bash +npm run dev +``` + +All pages should now be protected and require a GitHub account to access. + +![GitHub authentication](./img/github-auth.png) + +## Running behind a reverse proxy (`redirectProxyUrl`) + +When running behind a reverse proxy or load balancer (Kubernetes/AKS, Nginx, Cloudflare, AWS ALB/ECS, etc.), GitHub sign-in can break with: + +> The redirect_uri is not associated with this application. + +This happens when the OAuth `redirect_uri` ends up as `http://` (or an internal host) instead of your real `https://` URL, because the proxy terminates TLS and forwards the request internally. You may also see `InvalidCheck: pkceCodeVerifier value could not be parsed` in your logs from the same wrong base URL. + +[`AUTH_TRUST_HOST=true`](/docs/development/authentication/enabling-authentication#auth_trust_host) fixes this for most setups. If your proxy doesn't reliably forward the `x-forwarded-host` / `x-forwarded-proto` headers, set `redirectProxyUrl` to your canonical public URL to force the correct callback: + +```js title="eventcatalog.auth.js" +export default { + providers: { + github: { + clientId: process.env.AUTH_GITHUB_CLIENT_ID, + clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET, + // Canonical public URL of your site, including /api/auth + redirectProxyUrl: 'https://catalog.example.com/api/auth', + }, + }, +}; +``` + +The host must match the **Authorization callback URL** on your GitHub OAuth app (`https://catalog.example.com/api/auth/callback/github`). Not needed on Vercel, Cloudflare Pages, or local dev. See the [Auth.js reference](https://authjs.dev/reference/core#redirectproxyurl) for more. + +## Found an issue? + +Remember to setup the prerequisites for this guide: + +- [Enabling authentication](/docs/development/authentication/enabling-authentication) + +If you still have problems, please [let us know](https://github.com/eventcatalog/eventcatalog/issues/new/choose). + diff --git a/packages/core/docs/development/authentication/providers/03a-setting-up-google.md b/packages/core/docs/development/authentication/providers/03a-setting-up-google.md new file mode 100644 index 000000000..f23751bf8 --- /dev/null +++ b/packages/core/docs/development/authentication/providers/03a-setting-up-google.md @@ -0,0 +1,91 @@ +--- +sidebar_position: 3 +keywords: +- EventCatalog Google Authentication +sidebar_label: Google +title: Setting up Google +description: Setting up Google authentication for EventCatalog +id: setting-up-google +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + +:::info +This guide takes your through setting up a protected sign-in screen for your docs. Before going through this guide, make sure you've first gone through [Enabling authentication](/docs/development/authentication/enabling-authentication). +::: + +To setup your EventCatalog site with visitor authentication using [Google](https://accounts.google.com/), the process looks as follows: + +1. Create a new Google OAuth app +2. Configure the OAuth app in EventCatalog +3. Test the authentication + +## Create a new Google OAuth app + +First, you will need to create a new Google OAuth app in the Google Cloud Console. + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select an existing one +3. Navigate to "APIs & Services" → "Library" +4. Search for and enable the "Google+ API" +5. Go to "APIs & Services" → "Credentials" +6. Click "Create Credentials" → "OAuth client ID" +7. If prompted, configure the OAuth consent screen: + - Choose "External" for testing + - Fill in app name: `EventCatalog` + - Add your user support email and developer contact email + - Save and continue through the remaining screens +8. Create the OAuth client ID: + - **Application type:** Web application + - **Name:** `EventCatalog` + - **Authorized JavaScript origins:** `{YOUR_EVENTCATALOG_SITE_URL}` + - Local development: `http://localhost:3000` + - **Authorized redirect URIs:** `{YOUR_EVENTCATALOG_SITE_URL}/api/auth/callback/google` + - Local development: `http://localhost:3000/api/auth/callback/google` +9. Click "Create" and copy the Client ID and Client Secret + +## Configure the OAuth app in EventCatalog + +Add your Google Client ID and Client Secret to your `.env` file. + +```env title=".env" +AUTH_GOOGLE_CLIENT_ID={YOUR_GOOGLE_CLIENT_ID} +AUTH_GOOGLE_CLIENT_SECRET={YOUR_GOOGLE_CLIENT_SECRET} +``` + +In your eventcatalog.auth.js file, add the following: + +```js title="eventcatalog.auth.js" +export default { + enabled: true, + google: { + clientId: process.env.AUTH_GOOGLE_CLIENT_ID, + clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET, + }, +} +``` + +## Test the authentication + +Restart your EventCatalog server and test the authentication. + +```bash +npm run dev +``` + +All pages should now be protected and require a Google account to access. + +![Google authentication](./img/google-auth.png) + +## Found an issue? + +Remember to setup the prerequisites for this guide: + +- [Enabling authentication](/docs/development/authentication/enabling-authentication) + +If you still have problems, please [let us know](https://github.com/eventcatalog/eventcatalog/issues/new/choose). + diff --git a/packages/core/docs/development/authentication/providers/04-setting-up-azure-ad.md b/packages/core/docs/development/authentication/providers/04-setting-up-azure-ad.md new file mode 100644 index 000000000..05f90f1f5 --- /dev/null +++ b/packages/core/docs/development/authentication/providers/04-setting-up-azure-ad.md @@ -0,0 +1,100 @@ +--- +sidebar_position: 4 +keywords: +- EventCatalog components +sidebar_label: Azure AD (Entra ID) +title: Setting up Azure AD (Entra ID) +description: Setting up Microsoft Entra ID authentication for EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + +:::info +This guide takes your through setting up a protected sign-in screen for your docs. Before going through this guide, make sure you've first gone through [Enabling authentication](/docs/development/authentication/enabling-authentication). +::: + +To setup your EventCatalog site with visitor authentication using [Microsoft Entra ID](https://entra.microsoft.com/) (formerly Azure Active Directory), the process looks as follows: + +1. [Create a new Azure app registration](#create-a-new-azure-app-registration) +2. [Configure the Azure app in EventCatalog](#configure-the-azure-app-in-eventcatalog) +3. [Test the authentication](#test-the-authentication) + +## Create a new Azure app registration + +First, you will need to create a new app registration in the Azure portal. + +1. Go to [portal.azure.com](https://portal.azure.com) and sign up for a free account or login +2. Search for **Microsoft Entra ID** (or **Azure Active Directory**) in the top search bar +3. Navigate to **App registrations** → **New registration** +4. Fill in the application details: + - **Name:** `EventCatalog` + - **Supported account types:** Select **Accounts in any organizational directory and personal Microsoft accounts** (for broader compatibility) + - **Redirect URI:** Select **Web** and enter: + - Local development: `http://localhost:3000/api/auth/callback/microsoft-entra-id` +5. Click **Register** +6. After registration, note the **Application (client) ID** and **Directory (tenant) ID** from the Overview page +7. Navigate to **Certificates & secrets** → **New client secret** +8. Add a description (e.g., "EventCatalog Secret") and choose an expiration period +9. Click **Add** and immediately copy the secret **Value** (you won't see it again) +10. Go to **Authentication** in the sidebar and configure: + - Add additional redirect URIs for production: `{YOUR_EVENTCATALOG_SITE_URL}/api/auth/callback/microsoft-entra-id` + - Under **Implicit grant and hybrid flows**, check **ID tokens** + - Click **Save** + +## Configure the Azure app in EventCatalog + +Add your Azure AD Client ID, Client Secret, and Tenant ID to your `.env` file. + +```env title=".env" +AUTH_MICROSOFT_ENTRA_ID_ID={YOUR_AZURE_CLIENT_ID} +AUTH_MICROSOFT_ENTRA_ID_SECRET={YOUR_AZURE_CLIENT_SECRET} +AUTH_MICROSOFT_ENTRA_ID_ISSUER=https://login.microsoftonline.com/{YOUR_AZURE_TENANT_ID}/v2.0 +``` + +Your Azure AD tenant ID can be found in your app registration's Overview page in the Azure portal. + +In your eventcatalog.auth.js file, add the following: + +```js title="eventcatalog.auth.js" +export default { + enabled: true, + providers: { + entra: { + clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID, + clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET, + issuer: process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER, + }, + }, +}; +``` + +## Test the authentication + +![Okta authentication](./img/microsoft-auth.png) + +Restart your EventCatalog server and test the authentication. + +```bash +npm run dev +``` + +All pages should now be protected and require a Microsoft account to access. + +1. Navigate to your EventCatalog site +1. You should be redirected to the sign-in page +1. Click Sign in with Azure AD +1. You'll be redirected to Microsoft's login page +1. Enter your Microsoft account credentials (personal or organizational) +1. After successful authentication, you'll be redirected back to EventCatalog + +## Found an issue? + +Remember to setup the prerequisites for this guide: + +- [Enabling authentication](/docs/development/authentication/enabling-authentication) + +If you still have problems, please [let us know](https://github.com/eventcatalog/eventcatalog/issues/new/choose). \ No newline at end of file diff --git a/packages/core/docs/development/authentication/providers/05-setting-up-okta.md b/packages/core/docs/development/authentication/providers/05-setting-up-okta.md new file mode 100644 index 000000000..f2e26d312 --- /dev/null +++ b/packages/core/docs/development/authentication/providers/05-setting-up-okta.md @@ -0,0 +1,105 @@ +--- +sidebar_position: 5 +keywords: +- EventCatalog components +sidebar_label: Okta +title: Setting up Okta +description: Setting up Okta authentication for EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + +:::info +This guide takes your through setting up a protected sign-in screen for your docs. Before going through this guide, make sure you've first gone through [Enabling authentication](/docs/development/authentication/enabling-authentication). +::: + +To setup your EventCatalog site with visitor authentication using [Okta](https://www.okta.com/), the process looks as follows: + +1. Create a new Okta OAuth app +2. Configure the OAuth app in EventCatalog +3. Test the authentication + +## Create a new Okta OAuth app + +First, you will need to create a new Okta OAuth app in your Okta Admin Console. + +1. Log in to your **[Okta Admin Console](https://login.okta.com/signin)** +2. Navigate to **Applications** → **Applications** +3. Click **Create App Integration** +4. Select **OIDC - OpenID Connect** as the sign-in method +5. Select **Web Application** as the application type +6. Click **Next** +7. Fill in the application details: + - **App integration name:** `EventCatalog` + - **Grant types:** Check `Authorization Code` + - **Sign-in redirect URIs:** + - Production: `{YOUR_EVENTCATALOG_SITE_URL}/api/auth/callback/okta` + - Local development: `http://localhost:3000/api/auth/callback/okta` + - **Sign-out redirect URIs:** + - Production: `{YOUR_EVENTCATALOG_SITE_URL}` + - Local development: `http://localhost:3000` +8. Under **Assignments**, choose who can access this application: + - **Allow everyone in your organization to access** (recommended) + - Or assign to specific groups +9. Click **Save** +10. Copy the **Client ID** and **Client Secret** from the app settings +11. Note your **Okta domain** (e.g., `https://your-domain.okta.com`) + +## Configure the OAuth app in EventCatalog + +Add your Okta Client ID, Client Secret, and Issuer to your `.env` file. + +```env title=".env" +AUTH_OKTA_CLIENT_ID={YOUR_OKTA_CLIENT_ID} +AUTH_OKTA_CLIENT_SECRET={YOUR_OKTA_CLIENT_SECRET} +AUTH_OKTA_ISSUER=https://{YOUR_OKTA_DOMAIN} +``` + +Your Okta issuer URL should be in the format: https://your-domain.okta.com (without /oauth2/default unless you're using a custom authorization server). +In your eventcatalog.auth.js file, add the following: + +```js title="eventcatalog.auth.js" +export default { + enabled: true, + providers: { + okta: { + clientId: process.env.AUTH_OKTA_CLIENT_ID, + clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET, + issuer: process.env.AUTH_OKTA_ISSUER, + }, + }, +}; +``` + +## Test the authentication + +![Okta authentication](./img/okta-auth.png) + +Restart your EventCatalog server and test the authentication. + +```bash +npm run dev +``` + +All pages should now be protected and require an Okta account to access. + +1. Navigate to your EventCatalog site +1. You should be redirected to the sign-in page +1. Click Sign in with Okta +1. You'll be redirected to your Okta login page +1. Enter your Okta credentials +1. After successful authentication, you'll be redirected back to EventCatalog + + +## Found an issue? + +Remember to setup the prerequisites for this guide: + +- [Enabling authentication](/docs/development/authentication/enabling-authentication) + +If you still have problems, please [let us know](https://github.com/eventcatalog/eventcatalog/issues/new/choose). + diff --git a/packages/core/docs/development/authentication/providers/06-setting-up-auth0.md b/packages/core/docs/development/authentication/providers/06-setting-up-auth0.md new file mode 100644 index 000000000..9858e2d8e --- /dev/null +++ b/packages/core/docs/development/authentication/providers/06-setting-up-auth0.md @@ -0,0 +1,106 @@ +--- +sidebar_position: 6 +keywords: +- EventCatalog components +sidebar_label: Auth0 +title: Setting up Auth0 +description: Setting up Auth0 authentication for EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + +:::info +This guide takes your through setting up a protected sign-in screen for your docs. Before going through this guide, make sure you've first gone through [Enabling authentication](/docs/development/authentication/enabling-authentication). +::: + +To setup your EventCatalog site with visitor authentication using [Auth0](https://auth0.com/), the process looks as follows: + +1. [Create a new Auth0 application](#create-a-new-auth0-application) +2. [Configure the Auth0 app in EventCatalog](#configure-the-auth0-app-in-eventcatalog) +3. [Test the authentication](#test-the-authentication) + +## Create a new Auth0 application + +First, you will need to create a new Auth0 application in your Auth0 Dashboard. + +1. Go to [auth0.com](https://auth0.com) and sign up for a free account or login +2. In the Auth0 Dashboard, navigate to **Applications** → **Applications** +3. Click **Create Application** +4. Fill in the application details: + - **Name:** `EventCatalog` + - **Application Type:** Select **Regular Web Applications** +5. Click **Create** +6. In your new application's **Settings** tab, configure the following: + - **Allowed Callback URLs:** + - Production: `{YOUR_EVENTCATALOG_SITE_URL}/api/auth/callback/auth0` + - Local development: `http://localhost:3000/api/auth/callback/auth0` + - **Allowed Logout URLs:** + - Production: `{YOUR_EVENTCATALOG_SITE_URL}` + - Local development: `http://localhost:3000` + - **Allowed Web Origins:** + - Production: `{YOUR_EVENTCATALOG_SITE_URL}` + - Local development: `http://localhost:3000` + - **Allowed Origins (CORS):** + - Production: `{YOUR_EVENTCATALOG_SITE_URL}` + - Local development: `http://localhost:3000` +7. Leave **Initiate Login URI** empty (not required) +8. Click **Save Changes** +9. Copy the **Domain**, **Client ID**, and **Client Secret** from the app settings + +## Configure the Auth0 app in EventCatalog + +Add your Auth0 Domain, Client ID, and Client Secret to your `.env` file. + +```env title=".env" +AUTH_AUTH0_ID={YOUR_AUTH0_CLIENT_ID} +AUTH_AUTH0_SECRET={YOUR_AUTH0_CLIENT_SECRET} +AUTH_AUTH0_ISSUER=https://{YOUR_AUTH0_DOMAIN} +``` + +Your Auth0 issuer URL should be in the format: https://your-tenant.auth0.com (this is the Domain from your Auth0 application settings). + +In your eventcatalog.auth.js file, add the following: + +```js title="eventcatalog.auth.js" +export default { + enabled: true, + providers: { + auth0: { + clientId: process.env.AUTH_AUTH0_ID, + clientSecret: process.env.AUTH_AUTH0_SECRET, + issuer: process.env.AUTH_AUTH0_ISSUER, + }, + }, +}; +``` + +## Test the authentication + +![Okta authentication](./img/auth0-auth.png) + +Restart your EventCatalog server and test the authentication. + +```bash +npm run dev +``` + +All pages should now be protected and require an Auth0 account to access. + +1. Navigate to your EventCatalog site +1. You should be redirected to the sign-in page +1. Click Sign in with Auth0 +1. You'll be redirected to your Auth0 login page +1. Enter your credentials or sign up for a new account +1. After successful authentication, you'll be redirected back to EventCatalog + +## Found an issue? + +Remember to setup the prerequisites for this guide: + +- [Enabling authentication](/docs/development/authentication/enabling-authentication) + +If you still have problems, please [let us know](https://github.com/eventcatalog/eventcatalog/issues/new/choose). \ No newline at end of file diff --git a/packages/core/docs/development/authentication/providers/_category_.json b/packages/core/docs/development/authentication/providers/_category_.json new file mode 100644 index 000000000..c6cecc51a --- /dev/null +++ b/packages/core/docs/development/authentication/providers/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Providers", + "position": 15, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "/development/authentication/providers", + "description": "Authentication for EventCatalog" + } +} \ No newline at end of file diff --git a/packages/core/docs/development/bring-your-own-documentation/01-introduction.md b/packages/core/docs/development/bring-your-own-documentation/01-introduction.md new file mode 100644 index 000000000..8d89370a8 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/01-introduction.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog custom documentation +sidebar_label: Introduction +title: Bring your own documentation +description: Bring your own documentation to EventCatalog +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +EventCatalog lets you bring your own documentation and diagrams into your catalog. + +This is useful when your documentation is spread across multiple repositories, project folders, wikis, or internal tools. You can bring that context into EventCatalog and make it part of the same experience people already use to explore your architecture. + +You can document anything that helps your teams understand, operate, and govern your systems, including: + +- Team onboarding documentation +- Best practices and engineering standards +- Runbooks and operational guides +- CI/CD and deployment documentation +- User journeys and business processes +- Technical debt and migration notes + +EventCatalog has a few ways to bring your own information to your catalog: + +1. [High level documentation](#high-level-documentation) + - for catalog-wide knowledge that is not tied to one specific resource +1. [Resource-level documentation](#resource-level-documentation) + - for documentation attached to a specific domain, service, event, API, or other catalog resource +1. [Diagrams](/docs/development/bring-your-own-documentation/diagrams/introduction) + - bring your own diagrams (e.g Miro, DrawIO, Mermaid) to your catalog. + +--- + +### High level documentation + +High level documentation is for top-level knowledge that should live in your catalog but does not belong to one specific resource. + +Use high level documentation for things like engineering standards, onboarding guides, architecture principles, platform runbooks, team processes, or shared API guidance. These pages appear in your catalog's documentation area and can be organized independently from your domains, services, and events. + +[Read the high level documentation guide](/docs/development/bring-your-own-documentation/custom-pages/introduction). + +### Resource-level documentation + +Resource-level documentation is for knowledge that belongs with a specific EventCatalog resource. + +Use resource-level documentation when the context should appear next to the thing it describes. For example, you can attach service runbooks to a service, onboarding material to a domain, operational notes to a system, or extra API documentation to an API. + +EventCatalog renders this documentation alongside the resource, so readers can move between the resource overview and its supporting documentation without leaving the catalog. + +[Read the resource-level documentation guide](/docs/development/bring-your-own-documentation/resource-docs/introduction). diff --git a/packages/core/docs/development/bring-your-own-documentation/_category_.json b/packages/core/docs/development/bring-your-own-documentation/_category_.json new file mode 100644 index 000000000..6d53511c2 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Bring your own documentation", + "position": 5, + "collapsible": true, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "bring-your-own-documentation", + "title": "Bring your own documentation", + "description": "Bring your own documentation to EventCatalog. Add global custom pages or attach docs directly to individual resources." + } +} diff --git a/packages/core/docs/development/bring-your-own-documentation/custom-pages/01-introduction.md b/packages/core/docs/development/bring-your-own-documentation/custom-pages/01-introduction.md new file mode 100644 index 000000000..3a0ae1e01 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/custom-pages/01-introduction.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog custom documentation +sidebar_label: Introduction +title: Introduction +description: Customize documentation in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + + + +--- + +High level documentation is a way to add catalog-wide custom documentation pages to your catalog. + +This can be a great way to extend your catalog beyond what is provided, and bring your own documentation to EventCatalog, rather than having documentation spread across multiple tools. + + +![Example](./img/custom-docs.png) +View demo + +High level documentation is not limited, here are some examples of what you can do: + +- Document infrastructure & operations +- Document CI/CD pipelines +- Document user journeys +- Document API documentation +- Document technical debt +- Document team processes +- Document onboarding information +- Document best practices +- Document standards + +It's really up to you what you add here. + +### How high level documentation can help + +EventCatalog provides the ability to document your architecture with domains, services and messages. + +Users still have third party tools to document other parts of their architecture (e.g confluence, Google docs, etc), so this is an option to help you keep all your documentation in one place. + +### What can I do with high level documentation in EventCatalog? + +You can add any custom documentation to your catalog, this also gives you access to the [EventCatalog components](/docs/components). +Your custom documentation is powered by markdown, meaning you can use EventCatalog components within your documentation. + +### Roadmap for high level documentation + +This is the initial version of high level documentation in EventCatalog. + +We plan to add the following features: + +- Add support to embed EventCatalog visualizations into your documentation pages +- Embed EventCatalog resources into your custom documentation pages +- Add ability to add runtime blocks into your pages (e.g making requests to get third party data to display) diff --git a/packages/core/docs/development/bring-your-own-documentation/custom-pages/02-adding-custom-docs.md b/packages/core/docs/development/bring-your-own-documentation/custom-pages/02-adding-custom-docs.md new file mode 100644 index 000000000..9111c6be8 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/custom-pages/02-adding-custom-docs.md @@ -0,0 +1,245 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog high level documentation +sidebar_label: Adding high level documentation +title: Adding high level documentation +description: Adding high level documentation to your EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + + + + +## Getting started + +High level documentation requires an EventCatalog Scale license key. You can get a 30-day free trial from [EventCatalog Cloud](https://eventcatalog.cloud). + +## Adding high level documentation + +High level documentation is split into two parts: + +1. [Creating your documentation (markdown files)](#creating-your-documentation) +2. [Configuring your sidebar (eventcatalog.config.js)](#configuring-your-sidebar) + +### Creating documentation + +High level documentation is stored in the `/docs` folder in your catalog [(see example on GitHub)](https://github.com/event-catalog/eventcatalog/tree/main/examples/default/docs), and can be accessed via the url `/docs/custom/`. + +All documentation is a `.mdx` (markdown extended) file. You can organize these however you want, for example: + + + +In this example we have high level documentation for: + +- architecture Decision Records +- Runbooks + +:::tip Organize your high level documentation +We added numbers (01, 02, 03) to the files to help with maintenance, you can configure the order of the files in your configuration. +::: + + +#### Structure of markdown files for high level documentation + +Each markdown file is split into two parts: + +1. The frontmatter (properties at the top of the file) +2. The content (the rest of the file) + +```mdx +--- +title: Architecture decision records +summary: A comprehensive guide to creating new microservices at FlowMart following our best practices and standards +owners: + - dboyne +badges: + - content: 'Guide' + backgroundColor: 'teal' + textColor: 'teal' +--- + +This is your content for your file. + +``` + +**Frontmatter properties** + +| Property | Required | Description | +|----------|----------|-------------| +| `title` | Yes | The title of the page | +| `summary` | Yes | A summary of the page | +| `slug` | No | Ability to override the slug (url) of the page | +| `owners` | No | Owners of the page (EventCatalog owners) | +| `badges` | No | The badges of the page | + +#### Sidebar configuration + +You need to configure the sidebar to include your custom documentation, we do this by using the `customDocs` property in your `eventcatalog.config.js` file. + +We have two options for the sidebar: + +1. Manually map files and paths to the sidebar +2. Use the autogenerated option to generate the sidebar items + +:::tip Autogenerated sidebar +The autogenerated sidebar is the easiest option, it will render all the files in the directory and subdirectories. +If you want to choose which files and order they are displayed in, you can manually map the files and paths to the sidebar. +::: + +```js +export default { + // rest of your config + customDocs: { + sidebar: [ + { + label: 'architecture Decision Records', + badge: { + text: 'New', color: 'green' + }, + collapsed: false, + items: [ + { + // Here we map the directory, and use autogenerated to generate the sidebar items + // Any item added to the folder will be rendered automatically in EventCatalog + label: 'Drafts', autogenerated: { directory: '/architecture-decision-records/drafts' } + }, + { + label: 'Published', autogenerated: { directory: '/architecture-decision-records/published' } + }, + { + label: 'Examples', autogenerated: { directory: '/architecture-decision-records/examples' } + } + ] + }, + { + label: 'Runbooks', + badge: { + text: 'New', color: 'green' + }, + collapsed: false, + items: [ + { + // Here we create custom pages in the sidebar + // We can manually map the slug to a file, or use the autogenerated to generate the sidebar items + label: 'Creating a new runbook', items: [ + { label: 'Introduction', slug: 'runbooks/01-introduction' }, + { label: 'Deployment Procedure', slug: 'runbooks/02-deployment-procedure' }, + { label: 'Disaster Recovery', slug: 'runbooks/03-disaster-recovery' }, + { label: 'Incident Response', slug: 'runbooks/04-incident-response' }, + ] + }, + ] + } + ] + } +}; + ``` + + +#### Rendered output + +The rendered output will be the content of the markdown file, with the frontmatter properties. + +![Example](./img/custom-docs.png) +View demo + +### Custom HTML attributes + + + +Links can also include an attrs property to add custom HTML attributes to the link element. + +In the following example, attrs is used to add a target="_blank" attribute, so that the link opens in a new tab, and to apply a custom style attribute to italicize the link label: + +```js +export default { + // rest of your config + customDocs: { + sidebar: [ + { + label: 'My Links', + items: [ + { + label: 'Read more on GitHub', + link: 'https://github.com/event-catalog/eventcatalog', + attrs: { target: '_blank', style: 'font-style: italic;' }, + }, + ] + }, + ] + } +}; + ``` + + #### Rendered output + +![Example](./img/custom-attrs.png) + + + + + + + + + + diff --git a/packages/core/docs/development/bring-your-own-documentation/custom-pages/03-components.md b/packages/core/docs/development/bring-your-own-documentation/custom-pages/03-components.md new file mode 100644 index 000000000..86d600365 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/custom-pages/03-components.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 3 +keywords: + - components + - domains +sidebar_label: Components +title: Components +description: Component list for custom pages +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +You can add custom EventCatalog components to your custom pages. + +[See the list of components and how to use them](/docs/development/components/using-components). + +You can also added components designed specifically for custom documentation. + +### Embed EventCatalog Visualizer (NodeGraph) + +In your custom documentation pages you can embed the EventCatalog Visualizer (NodeGraph) using the `NodeGraph` component. + +![Example](./img/custom-docs-with-nodegraph.png) + +This let's you embed any visualization (domain, service, message) into your custom documentation page. + +To embed the NodeGraph component, you need to use the component, and pass in the `id` of the resource, `version` and `type`. + +```md +This is my custom documentation page, here is a NodeGraph: + + + + + +## Getting Started + +Rest of my markdown content... +``` + +:::note Required props +In custom documentation pages, `id`, `version`, and `type` are all required. A `` missing any of these props is silently ignored. +::: + +#### NodeGraph props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `id` | string | Yes | The id of the resource (domain, service, or message) | +| `version` | string | Yes | The version of the resource | +| `type` | string | Yes | The type of the resource (domain, service, command, event, query) | +| `search` | boolean | No | Show or hide the search bar. Accepts `true`/`false` or `"true"`/`"false"`. Defaults to `true`. | +| `legend` | boolean | No | Show or hide the legend. Accepts `true`/`false` or `"true"`/`"false"`. Defaults to `true`. | +| `mode` | string | No | `"simple"` or `"full"`. Defaults to `"simple"`. | diff --git a/packages/core/docs/development/bring-your-own-documentation/custom-pages/04-owners.md b/packages/core/docs/development/bring-your-own-documentation/custom-pages/04-owners.md new file mode 100644 index 000000000..ede315648 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/custom-pages/04-owners.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 4 +keywords: +- EventCatalog +- channels +- owners +sidebar_label: Owners +title: Adding custom page owners +description: Adding owners to custom pages with EventCatalog. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + +Owners in EventCatalog are either **users** or **teams** and are **optional**. + +To add an owner or a team, you need to add the user or team to the owners field of the custom page. + +## Adding owners using frontmatter + +To add owners within a custom page you need to add them to the `owners` array within your custom page frontmatter API. + +You need to add the `id` of the owner. + +```md title="/docs/guides/creating-a-new-producer/index.mdx (example)" +--- +id: OrderChannel +... # other custom page frontmatter +owners: + - dboyne # represents a user + - webTeam # represents a team +--- + + + +``` + +Assigning owners to your custom pages can provide others with context of who owns this custom page and how to contact them. + +:::tip Creating users and teams + EventCatalog gives you the ability to create users and teams. You can read the documentation to get started. +::: + diff --git a/packages/core/docs/development/bring-your-own-documentation/custom-pages/_category_.json b/packages/core/docs/development/bring-your-own-documentation/custom-pages/_category_.json new file mode 100644 index 000000000..fbc3ecc92 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/custom-pages/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "High level documentation", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "guides/bring-your-own-documentation/custom-pages", + "description": "Add catalog-wide custom documentation pages to your catalog." + } +} \ No newline at end of file diff --git a/packages/core/docs/development/bring-your-own-documentation/diagrams/01-introduction.md b/packages/core/docs/development/bring-your-own-documentation/diagrams/01-introduction.md new file mode 100644 index 000000000..214ef4e80 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/diagrams/01-introduction.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog diagrams +- Architecture diagrams +- Custom diagrams +- Mermaid +- PlantUML +- Miro +- IcePanel +sidebar_label: What are diagrams? +title: What are diagrams? +description: Bring your own diagrams to EventCatalog - version them, compare them, and assign them to any resource +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +You can bring your own diagrams (e.g Miro, DrawIO, Mermaid, PlantUML) into your catalog and assign them to any resource in your catalog. + +With diagrams in EventCatalog you can: + +- **Bring any diagram type** - Mermaid, PlantUML, Miro embeds, IcePanel, Lucidchart, draw.io, images, or any MDX content +- **Version and compare your diagrams** - Track how your architecture visualizations evolve over time and compare them +- **Assign to any resource** - Link to any catalog resource (e.g domain, system, service, message) +- **Context to your AI** - EventCatalog MCP exposes your diagrams to your AI agents. + + diff --git a/packages/core/docs/development/bring-your-own-documentation/diagrams/02-creating-diagrams.md b/packages/core/docs/development/bring-your-own-documentation/diagrams/02-creating-diagrams.md new file mode 100644 index 000000000..c222d8b7d --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/diagrams/02-creating-diagrams.md @@ -0,0 +1,262 @@ +--- +sidebar_position: 2 +keywords: +- EventCatalog diagrams +- Creating diagrams +sidebar_label: Create a diagram +title: Create a diagram +description: How to create and organize diagrams in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PromptBox from '@site/src/components/MDX/PromptBox'; +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + +EventCatalog Diagrams let's you bring your own diagrams into your Catalog. + +![System Architecture diagram](./img/diagrams.png) + +## Creating a diagram + +### Automatic Creation + + +Read https://www.eventcatalog.dev/docs/development/bring-your-own-documentation/diagrams/creating-diagrams.md and https://raw.githubusercontent.com/event-catalog/skills/refs/heads/main/skills/catalog-documentation-creator/references/diagrams.md then help me create a new EventCatalog diagram in my catalog. + +Ask me for the diagram name, what it shows, summary, whether it belongs at the root of the catalog or inside a resource, and what diagram format I want to use such as Mermaid, PlantUML, an embedded tool, an image, or markdown. Then create the correct diagrams/{'{Diagram Name}'}/index.mdx, domains/{'{Domain Name}'}/diagrams/{'{Diagram Name}'}/index.mdx, services/{'{Service Name}'}/diagrams/{'{Diagram Name}'}/index.mdx, or another relevant resource-level diagrams/{'{Diagram Name}'}/index.mdx file with frontmatter and starter markdown. + +If I want to embed an external diagram, ask for the embed URL and use the correct EventCatalog diagram component if one exists. + +If the catalog does not have a relevant resource, put it into the root diagrams folder. + +You can use MDX components found here https://raw.githubusercontent.com/event-catalog/skills/refs/heads/main/skills/catalog-documentation-creator/references/components.md + + +Copy this prompt and paste it into your coding agent. Your agent can help you choose where the diagram should live, create the right folder structure, and add the first version of the diagram documentation. + +### Manual Creation + +Diagrams live in a `diagrams` folder. EventCatalog discovers any `index.mdx` file inside a `diagrams` directory, regardless of where that directory lives in your catalog. + +You can place diagrams: + +At the root of your catalog: + + + +Inside a domain: + + + +Inside a service: + + + +:::tip +Organize diagrams close to where they're most relevant. System-wide diagrams can be placed at the root level, while resource-specific diagrams should live within that resource's folder. +::: + +## Create the diagram file + +Create an `index.mdx` file for the diagram. + +Here is an example of a system architecture diagram: + +```md title="/diagrams/system-overview/index.mdx (example)" +--- +id: system-overview +name: System Overview +version: 1.0.0 +summary: High-level architecture showing all microservices and their interactions +--- + +## System Architecture + +This diagram shows our microservices architecture: + +\`\`\`mermaid +graph TB + subgraph "Frontend" + WebApp[Web Application] + MobileApp[Mobile App] + end + + subgraph "Backend Services" + Gateway[API Gateway] + OrderService[Order Service] + PaymentService[Payment Service] + InventoryService[Inventory Service] + end + + subgraph "Data Layer" + OrderDB[(Orders DB)] + PaymentDB[(Payments DB)] + Kafka[Event Stream] + end + + WebApp --> Gateway + MobileApp --> Gateway + Gateway --> OrderService + Gateway --> PaymentService + OrderService --> Kafka + PaymentService --> Kafka + OrderService --> OrderDB + PaymentService --> PaymentDB +\`\`\` + +### Key Components + +- **API Gateway**: Single entry point for all client requests +- **Order Service**: Handles order creation and management +- **Payment Service**: Processes payments and refunds +- **Event Stream**: Kafka for asynchronous communication +``` + + +### Example with PlantUML + +```md title="/diagrams/order-flow/index.mdx (example)" +--- +id: order-flow +name: Order Processing Flow +version: 1.0.0 +summary: Sequence diagram showing the complete order processing flow +--- + +\`\`\`plantuml +@startuml +actor Customer +participant "Order Service" as Order +participant "Payment Service" as Payment +participant "Inventory Service" as Inventory + +Customer -> Order: Create Order +Order -> Inventory: Check Stock +Inventory --> Order: Stock Available +Order -> Payment: Process Payment +Payment --> Order: Payment Confirmed +Order --> Customer: Order Confirmed +@enduml +\`\`\` + +## Order Processing Flow + +This sequence diagram illustrates the order processing workflow: + +1. Customer initiates order creation +2. Order service validates inventory availability +3. Payment is processed +4. Order confirmation is sent to customer +``` + +### Example with embedded diagram + +EventCatalog provides built-in components to embed diagrams from popular tools like Miro, IcePanel, Lucidchart, draw.io, and FigJam. This lets you bring your existing collaborative diagrams directly into your catalog. + +```md title="/diagrams/architecture-overview/index.mdx (example)" +--- +id: architecture-overview +name: Architecture Overview +version: 1.0.0 +summary: Miro board showing our system architecture and design decisions +--- + + + +## Architecture Overview + +This Miro board captures our architecture decisions and system design. +Key areas covered: + +- System context +- Container architecture +- Component relationships +- Technology choices +``` + +:::tip +Check out the [Diagrams documentation](/docs/components/diagram-syntax) to see all available diagram syntax and embed components including ``, ``, ``, ``, and ``. +::: + +## Next steps + +Once you've created diagrams, you can: + +- [Reference them from resources](/docs/development/bring-your-own-documentation/diagrams/referencing-diagrams) like domains, services, and messages +- [Compare diagram versions](/docs/development/bring-your-own-documentation/diagrams/comparing-diagrams) side-by-side diff --git a/packages/core/docs/development/bring-your-own-documentation/diagrams/03-referencing-diagrams.md b/packages/core/docs/development/bring-your-own-documentation/diagrams/03-referencing-diagrams.md new file mode 100644 index 000000000..22f16a886 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/diagrams/03-referencing-diagrams.md @@ -0,0 +1,111 @@ +--- +sidebar_position: 3 +keywords: +- EventCatalog diagrams +- Referencing diagrams +sidebar_label: Add diagrams to resources +title: Add diagrams to resources +description: How to link diagrams to domains, services, messages, and other resources +--- + +You can assign your diagrams to resources in EventCatalog. + +When you reference a diagram from a resource, EventCatalog automatically: + +- Adds the diagram to the resource's sidebar under a "Diagrams" section +- Creates a clickable link to the full diagram page +- Shows the diagram name and summary in the sidebar + +
    + Diagram references +
    + +## Referencing diagrams in frontmatter + +To reference diagrams from any resource, use the `diagrams` field in the frontmatter: + +```yaml +diagrams: + - id: diagram-id + # version is optional and defaults to latest if not specified + version: 1.0.0 +``` + +The `version` field is optional and defaults to `latest` if not specified. + +## Examples + +### Referencing diagrams from a domain + +Domain-level diagrams often show the overall architecture, domain boundaries, or integration patterns. + +```md title="/domains/E-Commerce/index.mdx" +--- +id: E-Commerce +name: E-Commerce Domain +version: 1.0.0 +summary: Core business domain for our e-commerce platform +diagrams: + - id: target-architecture + version: 1.0.0 + - id: order-flow + version: 1.0.0 +--- + +## Overview + +The E-Commerce domain handles all order processing... +``` + +When users view the E-Commerce domain, they'll see a "Diagrams" section in the sidebar with links to both the "Target Architecture" and "Order Flow" diagrams. + +### Referencing diagrams from a service + +Service-level diagrams typically show API flows, service interactions, or internal component architecture. + +```md title="/services/OrderService/index.mdx" +--- +id: OrderService +name: Order Service +version: 2.0.0 +summary: Manages order lifecycle and orchestration +diagrams: + - id: order-api-flow + version: 2.0.0 + - id: order-state-machine + version: 1.0.0 +--- + +## Overview + +The Order Service is responsible for... +``` + +### Referencing diagrams from a message + +Message-level diagrams can show sequence flows, event propagation, or payload structures. + +```md title="/events/OrderCreated/index.mdx" +--- +id: OrderCreated +name: Order Created +version: 1.0.0 +summary: Published when a new order is created +diagrams: + - id: order-creation-flow + version: 1.0.0 +--- + +## Event Details + +This event is published when... +``` + +## Viewing referenced diagrams + +When viewing a resource that references diagrams, users will see: + +1. A "Diagrams" section in the sidebar navigation +2. Each diagram listed with its name +3. Clicking a diagram navigates to the full diagram page +4. The diagram page includes version selection and full content diff --git a/packages/core/docs/development/bring-your-own-documentation/diagrams/05-comparing-diagrams.md b/packages/core/docs/development/bring-your-own-documentation/diagrams/05-comparing-diagrams.md new file mode 100644 index 000000000..09b56d0cf --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/diagrams/05-comparing-diagrams.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 5 +keywords: +- EventCatalog diagrams +- Diagram comparison +sidebar_label: Compare diagram versions +title: Comparing diagram versions +description: Side-by-side version comparison for diagrams in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +The diagram comparison feature allows you to view two versions of a diagram side-by-side, making it easy to understand architectural changes and evolution over time. + +This can be useful if you want to plan for changes, or track changes over time. + +![Compare diagram versions](./img/diagrams-compare.png) + +### How to compare diagrams + +1. Navigate to any diagram page that has multiple versions +2. Click the "Compare diagram versions" button in the header +3. Select which two versions you want to compare +4. View both diagrams side-by-side with synchronized scrolling + diff --git a/packages/core/docs/development/bring-your-own-documentation/diagrams/07-reference.md b/packages/core/docs/development/bring-your-own-documentation/diagrams/07-reference.md new file mode 100644 index 000000000..33affd555 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/diagrams/07-reference.md @@ -0,0 +1,144 @@ +--- +keywords: +- EventCatalog diagrams +- Diagram frontmatter +sidebar_label: Reference +title: Diagrams reference +description: Frontmatter fields, paths, and routes for diagrams in EventCatalog. +--- + +This page lists the fields, paths, and routes supported by diagrams. + +## Paths + +Diagrams can be created in any `diagrams` folder: + +```txt +/diagrams/{Diagram Name}/index.mdx +/domains/{Domain Name}/diagrams/{Diagram Name}/index.mdx +/systems/{System Name}/diagrams/{Diagram Name}/index.mdx +``` + +Versioned diagrams use: + +```txt +/diagrams/{Diagram Name}/versioned/{version}/index.mdx +``` + +## Routes + +| Route | Description | +|-------|-------------| +| `/docs/diagrams/{diagram-id}/{version}` | Diagram documentation page. | + +## Required fields + +### `id` {#id} + +- Type: `string` + +Unique id of the diagram. EventCatalog uses this for URLs and resource references. + +```md title="Example" +--- +id: checkout-sequence +--- +``` + +### `name` {#name} + +- Type: `string` + +Display name of the diagram. + +```md title="Example" +--- +name: Checkout Sequence +--- +``` + +### `version` {#version} + +- Type: `string` + +Version of the diagram documentation. + +```md title="Example" +--- +version: 1.0.0 +--- +``` + +## Optional fields + +### `summary` {#summary} + +- Type: `string` + +Short description of the diagram. + +```md title="Example" +--- +summary: Sequence diagram for checkout orchestration. +--- +``` + +### `owners` {#owners} + +- Type: `array` + +An array of team or user ids that own the diagram. + +```md title="Example" +--- +owners: + - checkout-platform +--- +``` + +### `badges` {#badges} + +- Type: `array` + +Badges rendered on the diagram page. + +```md title="Example" +--- +badges: + - content: Mermaid + backgroundColor: blue + textColor: blue +--- +``` + +### `repository` {#repository} + +- Type: `object` + +Repository metadata for the diagram. + +```md title="Example" +--- +repository: + url: https://github.com/acme/architecture +--- +``` + +### `attachments` {#attachments} + +- Type: `array` + +External links or supporting documents attached to the diagram. + +```md title="Example" +--- +attachments: + - title: Source diagram + url: https://miro.com/app/board/example + type: miro +--- +``` + +## Content + +Diagram pages can contain any MDX supported by EventCatalog, including Mermaid, PlantUML, Miro, IcePanel, Lucid, Draw.io, FigJam, images, and normal markdown. diff --git a/packages/core/docs/development/bring-your-own-documentation/diagrams/_category_.json b/packages/core/docs/development/bring-your-own-documentation/diagrams/_category_.json new file mode 100644 index 000000000..964bf042a --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/diagrams/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Diagrams", + "position": 4, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "bring-your-own-documentation/diagrams", + "description": "Learn how to document your architecture with reusable diagrams" + } +} diff --git a/packages/core/docs/development/bring-your-own-documentation/resource-docs/01-introduction.md b/packages/core/docs/development/bring-your-own-documentation/resource-docs/01-introduction.md new file mode 100644 index 000000000..3ce82998a --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/resource-docs/01-introduction.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +title: Resource-level documentation +description: Attach documentation pages to any resource in your catalog. +keywords: + - EventCatalog resource-level documentation + - ADRs + - runbooks + - resource-level documentation +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + +Resource-level documentation lets you attach documentation pages directly to any resource in your catalog. Instead of keeping ADRs, runbooks, contracts, and guides in a separate tool, you can place them alongside the resource they describe. + +![](./img/docs.png) + +Attached docs appear in the resource sidebar, grouped by custom defined types, so readers can navigate between the resource overview and its supporting documentation without leaving the catalog. + +### Supported resources + +Resource-level documentation can be attached to any of the following resource types: + +- Domains and subdomains +- Services +- Events, Commands, Queries +- Flows, Channels, Containers, Entities, Data products diff --git a/packages/core/docs/development/bring-your-own-documentation/resource-docs/02-adding-resource-docs.md b/packages/core/docs/development/bring-your-own-documentation/resource-docs/02-adding-resource-docs.md new file mode 100644 index 000000000..ec1e4925b --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/resource-docs/02-adding-resource-docs.md @@ -0,0 +1,238 @@ +--- +sidebar_position: 2 +sidebar_label: Adding resource-level documentation +title: Adding resource-level documentation +description: Place MDX files in a docs/ directory alongside any resource. +keywords: + - EventCatalog resource docs + - ADRs + - runbooks + - resource documentation +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + + + + +## Adding resource-level documentation + +Place `.mdx` files inside a `docs/` directory under any resource. EventCatalog will automatically pick them up and display them in the resource's sidebar. + + + +### Frontmatter properties + +Each doc file supports the following frontmatter. + +| Property | Required | Description | +|-----------|----------|--------------------------------------------------| +| `title` | No | Display name of the doc. Defaults to the file name. | +| `summary` | No | Short description shown below the title. | +| `type` | No | Groups the doc in the sidebar. Defaults to the folder name, or `pages` if placed directly in `docs/`. | +| `version` | No | Doc version. Defaults to the resource version. | +| `order` | No | Explicit sort position within the group. | +| `badges` | No | Badges shown on the doc page. | + + +#### Example of resource-level documentation + +```mdx title="services/OrdersService/docs/01-deployment.mdx" +--- +title: Deployment runbook +summary: Step-by-step guide for deploying the Orders service to production. +version: 1.0.0 +--- + +## Pre-deployment checklist + +1. Confirm staging tests pass. +2. Notify the on-call engineer. +... +``` + +### Order docs with numeric prefixes + +Files are sorted alphabetically by default. Prefix the file name with a number to control the order. + + + +The numeric prefix is stripped from the doc's ID and URL, so `01-deployment.mdx` is accessible at `.../pages/deployment`. + +You can also set an explicit `order` value in frontmatter, which takes precedence over the numeric prefix. + +### Use with domains and subdomains + +Domains and subdomains follow the same pattern. + + + +## Navigate to a resource doc + +Resource docs are served at: + +``` +http://localhost:3000/docs/{resourceType}/{resourceId}/{version}/{docType}/{docId} +``` + +For example, `01-deployment.mdx` for `OrdersService` version `1.0.0` would be at: + +``` +http://localhost:3000/docs/services/OrdersService/1.0.0/pages/deployment +``` + +## Grouping docs by type + +Docs can be organised into groups using subdirectories inside `docs/`. Each subdirectory becomes a **doc type** and gets its own section in the resource sidebar. + + + +The folder name is used as the group label by default. The built-in types `adrs`, `runbooks`, `contracts`, `troubleshooting`, and `guides` are automatically formatted with friendly labels in the sidebar. + +You can override the type for any doc using the `type` frontmatter field, regardless of which folder it lives in: + +```mdx title="services/OrdersService/docs/adrs/01-use-postgres.mdx" +--- +title: Use PostgreSQL for order storage +type: architecture-records +--- +``` + +The type resolution order is: + +1. `type` frontmatter — takes highest precedence +2. Folder name — used when `type` is not set +3. `pages` — fallback when the doc is placed directly in `docs/` with no subfolder diff --git a/packages/core/docs/development/bring-your-own-documentation/resource-docs/03-categories.md b/packages/core/docs/development/bring-your-own-documentation/resource-docs/03-categories.md new file mode 100644 index 000000000..5ca055f46 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/resource-docs/03-categories.md @@ -0,0 +1,97 @@ +--- +sidebar_position: 3 +sidebar_label: Configure categories +title: Configure categories +description: Control sidebar labels and ordering for doc type groups using category files. +keywords: + - EventCatalog resource docs categories + - category.json + - resource documentation +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + + + + +When you group resource docs into subdirectories, each subdirectory becomes a section in the resource sidebar. By default, the section label is the folder name and sections are sorted alphabetically. + +**Categories** let you customise this — you can give a group a friendlier display label and control the order it appears relative to other groups, without renaming the folder. + +## Category files + +Place a `category.json` file inside any doc type folder to configure its sidebar label and position. + + + +```json title="services/OrdersService/docs/runbooks/category.json" +{ + "label": "Runbooks", + "position": 1 +} +``` + +### Category file properties + +| Property | Required | Description | +|------------|----------|------------------------------------------------------------------| +| `label` | No | Display name for the doc type group in the sidebar. | +| `position` | No | Sort order of the group relative to other doc type groups. | + +### Use `_category_.json` as an alternative + +EventCatalog also accepts `_category_.json` as the file name. When both files exist in the same folder, `category.json` takes precedence. + +### Control group ordering + +Set `position` on each group to control the order they appear in the sidebar. Groups without a position are sorted alphabetically after positioned groups. + +```json title="docs/adrs/category.json" +{ + "label": "Architecture decisions", + "position": 1 +} +``` + +```json title="docs/runbooks/category.json" +{ + "label": "Runbooks", + "position": 2 +} +``` diff --git a/packages/core/docs/development/bring-your-own-documentation/resource-docs/04-versioning.md b/packages/core/docs/development/bring-your-own-documentation/resource-docs/04-versioning.md new file mode 100644 index 000000000..905dcc132 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/resource-docs/04-versioning.md @@ -0,0 +1,94 @@ +--- +sidebar_position: 4 +sidebar_label: Version documents +title: Version documents +description: Keep historical versions of resource docs alongside versioned resources. +keywords: + - EventCatalog resource docs versioning + - versioned documentation +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + + + + +## How versioning works + +Resource docs are associated with a specific version of a resource. When you version a resource, the docs in that resource's `docs/` directory apply to the current (latest) version. + +Previous versions of a doc are stored inside the resource's `versioned/` directory, following the same pattern used for versioned resources. + + + +## Navigate between versions + +When a doc has multiple versions, a **Versions** panel is shown in the right-hand column of the doc page. You can use it to jump to any historical version of that document. + +A banner is shown at the top of older versions to make clear that a newer version exists. + +## Doc version vs resource version + +A doc file's `version` frontmatter field refers to the **doc's own version**, not the resource version. This allows a doc to be updated independently of the resource it belongs to. + +If `version` is omitted from frontmatter, EventCatalog infers it from the resource version the doc file lives under. diff --git a/packages/core/docs/development/bring-your-own-documentation/resource-docs/_category_.json b/packages/core/docs/development/bring-your-own-documentation/resource-docs/_category_.json new file mode 100644 index 000000000..2334c2b67 --- /dev/null +++ b/packages/core/docs/development/bring-your-own-documentation/resource-docs/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Resource-level documentation", + "position": 3, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "guides/bring-your-own-documentation/resource-docs", + "description": "Attach documentation pages directly to any resource in your catalog." + } +} diff --git a/packages/core/docs/development/components/04-snippets.md b/packages/core/docs/development/components/04-snippets.md new file mode 100644 index 000000000..8ec03c77d --- /dev/null +++ b/packages/core/docs/development/components/04-snippets.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 6 +keywords: +- snippets +sidebar_label: Reusable snippets +title: Reusable snippets +description: Understanding how to use snippets with EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +Keep your documentation consistent and maintainable with reusable snippets. + +In EventCatalog, staying DRY (Don't Repeat Yourself) isn't just for code—it's for documentation too. If you're repeating the same content across multiple pages, consider using a snippet to centralize it. This makes your docs easier to manage and keeps everything in sync. + +## Creating a Snippet + +:::important +Snippets must live in the `/snippets` directory to be recognized by EventCatalog. Files in this folder won’t generate standalone pages—they’re designed to be imported wherever needed. +::: + +## Basic Usage + +1. Create your snippet + +```md title="snippets/my-snippet.mdx" +Hello world! This is my content I want to reuse across pages. +``` + +2. Import and use in any page: + +```markdown title="/domains/my-domain/index.mdx" +--- +id: E-Commerce +name: E-Commerce +version: 1.0.0 +--- + + +import MySnippet from '@eventcatalog/snippets/my-snippet.mdx'; + + + + +``` + + +## Passing Variables to Snippets + +Snippets can accept props for dynamic content. Here's how: + +1. Create a snippet with props: + +```md title="snippets/my-snippet.mdx" +Hello {props.name}! This is my content I want to reuse across pages. +``` + +2. Import and use with props: + +```md title="/domains/my-domain/index.mdx" +--- +id: E-Commerce +name: E-Commerce +version: 1.0.0 +--- + + +import MySnippet from '@eventcatalog/snippets/my-snippet.mdx'; + + + + +``` + +## Exporting Variables + +You can also export constants or objects from a snippet for use elsewhere. + +1. Export a variable from the snippet: + +```md title="snippets/my-snippet.mdx" +export const platformName = 'EventCatalog'; +export const colors = { primary: '#000000' }; +``` + +2. Import the variable in your page: + +```md title="/domains/my-domain/index.mdx" +--- +id: E-Commerce +name: E-Commerce +version: 1.0.0 +--- + + +import { platformName, colors } from '/snippets/constants.mdx'; + + +

    Hello {platformName}!

    +

    The primary color is {colors.primary}.

    + +``` + +## JSX-Based Snippets + +Need something more dynamic? Use a snippet as a JSX component: + +```md title="snippets/my-snippet.mdx" +export default function MySnippet({ name }) { + return
    Hello {name}!
    ; +} +``` + +Then import and use it like this: + +```md title="/domains/my-domain/index.mdx" +--- +id: E-Commerce +name: E-Commerce +version: 1.0.0 +--- + + +import MySnippet from '@eventcatalog/snippets/my-snippet.mdx'; + + + + +``` + + + diff --git a/packages/core/docs/development/components/05-using-components.md b/packages/core/docs/development/components/05-using-components.md new file mode 100644 index 000000000..e9ae6f3ce --- /dev/null +++ b/packages/core/docs/development/components/05-using-components.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 1 +keywords: +- components +sidebar_label: Introduction +title: Using components +description: Understanding components +--- + +EventCatalog is powered by [Astro](https://astro.build/) and [MDX](https://mdxjs.com/). This gives you the flexibility to use [prebuilt components](/docs/components/list) or [create your own components](/docs/components/custom-components) for your documentation. + +### Using prebuild components + +EventCatalog comes with a range of prebuilt components, you can use these by adding them to the markdown content of any resource. + +This example renders an Accordion component within an event. + +```md title="/events/Orders/OrderAmended/index.mdx" +--- +id: OrderAmended +name: Order amended +version: 0.0.1 +summary: | + Indicates an order has been changed +owners: + - dboyne + - msmith +badges: + - content: Recently updated! + backgroundColor: green + textColor: green + - content: Channel:Apache Kafka + backgroundColor: yellow + textColor: yellow +--- + +## Overview + +Event is raised when an order has been changed. + + + You can run the following command to raise this event. + + ```sh + bin/kafka-topics.sh --create --topic OrderAmended --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1 + `` + + +``` + + +![Example](./img/accordian.png) + + + diff --git a/packages/core/docs/development/components/07-resource-references.md b/packages/core/docs/development/components/07-resource-references.md new file mode 100644 index 000000000..012a74b5e --- /dev/null +++ b/packages/core/docs/development/components/07-resource-references.md @@ -0,0 +1,136 @@ +--- +sidebar_position: 5 +keywords: +- resource references +- links +- cross-references +sidebar_label: Resource references +title: Resource references +description: Create inline links to resources with hover tooltips +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +Create inline references to resources in your documentation using wiki-style syntax. References render as styled links with interactive tooltips showing resource details. + + + +## Basic syntax + +Use double square brackets to create a resource reference. + +```md +[[type|ResourceName]] +``` + +The reference automatically links to the resource and displays a tooltip on hover showing key information like version, summary, and related resources. + +### Example + +```md +--- +id: OrdersService +name: Orders Service +version: 0.0.1 +summary: | + Handles all order processing +owners: + - dboyne + - msmith +--- + +The [[service|OrdersService]] handles all order processing and will publish the event [[event|OrderCreated]] when an order is created. +``` + +This creates a link to the OrdersService with a tooltip showing service details, published messages, API specifications, and owners. + +## Supported resource types + +Reference any resource type in your catalog. + +| Type | Syntax | Links to | +|------|--------|----------| +| Service | `[[service\|OrdersService]]` | Service documentation | +| Event | `[[event\|OrderCreated]]` | Event documentation | +| Command | `[[command\|CreateOrder]]` | Command documentation | +| Query | `[[query\|GetOrderStatus]]` | Query documentation | +| Domain | `[[domain\|E-Commerce]]` | Domain documentation | +| Flow | `[[flow\|PaymentFlow]]` | Flow documentation | +| Channel | `[[channel\|OrderChannel]]` | Channel documentation | +| Entity | `[[entity\|Order]]` or `[[Order]]` | Entity documentation | +| Diagram | `[[diagram\|target-architecture]]` | Diagram page | +| Container | `[[container\|APIGateway]]` | Container documentation | +| User | `[[user\|dboyne]]` | User profile | +| Team | `[[team\|backend-team]]` | Team profile | +| Custom doc | `[[doc\|guides/getting-started]]` | Custom documentation page | + +### Reference custom docs + +Link to pages in your [custom documentation](/docs/development/bring-your-own-documentation/custom-pages/introduction) using path-based identifiers. + +```md +[[doc|guides/getting-started]] +[[doc|operations-and-support/runbooks/payment-service-runbook]] +``` + +The path matches the file location inside your `docs/` directory, relative to the catalog root. Paths are case-insensitive and leading `/docs/custom/` prefixes are stripped automatically. + +Doc references do not support version pinning since custom documentation pages are not versioned. + +### Default to entity + +Reference entities without specifying the type. + +```md +The [[Customer]] entity stores user information. +``` + +This defaults to entity type and is equivalent to `[[entity|Customer]]`. + +## Version pinning + +Reference a specific version of a resource. + +```md +[[service|OrdersService@1.0.0]] +``` + +Without a version, the reference uses the latest version. Pin versions when documenting specific implementations or historical states. + +### Example + +```md +Our legacy [[service|PaymentService@0.9.0]] is being replaced by [[service|PaymentGatewayService]]. +``` + +## Interactive tooltips + +Hover over any reference to see detailed information without leaving the page. + + + +Tooltips show different information based on resource type. + +### Combine with other components + +Mix references with other EventCatalog components. + +```md +## Architecture + +The [[service|OrdersService]] coordinates between inventory and payment: + + +``` + +### Review references regularly + +Regularly audit references to ensure they point to current resources and remove outdated links. diff --git a/packages/core/docs/development/components/_category_.json b/packages/core/docs/development/components/_category_.json new file mode 100644 index 000000000..5c3dbb260 --- /dev/null +++ b/packages/core/docs/development/components/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Components", + "position": 7, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "components", + "title": "Components", + "description": "Use MDX components, diagram syntax, and external embeds in EventCatalog documentation." + } +} diff --git a/packages/core/docs/development/components/components/01-accordian.md b/packages/core/docs/development/components/components/01-accordian.md new file mode 100644 index 000000000..a8ca7d127 --- /dev/null +++ b/packages/core/docs/development/components/components/01-accordian.md @@ -0,0 +1,41 @@ +--- +sidebar_position: 1 +keywords: +- components +sidebar_label: Accordion +title: Accordion +description: Component for EventCatalog +--- + +The accordion component renders collapsable section in EventCatalog. + +```jsx /events/OrderAmended/index.mdx + + This will be rendered as a child inside a collapsible section. + +``` +**Example with code as child** + +```jsx /events/OrderAmended/index.mdx + + You can run the following command to raise this event. + + ```sh + bin/kafka-topics.sh --create --topic OrderAmended --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1 + `` + +``` + +### Output + +![Example output](./img/accordian.png) + +### Props +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `title` (required) | `string` | (empty) | Title to render in your accordion block | +| `children` | `string` | (empty) | Content that goes inside your accordion| + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/02-accordian-group.md b/packages/core/docs/development/components/components/02-accordian-group.md new file mode 100644 index 000000000..84a35f581 --- /dev/null +++ b/packages/core/docs/development/components/components/02-accordian-group.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 2 +keywords: +- components +sidebar_label: AccordionGroup +title: AccordionGroup +description: Component for EventCatalog +--- + +The accordion group component renders a collection of accordions. + +**Basic Example** + +```jsx /events/OrderAmended/index.mdx + + Hello + Hello this is an example + Another example + Final example + +``` + +**Code example** + +Add code inside the Accordion to render code snippets. + +```jsx /events/OrderAmended/index.mdx + + + ```js + console.log('My code here'); + `` + + + ```js + console.log('My other code snippet'); + `` + + + ```json + { "test": true} + `` + + +``` + +### Output +![Example output](./img/accordiangroup.png) + +### Props +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `children` | `Accordion` | (empty) | Accordion components that are contained within the group| + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/03-admonitions.md b/packages/core/docs/development/components/components/03-admonitions.md new file mode 100644 index 000000000..b52df15ea --- /dev/null +++ b/packages/core/docs/development/components/components/03-admonitions.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 3 +keywords: +- components +sidebar_label: Admonitions +title: Admonitions +description: Component for EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The admonitions syntax is used to render a block of text with a specific style. You can use it to highlight important information, warnings, or other types of content. + +**Basic Example** + +```md /events/OrderAmended/index.mdx +:::note +Some content with _Markdown_ syntax. Check [this api](https://eventcatalog.dev/). +::: + +:::tip +Some content with _Markdown_ syntax. Check [this api](https://eventcatalog.dev/). +::: + +:::warning +Some content with _Markdown_ syntax. Check [this api](https://eventcatalog.dev/). +::: + +:::danger +Some content with _Markdown_ syntax. Check [this api](https://eventcatalog.dev/). +::: + +``` + + +### Output +![Example output](./img/admonitions.png) + +### Support + +The markdown syntax is supported in all pages in EventCatalog. diff --git a/packages/core/docs/development/components/components/04-agent-tools.md b/packages/core/docs/development/components/components/04-agent-tools.md new file mode 100644 index 000000000..fe8e9da42 --- /dev/null +++ b/packages/core/docs/development/components/components/04-agent-tools.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 4 +keywords: +- components +- agents +- agent tools +sidebar_label: AgentTools +title: AgentTools +description: Component for displaying the tools an agent can call in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The `` component renders a table of the tools an agent can call (MCP servers, REST APIs, internal search indexes, databases, and so on). + +The component reads the `tools` array from the current agent's frontmatter, so it requires no props. + +### Use case + +- Display all the external capabilities an agent reaches out to at runtime. +- Show MCP server endpoints, REST APIs, and other integrations on the agent page. + +**Basic Example** + +```jsx /agents/OrderSupportAgent/index.mdx + +``` + +### Output + +![Example output](./img/agent-tools.png) + +### Props + +The `` component takes no props — it reads the `tools` array from the current agent's frontmatter. + +For details on the `tools` frontmatter shape, see [Agent tools](/docs/development/guides/resources/agents/adding-tools). + +### Support + +The `` component is supported in agent pages only. If you add it to a service, message, or domain page, the catalog will display a warning and skip the table. diff --git a/packages/core/docs/development/components/components/05-attachments.md b/packages/core/docs/development/components/components/05-attachments.md new file mode 100644 index 000000000..b7f232788 --- /dev/null +++ b/packages/core/docs/development/components/components/05-attachments.md @@ -0,0 +1,56 @@ +--- +sidebar_position: 5 +keywords: +- attachments +- documentation +- ADR +- diagrams +sidebar_label: Attachments +title: Attachments +description: Learn how to add attachments to your EventCatalog resources +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The Attachments component allows you to link related documents, diagrams, and resources to any EventCatalog resource. This is perfect for connecting Architecture Decision Records (ADRs), design documents, external diagrams, or any other relevant documentation. + +Attachments can be a url (string) or an object with additional properties. + +Here we have a domain with two attachments, one is a simple url and the other is an object with additional properties. + +```md title="domains/E-Commerce/index.mdx" +--- +id: E-Commerce +name: E-Commerce Domain +attachments: + - https://example.com/adr/001-microservices-architecture + - url: https://example.com/adr/001 + title: ADR-001 - Use Kafka for asynchronous messaging + description: Learn more about why we chose Kafka for asynchronous messaging in this architecture decision record. + type: 'architecture-decisions' + icon: FileTextIcon +--- + +## Domain Overview + +This domain handles all e-commerce operations. + + + +``` + +### Output + +![Example output](./img/attachments.png) + +### Props +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `title` (optional) | `string` | (empty) | Title to render in your attachments block | +| `description` (optional) | `string` | (empty) | Any additional description to render in your attachments block| + +## Support + +The `` component is supported in all EventCatalog resources, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/06-channel-information.md b/packages/core/docs/development/components/components/06-channel-information.md new file mode 100644 index 000000000..46a481c92 --- /dev/null +++ b/packages/core/docs/development/components/components/06-channel-information.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 6 +keywords: + - components +sidebar_label: ChannelInformation +title: ChannelInformation +description: Render channel into EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The ChannelInformation component is a EventCatalog component that will render a table into your markdown page that contains information about the Channel. + +**Example** + +```jsx /services/MyService/index.mdx + +``` + +### Output +![Example output](./img/channel.png) + +See example in the [demo EventCatalog application](https://demo.eventcatalog.dev/docs/channels/inventory.%7Benv%7D.events/1.0.0). + +### Support + +The `` component is supported in channels. diff --git a/packages/core/docs/development/components/components/07-design.md b/packages/core/docs/development/components/components/07-design.md new file mode 100644 index 000000000..9d7f17b24 --- /dev/null +++ b/packages/core/docs/development/components/components/07-design.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 7 +keywords: +- components +- remote schema +- fetch +- runtime +sidebar_label: Design +title: Design +description: Component for embedding EventCatalog Studio diagrams into your documentation +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The **Design** component is a EventCatalog component that will render a [EventCatalog Studio diagram](https://eventcatalog.studio) into your markdown page. + +## Interactive controls + + + +Design visualizations include the same interactive controls as NodeGraph components. + +Switch between React Flow and Mermaid view modes, export as Mermaid diagram code, or share URLs with your team. + +[Read more about interactive controls](/docs/development/components/components/nodegraph#interactive-controls) + +Design visualizations also support [layout persistence](/docs/development/components/components/nodegraph#layout-persistence) in dev mode, allowing you to save custom node positions. + +### Usage + +**Basic Example** + +```jsx /events/MyEvent/index.mdx + +``` + +**With Custom Title and Height** + +```jsx /events/MyEvent/index.mdx + +``` + +### Output +When you use the `` component, it will render the diagram in your EventCatalog page. + +![Example output](/img/embed-ec.png) + +### Props + +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `file` (required) | `string` | (empty) | The `.ecstudio` file to load into the diagram block. Path is resolved by EventCatalog. | +| `title` (optional) | `string` | "Remote Schema" | Title to display above the diagram | +| `maxHeight` (optional) | `string` | "400" | Maximum height of the diagram in pixels | +| `search` (optional) | `boolean` | `false` | Show or hide the search bar in the diagram | + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/08-entitymap.md b/packages/core/docs/development/components/components/08-entitymap.md new file mode 100644 index 000000000..d6ddcdaef --- /dev/null +++ b/packages/core/docs/development/components/components/08-entitymap.md @@ -0,0 +1,71 @@ +--- +sidebar_position: 8 +keywords: +- components +sidebar_label: EntityMap +title: EntityMap +description: Component for EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +A component to visually render a domain's entity map. + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. + +:::info +To visualize entities [you need to create entities in EventCatalog](/docs/development/guides/resources/entities/introduction) and assign them to your services or domains. +::: + +## Interactive controls + + + +Entity map visualizations include the same interactive controls as NodeGraph components. + +Switch between React Flow and Mermaid view modes, export as Mermaid diagram code, or share URLs with your team. + +[Read more about interactive controls](/docs/development/components/components/nodegraph#interactive-controls) + +Entity maps also support [layout persistence](/docs/development/components/components/nodegraph#layout-persistence) in dev mode, allowing you to save custom node positions. + +## Usage + +**Example** + +```jsx /domains/OrdersDomain/index.mdx + +``` + +#### Output +![Example output](./img/entity-map.png) + +#### Props +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `id` (optional) | `string` | - | The id of the domain or service entity map to render. By default the component will use the id of the current page. If you want to embed this component into custom documentation pages, you can use the `id` prop to specify the id of the domain or service to render. | +| `version` (optional) | `string` | - | The version of the domain or service to render. By default the component will use the version of the current page. If you want to embed this component into custom documentation pages, you can use the `version` prop to specify the version of the domain or service to render. | +| `title` (optional) | `string` | - | The title of the entity map to render. | +| `maxHeight` (optional) | `string` | 30 | Max height to set the nodegraph in your document. | +| `includeKey` (optional) | `boolean` | true | Show or hide the legend. | +| `entities` (optional) | `array` | [] | Array of entities (ids or names) to filter in the map. Useful if you want to render a subset of the entities in the map. (Added in v2.51.1). [Read more about filtering](#filter-entities) | + +#### Filter Entities + + + +The `` component will render all entities by default for a given domain. + +If you want to render a subset of the entities in the map, you can use the `entities` prop. + +**Example** + +```jsx /events/MyEvents/index.mdx + +``` + +In this example, only the `Order` and `Customer` entities will be chosen to be rendered in the map. + +If the `Order` or `Customer` entity references other entities, they will also be rendered in the map \ No newline at end of file diff --git a/packages/core/docs/development/components/components/09-flow.md b/packages/core/docs/development/components/components/09-flow.md new file mode 100644 index 000000000..f1de1f97b --- /dev/null +++ b/packages/core/docs/development/components/components/09-flow.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 9 +keywords: +- components +sidebar_label: Flow +title: Flow +description: Render a Flow in any EventCatalog page +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +Embed flows into your pages. + +**Example** + +```jsx /domains/MyDomain/index.mdx + +``` + +### Output +![Example output](./img/flows.png) + +### Props + + + +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `id` (required) | `string` | (empty) | Flow id to render in your page | +| `version` (optional) | `string` | `"latest"` | Version of the flow to render. Supports exact version and semver versions (e.g 1.0.x, ^1.3.5, latest)| +| `legend` (optional) | `boolean` | `true` | Show or hide the diagram key. Accepts `true`/`false` or `"true"`/`"false"`. `includeKey` is still accepted as an alias. | +| `search` (optional) | `boolean` | `false` | Show or hide the search bar in the flow. Accepts `true`/`false` or `"true"`/`"false"`. | +| `walkthrough` (optional) | `boolean` | `false` | Show or hide the step walkthrough controls. Accepts `true`/`false` or `"true"`/`"false"`. | +| `mode` (optional) | `string` ("simple" or "full") | `"simple"` | `simple` renders the flow in a simplified view without descriptions. `full` renders descriptions and other information.| + +:::tip Multiple embeds per page +You can embed multiple `` components on the same page and each one renders independently. +::: + + +## Interactive controls + + + +Flow visualizations include the same interactive controls as NodeGraph components. + +Switch between React Flow and Mermaid view modes, export as Mermaid diagram code, or share URLs with your team. + +[Read more about interactive controls](/docs/development/components/components/nodegraph#interactive-controls) + +Flow visualizations also support [layout persistence](/docs/development/components/components/nodegraph#layout-persistence) in dev mode, allowing you to save custom node positions. + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/10-link.md b/packages/core/docs/development/components/components/10-link.md new file mode 100644 index 000000000..adfafa135 --- /dev/null +++ b/packages/core/docs/development/components/components/10-link.md @@ -0,0 +1,32 @@ +--- +sidebar_position: 10 +keywords: +- components +sidebar_label: Link +title: Link +description: Create links in your documentation +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The `` component renders a link to a resource in EventCatalog. + +EventCatalog handles links depending on your configuration file (e.g trailing slashes, etc), using this component creates links that are consistent with your EventCatalog configuration. + +**Basic Example** + +```jsx /domains/MyDomain/index.mdx +My Awesome Page + +``` + +### Props +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `href` | `string` | none | The href of the link. | + +### Support + +The `` component is supported on all EventCatalog pages. diff --git a/packages/core/docs/development/components/components/11-mermaid-file-loader.md b/packages/core/docs/development/components/components/11-mermaid-file-loader.md new file mode 100644 index 000000000..f48b53d67 --- /dev/null +++ b/packages/core/docs/development/components/components/11-mermaid-file-loader.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 11 +keywords: +- components +- remote schema +- fetch +- runtime +sidebar_label: MermaidFileLoader +title: MermaidFileLoader +description: Component for embedding EventCatalog Studio diagrams into your documentation +--- +import AddedIn from '@site/src/components/MDX/AddedIn'; + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +The **MermaidFileLoader** component is a EventCatalog component that will render a [Mermaid](https://mermaid.js.org/) file into your markdown page. + +**Basic Example** + + + + +```jsx /events/MyEvent/index.mdx + +``` + + + ``` + sequenceDiagram + participant Customer + participant OrdersService + participant InventoryService + participant NotificationService + + Customer->>OrdersService: Place Order + OrdersService->>InventoryService: Check Inventory + InventoryService-->>OrdersService: Inventory Available + OrdersService->>InventoryService: Reserve Inventory + OrdersService->>NotificationService: Send Order Confirmation + NotificationService-->>Customer: Order Confirmation + OrdersService->>Customer: Order Placed Successfully + OrdersService->>InventoryService: Update Inventory + ``` + + + +### Output +When you use the `` component, it will render the diagram in your EventCatalog page. + +![Example output](./img/mermaid.png) + +### Props + +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `file` (required) | `string` | (empty) | The `.mmd` or `.mermaid` file to load into the diagram block. Path is resolved by EventCatalog. | + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/12-message-table.md b/packages/core/docs/development/components/components/12-message-table.md new file mode 100644 index 000000000..53d67e9d9 --- /dev/null +++ b/packages/core/docs/development/components/components/12-message-table.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 12 +keywords: +- components +sidebar_label: MessageTable +title: MessageTable +description: Component for displaying messages for services and domains in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The `` component renders a table of messages for a service or domain in EventCatalog. + +The component renders a paginated table of messages, with the ability to filter by message type (event, command, query), and text search. + +### Use case + +- Display all the messages that a service sends and receives. +- Display all the messages that a domain sends and receives. + - These are all messages that are sent and received in child services of the domain. + +**Basic Example** + +```jsx /domains/MyDomain/index.mdx + +``` +### Output +![Example output](./img/message-table.png) + +See the [demo](https://demo.eventcatalog.dev/docs/domains/Orders/0.0.3) for a full example. + +### Props +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `format` | `string` | 'all' | Which messages to render. `all` will render messages that are sent and received. `sends` will render messages that are sent and `receives` will render messages that are received. | +| `limit` | `number` | 10 | The number of messages to render in the table. Results are paginated. | +| `showChannels` | `boolean` | true | Whether to show the channel information in the table for each message. | + +### Support + +The `` component is supported in domains and services. diff --git a/packages/core/docs/development/components/components/13-nodegraph.md b/packages/core/docs/development/components/components/13-nodegraph.md new file mode 100644 index 000000000..7d9fbfc9a --- /dev/null +++ b/packages/core/docs/development/components/components/13-nodegraph.md @@ -0,0 +1,170 @@ +--- +sidebar_position: 13 +keywords: +- components +sidebar_label: NodeGraph +title: NodeGraph +description: Component for EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +A component to visually render your information. + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. + +### Single NodeGraph + +**Example** + +```jsx /events/MyEvents/index.mdx + +``` + +#### Output +![Example output](./img/nodegraph.png) + +#### Props + + + +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `maxHeight` (optional) | `string` | `30` | Max height to set the nodegraph in your document| +| `search` (optional) | `boolean` | `true` | Show or hide the search bar. Accepts `true`/`false` or `"true"`/`"false"`. | +| `legend` (optional) | `boolean` | `true` | Show or hide the legend. Accepts `true`/`false` or `"true"`/`"false"`. | +| `mode` (optional) | `string` ("simple" or "full") | `"simple"` | `simple` renders a simplified view without descriptions. `full` renders descriptions and other information.| + + +## Interactive controls + + + +All NodeGraph visualizations include interactive controls for better viewing, exploration, and sharing. + +![NodeGraph interactive controls](./img/interactive-controls.png) + +### View modes + +Switch between two visualization modes using the view switcher dropdown. + +**React Flow** - Interactive node-based visualization with drag and pan controls. + +**Mermaid** - Standard Mermaid diagram view with automatic layout and pan/zoom controls. + +### Export as Mermaid + +Copy the NodeGraph as Mermaid diagram code to your clipboard. The diagram preserves all nodes, edges, and styling information. + +Useful for sharing architectures with LLMs, documentation tools, or pasting into Mermaid-compatible renderers like [mermaid.live](https://mermaid.live). + +:::tip Large diagram exports +If you export large NodeGraphs and they fail to render as Mermaid diagrams, increase the `maxTextSize` configuration. [Learn more about maxTextSize](/docs/api/config#mermaid). +::: + +### Share URL + +Copy the current page URL to share the visualization with your team. + +## Layout persistence + + + +Save and restore custom node positions in the visualizer during development. + +When you drag nodes to arrange your visualization, a "Layout changed" indicator appears with a quick save button. Layouts are saved to `_data/visualizer-layouts/` as JSON files and can be committed to git to share with your team. + +:::info Dev mode only +Layout persistence is only available when running EventCatalog in development mode (`npm run start`). This ensures saved layouts don't affect production builds. +::: + +### Saving layouts + +Drag any node to reposition it. When changes are detected, the "Layout changed" indicator appears in the bottom-left corner with a quick save button. + +You can also save layouts from the dropdown menu: + +1. Click the menu icon (three dots) in the top-right corner +2. Select **Layout** > **Save Layout** + +Layouts are saved to `_data/visualizer-layouts/{collection}/{id}/{version}.json` in your catalog directory. + +### Resetting layouts + +Reset a saved layout to return to auto-positioning: + +1. Click the menu icon (three dots) in the top-right corner +2. Select **Layout** > **Reset Layout** +3. Confirm the reset + +This deletes the saved layout file and reloads the page with auto-calculated positions. + +### Layout files + +Layout files are keyed by resource (collection/id/version), so the same layout is used whether viewing in the visualizer or embedded in documentation. + +Works across all visualizers including services, events, flows, domains, entity maps, and designs. + +**Example layout file structure:** + +```json +{ + "version": 1, + "savedAt": "2026-01-29T12:00:00.000Z", + "resourceKey": "services/OrderService/1.0.0", + "positions": { + "node-1": { "x": 100, "y": 200 }, + "node-2": { "x": 300, "y": 400 } + } +} +``` + +### Sharing layouts with your team + +Saved layouts can be committed to git and shared with your team. When team members pull the changes, they'll see the same node positions in their local development environment. + +Add the layout directory to your git repository: + +```bash +git add _data/visualizer-layouts +git commit -m "Add custom visualizer layouts" +``` + + +### Multiple NodeGraphs + + + +You can add multiple NodeGraphs to your document by using the `NodeGraph` component multiple times. + +Here is an example of how to add multiple NodeGraphs to your document: + +```jsx /events/MyEvents/index.mdx + + + + +
    + + + + +
    +``` + +#### Output +![Example output](./img/multi-nodegraph.png) + +You can see a demo of this [here](https://demo.eventcatalog.dev/docs/domains/E-Commerce/1.0.0) + +#### Props + +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `id` | `string` | `undefined` | The id of the NodeGraph to render. If not provided, the current page's NodeGraph will be rendered. | +| `version` | `string` | `undefined` | The version of the NodeGraph to render. If not provided, the current page's NodeGraph will be rendered. | +| `type` | `string` | `undefined` | The type of the NodeGraph to render. If not provided, the current page's NodeGraph will be rendered. | + + + + diff --git a/packages/core/docs/development/components/components/14-openapi.md b/packages/core/docs/development/components/components/14-openapi.md new file mode 100644 index 000000000..53faf85eb --- /dev/null +++ b/packages/core/docs/development/components/components/14-openapi.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 14 +keywords: +- components +sidebar_label: OpenAPI +title: OpenAPI +description: Component for EventCatalog +--- + + +:::warning +This component is now deprecated. Please read the frontmatter API documentation [here](/docs/development/guides/resources/schemas/add-specifications-to-services/add-openapi-specifications). +::: + +You can add specifications to any service in EventCatalog using the specifications frontmatter API. You can read more about it [here](/docs/development/guides/resources/schemas/add-specifications-to-services/add-openapi-specifications). + +If you are interested in automating your EventCatalog with OpenAPI files, you can use the [OpenAPI plugin](/docs/plugins/openapi/intro). + + + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/15-prompt.md b/packages/core/docs/development/components/components/15-prompt.md new file mode 100644 index 000000000..68c8ae64a --- /dev/null +++ b/packages/core/docs/development/components/components/15-prompt.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 15 +keywords: + - components + - prompt + - ai + - cursor + - clipboard +sidebar_label: Prompt +title: Prompt +description: Embed copyable AI prompts into resource documentation +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +`` lets you embed ready-to-use AI prompts directly in your documentation pages. Readers can copy the prompt to their clipboard or open it straight in Cursor with one click. + +Place the prompt text as the slot content and configure the label and actions via props. + +**Copy only (default)** + +```jsx /events/OrderCreated/index.mdx + + You are a senior QA engineer. Review the OrderCreated event schema above and write + Jest test cases that cover the happy path and the top three failure modes. + +``` + +**Copy and open in Cursor** + +```jsx /services/OrdersService/index.mdx + + You are a senior QA engineer. Read the attached service documentation and produce + Gherkin Given/When/Then scenarios covering the happy path and the top three failure modes. + +``` + +### Output +![Example output](./img/prompt.png) + +### Props + +| Name | Type | Default | Description | +| ---- | ---- | ------- | ----------- | +| `description` (required) | `string` | — | Short label shown to the reader, e.g. "Generate test cases for this event". | +| `actions` (optional) | `('copy' \| 'cursor')[]` | `['copy']` | Buttons to render. `'copy'` adds a clipboard button; `'cursor'` adds an "Open in Cursor" deep-link button. | +| `icon` (optional) | `string` | — | Lucide icon name to display beside the description. Accepts kebab-case, snake_case, or space-separated input (e.g. `"sparkles"`, `"bot-message-square"`). | + +### Understand the actions + +Use `actions={["copy"]}` (the default) when you only need clipboard support. Add `"cursor"` to the array when your team works in Cursor and you want a one-click "Open in Cursor" button that pre-fills the prompt. + +Both actions derive their text from the slot content. HTML tags are stripped automatically, so plain text is always passed to the clipboard and the Cursor deep-link. + +### Use the icon prop + +Pass any [Lucide](https://lucide.dev/icons/) icon name to the `icon` prop. The component normalises the value to PascalCase internally, so `"bot-message-square"`, `"bot_message_square"`, and `"Bot Message Square"` all resolve to the same icon. + +If the icon name is not found in the Lucide library, no icon is rendered and no error is thrown. + +### Support + +The `` component is supported in domains, services, all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/16-remote-schema.md b/packages/core/docs/development/components/components/16-remote-schema.md new file mode 100644 index 000000000..41598c19a --- /dev/null +++ b/packages/core/docs/development/components/components/16-remote-schema.md @@ -0,0 +1,176 @@ +--- +sidebar_position: 16 +keywords: +- components +- remote schema +- fetch +- runtime +sidebar_label: RemoteSchema +title: RemoteSchema +description: Component for fetching and rendering remote schemas in EventCatalog +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The **RemoteSchema** component fetches and renders schemas from remote URLs at runtime, keeping your documentation automatically synchronized with external schema sources. + +**Schemas can be fetched from any accessible URL and support authentication headers for private APIs.** + +:::tip +**The `` component fetches schemas at runtime.** + +This ensures your documentation stays up-to-date with the latest schema versions without manual updates. +::: + +:::info SSR Mode Required +The `` component only works in Server-Side Rendering (SSR) mode. Make sure your EventCatalog is configured to run in SSR mode to use this component. You can read more about how to configure your EventCatalog to run in SSR mode [here](/docs/development/deployment/build-ssr-mode). +::: + +### Usage + +**Basic Example** + +```jsx /events/MyEvent/index.mdx + +``` + +**With Custom Title and Height** + +```jsx /events/MyEvent/index.mdx + +``` + +### Fetching from Private APIs + + + +For private APIs requiring authentication, you can provide headers: + +```jsx /events/MyEvent/index.mdx + +``` + +:::tip Loading environment variables + +Add your environment variables to the `.env` file in the root of your EventCatalog project. + +The RemoteSchema component will automatically map the environment variables to the template string. +::: + +**Using JSONPath to Extract Specific Schema Parts** + +When your API returns nested schemas, use JSONPath to extract specific parts: + +```jsx /events/MyEvent/index.mdx + +``` + +**Rendering as Raw JSON** + +Force rendering as raw JSON instead of the schema viewer: + +```jsx /events/MyEvent/index.mdx + +``` + +### Output +The component automatically detects JSON Schema format and renders an interactive [Schema Viewer](/docs/development/components/components/schema-viewer), or displays raw content for other formats. + +### Props + +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `url` (required) | `string` | (empty) | The URL to fetch the schema from. Supports environment variable templating with `${VAR_NAME}` syntax. | +| `title` (optional) | `string` | "Remote Schema" | Title to display above the schema | +| `maxHeight` (optional) | `string` | "400" | Maximum height of the schema viewer in pixels | +| `headers` (optional) | `object` | `{}` | HTTP headers for authentication (requires EventCatalog Scale plan) | +| `jsonPath` (optional) | `string` | (empty) | [JSONPath expression](https://github.com/dchester/jsonpath) to extract specific parts of the response | +| `renderAs` (optional) | `string` | "auto" | Rendering mode: "auto", "schema", or "raw" | + +### Environment Variable Support + +The `url` and `headers` props support environment variable templating using `${VARIABLE_NAME}` syntax: + +```md + +``` + +:::tip Loading environment variables +Add your environment variables to the `.env` file in the root of your EventCatalog project. + +The RemoteSchema component will automatically map the environment variables to the template string. +::: + +### JSONPath Examples + +Extract specific schema definitions from complex API responses using [JSONPath](https://github.com/dchester/jsonpath). + +```md + + + + + + + + +``` + +### Error Handling + +The component provides clear error messages for common issues: + +- **Network errors**: Connection timeouts, DNS resolution failures +- **Authentication errors**: Invalid credentials or missing headers +- **JSONPath errors**: Invalid JSONPath expressions +- **Format errors**: Unsupported schema formats + +### Benefits for Teams + +- **Always up-to-date**: Schemas are fetched at runtime, ensuring documentation reflects the latest API changes +- **Reduced maintenance**: No need to manually update schema files when APIs change +- **Single source of truth**: Documentation pulls directly from your API definitions +- **Private API support**: Secure access to internal schemas with authentication headers +- **Flexible extraction**: Use JSONPath to document specific parts of large API specifications + +This component bridges the gap between your live APIs and documentation, ensuring your EventCatalog always reflects the current state of your systems. + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/17-resource-group-table.md b/packages/core/docs/development/components/components/17-resource-group-table.md new file mode 100644 index 000000000..bbef8996f --- /dev/null +++ b/packages/core/docs/development/components/components/17-resource-group-table.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 17 +keywords: +- components +sidebar_label: ResourceGroupTable +title: ResourceGroupTable +description: Component for displaying EventCatalog grouped resources in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The `` component renders a table of resources for your page in EventCtalog. + +The component renders a paginated table of (messages, services, domains, flows, channels), with the ability to filter by resource type, and text search. + +### Why use ResourceGroupTable? + +Sometimes in your catalog you may want to link related resources together or group them in particular ways that EventCatalog does not naturally support. + +Examples: + +- List a custom list of messages in a domain page +- Create a "Related Resources" section on your docs for your documentation readers +- Create a custom list of resources in your page and let people search them easily. + +### Usage + +The `` component requires **frontmatter** property called **resourceGroups** defined in your resource, and then the component itself. + +1. Adding `resourceGroup` property to your resource +```md title="example /domains/Orders/index.mdx" +--- +id: Orders +name: Orders +version: 0.0.3 +owners: + - dboyne + - full-stack +# Here we define the resourceGroups, in this example we create a group called core resources +# We create a list called `related-resources` and we group these services. +# Remember you can define any group of information you want, and link to any catalog resource +resourceGroups: + - id: related-resources + title: Core resources + items: + - id: InventoryService + type: service + - id: OrdersService + type: service + - id: NotificationService + type: service + - id: ShippingService + type: service +--- +``` + + +**Basic Example** + +```md /domains/MyDomain/index.mdx +--- +#domain frontmatter +--- + + +``` +### Output +![Example output](./img/resource-group-table.png) + +You can see the demo of this in the [Orders domain page](https://demo.eventcatalog.dev/domains/Orders/0.0.3). + +### Props +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `id` | `string` | none | Id of the resource group to render. | +| `title` | `string` | 'Related Resources' | The title shown above the table. | +| `subtitle` | `string` | 'Resources' | The title shown inside the table. | +| `description` | `string` | 'Resources that are related to the current resource.' | The description of the table. | +| `limit` | `number` | 10 | The number of resources to render in the table. Results are paginated. | +| `showOwners` | `boolean` | false | Whether to show the owners of the resources in the table. | + +### Support + +The `` component is supported on all EventCatalog pages. diff --git a/packages/core/docs/development/components/components/18-resource-link.md b/packages/core/docs/development/components/components/18-resource-link.md new file mode 100644 index 000000000..72037d12f --- /dev/null +++ b/packages/core/docs/development/components/components/18-resource-link.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 18 +keywords: +- components +sidebar_label: ResourceLink +title: ResourceLink +description: Create links in your documentation to resources in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +:::tip Prefer wiki-style syntax +For most use cases, we recommend using the newer [resource references](/docs/development/components/resource-references) syntax instead: + +```md +[[service|InventoryService]] +``` + +This provides interactive tooltips and a more natural writing experience. Use `` only when you need custom link text or explicit component control. +::: + +The `` component renders a link to a resource in EventCatalog. + +**Basic Example** + +```jsx /domains/MyDomain/index.mdx + + + + +This is a custom link +``` + +### Props +| Name | Type | Default | Required | Description | +| ----------------------- | --------- | ----------------- | -------- | ----------------------------------------------------------------- | +| `id` | `string` | none | Yes | Id of the resource group to render. | +| `type` | `string` | none | Yes | The type of resource to render (service, event, query, command, channel, flow, domain, user, team) | +| `version` | `string` | none | Optional. The version of the resource to render, by default the latest version is used. | Version of the resource to link to. + +### Recommended alternative + + + +For most documentation, prefer the wiki-style syntax which provides interactive tooltips: + +```md +The [[service|InventoryService]] manages inventory. +``` + +See [Resource references](/docs/development/components/resource-references) for full documentation. + +### Support + +The `` component is supported on all EventCatalog pages. diff --git a/packages/core/docs/development/components/components/19-schema.md b/packages/core/docs/development/components/components/19-schema.md new file mode 100644 index 000000000..3b6e739ca --- /dev/null +++ b/packages/core/docs/development/components/components/19-schema.md @@ -0,0 +1,44 @@ +--- +sidebar_position: 19 +keywords: +- components +sidebar_label: Schema +title: Schema +description: Component for EventCatalog +--- + +The schema component renders a given schema into the page. + +**Schemas can be any file format (.avro, .json etc).** + +:::tip +**The `` component renders any schema format.** + +If you need to render JSON Schema, you can also use the [\ component](/docs/development/components/components/schema-viewer). + +::: + +### Usage + +1. Add your schema file to your folder. + - e.g `/events/MyEvent/schema.avro` + +**Example** + +```jsx /events/MyEvent/index.mdx + +``` + +### Output +![Example output](./img/schema.png) + +### Props +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `file` (required) | `string` | (empty) | The file to load into the schema block. Path is resolved by EventCatalog. | +| `title` (optional) | `string` | (empty) | Title to render in your schema block | +| `lang` (optional) | `string` | `json` | The code language of the schema. Defaults to `json`. Over 100 languages are supported. [List of options can be found here.](https://github.com/shikijs/textmate-grammars-themes/blob/main/packages/tm-grammars/README.md) | + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/20-schema-viewer.md b/packages/core/docs/development/components/components/20-schema-viewer.md new file mode 100644 index 000000000..8c51d947a --- /dev/null +++ b/packages/core/docs/development/components/components/20-schema-viewer.md @@ -0,0 +1,69 @@ +--- +sidebar_position: 20 +keywords: +- components +sidebar_label: SchemaViewer +title: SchemaViewer +description: Render JSON schema in EventCatalog +--- + +A Schema Viewer component for EventCatalog that supports JSON schemas and Avro schemas. + +Renders the given schema (.json, .yaml, .avro, .avsc) into the page. + +:::tip +**The `` component only works with JSON Schema and Avro schemas.** + +If you need to render other schema formats, please use the [\ component](/docs/development/components/components/schema). + +_Avro support was added in 2.64.0. Please upgrade to the latest version of EventCatalog to use this component._ +::: + +### Usage + +1. Add a JSON or Avro schema file to your folder. + - e.g `/events/MyEvent/schema.json` + - e.g `/events/MyEvent/schema.avro` + +**Example** + +```jsx /events/MyEvent/index.mdx + + + + + +``` + +### Output +![Example output](./img/schemaviewer.png) + +### Props +| Name | Type | Default | Description | +| ----------------------- | --------- | ----------------- | ----------------------------------------------------------------- | +| `title` (optional) | `string` | (empty) | Title to render above your schema | +| `file` (required) | `string` | (empty) | The file to load into the schema block. Path is resolved by EventCatalog. | +| `maxHeight` (optional) | `string` | "500" | Max height of the JSON Schema viewer (in pixels). | +| `search` (optional) | `boolean` | false | Renders a search input in the viewer | +| `expand` (optional) | `boolean` | false | Expands all properties by default | +| `showRequired` (optional) | `boolean` | false | **Avro Schemas only**: Will show which fields are required. Fields are required if they don't have a default value or null as their type. As documented in the [Avro Specification](https://avro.apache.org/docs/++version++/specification/) | + + +## Rendering multiple schemas + +You can use the SchemaViewer multiple times in your page. + +**Example** + +```md /events/MyEvents.index.mdx +The Inventory Adjusted event is triggered whenever there is a change in the inventory levels of a product. +This could occur due to various reasons such as receiving new stock, sales, returns, or manual adjustments by the inventory management team. + + + + +``` + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/21-steps.md b/packages/core/docs/development/components/components/21-steps.md new file mode 100644 index 000000000..b3b603cde --- /dev/null +++ b/packages/core/docs/development/components/components/21-steps.md @@ -0,0 +1,83 @@ +--- +sidebar_position: 21 +keywords: + - components +sidebar_label: Steps +title: Steps +description: Render steps into EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The Steps component is a powerful tool for creating structured, sequential guides in EventCatalog. It's ideal for presenting step-by-step instructions, tutorials, or workflows, particularly when explaining processes, API integrations, or code implementations. + +### Key Features: + +- Automatic Numbering: Steps are automatically numbered, providing clear sequence and structure. +- Flexible Content: Supports various content types including text, code snippets, and embedded components. + +### Use Cases: + +- Walking users through setup procedures +- Explaining complex workflows +- Presenting API integration steps +- Showcasing code examples with explanations + +**Example** + +```jsx /services/MyService/index.mdx + + + Request API credentials from the Inventory Service team. + + + Run the following command in your project directory: + ```bash + npm install inventory-service-sdk + ```_ + + + Use the following code to initialize the Inventory Service client: + ```js + const InventoryService = require('inventory-service-sdk'); + const client = new InventoryService.Client({ + clientId: 'YOUR_CLIENT_ID', + clientSecret: 'YOUR_CLIENT_SECRET', + apiUrl: 'https://api.inventoryservice.com/v1' + }); + ```_ + + + + You can now use the client to make API calls. For example, to get all products: + ```js + client.getProducts() + .then(products => console.log(products)) + .catch(error => console.error(error)); + ```_ + + +``` + +### Output +![Example output](./img/steps.png) + +See example in the [demo EventCatalog application](https://demo.eventcatalog.dev/docs/services/InventoryService/0.0.2). + +### Props (``) + +| Name | Type | Default | Description | +| ------------------ | -------- | ------- | --------------------------------------- | +| `title` (optional) | `string` | (empty) | Title that gets renders above the steps | + +### Props (``) + +| Name | Type | Default | Description | +| ------------------------ | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` (required) | `string` | (empty) | Title for the step | + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/22-tabs.md b/packages/core/docs/development/components/components/22-tabs.md new file mode 100644 index 000000000..691b0b730 --- /dev/null +++ b/packages/core/docs/development/components/components/22-tabs.md @@ -0,0 +1,55 @@ +--- +sidebar_position: 22 +keywords: + - components +sidebar_label: Tabs +title: Tabs +description: Render tabs in your EventCatalog pages +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The `` component is a EventCatalog component that will render a tabs into your markdown page. + +Add Tabs and TabItems into your markdown file. + +**Example** + +```jsx /services/MyService/index.mdx + + + This is the content for tab 1 + + + This is the content for tab 2 + + +``` + +**Example with code as child** + +```jsx /services/MyService/index.mdx + + + ``sh + This is the content for tab 1 + `` + + + ``js + console.log('This is the content for tab 2'); + `` + + +``` + +### Output +![Example output](./img/tabs.png) + +See example in the [demo EventCatalog application](https://demo.eventcatalog.dev/docs/events/InventoryAdjusted/1.0.1). + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/23-tiles.md b/packages/core/docs/development/components/components/23-tiles.md new file mode 100644 index 000000000..8ee1de044 --- /dev/null +++ b/packages/core/docs/development/components/components/23-tiles.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 23 +keywords: + - components +sidebar_label: Tiles +title: Tiles +description: Render tiles into EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +Renders Tiles in EventCatalog, can be great for internal and external links. + +Tile icons are from [hero icons](https://heroicons.com/), you can find a [list of them here](https://unpkg.com/browse/@heroicons/react@2.1.5/24/outline/). + +**Example** + +```jsx /services/MyService/index.mdx + + + + + + +``` + +### Output +![Example output](./img/tiles.png) + +See example in the [demo EventCatalog application](https://demo.eventcatalog.dev/docs/services/InventoryService/0.0.2). + +### Props (``) + +| Name | Type | Default | Description | +| ------------------ | -------- | ------- | --------------------------------------- | +| `title` (optional) | `string` | (empty) | Title that gets renders above the tiles | +| `columns` (optional) | `number` | 2 | Number of columns to render the tiles in | +### Props (``) + +| Name | Type | Default | Description | +| ------------------------ | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` (optional) | `string` | (empty) | Title for the tile | +| `description` (optional) | `string` | (empty) | Description for the tile | +| `icon` (optional) | `string` | (empty) | Icon to show in the tile. Iconss are [HeroIcons](https://heroicons.com/). [Find a list here](https://unpkg.com/browse/@heroicons/react@2.1.5/24/outline/). | +| `iconColor` (optional) | `string` | `text-purple-500` | The color of the icon, using tailwind classes. | +| `href` (optional) | `string` | (empty) | URL for the tile | +| `openWindow` (optional) | `boolean` | (empty) | Open the URL in a new window | + +### Support + +The `` component is supported in domains, services, and all messages, changelogs, and custom documentation pages. diff --git a/packages/core/docs/development/components/components/24-visibility.md b/packages/core/docs/development/components/components/24-visibility.md new file mode 100644 index 000000000..673fce246 --- /dev/null +++ b/packages/core/docs/development/components/components/24-visibility.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 24 +keywords: + - components + - visibility + - agents + - ai + - llm +sidebar_label: Visibility +title: Visibility +description: Gate content by audience — humans in the UI, agents in raw markdown +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +`` lets you write a single MDX file that serves two audiences: human readers in the browser and AI agents or LLMs consuming raw markdown. Wrap content in the appropriate block and EventCatalog handles the rest — no content forking required. + +**Example** + +```jsx /queries/GetInventoryList/index.mdx + + Use the **Schema** and **Visualiser** actions on this page to inspect the + inventory query contract and its relationships. + + + + When reasoning about this query, treat `GET /inventory` as a read-only + inventory lookup. Prefer linking this query to inventory reporting, order + placement, and stock reservation workflows. + +``` + +### How it works + +In the browser UI, only `for="humans"` blocks render. The `for="agents"` block returns nothing and is never visible to human readers. + +When an AI agent or LLM fetches the raw markdown for a resource — via the `.md` / `.mdx` endpoints, the `/llms-full.txt` feed, custom docs, team/user pages, language pages, or diagram pages — the `for="humans"` blocks are stripped and only `for="agents"` content remains. + +This means the same file can give a human reader UI-oriented guidance ("click the Schema tab") while giving an agent structured, machine-actionable context ("treat this as a read-only lookup") without any duplication. + +### Props + +| Name | Type | Required | Description | +| ---- | ---- | -------- | ----------- | +| `for` | `"agents"` \| `"humans"` | Yes | The target audience for the wrapped content. | + +### Write effective agent content + +Use `for="agents"` blocks to provide context that helps AI tools reason about your architecture. Good agent content includes: + +- The intent and constraints of the resource ("read-only", "idempotent", "fires on every state change") +- How the resource relates to other services or workflows +- Edge cases or business rules that are not obvious from the schema + +Use `for="humans"` blocks for UI-oriented guidance: links to tabs, interactive tools, visual walkthroughs, or anything that only makes sense in a browser context. + +### Support + +`` is supported in domains, services, all messages, changelogs, custom documentation pages, team pages, user pages, language pages, and diagram pages. diff --git a/packages/core/docs/development/components/components/_category_.json b/packages/core/docs/development/components/components/_category_.json new file mode 100644 index 000000000..7db9b0893 --- /dev/null +++ b/packages/core/docs/development/components/components/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Components", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "components/list", + "title": "Components", + "description": "Reference for EventCatalog MDX components." + } +} diff --git a/packages/core/docs/development/components/custom-components/01-introduction.md b/packages/core/docs/development/components/custom-components/01-introduction.md new file mode 100644 index 000000000..1fff8360f --- /dev/null +++ b/packages/core/docs/development/components/custom-components/01-introduction.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +title: Write your own components +description: Learn when and why to create custom components in EventCatalog. +--- + +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + +EventCatalog is powered by [Astro](https://docs.astro.build/en/basics/astro-components/). This means you can create your own components and use them inside your catalog pages. + +Custom components are useful when you want to reuse the same block of UI or logic across services, events, domains, teams, and custom documentation pages. + +For example, you can create components for: + +- service health cards +- ownership summaries +- links to runbooks or dashboards +- API status panels +- scorecards +- custom diagrams +- metadata from another system + +## What you will create + +In this section, we will create a small component, render it in a service page, pass catalog data into it, fetch data from another system, and add simple browser behavior. + +By the end, you will have the main patterns you need to build your own reusable components. + +## Component types + +EventCatalog supports two types of custom component: + +- `.astro` components for reusable UI, JavaScript, data fetching, and dynamic rendering +- `.mdx` components for reusable Markdown content + +Most custom components should be `.astro` files. They give you the most flexibility and match how EventCatalog renders built-in components. + +## Where components live + +Create custom components in the `components` directory at the root of your catalog. + + + +You can import these components into any MDX page using the `@catalog/components` alias. + +```tsx title="/services/OrderService/index.mdx" +import ServiceSummary from '@catalog/components/service-summary.astro'; + + +``` + +:::tip +Astro has its own component model. If you want to go deeper, read the [Astro components documentation](https://docs.astro.build/en/basics/astro-components/). +::: diff --git a/packages/core/docs/development/components/custom-components/02-create-your-first-component.md b/packages/core/docs/development/components/custom-components/02-create-your-first-component.md new file mode 100644 index 000000000..0b634436f --- /dev/null +++ b/packages/core/docs/development/components/custom-components/02-create-your-first-component.md @@ -0,0 +1,71 @@ +--- +sidebar_position: 2 +sidebar_label: Create your first component +title: Create your first component +description: Create a small reusable component and render it in a catalog page. +--- + +In this guide, we will create a simple component and add it to a service page. + +## Create the component + +Create a new file in your catalog. + +```txt title="File" +/components/service-note.astro +``` + +Add the component markup. + +```jsx title="/components/service-note.astro" +--- +const { title = 'Service note' } = Astro.props; +--- + + +``` + +The `not-prose` class keeps the documentation page typography from changing the spacing and styles inside your component. + +## Add it to a page + +Open one of your service pages and import the component. + +```markdown title="/services/OrderService/index.mdx" +--- +id: OrderService +name: Order Service +version: 0.0.1 +summary: Handles customer orders. +--- + +import ServiceNote from '@catalog/components/service-note.astro'; + +# Order Service + + +``` + +## Check the page + +Start your catalog and open the service page. + +```bash +npm run dev +``` + +You should see the custom note rendered inside the service documentation. + +
    + Custom service note component rendered in EventCatalog + A custom component rendered inside an EventCatalog service page. +
    + +:::tip +Start small. Once you can render one component, the same pattern works for richer cards, tables, status panels, and diagrams. +::: diff --git a/packages/core/docs/development/components/custom-components/03-pass-data-into-components.md b/packages/core/docs/development/components/custom-components/03-pass-data-into-components.md new file mode 100644 index 000000000..3393cb1ac --- /dev/null +++ b/packages/core/docs/development/components/custom-components/03-pass-data-into-components.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 3 +sidebar_label: Pass data into components +title: Pass data into components +description: Pass props, frontmatter, variables, and config data into custom components. +--- + +Components become more useful when they can render data from the page they are used on. + +## Use props + +Props are values passed into the component when you render it. + +```jsx title="/components/service-owner.astro" +--- +const { team } = Astro.props; +--- + +

    + Owned by {team} +

    +``` + +Render the component from a catalog page. + +```tsx title="/services/OrderService/index.mdx" +import ServiceOwner from '@catalog/components/service-owner.astro'; + + +``` + +## Use frontmatter + +Every EventCatalog resource page has frontmatter. You can pass that data into your component. + +```markdown title="/services/OrderService/index.mdx" +--- +id: OrderService +name: Order Service +version: 0.0.1 +summary: Handles customer orders. +--- + +import ServiceSummary from '@catalog/components/service-summary.astro'; + + +``` + +Then read those props in your component. + +```jsx title="/components/service-summary.astro" +--- +const { name, summary } = Astro.props; +--- + +
    +

    {name}

    +

    {summary}

    +
    +``` + +## Use page variables + +You can export variables from an MDX page and pass them into a component. + +```tsx title="/services/OrderService/index.mdx" +import ServiceStatus from '@catalog/components/service-status.astro'; + +export const status = 'Stable'; + + +``` + +## Use catalog config + +You can import values from `eventcatalog.config.js` using `@config`. + +```jsx title="/components/catalog-name.astro" +--- +import config from '@config'; +--- + +{config.title} +``` + +Use this when a component needs shared catalog settings, organization names, or other global values. diff --git a/packages/core/docs/development/components/custom-components/04-fetch-data-in-components.md b/packages/core/docs/development/components/custom-components/04-fetch-data-in-components.md new file mode 100644 index 000000000..4a6ea0ef7 --- /dev/null +++ b/packages/core/docs/development/components/custom-components/04-fetch-data-in-components.md @@ -0,0 +1,191 @@ +--- +sidebar_position: 4 +sidebar_label: Fetch data +title: Fetch data in components +description: Fetch build-time, runtime, or browser data from custom EventCatalog components. +--- + +Custom components can fetch data from other systems. This is useful when your documentation needs to show information from tools such as internal platforms, service catalogs, monitoring systems, observability platforms, on-call tools, custom registries, schema registries, or deployment platforms. + +Astro components can use `await fetch()` in the component frontmatter. EventCatalog is powered by Astro, so the same pattern works in custom EventCatalog components. + +## Fetch and render data + +This example fetches a random user and renders their name and location. + +```jsx title="/components/random-user.astro" +--- +const response = await fetch('https://randomuser.me/api/'); +const data = await response.json(); +const randomUser = data.results[0]; +--- + +
    +

    + {randomUser.name.first} {randomUser.name.last} +

    +

    + {randomUser.location.city}, {randomUser.location.country} +

    +
    +``` + +Use the component in a catalog page. + +```tsx title="/services/OrderService/index.mdx" +import RandomUser from '@catalog/components/random-user.astro'; + + +``` + +The same pattern can be used to render data from systems your team already uses. + +For example, you might fetch: + +- a service owner from an internal platform API +- a support rotation from an on-call tool +- a maturity score from a scorecard system +- repository metadata from GitHub +- deployment metadata from your platform + +## Static builds + +By default, EventCatalog builds a static website. + +In a static build, `fetch()` calls in Astro component frontmatter run when the catalog is built. The fetched data is written into the generated HTML. + +This means: + +- the data is captured at build time +- the page will not refetch that data for every viewer +- the page updates when you rebuild and redeploy the catalog +- a failed API request can fail the build unless you handle the error + +Static fetching works well for documentation data that does not need to change every time someone opens the page. + +```jsx title="/components/service-score.astro" +--- +const { service } = Astro.props; + +let score = 'Unknown'; + +try { + const response = await fetch(`https://platform.example.com/services/${service}/score`); + const data = await response.json(); + score = data.score; +} catch { + score = 'Unavailable'; +} +--- + + + Score: {score} + +``` + +Use this pattern when the data can be slightly stale and tied to your documentation release process. + +## SSR builds + +EventCatalog can also run in [SSR mode](/docs/development/deployment/build-ssr-mode). + +In SSR mode, pages are rendered by a server. Astro frontmatter fetches run at runtime instead of only during the static build. + +This means: + +- the data can be fresher +- the request can use server-side environment variables +- the catalog needs a running server +- slow APIs can slow down the page unless you cache or handle failures + +SSR mode is a better fit when the component needs private or frequently changing data. + +You can enable server output in `eventcatalog.config.js`. + +```js title="/eventcatalog.config.js" +export default { + output: 'server', +}; +``` + +Read more about [EventCatalog static and server output](/docs/development/getting-started/develop-and-build#eventcatalog-static-vs-server-output). + +## Private APIs and API keys + +If the API requires a token, keep the token on the server. + +For static builds, the token can be available in your build environment. The fetch runs during the build, and only the rendered result is included in the generated HTML. You can store secrets in your `.env` file or deployment environment variables. Read more about [configuring environment variables](/docs/development/getting-started/configuration-overview#configuring-environment-variables). + +```jsx title="/components/internal-service-score.astro" +--- +const { service } = Astro.props; + +const response = await fetch(`https://platform.example.com/services/${service}/score`, { + headers: { + Authorization: `Bearer ${process.env.PLATFORM_API_TOKEN}`, + }, +}); + +const data = await response.json(); +--- + + + Score: {data.score} + +``` + +For SSR builds, the token is read by the server at runtime. + +:::caution +Do not render API keys into the page, pass them to client-side scripts, or expose them through props that end up in HTML. +::: + +If the request must happen in the browser, do not call private APIs directly with a secret. Use a server-side endpoint, proxy, or SSR component that keeps the token on the server. + +## Browser fetching + +Use browser fetching when the data should update after the page loads and does not require a private secret. + +```jsx title="/components/live-status.astro" +--- +const { endpoint } = Astro.props; +--- + +
    + Loading status... +
    + + +``` + +Use the component in a page. + +```tsx title="/services/OrderService/index.mdx" +import LiveStatus from '@catalog/components/live-status.astro'; + + +``` + +## Which pattern should you use? + +Use static build fetching when the data can be part of the generated documentation. + +Use SSR fetching when the data should be fresh at request time or needs private server-side credentials. + +Use browser fetching when the data is public and should update after the page loads. + +For more examples, read the [Astro data fetching documentation](https://docs.astro.build/en/guides/data-fetching/). diff --git a/packages/core/docs/development/components/custom-components/05-style-components.md b/packages/core/docs/development/components/custom-components/05-style-components.md new file mode 100644 index 000000000..e0bc4c04d --- /dev/null +++ b/packages/core/docs/development/components/custom-components/05-style-components.md @@ -0,0 +1,60 @@ +--- +sidebar_position: 5 +sidebar_label: Style components +title: Style components +description: Style custom EventCatalog components with Tailwind CSS. +--- + +EventCatalog includes Tailwind CSS. You can use Tailwind classes inside your custom components. + +```jsx title="/components/service-card.astro" +--- +const { name, summary } = Astro.props; +--- + +
    +

    {name}

    +

    {summary}

    +
    +``` + +Use the component from a catalog page. + +```tsx title="/services/OrderService/index.mdx" +import ServiceCard from '@catalog/components/service-card.astro'; + + +``` + +## Keep components readable + +Custom components usually sit inside documentation pages, so keep them simple. + +- Use small, focused components. +- Prefer readable text over dense layouts. +- Support light and dark mode. +- Avoid large custom layouts unless the component needs them. + +## Use local styles when needed + +Astro components can include a ` +``` + +Use Tailwind for most styling, and local CSS when a component needs a small custom rule. diff --git a/packages/core/docs/development/components/custom-components/06-add-client-side-scripts.md b/packages/core/docs/development/components/custom-components/06-add-client-side-scripts.md new file mode 100644 index 000000000..f06369946 --- /dev/null +++ b/packages/core/docs/development/components/custom-components/06-add-client-side-scripts.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 6 +sidebar_label: Client-side scripts +title: Add client-side scripts +description: Add browser JavaScript to custom EventCatalog components. +--- + +Astro components render HTML by default. If your component needs browser behavior, add a ` +``` + +Use it in a page. + +```tsx title="/docs/runbooks/deploy-order-service.mdx" +import CopyCommand from '@catalog/components/copy-command.astro'; + + +``` + +## Pass data through HTML attributes + +The component script runs when the page loads in the browser. If it needs values from `Astro.props`, render them as HTML attributes first. + +```jsx +--- +const { message } = Astro.props; +--- + + + + +``` + +Read the [Astro client-side scripts documentation](https://docs.astro.build/en/guides/client-side-scripts/) for more details. diff --git a/packages/core/docs/development/components/custom-components/07-reference.md b/packages/core/docs/development/components/custom-components/07-reference.md new file mode 100644 index 000000000..8bf93c817 --- /dev/null +++ b/packages/core/docs/development/components/custom-components/07-reference.md @@ -0,0 +1,115 @@ +--- +sidebar_position: 7 +sidebar_label: Reference +title: Custom components reference +description: Reference for custom components in EventCatalog. +--- + +## Component directory + +Custom components live in the `components` directory at the root of your catalog. + +```txt +my-catalog/ +└── components/ + ├── service-card.astro + └── reusable-note.mdx +``` + +## Import path + +Use the `@catalog/components` alias to import custom components. + +```tsx +import ServiceCard from '@catalog/components/service-card.astro'; +``` + +## Supported file types + +| File type | Use for | +| --- | --- | +| `.astro` | Reusable UI, props, data fetching, browser scripts, and styling | +| `.mdx` | Reusable Markdown content | + +## Astro component structure + +Astro components have a component script and a component template. + +```jsx +--- +// Component script +const { title } = Astro.props; +--- + + +

    {title}

    +``` + +Read the [Astro components documentation](https://docs.astro.build/en/basics/astro-components/) for the full syntax. + +## Props + +Read props with `Astro.props`. + +```jsx +--- +const { title, summary } = Astro.props; +--- +``` + +Pass props from MDX. + +```tsx + +``` + +## Frontmatter + +Resource frontmatter can be passed into components from MDX pages. + +```tsx + +``` + +## Catalog config + +Import `eventcatalog.config.js` values with `@config`. + +```jsx +--- +import config from '@config'; +--- + +{config.title} +``` + +## Data fetching + +Use `await fetch()` in Astro component frontmatter for build-time data. + +```jsx +--- +const response = await fetch('https://api.example.com/status'); +const status = await response.json(); +--- +``` + +Use browser JavaScript when data should be fetched after the page loads. + +```jsx +
    Loading...
    + + +``` + +## Related documentation + +- [Using components](/docs/development/components/using-components) +- [Built-in components](/docs/components) +- [Astro components](https://docs.astro.build/en/basics/astro-components/) +- [Astro data fetching](https://docs.astro.build/en/guides/data-fetching/) +- [Astro client-side scripts](https://docs.astro.build/en/guides/client-side-scripts/) diff --git a/packages/core/docs/development/components/custom-components/_category_.json b/packages/core/docs/development/components/custom-components/_category_.json new file mode 100644 index 000000000..65df6e572 --- /dev/null +++ b/packages/core/docs/development/components/custom-components/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Write your own components", + "position": 7, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "components/custom-components", + "description": "Learn how to create reusable components for your EventCatalog documentation." + } +} diff --git a/packages/core/docs/development/components/diagram-syntax/01-drawio.md b/packages/core/docs/development/components/diagram-syntax/01-drawio.md new file mode 100644 index 000000000..e6fb8988b --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/01-drawio.md @@ -0,0 +1,45 @@ +--- +sidebar_position: 1 +keywords: +- components +sidebar_label: DrawIO +title: +description: Embed a DrawIO diagram in your documentation +id: drawio +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The `` component renders a [DrawIO diagram](https://draw.io/) in your documentation. + +### Support + +The `` component is supported in domains, services, all messages and [custom documentation](/docs/development/bring-your-own-documentation/custom-pages/introduction). + +### Usage + +1. Include the `` component inside the markdown + - e.g `/events/MyEvent/index.mdx` + +**Basic Example** + +```md /events/OrderAmended/index.mdx +--- +#event frontmatter +--- + + +``` + +### Output example in EventCatalog + +![Example output](../components/img/drawio.png) + +- [Example: View demo in custom documentation page](https://demo.eventcatalog.dev/docs/custom/guides/event-storming/01-index) + +### Props +| Name | Type | Default | Required | Description | +| ----------------------- | --------- | ----------------- | -------- | ----------------------------------------------------------------- | +| `url` | `string` | | Yes | The URL of the DrawIO diagram to embed. To get the URL, click "File", "embed", then "Notion...", copy the URL to the component. diff --git a/packages/core/docs/development/components/diagram-syntax/02-figjam.md b/packages/core/docs/development/components/diagram-syntax/02-figjam.md new file mode 100644 index 000000000..1fc5c3a39 --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/02-figjam.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 2 +keywords: +- components +sidebar_label: FigJam +title: +description: Embed a FigJam diagram in your documentation +id: figjam +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The `` component renders a [FigJam diagram](https://www.figma.com/figjam/) in your documentation. + +### Support + +The `` component is supported in domains, services, all messages and [custom documentation](/docs/development/bring-your-own-documentation/custom-pages/introduction). + +### Usage + +1. Include the `` component inside the markdown + - e.g `/events/MyEvent/index.mdx` + +**Basic Example** + +```md /events/OrderAmended/index.mdx +--- +#event frontmatter +--- + + +``` + +### Output example in EventCatalog + +![Example output](../components/img/figjam.png) + +### Props +| Name | Type | Default | Required | Description | +| ----------------------- | --------- | ----------------- | -------- | ----------------------------------------------------------------- | +| `url` | `string` | | Yes | The embed URL of the FigJam diagram. To get the URL, click "Shared", "Get embed code", then copy the URL from the iframe. diff --git a/packages/core/docs/development/components/diagram-syntax/03-icepanel.md b/packages/core/docs/development/components/diagram-syntax/03-icepanel.md new file mode 100644 index 000000000..f64fb137d --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/03-icepanel.md @@ -0,0 +1,74 @@ +--- +sidebar_position: 3 +keywords: +- icepanel +- diagrams +- architecture +sidebar_label: IcePanel +title: Using IcePanel +description: Understanding how to embed IcePanel diagrams in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +EventCatalog supports embedding [IcePanel](https://icepanel.io/) diagrams in all your markdown files. + +IcePanel is a collaborative diagramming tool designed for visualizing software architecture using the [C4 model](https://c4model.com/). It helps teams create interactive system context, container, and component diagrams that can be shared and explored. + +![Example output](/img/ice-panel.png) + +## Using IcePanel in EventCatalog + +To embed an IcePanel diagram, use the `` component in any markdown file. + +### Getting your IcePanel share URL + +1. Open your diagram in IcePanel +2. Click the **Share** button +3. Copy the share/embed URL (it should look like `https://s.icepanel.io/...`) + +### Basic Example + +```markdown +--- +# your frontmatter +--- + + +``` + +### Example with title and description + +```markdown +--- +# your frontmatter +--- + + +``` + +### Full screen mode + +Each embedded IcePanel diagram includes a full screen button, allowing your teams to explore the diagram in detail without leaving EventCatalog. + +### Props + +| Name | Type | Default | Required | Description | +| ------------- | -------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------- | +| `url` | `string` | | Yes | The share URL of the IcePanel diagram. To get the URL, click "Share" in IcePanel and copy the embed/share URL. | +| `title` | `string` | | No | The title to display above the diagram. | +| `description` | `string` | | No | A description to display below the title. | +| `height` | `string` | `600` | No | The height of the embedded diagram in pixels. | +| `width` | `string` | `100%` | No | The width of the embedded diagram. | + +### More resources + +- [IcePanel documentation](https://docs.icepanel.io/) - Learn more about IcePanel and how to use it +- [C4 model](https://c4model.com/) - Learn about the C4 model for visualizing software architecture diff --git a/packages/core/docs/development/components/diagram-syntax/04-likec4.md b/packages/core/docs/development/components/diagram-syntax/04-likec4.md new file mode 100644 index 000000000..737d65bed --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/04-likec4.md @@ -0,0 +1,136 @@ +--- +sidebar_position: 4 +keywords: +- likec4 +- diagrams +- architecture +- c4 +sidebar_label: LikeC4 +title: Using LikeC4 +description: Embed LikeC4 architecture diagrams in EventCatalog pages +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +EventCatalog supports embedding [LikeC4](https://likec4.dev) diagrams in all your markdown files via the `` component. + +LikeC4 is an architecture-as-code tool that lets you model and visualize your system landscape using `.c4` source files. Views defined in those files can be rendered directly inside EventCatalog documentation pages. + +## Install dependencies + +LikeC4 support is opt-in. Add the required packages as dev dependencies in your EventCatalog project: + +```bash +# npm +npm install --save-dev likec4@1.55.1 @likec4/icons@1.46.4 + +# pnpm +pnpm add -D likec4@1.55.1 @likec4/icons@1.46.4 + +# yarn +yarn add -D likec4@1.55.1 @likec4/icons@1.46.4 +``` + +EventCatalog uses React 18, so install `likec4@1.55.1`. Newer LikeC4 versions may require React 19. + +EventCatalog auto-detects `.c4` files in your project. When it finds them it loads the LikeC4 Vite plugin automatically. If `.c4` files are present but the `likec4` package is not installed, EventCatalog will show an install message at startup. + +## Basic usage + +Add `.c4` source files anywhere in your EventCatalog project (outside `node_modules`), then reference a view by its id using `` in any markdown file. + +```likec4 /likec4/order-flow.c4 +specification { + element actor { + style { + shape person + } + } + element system +} + +model { + customer = actor 'Customer' + ordering = system 'Ordering service' + payment = system 'Payment service' + fulfillment = system 'Fulfillment service' + + customer -> ordering 'places order' + ordering -> payment 'authorizes payment' + ordering -> fulfillment 'requests shipment' +} + +views { + view OrderFlow { + include * + autoLayout LeftRight + } +} +``` + +```md /events/OrderCreated/index.mdx +--- +# event frontmatter +--- + + +``` + +## Multi-project setup + +When your workspace contains more than one LikeC4 project, each project needs a `likec4.config.json` file with a `name` field. EventCatalog reads these files at build time and registers each named project separately. + +```json likec4.config.json +{ + "name": "payments" +} +``` + +A common layout is to keep each project in its own folder, with the `likec4.config.json` alongside the `.c4` files that belong to it. EventCatalog scans your whole project (outside `node_modules`), so you can place these folders wherever makes sense for your catalog: + +``` +your-catalog/ +├── eventcatalog.config.js +├── likec4/ +│ ├── payments/ +│ │ ├── likec4.config.json # { "name": "payments" } +│ │ ├── model.c4 +│ │ └── views.c4 +│ └── orders/ +│ ├── likec4.config.json # { "name": "orders" } +│ ├── model.c4 +│ └── views.c4 +├── services/ +│ └── PaymentsService/ +│ └── index.mdx # +└── events/ + └── OrderCreated/ + └── index.mdx +``` + +Pass the `project` prop to load a view from a specific project: + +```md /services/PaymentsService/index.mdx +--- +# service frontmatter +--- + + +``` + +When `project` is omitted, the component uses the default (unnamed) project. + +## Props + +| Name | Type | Default | Required | Description | +| --------- | -------- | -------- | -------- | ----------------------------------------------------------------------------- | +| `viewId` | `string` | | Yes | The id of the LikeC4 view to render. | +| `project` | `string` | | No | The name of the LikeC4 project (from `likec4.config.json`). Defaults to the default project. | +| `height` | `string` | `600px` | No | CSS height of the embedded diagram. | + +## More resources + +- [LikeC4 documentation](https://likec4.dev/docs/) - Learn how to model your architecture with LikeC4 +- [LikeC4 view syntax](https://likec4.dev/docs/dsl/views/) - Reference for defining views in `.c4` files diff --git a/packages/core/docs/development/components/diagram-syntax/05-lucid.md b/packages/core/docs/development/components/diagram-syntax/05-lucid.md new file mode 100644 index 000000000..b12133ddf --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/05-lucid.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 5 +keywords: +- components +sidebar_label: Lucid +title: +description: Embed a Lucid diagram in your documentation +id: lucid +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The `` component renders a [Lucid diagram](https://lucid.app/) in your documentation. + +### Support + +The `` component is supported in domains, services, all messages and [custom documentation](/docs/development/bring-your-own-documentation/custom-pages/introduction). + +### Usage + +1. Include the `` component inside the markdown + - e.g `/events/MyEvent/index.mdx` + +**Basic Example** + +```md /events/OrderAmended/index.mdx +--- +#event frontmatter +--- + + +``` + +### Output example in EventCatalog + +![Example output](../components/img/lucid.png) + +- [Example: View demo in a domain page](https://demo.eventcatalog.dev/docs/domains/Orders/0.0.3) +- [Example: View demo in custom documentation page](https://demo.eventcatalog.dev/docs/custom/guides/event-storming/01-index) + +### Props +| Name | Type | Default | Required | Description | +| ----------------------- | --------- | ----------------- | -------- | ----------------------------------------------------------------- | +| `diagramId` | `string` | | Yes | The ID of the Lucid diagram to embed. You can find the ID in the URL of the Lucid diagram or by clicking "Share" in Lucid and copying the ID from the URL. diff --git a/packages/core/docs/development/components/diagram-syntax/06-mermaid.md b/packages/core/docs/development/components/diagram-syntax/06-mermaid.md new file mode 100644 index 000000000..df672551c --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/06-mermaid.md @@ -0,0 +1,218 @@ +--- +sidebar_position: 6 +keywords: +- mermaid +sidebar_label: Mermaid +title: Using mermaid +description: Understanding how to use mermaid with EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +EventCatalog supports [mermaid](https://www.npmjs.com/package/mermaid) (v11.x) in all your markdown files. + +This let's you create [Class Diagrams](https://mermaid.js.org/syntax/classDiagram.html), [Sequence Diagrams](https://mermaid.js.org/syntax/sequenceDiagram.html), [Entity Relationship Diagrams](https://mermaid.js.org/syntax/entityRelationshipDiagram.html), [Architecture Diagrams](https://mermaid.js.org/syntax/architecture.html) and much more. + +## Using mermaid in EventCatalog + +There are two ways to use mermaid in EventCatalog. + +1. [Using the `mermaid` code block in any markdown file.](#using-the-mermaid-code-block-in-any-markdown-file) +2. [Using Mermaid files `.mmd` and `.mermaid` files and loading them into your EventCatalog page.](#loading-mermaid-files-into-your-eventcatalog-page) + - _Added in EventCatalog 2.56.4_ + +:::tip Diagrams not rendering? +If you have large diagrams that fail to render, increase the `maxTextSize` configuration value. [Learn more about maxTextSize configuration](/docs/api/config#mermaid). +::: + +### Using the `mermaid` code block in any markdown file. + +To use mermaid you need to use the `mermaid` code block in any markdown file. + +#### Example + +```markdown +```mermaid +sequenceDiagram + participant Customer + participant OrdersService + participant InventoryService + participant NotificationService + + Customer->>OrdersService: Place Order + OrdersService->>InventoryService: Check Inventory + InventoryService-->>OrdersService: Inventory Available + OrdersService->>InventoryService: Reserve Inventory + OrdersService->>NotificationService: Send Order Confirmation + NotificationService-->>Customer: Order Confirmation + OrdersService->>Customer: Order Placed Successfully + OrdersService->>InventoryService: Update Inventory +```_ +``` + +This example will output the following in the markdown file. + +![Example output of mermaid](../img/mermaid.png) + +### Loading Mermaid files into your EventCatalog page. + + + +You can load Mermaid files using the [``](/docs/development/components/components/mermaid-file-loader) component, if you prefer to use a file instead of a code block. + +Add your .mmd or .mermaid file to your folder (e.g `/events/MyEvent/mermaid.mmd`) + +```md /events/MyEvent/index.mdx +--- +#event frontmatter +--- + + + + + + +``` + +This example will load a mermaid file (.mmd or .mermaid) into your EventCatalog page. + +**The file must be in the same directory as the markdown file.** + +--- + +## Architecture diagrams with mermaid + + + +Mermaid 11 introduced the [ability to create architecture diagrams](https://mermaid.js.org/syntax/architecture.html). + +You can use these diagrams to document your architecture. + +#### Example + +```markdown +```mermaid +architecture-beta + group api(cloud)[API] + + service db(database)[Database] in api + service disk1(disk)[Storage] in api + service disk2(disk)[Storage] in api + service server(server)[Server] in api + + db:L -- R:server + disk1:T -- B:server + disk2:T -- B:db + +```_ +``` + +This example will output the following in the markdown file. + +Example output of mermaid + +## Architecture diagrams with icons + + + +EventCatalog supports over **200,000 icons** from [icones.js.org](https://icones.js.org/collection/logos). + +To add icon support you need to add the icon pack into your `eventcatalog.config.js` file. + +```js +// eventcatalog.config.js +mermaid: { + iconPacks: ['logos'] // will load https://icones.js.org/collection/logos into eventcatalog +} +``` + +In this example above we import the icon pack [logos](https://icones.js.org/collection/logos) from [icones.js.org](https://icones.js.org/collection/logos), but you can import any icon pack you like from [icones.js.org](https://icones.js.org/collection/logos). + +To use the icons in your mermaid diagrams you need to prefix the icon name with pack name. + +In this example we are using the `logos` pack, so we prefix the icon name with `logos:`. + +```markdown +```mermaid +architecture-beta + group api(logos:aws-lambda)[API] + + service db(logos:aws-aurora)[Database] in api + service disk1(logos:aws-glacier)[Storage] in api + service disk2(logos:aws-s3)[Storage] in api + service server(logos:aws-ec2)[Server] in api + + db:L -- R:server + disk1:T -- B:server + disk2:T -- B:db +```_ +``` + +EventCatalog will then import the icons from the icon pack and render them in the diagram. + +Example output of mermaid + +## Mermaid with ELK (Eclipse Layout Kernel) layout algorithm + + + +EventCatalog supports the [ELK (Eclipse Layout Kernel)](https://eclipse.dev/elk/) layout algorithm for mermaid diagrams. + +To add support for the ELK layout algorithm you need to add the following to your `eventcatalog.config.js` file. + +```js +// eventcatalog.config.js +mermaid: { + // default value is false + enableSupportForElkLayout: true +} +``` + +After you set the value, mermaid will be configured to use the ELK layout algorithm. + +## Interactive controls + + + +All Mermaid diagrams include interactive controls for better viewing and exploration. + +![Example output of mermaid](../img/interactive.png) + +### Zoom and pan + +Click and drag to pan around the diagram, or use the zoom controls in the bottom-left corner to zoom in and out. Double-click the diagram to zoom in quickly. + +### Presentation mode + +Click the presentation button in the top-left corner to view the diagram in fullscreen. In presentation mode, mouse wheel zooming is enabled for precise control. + +Press `Escape` to exit presentation mode. + +### Copy diagram code + +Click the copy button in the top-right corner to copy the diagram code to your clipboard. + +Useful for copying diagrams into LLM prompts. + +## Export NodeGraphs as Mermaid + + + +EventCatalog can export any NodeGraph visualization as Mermaid diagram code. + +Click the view switcher dropdown in any NodeGraph and select "Copy as Mermaid" to copy the diagram to your clipboard. The exported diagram preserves all nodes, edges, labels, and styling. + +You can then paste the Mermaid code into: +- LLM prompts for architecture discussions +- Other documentation tools +- [mermaid.live](https://mermaid.live) for further editing +- Mermaid code blocks in EventCatalog pages + +[Read more about NodeGraph controls](/docs/development/components/components/nodegraph#interactive-controls) + +### More resources + +- [Mermaid documentation](https://mermaid.js.org) - Learn more about mermaid and how to use it +- [Icon packs](https://icones.js.org/) - Explore over 200,000 icons from iconify diff --git a/packages/core/docs/development/components/diagram-syntax/07-miro.md b/packages/core/docs/development/components/diagram-syntax/07-miro.md new file mode 100644 index 000000000..bc5408767 --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/07-miro.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 7 +keywords: +- components +sidebar_label: Miro +title: +description: Embed a Miro board in your documentation +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +The `` component renders a [Miro board](https://miro.com/) in your documentation. + +Each Miro board embedded can also be loaded in a full screen mode, letting your teams edit the board live ([view demo](https://demo.eventcatalog.dev/miro?autoplay=true&embedMode=view_only_without_ui&moveToWidget=3074457347671667709&boardId=uXjVIHCImos=/&back=/docs/domains/Orders/0.0.3)). + +### Support + +The `` component is supported in domains, services, all messages and [custom documentation](/docs/development/bring-your-own-documentation/custom-pages/introduction). + +### Usage + +1. Include the `` component inside the markdown + - e.g `/events/MyEvent/index.mdx` + +**Basic Example** + +```md /events/OrderAmended/index.mdx +--- +#event frontmatter +--- + + +``` +**Example with edit enabled and scroll to default widget in Miro** + +```md /events/OrderAmended/index.mdx +--- +#event frontmatter +--- + + +``` + +### Output example in EventCatalog + +![Example output](../components/img/miro.png) + +- [Example: View demo in a domain page](https://demo.eventcatalog.dev/docs/domains/Orders/0.0.3) +- [Example: View demo in custom documentation page](https://demo.eventcatalog.dev/docs/custom/guides/event-storming/01-index) +- [Example: View demo of screen Miro board in EventCatalog](https://demo.eventcatalog.dev/miro?autoplay=true&embedMode=view_only_without_ui&moveToWidget=3074457347671667709&boardId=uXjVIHCImos=/&back=/docs/domains/Orders/0.0.3#undefined-miro-title) + +### Props +| Name | Type | Default | Required | Description | +| ----------------------- | --------- | ----------------- | -------- | ----------------------------------------------------------------- | +| `boardId` | `string` | | Yes | The ID of the Miro board to embed. ([miro docs](https://developers.miro.com/docs/miro-live-embed-with-a-direct-link#get-the-board-id)) +| `edit` | `boolean` | `false` | No | Whether to enable edit mode for the Miro board. | +| `moveToViewport` | `string` | | No | The ID of the widget to scroll to. ([miro docs](https://developers.miro.com/docs/miro-live-embed-with-a-direct-link#set-the-initial-view-based-on-a-specific-viewport)) | +| `moveToWidget` | `string` | | No | The ID of the widget to scroll to. ([miro docs](https://developers.miro.com/docs/miro-live-embed-with-a-direct-link#set-the-initial-view-based-on-a-specific-board-item)) | +| `height` | `string` | `500` | No | The height of the Miro board. | +| `title` | `string` | | No | The title of the Miro board. | +| `autoplay` | `boolean` | `true` | No | Whether to skip the preloader of the miro board. ([miro docs](https://developers.miro.com/docs/miro-live-embed-with-a-direct-link#skip-the-preloader-screen)) | diff --git a/packages/core/docs/development/components/diagram-syntax/08-plantuml.md b/packages/core/docs/development/components/diagram-syntax/08-plantuml.md new file mode 100644 index 000000000..87494ad14 --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/08-plantuml.md @@ -0,0 +1,140 @@ +--- +sidebar_position: 8 +keywords: +- plantuml +sidebar_label: PlantUML +title: Using PlantUML +description: Understanding how to use plantuml with EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +EventCatalog supports [PlantUML](https://plantuml.com/) in all your markdown files. + +This let's you create [Class Diagrams](https://plantuml.com/class-diagram), [Sequence Diagrams](https://plantuml.com/sequence-diagram), [Class Diagrams](https://plantuml.com/class-diagram), [State Diagrams](https://plantuml.com/state-diagram) and much more. + +## Using plantuml in EventCatalog + +To use plantuml you need to use the `plantuml` code block in any markdown file. + +#### Example + +```markdown +```plantuml +@startuml +!define Table(name,desc) class name as "desc" << (T,#E5E7EB) >> +!define PK(x) x +!define FK(x) x + +' ===== Core Tables ===== + +Table(Customers, "Customers") { + PK(customerId): UUID + firstName: VARCHAR + lastName: VARCHAR + email: VARCHAR + phone: VARCHAR + dateRegistered: TIMESTAMP +} + +Table(Orders, "Orders") { + PK(orderId): UUID + FK(customerId): UUID + orderDate: TIMESTAMP + status: VARCHAR + totalAmount: DECIMAL +} + +Table(Products, "Products") { + PK(productId): UUID + name: VARCHAR + description: TEXT + price: DECIMAL + stockQuantity: INT +} + +Table(OrderItems, "Order Items") { + PK(id): UUID + FK(orderId): UUID + FK(productId): UUID + quantity: INT + unitPrice: DECIMAL +} + +Table(Payments, "Payments") { + PK(paymentId): UUID + FK(orderId): UUID + amount: DECIMAL + method: VARCHAR + status: VARCHAR + paidAt: TIMESTAMP +} + +Table(InventoryEvents, "Inventory Events") { + PK(eventId): UUID + FK(productId): UUID + eventType: VARCHAR + quantityChange: INT + eventTime: TIMESTAMP +} + +Table(Subscription, "Subscriptions") { + PK(subscriptionId): UUID + FK(customerId): UUID + plan: VARCHAR + status: VARCHAR + startDate: TIMESTAMP + endDate: TIMESTAMP +} + +' ===== Relationships ===== + +Customers ||--o{ Orders : places +Orders ||--o{ OrderItems : contains +Products ||--o{ OrderItems : includes +Orders ||--o{ Payments : paid_by +Products ||--o{ InventoryEvents : logs +Customers ||--o{ Subscription : subscribes + +@enduml +```_ +``` + +This example will output the following in the markdown file. + +![Example output of plantuml](../img/plantuml.png) + +### How it works? + +The PlantUML implementation takes your content and converts it to a PNG image using https://www.plantuml.com/plantuml/svg. + +![Example output of plantuml](../img/interactive.png) + +## Interactive controls + + + +All PlantUML diagrams include interactive controls for better viewing and exploration. + +### Zoom and pan + +Click and drag to pan around the diagram, or use the zoom controls in the bottom-left corner to zoom in and out. Double-click the diagram to zoom in quickly. + +### Presentation mode + +Click the presentation button in the top-left corner to view the diagram in fullscreen. In presentation mode, mouse wheel zooming is enabled for precise control. + +Press `Escape` to exit presentation mode. + +### Copy diagram code + +Click the copy button in the top-right corner to copy the diagram code to your clipboard. + +Useful for copying diagrams into LLM prompts. + +### More resources + +- [PlantUML documentation](https://plantuml.com) - Learn more about plantuml and how to use it +- [Real world examples](https://real-world-plantuml.com/) - Real world examples of plantuml in use diff --git a/packages/core/docs/development/components/diagram-syntax/09-structurizr.md b/packages/core/docs/development/components/diagram-syntax/09-structurizr.md new file mode 100644 index 000000000..32441d9a1 --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/09-structurizr.md @@ -0,0 +1,24 @@ +--- +sidebar_position: 9 +keywords: +- mermaid +sidebar_label: Structurizr +title: Using Structurizr +description: Understanding how to use Structurizr with EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +You can embed your [Structurizr diagrams](https://structurizr.com/) into your EventCatalog pages using the [``](/docs/development/components/components/mermaid-file-loader) component. + +You will need to export your Structurizr diagrams into mermaid files and then use the [``](/docs/development/components/components/mermaid-file-loader) component to embed them into your EventCatalog pages. + +## How to export your Structurizr diagrams into mermaid files + +You will need to use the Structurizr CLI to export your diagrams into mermaid files. + +1. [Install the Structurizr CLI](https://docs.structurizr.com/cli/installation) +2. [Export your diagrams into mermaid files](https://docs.structurizr.com/cli/export) +3. Use the [``](/docs/development/components/components/mermaid-file-loader) component to embed them into your EventCatalog pages. diff --git a/packages/core/docs/development/components/diagram-syntax/_category_.json b/packages/core/docs/development/components/diagram-syntax/_category_.json new file mode 100644 index 000000000..5ceb52a3a --- /dev/null +++ b/packages/core/docs/development/components/diagram-syntax/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Embed Diagrams", + "position": 3, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "components/diagram-syntax", + "title": "Diagrams", + "description": "Use diagram languages and tools inside EventCatalog documentation." + } +} diff --git a/packages/core/docs/development/customization/01-customize-landing-page.md b/packages/core/docs/development/customization/01-customize-landing-page.md new file mode 100644 index 000000000..fd880a0fe --- /dev/null +++ b/packages/core/docs/development/customization/01-customize-landing-page.md @@ -0,0 +1,147 @@ +--- +sidebar_position: 3 +keywords: +- EventCatalog landing page +sidebar_label: Custom homepage +title: Customize landing page +description: Customize landing pages in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +EventCatalog provides a landing page for your catalog, this is the first page that users see when they visit your catalog. + +You can customize the landing page with your own content, HTML and EventCatalog components. + +### Example of a custom landing page + +You can add any HTML you want to your landing page, in this example we have added custom content using EventCatalog components, which includes the +visualizer for a domains, subdomains and flows. + + + +## How to customize your landing page + +1. Create a new folder called `pages` and an Astro file called `homepage.astro` + - Example: `/pages/homepage.astro` +1. Add the contents to your Astro file. + - You can see a full example of this on our [GitHub repository](https://github.com/event-catalog/eventcatalog/blob/main/examples/default/pages/homepage.astro). +1. Run `npm run dev` or `npm run build` to see your changes. + +:::info What is Astro? +[Astro](https://astro.build/) is a static site builder that powers EventCatalog. You can learn more about Astro [here](https://docs.astro.build/en/getting-started/). +::: + +**Example of homepage.astro** + +```md title="/pages/homepage.astro" +--- +# Import EventCatalog components to use in your landing page +const { Tile, Tiles, Flow, NodeGraph, Admonition } = Astro.props.components; +--- + +
    +

    Welcome to FlowMart's EventCatalog

    +

    Explore the events, services, and domains that power the FlowMart ecosystem. This catalog provides a centralized place to discover and understand our asynchronous architecture.

    + + +

    This is a demo of the EventCatalog and what it can do. The company is called FlowMart and they are an e-commerce company.

    +

    Using EventCatalog, we documented their systems (domains, services, events, commands, flows) and how they fit together.

    +
    + +
    +
    +

    E-Commerce Domain

    +

    The core domain of FlowMart, responsible for all e-commerce operations.

    + +
    +
    +

    Orders Domain

    +

    The sub-domain responsible for all orders.

    + +
    +
    +

    Payment Domain

    +

    The sub-domain responsible for all payments.

    + +
    +
    +

    Subscription Domain

    +

    The sub-domain responsible for all subscriptions.

    + +
    +
    + +
    +

    Discover Our Architecture

    +

    + Navigate through our Domains to understand the different business capabilities, explore Services to see the microservices involved, and dive into Events and Commands to see how they communicate. +

    +

    + Use the search bar above or browse the sections in the sidebar to get started. +

    +
    + +
    +
    +

    Cancel Subscription Flow

    +

    This flow is triggered when a user cancels their subscription.

    + +
    +
    +

    Payment Flow

    +

    This flow documents how a payment is processed at FlowMart.

    + +
    +
    + +
    +

    Quick Links

    +

    Learn how to get started with EventCatalog, create domains, services, events, and commands.

    + + + + + + + + +
    + +
    +

    Join the community

    + +

    Our project and community is growing fast. We have over 1000+ members in our Discord community.

    + + + + + +
    + +
    + + +``` + +See the example output [here](https://demo.eventcatalog.dev) + + +### Using components + +You can use EventCatalog components in your custom landing page. + +These include embedding visuals, flows, and more. + +Example: + +```md + + + +``` + +You can get a list of components [here](/docs/components). diff --git a/packages/core/docs/development/customization/02-themes.md b/packages/core/docs/development/customization/02-themes.md new file mode 100644 index 000000000..626ea197e --- /dev/null +++ b/packages/core/docs/development/customization/02-themes.md @@ -0,0 +1,430 @@ +--- +sidebar_position: 6 +keywords: + - EventCatalog themes + - dark mode + - light mode + - customization + - custom theme +sidebar_label: Themes +title: Themes +description: Customize the look and feel of your catalog with built-in themes, dark/light mode, and custom themes +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + +EventCatalog comes with a selection of built-in themes and supports both light and dark modes out of the box. + +## Choosing a theme + +To set a theme, add the `theme` property to your `eventcatalog.config.js` file: + +```js title="eventcatalog.config.js" +export default { + // ... other config + theme: 'sapphire', +}; +``` + +### Available themes + +EventCatalog includes the following built-in themes: + +| Theme | Description | +|-------|-------------| +| `default` | Purple accent - the original EventCatalog look | +| `ocean` | Teal accent - calm and professional | +| `sapphire` | Blue accent - premium enterprise feel | +| `sunset` | Orange accent - warm and energetic | +| `forest` | Dark green accent - natural and grounded | + + +## Light and dark mode + +EventCatalog supports both light and dark modes. Users can toggle between modes using the theme switcher in the header. + +### How it works + +- The theme toggle appears in the header navigation +- **System preference by default**: New visitors see light or dark mode based on their operating system preference +- **Remembers user choice**: Once a user manually toggles the theme, their preference is saved and used on future visits +- **Syncs across tabs**: Theme changes sync across browser tabs +- **Responds to system changes**: If no manual preference is set, the theme automatically updates when the user changes their OS dark mode setting + +### Theme persistence + +When a user manually selects light or dark mode, their preference is stored in `localStorage` under the key `eventcatalog-theme`. This ensures their choice persists across page reloads and browser sessions. + +If no preference is stored, EventCatalog automatically follows the user's system preference using the `prefers-color-scheme` media query. + +--- + +## Creating custom themes + + + +Custom themes allow you to fully brand EventCatalog with your organization's colors. You define your theme in `eventcatalog.styles.css` using CSS variables. + +### Step 1: Create your theme CSS + +Add your custom theme to `eventcatalog.styles.css` in your project root. Your theme needs to define CSS variables for both light and dark modes. + +```css title="eventcatalog.styles.css" +/** + * Custom "ruby" theme - Red accent colors + */ + +/* Light Mode */ +:root[data-catalog-theme="ruby"], +:root[data-catalog-theme="ruby"][data-theme="light"] { + /* Accent colors */ + --ec-accent: 220 38 38; /* red-600 */ + --ec-accent-hover: 185 28 28; /* red-700 */ + --ec-accent-subtle: 254 226 226; /* red-100 */ + --ec-accent-text: 153 27 27; /* red-800 */ + + /* Buttons */ + --ec-button-bg: 185 28 28; /* red-700 */ + --ec-button-bg-hover: 153 27 27; /* red-800 */ + --ec-button-text: 255 255 255; + + /* Sidebar */ + --ec-sidebar-active-bg: 185 28 28; + --ec-sidebar-active-text: 255 255 255; + --ec-sidebar-hover-bg: 185 28 28; + + /* You can override any CSS variable here */ +} + +/* Dark Mode */ +:root[data-catalog-theme="ruby"][data-theme="dark"] { + /* Accent colors - use lighter shades for dark mode */ + --ec-accent: 248 113 113; /* red-400 */ + --ec-accent-hover: 239 68 68; /* red-500 */ + --ec-accent-subtle: 127 29 29 / 0.3; + --ec-accent-text: 252 165 165; /* red-300 */ + + /* Buttons */ + --ec-button-bg: 220 38 38; /* red-600 */ + --ec-button-bg-hover: 239 68 68; /* red-500 */ + --ec-button-text: 255 255 255; + + /* Sidebar */ + --ec-sidebar-active-bg: 220 38 38; + --ec-sidebar-active-text: 255 255 255; +} +``` + +### Step 2: Set the theme in config + +Reference your custom theme name in `eventcatalog.config.js`: + +```js title="eventcatalog.config.js" +export default { + // ... other config + theme: 'ruby', // Matches data-catalog-theme="ruby" in your CSS +}; +``` + +### Step 3: Test both modes + +Start your catalog and test both light and dark modes to ensure all colors look correct: + +```bash +npm run dev +``` + +Toggle between light and dark mode using the theme switcher in the header. + +:::tip Tailwind Color Reference +Use [Tailwind CSS colors](https://tailwindcss.com/docs/customizing-colors) as a reference for your RGB values. For example, `red-600` is `220 38 38`. +::: + +:::caution RGB Format Required +CSS variables must use space-separated RGB values (e.g., `220 38 38`) **without** the `rgb()` wrapper. This allows EventCatalog to apply opacity modifiers like `rgb(var(--ec-accent) / 0.5)`. +::: + +--- + +## Full CSS variable reference + +Here are all available CSS variables you can customize: + +### Core colors + +| Variable | Description | +|----------|-------------| +| `--ec-accent` | Primary accent color for highlights and interactive elements | +| `--ec-accent-hover` | Accent color on hover | +| `--ec-accent-subtle` | Light accent background (badges, highlights) | +| `--ec-accent-text` | Text color on accent backgrounds | +| `--ec-accent-gradient-from` | Gradient start color | +| `--ec-accent-gradient-to` | Gradient end color | + +### Header + +| Variable | Description | +|----------|-------------| +| `--ec-header-bg` | Header background | +| `--ec-header-text` | Header text color | +| `--ec-header-border` | Header border color | + +### Sidebar + +| Variable | Description | +|----------|-------------| +| `--ec-sidebar-bg` | Sidebar background | +| `--ec-sidebar-bg-gradient` | Sidebar gradient end color | +| `--ec-sidebar-border` | Sidebar border | +| `--ec-sidebar-text` | Sidebar text color | +| `--ec-sidebar-active-bg` | Active item background | +| `--ec-sidebar-active-text` | Active item text | +| `--ec-sidebar-hover-bg` | Hover item background | + +### Rail + +| Variable | Description | +|----------|-------------| +| `--ec-rail-bg` | Inset panel/navigation rail background, sits slightly off the page background | +| `--ec-rail-active-bg` | Active item background within a rail | + +### Content area + +| Variable | Description | +|----------|-------------| +| `--ec-content-bg` | Nested sidebar content background | +| `--ec-content-text` | Content text | +| `--ec-content-text-muted` | Muted text | +| `--ec-content-text-secondary` | Secondary text | +| `--ec-content-border` | Content borders | +| `--ec-content-hover` | Hover background | +| `--ec-content-active` | Active item background | + +### Page + +| Variable | Description | +|----------|-------------| +| `--ec-page-bg` | Main page background | +| `--ec-page-text` | Page text color | +| `--ec-page-text-muted` | Muted/secondary text | +| `--ec-page-border` | Page borders | + +### Buttons + +| Variable | Description | +|----------|-------------| +| `--ec-button-bg` | Button background | +| `--ec-button-bg-hover` | Button hover background | +| `--ec-button-text` | Button text color | + +### Form inputs + +| Variable | Description | +|----------|-------------| +| `--ec-input-bg` | Input field background | +| `--ec-input-border` | Input field border | +| `--ec-input-text` | Input field text | +| `--ec-input-placeholder` | Placeholder text color | + +### Dropdowns + +| Variable | Description | +|----------|-------------| +| `--ec-dropdown-bg` | Dropdown background | +| `--ec-dropdown-text` | Dropdown text | +| `--ec-dropdown-hover` | Dropdown hover background | +| `--ec-dropdown-border` | Dropdown border | + +### Icons + +| Variable | Description | +|----------|-------------| +| `--ec-icon-color` | Default icon color | +| `--ec-icon-hover` | Icon hover color | + +### Other + +| Variable | Description | +|----------|-------------| +| `--ec-card-bg` | Card/elevated surface background | +| `--ec-code-bg` | Code block background | +| `--ec-group-icon-bg` | Group header icon background | +| `--ec-group-icon-text` | Group header icon text | + +--- + +## Dark mode prose overrides + +For custom themes, you may want to customize how markdown content (prose) appears in dark mode: + +```css title="eventcatalog.styles.css" +:root[data-catalog-theme="ruby"][data-theme="dark"] .prose { + --tw-prose-body: rgb(163 163 163); + --tw-prose-headings: rgb(245 245 245); + --tw-prose-links: rgb(248 113 113); /* Match your accent */ + --tw-prose-bold: rgb(245 245 245); + --tw-prose-code: rgb(245 245 245); + --tw-prose-pre-bg: rgb(23 23 23); +} + +:root[data-catalog-theme="ruby"][data-theme="dark"] .prose a { + color: rgb(248 113 113); +} + +:root[data-catalog-theme="ruby"][data-theme="dark"] .prose a:hover { + color: rgb(252 165 165); +} +``` + +--- + +## Example: Complete custom theme + +Here's a complete example of a custom "ruby" red theme you can use as a starting point: + +
    +View full ruby theme CSS + +```css title="eventcatalog.styles.css" +/** + * Ruby Theme - Custom Red Theme + */ + +/* Ruby Light Mode */ +:root[data-catalog-theme="ruby"], +:root[data-catalog-theme="ruby"][data-theme="light"] { + --ec-header-bg: 255 255 255; + --ec-header-text: 23 23 23; + --ec-header-border: 229 229 229; + + --ec-primary: 220 38 38; + --ec-primary-hover: 185 28 28; + + --ec-accent: 220 38 38; + --ec-accent-hover: 185 28 28; + --ec-accent-subtle: 254 226 226; + --ec-accent-text: 153 27 27; + --ec-accent-gradient-from: 239 68 68; + --ec-accent-gradient-to: 185 28 28; + + --ec-button-bg: 185 28 28; + --ec-button-bg-hover: 153 27 27; + --ec-button-text: 255 255 255; + + --ec-dropdown-bg: 255 255 255; + --ec-dropdown-text: 64 64 64; + --ec-dropdown-hover: 254 242 242; + --ec-dropdown-border: 229 229 229; + + --ec-icon-color: 82 82 82; + --ec-icon-hover: 185 28 28; + + --ec-sidebar-bg: 255 255 255; + --ec-sidebar-bg-gradient: 254 242 242; + --ec-sidebar-border: 229 229 229; + --ec-sidebar-text: 82 82 82; + --ec-sidebar-active-bg: 185 28 28; + --ec-sidebar-active-text: 255 255 255; + --ec-sidebar-hover-bg: 185 28 28; + + --ec-content-bg: 255 255 255; + --ec-content-text: 23 23 23; + --ec-content-text-muted: 82 82 82; + --ec-content-text-secondary: 115 115 115; + --ec-content-border: 229 229 229; + --ec-content-hover: 250 250 250; + --ec-content-active: 254 226 226; + + --ec-input-bg: 255 255 255; + --ec-input-border: 212 212 212; + --ec-input-text: 23 23 23; + --ec-input-placeholder: 163 163 163; + + --ec-group-icon-bg: 254 226 226; + --ec-group-icon-text: 153 27 27; + + --ec-page-bg: 255 255 255; + --ec-page-text: 23 23 23; + --ec-page-text-muted: 82 82 82; + --ec-page-border: 229 229 229; + + --ec-card-bg: 255 255 255; + --ec-code-bg: 250 250 250; +} + +/* Ruby Dark Mode */ +:root[data-catalog-theme="ruby"][data-theme="dark"] { + --ec-header-bg: 23 23 23; + --ec-header-text: 245 245 245; + --ec-header-border: 38 38 38; + + --ec-accent: 248 113 113; + --ec-accent-hover: 239 68 68; + --ec-accent-subtle: 127 29 29 / 0.3; + --ec-accent-text: 252 165 165; + --ec-accent-gradient-from: 248 113 113; + --ec-accent-gradient-to: 239 68 68; + + --ec-button-bg: 220 38 38; + --ec-button-bg-hover: 239 68 68; + --ec-button-text: 255 255 255; + + --ec-dropdown-bg: 23 23 23; + --ec-dropdown-text: 212 212 212; + --ec-dropdown-hover: 38 38 38; + --ec-dropdown-border: 64 64 64; + + --ec-icon-color: 163 163 163; + --ec-icon-hover: 252 165 165; + + --ec-sidebar-bg: 10 10 10; + --ec-sidebar-bg-gradient: 10 10 10; + --ec-sidebar-border: 38 38 38; + --ec-sidebar-text: 163 163 163; + --ec-sidebar-active-bg: 220 38 38; + --ec-sidebar-active-text: 255 255 255; + --ec-sidebar-hover-bg: 38 38 38; + + --ec-content-bg: 10 10 10; + --ec-content-text: 245 245 245; + --ec-content-text-muted: 163 163 163; + --ec-content-text-secondary: 163 163 163; + --ec-content-border: 38 38 38; + --ec-content-hover: 23 23 23; + --ec-content-active: 38 38 38; + + --ec-input-bg: 23 23 23; + --ec-input-border: 64 64 64; + --ec-input-text: 245 245 245; + --ec-input-placeholder: 115 115 115; + + --ec-group-icon-bg: 38 38 38; + --ec-group-icon-text: 252 165 165; + + --ec-page-bg: 10 10 10; + --ec-page-text: 245 245 245; + --ec-page-text-muted: 163 163 163; + --ec-page-border: 38 38 38; + + --ec-card-bg: 10 10 10; + --ec-code-bg: 23 23 23; +} + +/* Ruby theme prose overrides */ +:root[data-catalog-theme="ruby"][data-theme="dark"] .prose { + --tw-prose-body: rgb(163 163 163); + --tw-prose-headings: rgb(245 245 245); + --tw-prose-links: rgb(248 113 113); + --tw-prose-bold: rgb(245 245 245); +} + +:root[data-catalog-theme="ruby"][data-theme="dark"] .prose a { + color: rgb(248 113 113); +} + +:root[data-catalog-theme="ruby"][data-theme="dark"] .prose a:hover { + color: rgb(252 165 165); +} +``` + +
    diff --git a/packages/core/docs/development/customization/03-application-sidebar.md b/packages/core/docs/development/customization/03-application-sidebar.md new file mode 100644 index 000000000..e5d3823a8 --- /dev/null +++ b/packages/core/docs/development/customization/03-application-sidebar.md @@ -0,0 +1,246 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog application sidebar +sidebar_label: Application sidebar +title: Application Sidebar +description: Pick and customize the application sidebar. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +The application sidebar is the vertical sidebar that is rendered on every page in EventCatalog. + +![Example](../guides/img/custom-sidebars/application-sidebar.png) + +### Customize the application sidebar + +You can customize the application sidebar with the `navigation.groups` property in your `eventcatalog.config.js` file. + +Use this when you want to: + +- Reorder the application navigation. +- Hide built-in navigation items. +- Add custom links to internal or external pages. +- Group navigation items under labels. +- Pin navigation groups to the bottom of the sidebar. +- Configure which routes make an item active. + +:::info Application sidebar vs documentation sidebar + +`navigation.groups` customizes the vertical application sidebar. + +`navigation.pages` customizes the context-aware documentation sidebar shown on `/docs/*` pages. + +::: + +If you do not configure `navigation.groups`, EventCatalog renders the default application sidebar. + +If you do configure `navigation.groups`, your groups replace the default application sidebar. Add every item you want users to see. + +### Built-in navigation items + +EventCatalog provides built-in navigation items you can reference by `id`. + +| ID | Description | +| --- | --- | +| `home` | Home | +| `docs` | Custom documentation | +| `catalog` | Discover catalog | +| `schemas` | Schemas & APIs | +| `schema-insights` | Schema Insights | +| `teams` | Teams | +| `users` | Users | +| `settings` | Settings | + +You can also reference built-in route IDs directly, such as `/schemas/explorer`, `/directory/teams`, or `/settings/general`. + +### Group options + +Each group in `navigation.groups` supports: + +| Property | Type | Description | +| --- | --- | --- | +| `id` | `string` | Unique group identifier. | +| `label` | `string` | Optional label shown above the group. | +| `visible` | `boolean` | Set to `false` to hide the group. | +| `position` | `'top' \| 'bottom'` | Use `bottom` for settings-style groups pinned below the main navigation. Defaults to `top`. | +| `items` | `array` | Navigation items in this group. | + +### Item options + +Each item supports: + +| Property | Type | Description | +| --- | --- | --- | +| `id` | `string` | Built-in item id, route id, or custom item id. | +| `label` | `string` | Custom label. Required for custom items. | +| `icon` | `string` | Any icon exported by [lucide-react](https://lucide.dev/icons/), such as `House`, `BookOpen`, or `LifeBuoy`. | +| `href` | `string` | Required for custom items. Built-in items provide their own `href`. | +| `visible` | `boolean` | Set to `false` to hide the item. | +| `match` | `string \| string[]` | Paths that should mark the item as active. Useful for custom navigation items. | + +### Example: default-style navigation + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + groups: [ + { + id: 'main', + items: [{ id: 'home' }, { id: 'docs' }], + }, + { + id: 'browse', + label: 'Browse', + items: [ + { id: 'catalog' }, + { id: 'schemas' }, + { id: 'schema-insights' }, + ], + }, + { + id: 'organization', + label: 'Organization', + items: [{ id: 'teams' }, { id: 'users' }], + }, + { + id: 'settings', + position: 'bottom', + items: [{ id: 'settings' }], + }, + ], + }, +}; +``` + +### Hide items + +Use `visible: false` to hide a group or item. + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + groups: [ + { + id: 'browse', + label: 'Browse', + items: [ + { id: 'catalog' }, + { id: 'schemas' }, + // Hide Schema Insights + { id: 'schema-insights', visible: false }, + ], + }, + { + id: 'organization', + label: 'Organization', + items: [ + // Hide Teams + { id: 'teams', visible: false }, + { id: 'users' }, + ], + }, + ], + }, +}; +``` + +### Add custom links + +Custom items require an `id`, `label`, `icon`, and `href`. + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + groups: [ + { + id: 'platform', + label: 'Platform', + items: [ + { + id: 'platform-docs', + label: 'Platform Docs', + icon: 'BookOpen', + href: '/docs/custom/platform/overview', + match: ['/docs/custom/platform'], + }, + { + id: 'support', + label: 'Support', + icon: 'LifeBuoy', + href: 'https://support.example.com', + }, + ], + }, + ], + }, +}; +``` + +External `href` values open in a new tab. + +### Active route matching + +Built-in items know which routes should mark them as active. + +For custom links, use `match` when more than one route should activate the same navigation item. + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + groups: [ + { + id: 'platform', + items: [ + { + id: 'platform-docs', + label: 'Platform Docs', + icon: 'BookOpen', + href: '/docs/custom/platform/overview', + match: [ + '/docs/custom/platform', + '/docs/custom/engineering-standards', + ], + }, + ], + }, + ], + }, +}; +``` + +### Migrating from `sidebar` + +In v3 you could show or hide application sidebar items using the top-level `sidebar` property. + +In v4 this has been replaced by `navigation.groups`. + +```diff title="eventcatalog.config.js" +module.exports = { +- sidebar: [ +- { id: '/directory/teams', visible: false }, +- { id: '/schemas/fields', visible: false }, +- ], ++ navigation: { ++ groups: [ ++ { ++ id: 'browse', ++ label: 'Browse', ++ items: [ ++ { id: 'catalog' }, ++ { id: 'schemas' }, ++ { id: 'schema-insights', visible: false }, ++ ], ++ }, ++ { ++ id: 'organization', ++ label: 'Organization', ++ items: [{ id: 'teams', visible: false }, { id: 'users' }], ++ }, ++ ], ++ }, +}; +``` + +The `SideBarConfig` type is no longer exported. diff --git a/packages/core/docs/development/customization/03-search.md b/packages/core/docs/development/customization/03-search.md new file mode 100644 index 000000000..02d957318 --- /dev/null +++ b/packages/core/docs/development/customization/03-search.md @@ -0,0 +1,79 @@ +--- +sidebar_position: 4 +keywords: + - EventCatalog search + - full-content search + - indexed search + - pagefind + - search configuration +sidebar_label: Global Search +title: Search +description: Configure resource metadata search or opt-in to full-content indexed search +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +EventCatalog ships with two search modes. The default is a fast metadata search that works with no extra configuration. An opt-in indexed mode adds full-content search powered by [Pagefind](https://pagefind.app/). + +## Understand the modes + +**Resource search (default)** indexes the metadata of every catalog resource — names, summaries, badges, types, and identifiers. It is fast, requires no build step, and works in both `static` and `server` output modes. + +**Indexed search (opt-in)** builds a static index of the full markdown content of your catalog at build time. It can find matches inside the body of a page, changelogs, custom docs, and resource docs — not just the frontmatter fields. + +## Enable indexed search + +Set `search.type` to `'indexed'` in `eventcatalog.config.js`: + +```js title="eventcatalog.config.js" +module.exports = { + search: { + type: 'indexed', + }, +}; +``` + +No other changes are required. The index is built automatically as part of `eventcatalog build` and also during `eventcatalog dev`. + +## What gets indexed + +When indexed search is enabled, the following content is included in the index: + +- All catalog resources: events, commands, queries, services, domains, subdomains, channels, containers, data products, entities, and flows +- Custom docs pages +- Resource docs +- Changelogs + +## When the index is built + +During `eventcatalog build`, the indexer runs after the Astro build and writes the index to `/pagefind`. + +During `eventcatalog dev`, the index is built on startup and rebuilt automatically whenever any `.md` or `.mdx` file changes (debounced to avoid excessive rebuilds). The first build may take a few seconds depending on the size of your catalog. Subsequent incremental rebuilds are faster. + +## Result ranking + +Indexed results are ranked by relevance. Matches in the resource title and identifier score highest, followed by matches in the summary and type. Content-body matches score lower than title matches but are still surfaced when no title match is found. + +## Auth and the `/pagefind` assets + +:::warning Private catalogs +If your catalog uses authentication (an `eventcatalog.auth.js` file is present), the generated `/pagefind` directory is served as static client-readable assets. Anyone who can load the page can download the index, which contains the full text of all indexed content. + +Ensure your deployment platform protects the `/pagefind` path behind the same authentication layer as the rest of your catalog. +::: + +## Keep resource search + +To stay on the default resource search, either omit the `search` key entirely or set the type explicitly: + +```js title="eventcatalog.config.js" +module.exports = { + search: { + type: 'resource', // default, no index built + }, +}; +``` + +Existing catalogs are not affected by this feature — no changes are needed unless you want to opt in. diff --git a/packages/core/docs/development/customization/04-documentation-sidebar.md b/packages/core/docs/development/customization/04-documentation-sidebar.md new file mode 100644 index 000000000..18f22646a --- /dev/null +++ b/packages/core/docs/development/customization/04-documentation-sidebar.md @@ -0,0 +1,187 @@ +--- +sidebar_position: 2 +keywords: +- EventCatalog documentation sidebar +sidebar_label: Docs sidebar +title: Documentation sidebar +description: Pick and customize the documentation sidebar. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +The documentation sidebar is a [context aware sidebar](#what-is-context-aware-sidebar) that is shown on the `/docs/` pages. + +Clicking on any resource in the sidebar will show you related information to that selected resource ([see demo](https://demo.eventcatalog.dev/)). + +![](./img/docs-sidebar.png) + +## What is a context aware sidebar? + +**Many documentation tools use a flat navigation structure, which can be overwhelming and difficult to navigate.** + +EventCatalog's documentation sidebar is a context aware sidebar that shows you related information to the selected resource. + +This can help you navigate your documentation and find the information you need quickly, as you go deeper into the hierarchy of your architecture. + +For example, selecting a domain will show you the subdomains, related services, ubiquitous language, etc, where as selecting a message will show you the producers, consumers and schemas. + + +## Customizing the documentation sidebar + +By default EventCatalog will show you a list of all the resources in your catalog, but you can customize the navigation bar to show any resource you want. + +This can be useful if you want to show a specific resource or a group of resources in the sidebar, helping your teams find the information they need quickly. + +### How to customize the documentation sidebar + +To customize the documentation sidebar you need to set the `navigation.pages` property in your `eventcatalog.config.js` file. + +The example below will show you a list of the top-level domains and all resources in your catalog. + +```js title="eventcatalog.config.js" +module.exports = { + // ... rest of your config + navigation: { + // pick any key you want to show in the sidebar + pages: ['list:top-level-domains', 'list:all'], + }, +}; +``` + +![](./img/custom-docs-sidebar.png) + +#### Available navigation configuration + +You can specify the following options in the `navigation.pages` property: + +- [Top level options](#top-level-options) + - Useful if you want to show the top-level resources in your catalog in the sidebar. For example high level domains and let your users drill down. +- [List all resources (by type)](#list-all-resources-type) + - Useful if you want to show all resources of a specific type in the sidebar. For example all domains, services, messages, etc. +- [Pick specific resources to show](#pick-specific-resources-to-show) + - Useful if you want to show a specific resource or a group of resources in the sidebar. For example a specific domain, service, message, etc. +- [Custom groups and links](#custom-groups-and-links) + - Useful if you want to create custom groups and links to external pages in the sidebar. For example a group of resources, or a link to an external page. + +#### Top level options: + +| Key | Description | +| --- | --- | +| `list:all` | Lists all resources types in your catalog, organized by resource type (e.g domain, services, messages) | +| `list:top-level-domains` | Displays a list of the top-level domains in your catalog. (Does not include subdomains) | + +**Example** + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + pages: ['list:top-level-domains'], + }, +}; +``` + + +#### List all resources (by type): + +| Key | Description | +| --- | --- | +| `list:domains` | Lists all domains in your catalog. | +| `list:services` | Lists all services in your catalog. | +| `list:messages` | Lists all messages in your catalog. | +| `list:channels` | Lists all channels in your catalog. | +| `list:designs` | Lists all designs in your catalog. | +| `list:flows` | Lists all flows in your catalog. | +| `list:containers` | Lists all containers in your catalog. | + +**Example** + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + pages: ['list:domains', 'list:services'], + }, +}; +``` + +#### Chose which resources to show: + +You can specify any resource you want to show in the sidebar using the following key structure + +`:` or `::` + +*If no version is specified, the latest version will be used.* + +*Example:* + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + // Show the MyDomain domain, MyService service and the 0.0.1 version of the MyEvent event + pages: [ + // Show the latest version of the MyDomain domain + 'domain:MyDomain', + // Show the latest version of the MyService service + 'service:MyService', + // Show the 0.0.1 version of the MyMessage message + 'message:MyMessage:0.0.1', + ], + }, +}; +``` + +Available resource types: +| Resource Type | Description | +| --- | --- | +| `domain` | Domain resource type. | +| `service` | Service resource type. | +| `message` | Message resource type. | +| `channel` | Channel resource type. | +| `design` | Design resource type. | +| `flow` | Flow resource type. | +| `container` | Container resource type. | +| `user` | User resource type. | +| `team` | Team resource type. | +| `event` | Event resource type. | +| `command` | Command resource type. | +| `query` | Query resource type. | + +### Custom groups and links + +You can also create custom groups and links to external pages in the sidebar. + +```js title="eventcatalog.config.js" +module.exports = { + navigation: { + pages: [ + // Custom group + { + type: 'group', + // Custom group title + title: 'My Custom Group', + // Custom group icon from lucide icons + icon: 'Boxes', + // Custom group pages + pages: [ + // You can still reference resources, lists as you would normally do + 'domain:E-Commerce', + // Create a custom item link to an external page + { + type: 'item', + title: 'My Custom Link', + // Custom link to an external page + href: 'https://eventcatalog.dev', + } + ], + }, + // Or have a custom item on the root (outside a group) + { + type: 'item', + title: 'My Custom Item', + href: 'https://eventcatalog.dev', + } + // You can still use EventCatalog lists or resources as you would normally do + 'list:all', + ] + }, +}; +``` diff --git a/packages/core/docs/development/customization/05-custom-pages-and-api-routes/01-introduction.md b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/01-introduction.md new file mode 100644 index 000000000..3b86fa3f8 --- /dev/null +++ b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/01-introduction.md @@ -0,0 +1,94 @@ +--- +sidebar_position: 1 +keywords: + - EventCatalog custom pages + - EventCatalog API routes + - Astro pages + - custom routes +sidebar_label: Introduction +title: Custom pages and API routes +description: Understand custom pages and API routes in EventCatalog. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + + +EventCatalog gives you a structured catalog out of the box, but every organization eventually needs pages that are specific to how they work. + +Custom pages and API routes let you extend EventCatalog beyond the built-in catalog views. You can create any page you need, reuse your own components, keep pages static when the content is simple, or connect pages to other systems when you need live data. + +Use this when you want EventCatalog to become the place for catalog-specific tools and experiences, such as: + +- Internal reports and dashboards. +- Team onboarding pages. +- Architecture review workflows. +- Scorecards and ownership views. +- Lightweight API routes for catalog data. +- Pages that combine EventCatalog resources with data from systems such as service catalogs, deployment platforms, observability tools, or internal APIs. + +## What are custom pages? + +Custom pages are `.astro` files that EventCatalog turns into routes in your generated catalog. + +For example, `pages/reports.astro` becomes `/custom/reports` by default. + +Custom pages are useful when markdown is not enough. Use them when you need layout control, custom components, conditional rendering, or a small internal tool that belongs inside the catalog. + +They can be static pages built entirely from catalog data, or pages that call API routes and other systems to show live information. + +## What are API routes? + +API routes are server endpoints that live in `pages/api`. + +For example, `pages/api/services.ts` becomes `/custom/api/services` by default. + +API routes are useful when you want to expose catalog data, receive form submissions, power a custom page, or connect a catalog workflow to another internal system. + +For example, a custom page could show service ownership from EventCatalog, deployment status from your platform, and incidents from your observability tool in one view. + +## Custom pages, custom docs, and custom components + +EventCatalog has a few customization features. Pick the smallest one that fits the job. + +| Feature | Use it when | +| --- | --- | +| [Custom documentation](/docs/development/bring-your-own-documentation/custom-pages/introduction) | You want markdown documentation under `/docs/custom`. | +| [Custom components](/docs/development/components/custom-components/introduction) | You want reusable UI inside markdown or MDX pages. | +| [Custom homepage](/docs/development/customization/customize-landing-page) | You want to replace the catalog homepage at `/`. | +| Custom pages | You want new catalog routes with custom Astro code. | +| API routes | You want server endpoints inside the catalog. | + +:::info Custom homepage + +`pages/homepage.astro` is still the way to customize the catalog homepage. That file is rendered at `/` and is not served under the custom pages prefix. + +This section covers additional custom pages and API routes. + +::: + +## How routing works + +EventCatalog uses [Astro](https://astro.build/) under the hood. Files in your catalog's top-level `pages` directory become routes in your generated catalog. + +By default, custom pages are served under `/custom`. You can change this prefix. + +| File | Route | +| --- | --- | +| `pages/reports.astro` | `/custom/reports` | +| `pages/reports/index.astro` | `/custom/reports` | +| `pages/reports/[id].astro` | `/custom/reports/[id]` | +| `pages/api/teams.ts` | `/custom/api/teams` | +| `pages/data.json.ts` | `/custom/data.json` | + +EventCatalog supports Astro-style dynamic route segments such as `[id]` and `[...slug]`. + +After you create a custom page, you can add it to your catalog navigation by customizing the [application sidebar](/docs/development/customization/application-sidebar). + +## Next steps + +- [Create a custom page](/docs/development/customization/custom-pages-and-api-routes/create-a-custom-page) +- [Create an API route](/docs/development/customization/custom-pages-and-api-routes/create-an-api-route) +- [Routing and configuration reference](/docs/development/customization/custom-pages-and-api-routes/reference) diff --git a/packages/core/docs/development/customization/05-custom-pages-and-api-routes/02-create-a-custom-page.md b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/02-create-a-custom-page.md new file mode 100644 index 000000000..867a287c4 --- /dev/null +++ b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/02-create-a-custom-page.md @@ -0,0 +1,177 @@ +--- +sidebar_position: 2 +keywords: + - EventCatalog custom pages + - Astro page + - custom routes +sidebar_label: Create a custom page +title: Create a custom page +description: Add a custom Astro page to your EventCatalog. +--- + +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + +This guide shows you how to add a custom page to your catalog. + +Use this when you want a new route for an internal report, dashboard, onboarding page, or catalog-specific workflow. The page can be static, built from catalog data, or use browser-side code to call API routes and other systems. + +## Create the page file + +Create a top-level `pages` directory and add an `.astro` file. + + + +```js title="pages/reports.astro" +--- +// Use the EventCatalog layout so your page keeps the catalog header and sidebar. +import Layout from '@catalog/layouts/Layout.astro'; + +// Use @catalog/utils to read catalog resources from your custom page. +import { getServices } from '@catalog/utils'; + +// getAllVersions: false returns the latest version of each service. +const services = await getServices({ getAllVersions: false }); +--- + + +
    +
    +

    Service reports

    +

    There are {services.length} services in this catalog.

    +
    + +
      + {services.map((service) => ( +
    • +

      {service.data.name}

      +

      {service.data.summary}

      +
    • + ))} +
    +
    +
    +``` + +## Run your catalog + +Start EventCatalog. + +```bash +npm run dev +``` + +Open `/custom/reports`. + +## Add the page to your navigation + +Custom pages are available by URL as soon as you create them. To make the page easier for users to find, add it to your application sidebar. + +Add a custom navigation item in `eventcatalog.config.js`. + +```js title="eventcatalog.config.js" +export default { + navigation: { + groups: [ + { + id: 'tools', + label: 'Tools', + items: [ + { + id: 'service-reports', + label: 'Service reports', + icon: 'ChartBar', + href: '/custom/reports', + match: ['/custom/reports'], + }, + ], + }, + ], + }, +}; +``` + +The `href` is the custom page route. The `match` value tells EventCatalog when to mark the navigation item as active. + +For more navigation options, see [Application sidebar](/docs/development/customization/application-sidebar). + +## Add a custom component + +You can reuse Astro components from your catalog's top-level `components` directory. + + + +```js title="components/ReportCard.astro" +--- +// Props are values passed into this component from the page that renders it. +const { title, description } = Astro.props; +--- + +
    +

    {title}

    +

    {description}

    +
    +``` + +Import the component into your custom page. + +```js title="pages/reports.astro" +--- +import Layout from '@catalog/layouts/Layout.astro'; + +// Components in your catalog's top-level components directory are available from @catalog/components. +import ReportCard from '@catalog/components/ReportCard.astro'; +--- + + + + +``` + +## Add a dynamic page + +Use Astro dynamic route segments when the route needs a parameter. + +```js title="pages/reports/[id].astro" +--- +import Layout from '@catalog/layouts/Layout.astro'; + +// Dynamic route parameters come from the filename. Here, [id].astro provides Astro.params.id. +const { id } = Astro.params; +--- + + +

    Report: {id}

    +
    +``` + +This page is available at `/custom/reports/:id`, for example `/custom/reports/orders`. + +## Learn more + +Custom pages follow Astro's routing model. See the [Astro pages documentation](https://docs.astro.build/en/basics/astro-pages/) if you want to go deeper. diff --git a/packages/core/docs/development/customization/05-custom-pages-and-api-routes/03-create-an-api-route.md b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/03-create-an-api-route.md new file mode 100644 index 000000000..f757c8b81 --- /dev/null +++ b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/03-create-an-api-route.md @@ -0,0 +1,128 @@ +--- +sidebar_position: 3 +keywords: + - EventCatalog API routes + - Astro endpoints + - server mode +sidebar_label: Create an API route +title: Create an API route +description: Add a custom API route to your EventCatalog. +--- + +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + +This guide shows you how to add a custom API route to your catalog. + +Use this when you want to expose catalog data, receive form submissions, or power a custom page with server-side logic. API routes are also useful when a custom page needs data from another internal system and you want that integration to live inside EventCatalog. + +## Enable server mode + +Custom API routes require EventCatalog to run in server mode when you build the catalog. + +Set `output: 'server'` in `eventcatalog.config.js`. + +```js title="eventcatalog.config.js" +export default { + output: 'server', +}; +``` + +## Create the endpoint file + +Create a file inside `pages/api`. + + + +```js title="pages/api/services.ts" +import type { APIRoute } from 'astro'; + +// Use @catalog/utils to read catalog resources from your API route. +import { getServices } from '@catalog/utils'; + +// Export a function named after the HTTP method this route should handle. +export const GET: APIRoute = async () => { + // getAllVersions: false returns the latest version of each service. + const services = await getServices({ getAllVersions: false }); + + return Response.json({ + services: services.map((service) => ({ + id: service.data.id, + name: service.data.name, + version: service.data.version, + })), + }); +}; +``` + +This route is served at `/custom/api/services`. + +## Add another HTTP method + +Astro calls exported functions that match the request method, such as `GET`, `POST`, or `DELETE`. + +```js title="pages/api/reviews.ts" +import type { APIRoute } from 'astro'; + +// This function handles POST requests sent to /custom/api/reviews. +export const POST: APIRoute = async ({ request }) => { + const body = await request.json(); + + return Response.json({ + status: 'received', + resource: body.resource, + }); +}; +``` + +This route receives `POST` requests at `/custom/api/reviews`. + +## Call an API route from a custom page + +You can call your API route from a custom page like any other endpoint. + +```js title="pages/reports.astro" +--- +// Use the EventCatalog layout so your page keeps the catalog header and sidebar. +import Layout from '@catalog/layouts/Layout.astro'; +--- + + + +
    
    +
    + + +``` + +## Learn more + +Custom API routes follow Astro's endpoint model. See the [Astro endpoints documentation](https://docs.astro.build/en/guides/endpoints/) if you want to go deeper. diff --git a/packages/core/docs/development/customization/05-custom-pages-and-api-routes/04-reference.md b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/04-reference.md new file mode 100644 index 000000000..8f3979e8d --- /dev/null +++ b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/04-reference.md @@ -0,0 +1,138 @@ +--- +sidebar_position: 4 +keywords: + - EventCatalog custom pages reference + - EventCatalog pages prefix + - custom routes reference +sidebar_label: Reference +title: Custom pages and API routes reference +description: Route, file, and configuration reference for custom pages and API routes. +--- + +## Route prefix + +Custom pages and API routes are served under `/custom` by default. + +Use `pages.prefix` to serve them somewhere else. + +```js title="eventcatalog.config.js" +export default { + pages: { + prefix: 'internal/tools', + }, +}; +``` + +With this configuration: + +| File | Route | +| --- | --- | +| `pages/reports.astro` | `/internal/tools/reports` | +| `pages/api/services.ts` | `/internal/tools/api/services` | + +The prefix must use URL-safe characters: letters, numbers, `-`, and `_`, optionally separated with `/`. + +The prefix cannot be empty. + +Some top-level routes are reserved by EventCatalog. If a configured prefix conflicts with a reserved route, EventCatalog will ask you to choose a different prefix. + +## Routable files + +Code files in the top-level `pages` directory are treated as custom routes. + +| Extension | Route type | +| --- | --- | +| `.astro` | Page | +| `.ts` | Endpoint | +| `.js` | Endpoint | +| `.mjs` | Endpoint | + +Other files in `pages`, such as images, are copied as static assets. For example, `pages/diagram.png` can be referenced from `/generated/pages/diagram.png`. + +## Route patterns + +| File | Default route | +| --- | --- | +| `pages/reports.astro` | `/custom/reports` | +| `pages/reports/index.astro` | `/custom/reports` | +| `pages/reports/[id].astro` | `/custom/reports/[id]` | +| `pages/docs/[...slug].astro` | `/custom/docs/[...slug]` | +| `pages/api/teams.ts` | `/custom/api/teams` | +| `pages/data.json.ts` | `/custom/data.json` | + +## Ignored files and folders + +Files or folders that start with `_` are not treated as routes. + +Use this for partials, helpers, and colocated components. + +| File | Result | +| --- | --- | +| `pages/_partial.astro` | Ignored | +| `pages/_components/Card.astro` | Ignored | +| `pages/reports/_helpers.ts` | Ignored | +| `pages/reports/index.astro` | `/custom/reports` | + +## Homepage exception + +`pages/homepage.astro` is rendered as the catalog homepage at `/`. + +It is not served as `/custom/homepage`. + +## Server mode + +Custom pages can be used in static or server output. + +Custom API routes require server mode for production builds. + +```js title="eventcatalog.config.js" +export default { + output: 'server', +}; +``` + +If EventCatalog finds API routes in `pages/api` during a static build, the build fails and asks you to enable server mode or remove the `pages/api` directory. + +## Import aliases + +Use these aliases in custom pages. + +| Alias | Description | +| --- | --- | +| `@catalog/layouts/Layout.astro` | Stable EventCatalog layout shell for custom pages. | +| `@catalog/utils` | Stable catalog data helpers such as `getServices`, `getDomains`, and `getEvents`. | +| `@catalog/components/*` | Components from your catalog's top-level `components` directory. | + +## `@catalog/utils` + +Use `@catalog/utils` to read catalog resources from custom pages and API routes. + +```js title="pages/reports.astro" +--- +import { getServices, getDomains } from '@catalog/utils'; + +const services = await getServices({ getAllVersions: false }); +const domains = await getDomains({ getAllVersions: false }); +--- +``` + +Getter functions return hydrated, cached collection entries. Pass `{ getAllVersions: false }` when you only want the latest version of each resource. + +| Helper | Returns | +| --- | --- | +| `getDomains` | Domains | +| `getServices` | Services | +| `getSystems` | Systems | +| `getEvents` | Events | +| `getCommands` | Commands | +| `getQueries` | Queries | +| `getFlows` | Flows | +| `getChannels` | Channels | +| `getEntities` | Entities | +| `getAgents` | Agents | +| `getContainers` | Containers | +| `getDataProducts` | Data products | +| `getAdrs` | Architecture decision records | +| `getTeams` | Teams | +| `getUsers` | Users | +| `getItemsFromCollectionByIdAndSemverOrLatest` | A specific version, semver range, or latest item from a collection | diff --git a/packages/core/docs/development/customization/05-custom-pages-and-api-routes/_category_.json b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/_category_.json new file mode 100644 index 000000000..4bc924722 --- /dev/null +++ b/packages/core/docs/development/customization/05-custom-pages-and-api-routes/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Custom pages and API routes", + "position": 5, + "collapsible": true, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "/development/customization/custom-pages-and-api-routes", + "title": "Custom pages and API routes", + "description": "Add custom pages, internal tools, dashboards, and API routes to your EventCatalog." + } +} diff --git a/packages/core/docs/development/customization/06-customize-tables.md b/packages/core/docs/development/customization/06-customize-tables.md new file mode 100644 index 000000000..ccaf90e52 --- /dev/null +++ b/packages/core/docs/development/customization/06-customize-tables.md @@ -0,0 +1,194 @@ +--- +sidebar_position: 5 +keywords: +- EventCatalog tables +sidebar_label: Tables +title: Customize tables +description: Customize tables in EventCatalog +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +EventCatalog allows you to customize the columns and names on the [Explore page](https://demo.eventcatalog.dev/discover/events), [Teams](https://demo.eventcatalog.dev/directory/teams) and [Users](https://demo.eventcatalog.dev/directory/users) pages. + +![EventCatalog Custom Tables](./img/table-example.png) + +### How to customize tables + +You can customize the tables in EventCatalog by configuring them in your `eventcatalog.config.js` file. + +Example of how to customize the tables for the events table page: + + +```js title="eventcatalog.config.js" +events: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + producers: { visible: true, label: 'Producers' }, + consumers: { visible: true, label: 'Consumers' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + }, +}, +``` +### Configuration + +List of available configuration options for the table columns: +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `visible` | boolean | true | Whether the column is visible. | +| `label` | string | - | The label to display for the column. | + +
    + Events, Queries and Commands Table + +The key property is either `events`, `queries`, `commands`. + +In this example we set the **events** table configuration. + +```js title="eventcatalog.config.js" +// ... other configuration ... +// change property to `events`, `queries`, `commands` +events: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + producers: { visible: true, label: 'Producers' }, + consumers: { visible: true, label: 'Consumers' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + }, + }, +} +// ... other configuration ... +``` +
    +
    + Services Table + +```js title="eventcatalog.config.js" +// ... other configuration ... +services: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + receives: { visible: true, label: 'Receives' }, + sends: { visible: true, label: 'Sends' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + }, +} +// ... other configuration ... +``` +
    +
    + Domains Table + +```js title="eventcatalog.config.js" +// ... other configuration ... +domains: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + services: { visible: true, label: 'Owners' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + }, +} +// ... other configuration ... +``` +
    +
    + Data Table + +```js title="eventcatalog.config.js" +// ... other configuration ... +containers: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + summary: { visible: true, label: 'Summary' }, + writes: { visible: true, label: 'Writes' }, + reads: { visible: true, label: 'Reads' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + }, +} +// ... other configuration ... +``` +
    +
    + Flows Table + +```js title="eventcatalog.config.js" +// ... other configuration ... +flows: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + version: { visible: true, label: 'Version' }, + summary: { visible: true, label: 'Summary' }, + badges: { visible: true, label: 'Badges' }, + actions: { visible: true, label: 'Actions' }, + } + }, +} +// ... other configuration ... +``` +
    +
    + Users Table + +```js title="eventcatalog.config.js" +// ... other configuration ... +users: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' }, + ownedEvents: { visible: true, label: 'Owned Events' }, + ownedCommands: { visible: true, label: 'Owned Commands' }, + ownedQueries: { visible: true, label: 'Owned Queries' }, + ownedServices: { visible: true, label: 'Owned Services' }, + teams: { visible: true, label: 'Teams' }, + actions: { visible: true, label: 'Actions' }, + } + }, +} +// ... other configuration ... +``` +
    +
    + Teams Table + +```js title="eventcatalog.config.js" +// ... other configuration ... +teams: { + tableConfiguration: { + columns: { + name: { visible: true, label: 'Name' },] + ownedEvents: { visible: true, label: 'Owned Events' }, + ownedCommands: { visible: true, label: 'Owned Commands' }, + ownedQueries: { visible: true, label: 'Owned Queries' }, + ownedServices: { visible: true, label: 'Owned Services' }, + actions: { visible: true, label: 'Actions' }, + } + }, +} +// ... other configuration ... +``` +
    + + + +You can read the `eventcatalog.config.js` API reference for [more information on the table configuration](/docs/api/config#domains). diff --git a/packages/core/docs/development/customization/07-resource-icons.md b/packages/core/docs/development/customization/07-resource-icons.md new file mode 100644 index 000000000..2b20f5252 --- /dev/null +++ b/packages/core/docs/development/customization/07-resource-icons.md @@ -0,0 +1,190 @@ +--- +sidebar_position: 7 +keywords: +- EventCatalog resource icons +- custom icons +- resource customization +sidebar_label: Resource icons +title: Resource icons +description: Customize the icons shown for resources in EventCatalog. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import ProjectTree from '@site/src/components/MDX/ProjectTree'; + + + +Resource icons let you replace the default icon EventCatalog uses for a resource. + +Use them when you want services, messages, data stores, agents, or other catalog resources to show the technology, platform, vendor, or domain-specific concept they represent. + +Resource icons can appear in places like: + +- Resource page headers +- Search and discover results +- Resource references +- Sidebar navigation +- Visualizer nodes + +## Set a resource icon + +Add `styles.icon` to the resource frontmatter. + +```md title="/services/Orders/index.mdx" +--- +id: Orders +name: Orders +version: 1.0.0 +summary: Handles order placement and order history. +styles: + icon: /icons/languages/nodejs.svg +--- +``` + +The icon value must be either: + +- a path to a file in your catalog's `public` folder +- an absolute `https://` or `http://` URL + +## Use icons from your catalog + +Put icon files in your catalog's `public` folder. + + + +Files in `public` are served from the root of your catalog, so `public/icons/languages/nodejs.svg` is referenced as `/icons/languages/nodejs.svg`. + +## Use external icon URLs + +You can also use an absolute URL. + +```md title="/services/StripePayments/index.mdx" +--- +id: StripePayments +name: Stripe Payments +version: 1.0.0 +summary: External payment provider used for card payments. +externalSystem: true +styles: + icon: https://cdn.simpleicons.org/stripe +--- +``` + +[Simple Icons CDN](https://cdn.simpleicons.org) is useful for common technology and vendor logos. + +## Examples + +### Service icon + +```md title="/services/Orders/index.mdx" +--- +id: Orders +name: Orders +version: 1.0.0 +styles: + icon: /icons/languages/java.svg +--- +``` + +### Data store icon + +```md title="/containers/orders-db/index.mdx" +--- +id: orders-db +name: Orders DB +version: 1.0.0 +container_type: database +technology: postgres@16 +styles: + icon: /icons/database/postgresql.svg +--- +``` + +### Message icon + +```md title="/events/OrderPlaced/index.mdx" +--- +id: OrderPlaced +name: Order Placed +version: 1.0.0 +summary: Raised when an order is placed. +styles: + icon: /icons/events/eventbridge.svg +--- +``` + +## Supported file types + +Use image files that browsers can render, such as: + +- SVG +- PNG +- WEBP + +SVG icons usually work best because they stay sharp at different sizes. + +## Resource icons vs sidebar icons + +Resource icons use file paths or URLs through `styles.icon`. + +```md +styles: + icon: /icons/languages/nodejs.svg +``` + +Application and documentation sidebar icons are different. Those use icon names from [Lucide](https://lucide.dev/icons/) in your sidebar configuration. + +```js title="eventcatalog.config.js" +export default { + navigation: { + groups: [ + { + id: 'main', + items: [{ id: 'docs', icon: 'BookOpen' }], + }, + ], + }, +}; +``` + +Use `styles.icon` when you want to customize a resource. Use sidebar icon names when you want to customize navigation. + +## Tips + +- Keep icons simple and readable at small sizes. +- Prefer square or near-square assets. +- Store shared icons under `public/icons`. +- Use consistent folder names, such as `/icons/languages`, `/icons/database`, or `/icons/tools`. +- Use external URLs only when you are comfortable depending on that external service at runtime. diff --git a/packages/core/docs/development/customization/_category_.json b/packages/core/docs/development/customization/_category_.json new file mode 100644 index 000000000..5cadd4d35 --- /dev/null +++ b/packages/core/docs/development/customization/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Customization", + "position": 8, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "development/customization", + "title": "Customization Documentation", + "description": "Learn how to customize EventCatalog." + } +} diff --git a/packages/core/docs/development/customization/customize-visualizer/00-visualizer-nodes.md b/packages/core/docs/development/customization/customize-visualizer/00-visualizer-nodes.md new file mode 100644 index 000000000..ca5d5d219 --- /dev/null +++ b/packages/core/docs/development/customization/customize-visualizer/00-visualizer-nodes.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog visualizer nodes +sidebar_label: Customize nodes +title: Customize visualizer nodes +description: Customize the color, label and icon of the visualizer nodes. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +Every node in the visualizer has a color, label and icon. + +The example below shows the default node for a **Service** resource. + + +![Visualizer Node](./img/customize-node.png) + +By default the icon, color and label in the node is generated by EventCatalog. Helping you maintain consistency across your catalog. + +You can use the `styles` property to customize the icon, color and label of the node, if you want to customize the node to be more specific to your use case. + +```md title="/domains/MyDomain/services/MyService.md" +--- +id: NotificationService +version: 0.0.2 +name: Notification Service +summary: | + Service that handles orders +styles: + icon: "BellIcon" + node: + color: purple + label: "Custom" +--- +``` + +#### Rendered output + +![Customized Node](./img/customize-node-example.png) + +### Configuration + +| ID | Required | Default | Description | +| --- | --- | --- | --- | +| `styles.icon` | No | Depends on resource type | The icon to use for the resource. Icons are from [hero icons](https://heroicons.com/).
    You can find a list of icons [here](https://unpkg.com/browse/@heroicons/react@2.1.5/24/outline/). | +| `styles.node.color` | No | Depends on resource type | The color to use for the node. These are tailwind colors and you can find a list of them [here](https://tailwindcss.com/docs/colors). | +| `styles.node.label` | No | Depends on resource type | The label to use for the node (side label). | \ No newline at end of file diff --git a/packages/core/docs/development/customization/customize-visualizer/_category_.json b/packages/core/docs/development/customization/customize-visualizer/_category_.json new file mode 100644 index 000000000..d357dd6e3 --- /dev/null +++ b/packages/core/docs/development/customization/customize-visualizer/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Customize Visualizer", + "position": 8, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "slug": "/customize-visualizer", + "description": "A collection of guides to help you customize the visualizer in your catalog." + } +} diff --git a/packages/core/docs/development/deployment/_category_.json b/packages/core/docs/development/deployment/_category_.json new file mode 100644 index 000000000..4cb9c783d --- /dev/null +++ b/packages/core/docs/development/deployment/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Deployment", + "position": 12, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "development/deployment", + "title": "Deployment guides", + "description": "This section contains deployment tutorials for developers. This includes tutorials for deploying to Heroku, AWS, and other cloud providers." + } +} diff --git a/packages/core/docs/development/deployment/build-and-deploy.md b/packages/core/docs/development/deployment/build-and-deploy.md new file mode 100644 index 000000000..b2849b087 --- /dev/null +++ b/packages/core/docs/development/deployment/build-and-deploy.md @@ -0,0 +1,71 @@ +--- +sidebar_position: 5 +keywords: +- build +- deploy +sidebar_label: Build (Static Mode) +title: Building Eventcatalog +description: This document describes step by step how to deploy EventCatalog. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +By default, EventCatalog exports a static HTML site. This means you can deploy your application anywhere you want. + +:::tip Have large or slow deployments? +Some users have large catalogs, and slow deployments. This is because the static mode builds the entire catalog into HTML files. + +If you have a large catalog you may want to use [SSR mode](/docs/development/deployment/build-ssr-mode), this will give you a server-side rendered application. +This reduces build times, and renders pages on the fly. +::: + + +## Building your EventCatalog (Static) + +To build your Catalog you will need to run: + +```bash +npm run build +``` + +This will output one directory + +- `dist` - Your EventCatalog as Static HTML + +### Passing custom options + + + +EventCatalog uses [Astro](https://astro.build/) to build the application. You can pass custom options to the build command by using the `--` prefix. + +```bash title="Passing custom options" +npx eventcatalog dev --debug -- --env=production --port=3000 +``` + +### Compression + +You can opt into our build step which will compress your static assets. + +You can enable this by setting the [compress option](/docs/api/config#compress) to `true` in your `eventcatalog.config.js` file. + +:::info "Why is compression disabled by default?" +Compression can increase your build time and the amount of memory required to build your catalog. + +If you want to enable this feature, you might also want to increase your build memory using the `max_old_space_size` value. +::: + +### Memory limits + +If you get any `JavaScript heap out of memory` errors, you can increase the memory limit by setting the `NODE_OPTIONS` environment variable. This gives astro more memory to work with. + +```bash title="Increasing the memory limit" +NODE_OPTIONS=--max_old_space_size=8196 npm run build +``` + +If you are still experiencing issues, you can try: + + +- Turning off HTML compression ([see documentation](/docs/api/config#compress)) +- Turning on MDX optimization ([see documentation](/docs/api/config#mdxOptimize)) +- Rendering EventCatalog in [SSR mode](/docs/development/deployment/build-ssr-mode). + - This requires you to run your EventCatalog on a server. But your pages are rendered on the fly, so you don't need to build the entire catalog into HTML files. This can also save you a lot of time when deploying your catalog. diff --git a/packages/core/docs/development/deployment/build-ssr-mode.md b/packages/core/docs/development/deployment/build-ssr-mode.md new file mode 100644 index 000000000..5650b968b --- /dev/null +++ b/packages/core/docs/development/deployment/build-ssr-mode.md @@ -0,0 +1,50 @@ +--- +sidebar_position: 5 +keywords: +- build +- deploy +sidebar_label: Build (SSR Mode) +title: Building Eventcatalog +description: This document describes step by step how to deploy EventCatalog. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +You can also use EventCatalog in SSR mode, which means you can use EventCatalog as a server-side rendered application. + +This can be useful for large catalogs, or for users with slow deployment times. + +Certain features like **Authentication** and **EventCatalog Chat** require SSR mode. + +### How it works + +Rather than building the entire catalog into HTML files, EventCatalog will render the pages on the fly (using server-side rendering). + +This means you can use EventCatalog as a server-side rendered application. + +## Building your EventCatalog (SSR) + +First you need to update your `eventcatalog.config.js` file to use SSR mode. + +```js title="eventcatalog.config.js" +export default { + // defaults to static + output: 'server', +} +``` + +Next you need to build your EventCatalog + +```bash +npm run build +``` + +This will output one directory + +- `dist` - Your EventCatalog as a SSR application + +## Deployment + +You will need to deploy your EventCatalog to a server that can run Node.js. + +The easiest way to do this is to use [a docker container](/docs/development/deployment/hosting-options#hosting-static-website-with-docker). \ No newline at end of file diff --git a/packages/core/docs/development/deployment/deployment-workflows.md b/packages/core/docs/development/deployment/deployment-workflows.md new file mode 100644 index 000000000..c43edcfd8 --- /dev/null +++ b/packages/core/docs/development/deployment/deployment-workflows.md @@ -0,0 +1,43 @@ +--- +sidebar_position: 5 +keywords: +- build +- deploy +sidebar_label: Deployment Workflows +title: Deployment Workflows +description: This document describes different deployment workflows for EventCatalog. +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + +Many people are deploying EventCatalog in different ways, as it's self hosted you can rebuild and redeploy your catalog as often as you want. + +### Keeping EventCatalog up to date + +Keeping documentation up to date is a challenge, as it's easy to forget to update the documentation when you make changes to your code. + +One way to keep your documentation up to date, is to redeploy your catalog whenever changes are made to your code, specifications or schemas. + +EventCatalog has companies redeploying their catalogs hundreds of times a day.... it really depends on how often you make changes to your code, specifications or schemas, and how often you want to update your documentation. **You are in control of how often you redeploy your catalog.** + +Many folks using EventCatalog have information scattered across multiple repositories, schema registries and other systems. + +Users of EventCatalog either use [our integrations (e.g AsyncAPI, OpenAPI, Schema Registry)](/integrations) or have built their own automated systems using our [SDK](/docs/sdk). + +### CI/CD workflow to keep your documentation up to date + +EventCatalog is docs-as-code solution. This means you can store EventCatalog next to your code and in git repositories. + +You can setup your CI/CD pipeline to build and deploy your catalog whenever changes are made to your code, specifications or schemas. + + +```mermaid LR +flowchart LR + A[Code changes in any repository] --> B[Trigger CI/CD pipeline to build and deploy catalog] + B --> C[EventCatalog is rebuilt and deployed] +``` + +EventCatalog is flexible. And you can redeploy your catalog in various ways. + +This can let you setup automation to ensure your documentation can stay up to date with any changes to your code, specifications or schemas. + diff --git a/packages/core/docs/development/deployment/hosting-options.md b/packages/core/docs/development/deployment/hosting-options.md new file mode 100644 index 000000000..97989342f --- /dev/null +++ b/packages/core/docs/development/deployment/hosting-options.md @@ -0,0 +1,111 @@ +--- +sidebar_position: 6 +keywords: +- build +- deploy +sidebar_label: Hosting +title: Hosting +description: This document describes hosting options for EventCatalog. +--- +import AddedIn from '@site/src/components/MDX/AddedIn'; + +### Hosting Options + +EventCatalog can be hosted in two ways: + +- [Hosting a static website output (default)](#hosting-a-static-website) +- [Hosting EventCatalog Server](#hosting-as-a-server) + +### Hosting a static website + +By default EventCatalog will build a static website. + +Here are some guides and places you can host static content + +- [Host with Docker](#hosting-with-docker) +- [Deploy to NextJS](https://nextjs.org/docs/deployment) +- [Host in AWS S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html) + +**Community posts** +- [Using AWS CDK to Deploy EventCatalog](https://matt.martz.codes/using-aws-cdk-to-deploy-eventcatalog) +- [Using Netlify to host Static Content](https://www.netlify.com/blog/2016/10/27/a-step-by-step-guide-deploying-a-static-site-or-single-page-app/) +- [How to create an EventCatlaog with Azure](https://www.kallemarjokorpi.fi/blog/how-to-create-and-event-catalog.html) +- [Autonomous EventCatalog for documenting EventBridge Events](https://medium.com/@wrennkieran/autonomous-eventcatalog-for-documenting-eventbridge-events-73e6334f2400) + +### Hosting static website with Docker + +EventCatalog comes with a DockerFile you can build the image and deploy the container. The container exposes ports `3000`. + +To build the docker container you need to run: + +```bash +# Builds the container +docker build -t eventcatalog . + +# Runs the container locally +docker run -p 3000:80 -it eventcatalog +``` + +### Hosting as a server + + + +First you need to update your `eventcatalog.config.js` file to use SSR mode. + +```js title="eventcatalog.config.js" +export default { + // defaults to static + output: 'server', +} +``` + +A server output is required if you are using any EventCatalog feature that requires a server, these include: + +- [EventCatalog Chat (bring your own keys)](/features/ai-assistant) +- [EventCatalog Authentication](/docs/development/authentication/introduction) + +You can use the server Docker image to run the server, this is the recommended way to run the server. + +First you need to create a Dockerfile for the server (if you don't already have one). + +```bash title="/Dockerfile.server" +FROM node:lts AS runtime +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json ./ +RUN npm install + +COPY . . + +# Fix for Astro in Docker: https://github.com/withastro/astro/issues/2596 +ENV NODE_OPTIONS=--max_old_space_size=2048 +RUN npm run build + +ENV HOST=0.0.0.0 +ENV PORT=3000 +EXPOSE 3000 + +# Start the server +CMD npm run start +``` + +Then you can build the docker image with: + +```bash +docker build -f Dockerfile.server -t eventcatalog-server . +``` + +Then you can run the server with: + +```bash +docker run -p 3000:3000 eventcatalog-server +``` + +:::info "Why do I need a server to run EventCatalog?" + +Some features of EventCatalog require a server to run (e.g. [EventCatalog Chat](/features/ai-assistant) and [EventCatalog Authentication](/docs/development/authentication/introduction)). + +If you have a large catalog, you may want to use [SSR mode](/docs/development/deployment/build-ssr-mode) to reduce build times. + +::: diff --git a/packages/core/docs/development/design/_category_.json b/packages/core/docs/development/design/_category_.json new file mode 100644 index 000000000..a0837162b --- /dev/null +++ b/packages/core/docs/development/design/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Designing with Studio", + "position": 30, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "design", + "title": "Design", + "description": "This section describes how to use EventCatalog to design workflows and new ideas." + } +} \ No newline at end of file diff --git a/packages/core/docs/development/design/embed-designs-into-eventcatalog.md b/packages/core/docs/development/design/embed-designs-into-eventcatalog.md new file mode 100644 index 000000000..1af52a278 --- /dev/null +++ b/packages/core/docs/development/design/embed-designs-into-eventcatalog.md @@ -0,0 +1,29 @@ +--- +sidebar_position: 4 +keywords: +- design +- studio +- resources +sidebar_label: Embed Designs into EventCatalog +title: Embed Designs into EventCatalog +description: Embed Designs into EventCatalog with EventCatalog +unlisted: true +--- + +You can embed any diagram directly into your documentation using the [``](/docs/development/components/components/design) component. + +![Embed into EventCatalog](/img/embed-ec.png) + +To embed your diagrams you need to: + +1. Save your diagram as a `.ecstudio` file +1. Put your `.ecstudio` file in the location of your markdown file in EventCatalog. +2. Use the [``](/docs/development/components/components/design) component in your markdown file to embed your diagram into your documentation + +:::info Embed diagrams anywhere + +Our vision to to allow users to be embed diagrams anywhere, letting them embed diagrams into their tools they already use. + +This is currently on our roadmap, and we will be adding more support for this in the future. + +::: diff --git a/packages/core/docs/development/design/further-reading.md b/packages/core/docs/development/design/further-reading.md new file mode 100644 index 000000000..f2e555e42 --- /dev/null +++ b/packages/core/docs/development/design/further-reading.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 5 +keywords: +- design +- studio +- resources +sidebar_label: Further Reading +title: Further Reading +description: Further Reading with EventCatalog +unlisted: true +--- + +If you want to learn more about EventCatalog Studio you can [read our documentation here](/docs/studio/getting-started) or click on the links below to learn more. + + + +- [How to add nodes to your diagram](/docs/studio/diagrams/using-nodes) +- [Adding comments to your designs](/docs/studio/diagrams/adding-comments) +- [Import & Export your designs](/docs/studio/diagrams/import-export) diff --git a/packages/core/docs/development/design/import-resources.md b/packages/core/docs/development/design/import-resources.md new file mode 100644 index 000000000..7dbca7651 --- /dev/null +++ b/packages/core/docs/development/design/import-resources.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 3 +keywords: +- design +- studio +- resources +sidebar_label: Import Resources into Studio +title: Import Resources into Studio +description: Import Resources into Studio with EventCatalog +unlisted: true +--- + +To import your resources into EventCatalog Studio, click on the **EventCatalog Studio** button in the navigation bar, then click on the **Open EventCatalog Studio** button. + +![Import Resources](./img/studio.png) + +Then click on **Copy resources to clipboard** to copy your architecture primitives to the clipboard, then click on **Open EventCatalog Studio** to open the drag and drop editor. + +Copy Resources + +Then you can paste your resources into the drag and drop editor, and click **Import resources** to import your resources into the drag and drop editor. + +![Import Resources](./img/paste.png) + +Once you have imported your resources, you can start to use them to create new designs or workflows for your architecture. + +![Import Resources](./img/example.png) diff --git a/packages/core/docs/development/design/intro.md b/packages/core/docs/development/design/intro.md new file mode 100644 index 000000000..0cc599b85 --- /dev/null +++ b/packages/core/docs/development/design/intro.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 2 +keywords: +- components +sidebar_label: Documentation to Design +title: Documentation to Design +description: Documentation to Design with EventCatalog +unlisted: true +--- + +When you document your architecture with EventCatalog, you can use your architecture primitives (your domains, services, messages, channels, schemas, etc) to create designs of business workflows, and explore new ideas with your teams using our drag and drop editor [(EventCatalog Studio)](http://eventcatalog.studio/). + +This can be useful if you want to share new ideas with your teams, or document end to end business workflows in your organization using a drag and drop interface. +This means your developers, architects and business stakeholders can all work together to create workflows or designs together and embed them back into your documentation. + +![Design workflows](/img/studio-app-2.png) + +### How EventCatalog Studio works + +EventCatalog Studio is a drag and drop editor that lets you create designs (.ecstudio files) with your architecture primitives. + +You can embed these designs back into your documentation using the [`` component](/docs/development/components/components/design). diff --git a/packages/core/docs/development/developer-tools/_category_.json b/packages/core/docs/development/developer-tools/_category_.json new file mode 100644 index 000000000..a11f5d060 --- /dev/null +++ b/packages/core/docs/development/developer-tools/_category_.json @@ -0,0 +1,12 @@ +{ + "label": "Developer tools", + "position": 11, + "collapsible": false, + "collapsed": false, + "link": { + "type": "generated-index", + "slug": "developer-tools", + "title": "Developer tools", + "description": "Developer tools for EventCatalog." + } +} diff --git a/packages/core/docs/development/developer-tools/api-catalog.md b/packages/core/docs/development/developer-tools/api-catalog.md new file mode 100644 index 000000000..4a46c1f95 --- /dev/null +++ b/packages/core/docs/development/developer-tools/api-catalog.md @@ -0,0 +1,114 @@ +--- +sidebar_position: 5 +keywords: +- api-catalog +- RFC 9727 +- well-known +- API discovery +- OpenAPI +- AsyncAPI +- GraphQL +sidebar_label: api-catalog (RFC 9727) +title: api-catalog +description: Machine-readable catalog discovery endpoint for tools and agents +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; + + + +Let API tools, agents, and crawlers discover every service and domain specification in your catalog from a single endpoint, without parsing HTML. + +### What is RFC 9727? + +[RFC 9727](https://datatracker.ietf.org/doc/rfc9727/) defines the `/.well-known/api-catalog` well-known URI. It returns a [Linkset](https://www.rfc-editor.org/rfc/rfc9264) document that lists every API an organization publishes along with links to their specifications and documentation. + +Tools that understand RFC 9727 can point at your catalog URL and immediately enumerate all services and domains, their OpenAPI, AsyncAPI, and GraphQL specs, and their documentation pages. No scraping required. + +### How it works + +EventCatalog automatically publishes a Linkset at `/.well-known/api-catalog`. Every service and domain that has `specifications` defined in its frontmatter appears as an entry. + +Each entry contains: + +- **`anchor`** - the canonical URL of the service. EventCatalog reads the `servers[].url` field from OpenAPI or AsyncAPI specs and uses that. When no server URL is found it falls back to the EventCatalog documentation page. +- **`service-desc`** - one link per specification file, pointing at `/api-catalog/specifications/{collection}/{id}/{version}/{specification}` with the correct media type (`application/yaml`, `application/json`, or `application/graphql`). +- **`service-doc`** - two links per resource: the markdown source and the rendered HTML page. + +Resources marked `hidden: true` are excluded from the linkset. + +### Access the endpoint + +``` +GET /.well-known/api-catalog +HEAD /.well-known/api-catalog +``` + +The `GET` response body is `application/linkset+json` profiled against RFC 9727: + +```json +{ + "linkset": [ + { + "anchor": "https://api.example.com/orders", + "service-desc": [ + { + "href": "https://catalog.example.com/api-catalog/specifications/services/OrderService/1.0.0/openapi-b3BlbmFwaS55bWw", + "type": "application/yaml", + "title": "Order Service OpenAPI" + } + ], + "service-doc": [ + { + "href": "https://catalog.example.com/docs/services/OrderService/1.0.0.md", + "type": "text/markdown", + "title": "Order Service documentation" + }, + { + "href": "https://catalog.example.com/docs/services/OrderService/1.0.0", + "type": "text/html", + "title": "Order Service documentation" + } + ] + } + ] +} +``` + +The `HEAD` response includes a `Link` header so clients can confirm the endpoint exists before fetching the full body: + +``` +Link: ; rel="api-catalog" +``` + +### Fetch a specification file + +The raw specification files referenced in `service-desc` are served from: + +``` +GET /api-catalog/specifications/{collection}/{id}/{version}/{specification} +``` + +| Segment | Values | +|---------|--------| +| `collection` | `services`, `domains` | +| `id` | The resource `id` field | +| `version` | The resource `version` field | +| `specification` | Stable specification identifier, formatted as `{type}-{base64url(path)}` | + +Example: + +``` +GET /api-catalog/specifications/services/OrderService/1.0.0/openapi-b3BlbmFwaS55bWw +Content-Type: application/yaml +``` + +### MCP server entry + +When the EventCatalog MCP server is enabled, an additional entry pointing at `/docs/mcp` is appended to the linkset. This lets MCP-aware agents discover the catalog's machine interface alongside its API specifications. + +### What is included + +Only services and domains are included in v1 of this endpoint. Events, commands, queries, data products, schemas, diagrams, teams, and users are out of scope for this release. + +For a broader machine-readable index of your catalog content, see [llms.txt](/docs/development/ask-your-architecture/llms.txt) and [schemas.txt](/docs/development/ask-your-architecture/schemas.txt). diff --git a/packages/core/docs/development/developer-tools/eventcatalog-linter.md b/packages/core/docs/development/developer-tools/eventcatalog-linter.md new file mode 100644 index 000000000..7425be3bd --- /dev/null +++ b/packages/core/docs/development/developer-tools/eventcatalog-linter.md @@ -0,0 +1,732 @@ +--- +sidebar_position: 1 +keywords: +- EventCatalog linter +- schema validation +- reference validation +- CI/CD +- quality assurance +sidebar_label: EventCatalog Linter +title: EventCatalog Linter +description: Validate your EventCatalog frontmatter schemas and resource references with the comprehensive EventCatalog Linter +--- + +import AddedIn from '@site/src/components/MDX/AddedIn'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +A comprehensive linter for EventCatalog that validates frontmatter schemas and resource references to ensure your architecture documentation is correct and consistent. + +The EventCatalog Linter helps you catch issues early in your development process, ensuring your documentation maintains high quality and accuracy across all your EventCatalog resources. + +## Use cases + +- **Quality Assurance**: Ensure your documentation is correct and consistent +- **CI/CD**: Integrate the linter into your CI/CD pipeline to catch issues early +- **Documentation**: Run the linter regularly as part of your development workflow +- **Version Consistency**: Use consistent version patterns across your EventCatalog resources + +## Features + +- **📋 Schema Validation**: Validates all resource frontmatter against defined schemas using Zod +- **🔗 Reference Validation**: Ensures all referenced resources (services, events, domains, etc.) actually exist +- **📦 Semver Version Support**: Supports semantic versions, ranges (`^1.0.0`, `~1.2.0`), x-patterns (`0.0.x`), and `latest` +- **⚙️ Configurable Rules**: Optional `.eventcatalogrc.js` config file for customizing rule severity and behavior +- **🚫 Ignore Patterns**: Skip validation for specific file patterns (archived, drafts, etc.) +- **🎯 Rule Overrides**: Apply different rules to different file patterns for flexible team workflows +- **🎯 Comprehensive Coverage**: Supports all EventCatalog resource types +- **⚡ Fast Performance**: Efficiently scans large catalogs +- **🎨 ESLint-Inspired Output**: Clean, file-grouped error reporting with severity levels +- **⚠️ Warnings Support**: Distinguish between errors and warnings with `--fail-on-warning` option + +### Supported Resource Types + +The linter validates all EventCatalog resource types: + +- 🏢 **Domains** (including subdomains) +- ⚙️ **Services** +- 📨 **Events** +- 📤 **Commands** +- ❓ **Queries** +- 📡 **Channels** +- 🔄 **Flows** +- 📊 **Entities** +- 🤖 **Agents** +- 🧱 **Containers** (including the legacy data store alias) +- 📈 **Data Products** +- 🧭 **Diagrams** +- 📝 **ADRs** +- 👤 **Users** +- 👥 **Teams** + + + +Agents, Containers, Data Products, Diagrams, and ADRs are validated as of version 1.2.0. + +## Quick Start + +Get started with the EventCatalog Linter in three simple steps: + +### 1. Run the Linter + +Start linting your EventCatalog immediately with npx: + +```bash +npx @eventcatalog/linter +``` + +### 2. Configure Rules (Optional) + +Create a `.eventcatalogrc.js` file in your EventCatalog root to customize validation: + +```javascript +module.exports = { + rules: { + 'best-practices/summary-required': 'warn', + 'refs/owner-exists': 'error' + }, + ignorePatterns: ['**/drafts/**'] +}; +``` + +### 3. Integrate with CI/CD + +Add to your CI/CD pipeline for automated validation: + +```yaml +# GitHub Actions +- run: npx @eventcatalog/linter --fail-on-warning +``` + +## Installation + +### Use with npx (Recommended) + + + +```bash +npx @eventcatalog/linter +``` + + +```bash +pnpm dlx @eventcatalog/linter +``` + + + +### Global Installation + + + +```bash +npm install -g @eventcatalog/linter +``` + + +```bash +pnpm install -g @eventcatalog/linter +``` + + + +### Add to your project + + + +```bash +npm install --save-dev @eventcatalog/linter +``` + + +```bash +pnpm install --save-dev @eventcatalog/linter +``` + + + +## Usage + +### Basic Usage + +Run the linter in your EventCatalog directory: + +```js +// Lint current directory +eventcatalog-linter + +// Lint specific directory +eventcatalog-linter ./my-eventcatalog + +// Verbose output with detailed information +eventcatalog-linter --verbose + +// Show help +eventcatalog-linter --help +``` + +### CLI Options + +``` +Usage: eventcatalog-linter [options] [directory] + +Arguments: + directory EventCatalog directory to lint (default: ".") + +Options: + -V, --version output the version number + -v, --verbose Show verbose output (default: false) + --fail-on-warning Exit with non-zero code on warnings (default: false) + -h, --help display help for command +``` + +## Configuration + +The EventCatalog Linter supports optional configuration through a `.eventcatalogrc.js` file in your catalog root directory. This allows you to: + +- Turn rules on/off +- Configure rule severity levels (error, warn, off) +- Ignore specific file patterns +- Override rules for specific file patterns + +### Quick Start with Configuration + +Create a `.eventcatalogrc.js` file in your EventCatalog root directory: + +```javascript +// .eventcatalogrc.js +module.exports = { + rules: { + // Schema validation rules + 'schema/required-fields': 'error', + 'schema/valid-semver': 'error', + 'schema/valid-email': 'warn', + + // Reference validation rules + 'refs/owner-exists': 'error', + 'refs/valid-version-range': 'error', + + // Best practice rules + 'best-practices/summary-required': 'warn', + 'best-practices/owner-required': 'error', + }, + + // Ignore certain paths + ignorePatterns: ['**/archived/**', '**/drafts/**'], + + // Override rules for specific file patterns + overrides: [ + { + files: ['**/experimental/**'], + rules: { + 'best-practices/owner-required': 'off' + } + } + ] +}; +``` + +### Rule Severity Levels + +- **`'error'`** - Causes the linter to exit with error code 1 +- **`'warn'`** - Shows warnings but allows the linter to pass (unless `--fail-on-warning` is used) +- **`'off'`** - Disables the rule completely + +### Available Rules + +| Rule Name | Description | Accepted Values | Default | +|-----------|-------------|-----------------|----------| +| **Schema Validation Rules** | +| `schema/required-fields` | Validates that required fields are present in frontmatter | `error`, `warn`, `off` | `error` | +| `schema/valid-type` | Validates that field types are correct (strings, arrays, objects) | `error`, `warn`, `off` | `error` | +| `schema/valid-semver` | Validates semantic version format (1.0.0, 2.1.3-beta) | `error`, `warn`, `off` | `error` | +| `schema/valid-email` | Validates email address format in user frontmatter | `error`, `warn`, `off` | `error` | +| `schema/validation-error` | General schema validation errors | `error`, `warn`, `off` | `error` | +| **Reference Validation Rules** | +| `refs/owner-exists` | Ensures referenced owners (users/teams) exist | `error`, `warn`, `off` | `error` | +| `refs/valid-version-range` | Validates version references and patterns | `error`, `warn`, `off` | `error` | +| `refs/resource-exists` | Ensures referenced resources exist (always enabled for critical resources) | Always enabled | Always enabled | +| **Best Practice Rules** | +| `best-practices/summary-required` | Requires summary field for better documentation | `error`, `warn`, `off` | `error` | +| `best-practices/owner-required` | Requires at least one owner for accountability | `error`, `warn`, `off` | `error` | + +:::info Core Resource Validation +Core resource reference validation (services, domains, entities) is always enabled and cannot be disabled, ensuring referential integrity of your EventCatalog. +::: + +### Configuration Examples + +#### Relaxed Configuration for Development + +```javascript +module.exports = { + rules: { + 'best-practices/summary-required': 'warn', + 'best-practices/owner-required': 'warn', + 'refs/owner-exists': 'warn', + }, + ignorePatterns: ['**/drafts/**', '**/experimental/**'] +}; +``` + +#### Strict Configuration for Production + +```javascript +module.exports = { + rules: { + 'schema/required-fields': 'error', + 'refs/owner-exists': 'error', + 'best-practices/summary-required': 'error', + 'best-practices/owner-required': 'error', + } +}; +``` + +#### Team-Specific Overrides + +```javascript +module.exports = { + rules: { + 'best-practices/owner-required': 'error', + 'best-practices/summary-required': 'error', + }, + overrides: [ + { + files: ['**/legacy/**'], + rules: { + 'best-practices/owner-required': 'warn', + 'best-practices/summary-required': 'off' + } + }, + { + files: ['**/critical/**'], + rules: { + 'best-practices/summary-required': 'error', + 'refs/owner-exists': 'error' + } + } + ] +}; +``` + +### Using with CI/CD + +The configuration file allows you to have different validation rules for different environments: + +```bash +# Development - warnings allowed +npx @eventcatalog/linter + +# Production - fail on warnings +npx @eventcatalog/linter --fail-on-warning +``` + +### Default Behavior + +If no `.eventcatalogrc.js` file is found, the linter uses the default rules listed above. Most validations are errors by default, while documentation quality checks such as orphan messages, missing descriptions, missing schemas, and deprecated references default to warnings. + +### Package.json Integration + +Add to your `package.json` scripts: + +```json +{ + "scripts": { + "lint:eventcatalog": "eventcatalog-linter", + "lint:eventcatalog:verbose": "eventcatalog-linter --verbose" + } +} +``` + +## What It Validates + +### Frontmatter Schema Validation + +- ✅ Required fields are present (`id`, `name`, `version`) +- ✅ Field types are correct (strings, arrays, objects) +- ✅ Semantic versions follow proper format (`1.0.0`, `2.1.3-beta`) +- ✅ Version patterns supported (`latest`, `^1.0.0`, `~1.2.0`, `0.0.x`) +- ✅ URLs are valid format +- ✅ Email addresses are valid format +- ✅ Enum values are from allowed lists +- ✅ Nested object structures are correct +- ✅ Common resource configuration is supported, including `attachments`, `editUrl`, `diagrams`, `detailsPanel`, sidebar colors, and GraphQL specifications + +### Reference Validation + +- ✅ Services referenced in domains exist +- ✅ Agents, data products, flows, entities, and subdomains referenced in domains exist +- ✅ Events/Commands/Queries referenced in services exist +- ✅ Events/Commands/Queries referenced in agents exist +- ✅ Data product inputs and outputs reference existing resources +- ✅ ADR relationships and typed `appliesTo` references exist +- ✅ Entities referenced in domains/services exist +- ✅ Channels referenced in sends/receives `to`/`from`, routes, messages, and message channels exist +- ✅ Containers referenced in `writesTo`/`readsFrom` exist +- ✅ Diagrams referenced from other resources exist +- ✅ Users/Teams referenced as owners exist +- ✅ User/team owned resources and team members exist +- ✅ Flow steps reference existing services/messages/agents/containers/data products +- ✅ Entity properties reference existing entities +- ✅ Version-specific references are valid + + + +The expanded reference checks for agents, data products, ADRs, diagrams, and flow steps were added in version 1.2.0. + +## Example Output + +### ✅ Success Output + +```bash +$ eventcatalog-linter + +✔ No problems found! + + 42 files checked +``` + +### ❌ Error Output + +```bash +$ eventcatalog-linter + +services/user-service/index.mdx + ✖ error version: Invalid semantic version format [version] (schema/valid-semver) + ⚠ warning Summary is required for better documentation [summary] (best-practices/summary-required) + +✖ 2 problems + +domains/sales/index.mdx + ✖ error Referenced service "order-service" does not exist [services] (refs/resource-exists) + +✖ 1 problem + +flows/user-registration/index.mdx + ✖ error Referenced service "notification-service" (version: 2.0.0) does not exist [steps[1].service] (refs/valid-version-range) + +✖ 1 problem + +✖ 4 problems (3 errors, 1 warning) + 3 files checked +``` + +## Version Pattern Support + +The linter supports flexible version patterns for resource references: + +### Exact Versions + +```yaml +sends: + - id: user-created + version: 1.0.0 # Exact semantic version +``` + +### Latest Version + +```yaml +sends: + - id: user-created + version: latest # Always use the latest available version +``` + +### Semver Ranges + +```yaml +sends: + - id: user-created + version: ^1.0.0 # Compatible with 1.x.x (1.0.0, 1.2.3, but not 2.0.0) + - id: user-updated + version: ~1.2.0 # Compatible with 1.2.x (1.2.0, 1.2.5, but not 1.3.0) +``` + +### X-Pattern Matching + +```yaml +sends: + - id: user-created + version: 0.0.x # Matches 0.0.1, 0.0.5, 0.0.12, etc. + - id: order-placed + version: 1.x # Matches 1.0.0, 1.5.3, 1.99.0, etc. +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: EventCatalog Lint +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '18' + - run: npx @eventcatalog/linter +``` + +### GitLab CI + +```yaml +eventcatalog-lint: + stage: test + image: node:18 + script: + - npx @eventcatalog/linter +``` + +## Rule Names and Error Codes + +The linter provides descriptive rule names in parentheses to help identify and fix issues quickly. Each error shows the specific rule that was violated: + +### Schema Validation Rules + +- `(schema/required-fields)` - Required field is missing +- `(schema/valid-type)` - Field has wrong data type +- `(schema/valid-semver)` - Invalid semantic version format +- `(schema/valid-email)` - Invalid email address format +- `(schema/validation-error)` - General schema validation error + +### Reference Validation Rules + +- `(refs/owner-exists)` - Referenced owner (user/team) doesn't exist +- `(refs/valid-version-range)` - Referenced version doesn't exist or invalid pattern +- `(refs/resource-exists)` - Referenced resource doesn't exist +- `(refs/channel-exists)` - Referenced channel in sends/receives `to`/`from`, routes, messages, or message channels doesn't exist +- `(refs/container-exists)` - Referenced container in `writesTo`/`readsFrom` or flow steps doesn't exist +- `(refs/orphan-messages)` - Event/command/query has no producer and no consumer + +### Best Practice Rules + +- `(best-practices/summary-required)` - Summary field is missing +- `(best-practices/owner-required)` - At least one owner is required + +### Parse Errors + +- `(@eventcatalog/parse-error)` - YAML/frontmatter parsing error + +## Warnings Support + +The linter distinguishes between errors (critical issues) and warnings (suggestions for improvement): + +- **Errors**: Critical issues that must be fixed +- **Warnings**: Suggestions for better documentation + +Use `--fail-on-warning` to treat warnings as errors in CI/CD pipelines: + +```bash +# Exit with error code if warnings are found +eventcatalog-linter --fail-on-warning +``` + +## Common Validation Examples + +### Valid Frontmatter Examples + +#### Service + +```yaml +--- +id: user-service +name: User Service +version: 2.1.0 +summary: Manages user accounts and authentication +owners: + - platform-team + - john-doe +sends: + - id: user-created + version: 1.0.0 + - id: user-updated +receives: + - id: create-user + - id: update-user +entities: + - id: user +repository: + language: TypeScript + url: https://github.com/company/user-service +--- +``` + +#### Event + +```yaml +--- +id: user-created +name: User Created +version: 1.0.0 +summary: Triggered when a new user account is created +owners: + - platform-team +sidebar: + badge: POST + label: User Events +draft: false +deprecated: false +--- +``` + +#### Agent + + + +```yaml +--- +id: refund-agent +name: Refund Agent +version: 1.0.0 +summary: Coordinates customer refund decisions +owners: + - platform-team +receives: + - id: refund-requested + from: + - id: refunds +sends: + - id: refund-approved + to: + - id: public-events/orders +readsFrom: + - id: orders-db +model: + provider: OpenAI + name: gpt-4.1-mini +tools: + - name: Payment lookup + type: mcp +--- +``` + +#### Container + + + +```yaml +--- +id: orders-db +name: Orders DB +version: 1.0.0 +summary: Stores order records +container_type: database +technology: PostgreSQL +classification: internal +access_mode: readWrite +authoritative: true +--- +``` + +#### Data product + + + +```yaml +--- +id: refund-analytics +name: Refund Analytics +version: 1.0.0 +summary: Curated refund metrics for finance teams +inputs: + - id: refund-approved + - id: orders-db +outputs: + - id: public-events/orders + contract: + path: contracts/refund-analytics.json + name: Refund Analytics Contract +--- +``` + +#### ADR + + + +```yaml +--- +id: adr-001 +name: Use event-driven refunds +version: 1.0.0 +summary: Records the decision to coordinate refunds through events +status: accepted +date: 2026-05-26 +decisionMakers: + - id: platform-team + collection: teams +appliesTo: + - type: service + id: order-service + - type: data-product + id: refund-analytics +related: + - id: adr-000 +--- +``` + +#### Diagram + + + +```yaml +--- +id: refund-flow +name: Refund Flow Diagram +version: 1.0.0 +summary: Shows the refund workflow across services and agents +owners: + - platform-team +--- +``` + +### Common Validation Errors + +#### ❌ Missing Required Fields + +```yaml +--- +# Missing 'id' field +name: User Service +version: 1.0.0 +--- +``` + +#### ❌ Invalid Semantic Version + +```yaml +--- +id: user-service +name: User Service +version: v1.0 # Should be 1.0.0 +--- +``` + +#### ❌ Invalid Reference + +```yaml +--- +id: sales-domain +name: Sales Domain +version: 1.0.0 +services: + - id: non-existent-service # Service doesn't exist +--- +``` + +## Best Practices + +1. **Start with Configuration**: Create a `.eventcatalogrc.js` file to customize rules for your team's workflow +2. **Run in CI/CD**: Integrate the linter into your CI/CD pipeline to catch issues early +3. **Use `--fail-on-warning`**: Consider treating warnings as errors in production environments +4. **Regular Validation**: Run the linter regularly as part of your development workflow +5. **Fix Issues Promptly**: Address validation errors immediately to maintain documentation quality +6. **Version Consistency**: Use consistent version patterns across your EventCatalog resources +7. **Team Overrides**: Use file pattern overrides for different validation requirements across teams +8. **Ignore Patterns**: Use ignore patterns for draft or experimental content that shouldn't be validated yet + +## Issues? + +If you have any issues or feedback, please let us know by opening an issue on [GitHub](https://github.com/event-catalog/eventcatalog/issues) or joining our [Discord server](https://eventcatalog.dev/discord). \ No newline at end of file diff --git a/packages/core/docs/development/governance/_category_.json b/packages/core/docs/development/governance/_category_.json new file mode 100644 index 000000000..17602dbbf --- /dev/null +++ b/packages/core/docs/development/governance/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Governance Patterns", + "position": 9, + "className": "hidden", + "collapsible": true, + "collapsed": false +} diff --git a/packages/core/docs/development/governance/architecture-change-detection/01-introduction.md b/packages/core/docs/development/governance/architecture-change-detection/01-introduction.md new file mode 100644 index 000000000..f038e2c74 --- /dev/null +++ b/packages/core/docs/development/governance/architecture-change-detection/01-introduction.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 1 +sidebar_label: Introduction +title: Architecture Change Detection +description: Know when your event-driven architecture changes. Get notified when services start or stop producing and consuming messages. +--- + +import PlanBanner from '@site/src/components/MDX/PlanBanner'; + + + +Services get added. Consumers change. Producers disappear. In event-driven systems, these relationship changes often go unnoticed until something breaks downstream. + +**Architecture Change Detection** compares your catalog across branches and tells you exactly what changed: which services started consuming a message, which stopped producing one, and who is affected. + +You can configure webhooks to send notifications to tools like Slack or PagerDuty when changes are detected, including relationship changes, deprecations, and schema updates. Rules can also be configured to block CI/CD pipelines entirely using the `fail` action. + +:::info Scale plan required +Architecture Change Detection requires an [EventCatalog Scale plan](https://eventcatalog.dev/pricing). +::: + +## How it works + +1. Define rules in `governance.yaml` at the root of your catalog +2. Each rule specifies **triggers** (what kind of change to detect), **resources** (which services or messages to watch), and **actions** (how to get notified) +3. Run `eventcatalog governance check` to compare your catalog against a base branch +4. Matched changes print to the console, fire webhook notifications, and optionally exit with a non-zero code to block merges + +```mermaid +flowchart LR + A[governance.yaml] --> B[eventcatalog governance check] + B --> C{Compare branches} + C --> D[Detect changes] + D --> E[Console output] + D --> F[Webhook notifications] + D --> G[Fail action — exit code 1] +``` + +## Quick example + +```yaml title="governance.yaml" +rules: + - name: notify-consumer-changes + when: + - consumer_added + - consumer_removed + resources: + - "*" + actions: + - type: console + - type: webhook + url: $SLACK_WEBHOOK_URL +``` + +This rule detects whenever any service starts or stops consuming any message, and sends a notification to Slack. + +## Notifications when you want them + +Using the [`status` field](/docs/development/governance/architecture-change-detection/ci-cd#include-a-status-label) you can choose to detect changes that are proposed vs approved. This can be useful when you want to detect changes that are proposed vs approved in your architecture. + + + diff --git a/packages/core/docs/development/governance/architecture-change-detection/02-configuration.md b/packages/core/docs/development/governance/architecture-change-detection/02-configuration.md new file mode 100644 index 000000000..1531183e1 --- /dev/null +++ b/packages/core/docs/development/governance/architecture-change-detection/02-configuration.md @@ -0,0 +1,134 @@ +--- +sidebar_position: 2 +sidebar_label: Configuration +title: Configuration +description: Configure governance rules with triggers, resource filters, and actions +--- + +## Create a config file + +Add a `governance.yaml` file to the root of your catalog directory. Each rule has a name, a list of triggers, a resource filter, and one or more actions. + +```yaml title="governance.yaml" +rules: + # Name of your rule + - name: notify-consumer-changes + # List of triggers + when: + - consumer_added + - consumer_removed + # Configure which resources to watch + # * is everything + resources: + - "*" + # List of actions to perform + actions: + # Print to the console + - type: console + # Send a webhook to a Slack or PagerDuty webhook + - type: webhook + url: $SLACK_WEBHOOK_URL + headers: + Authorization: Bearer $API_TOKEN +``` + +## Triggers + +The `when` field lists the events that activate a rule. Relationship triggers compare the target state against the base branch. The `message_deprecated` and `schema_changed` triggers inspect the target branch directly. + +| Trigger | Description | +|---|---| +| `consumer_added` | A service starts receiving a message | +| `consumer_removed` | A service stops receiving a message | +| `producer_added` | A service starts sending a message | +| `producer_removed` | A service stops sending a message | +| `message_deprecated` | A producing service marks a message as deprecated | +| `schema_changed` | A message schema file is added, modified, or replaced | + +A single rule can listen to multiple triggers: + +```yaml +when: + - consumer_added + - consumer_removed +``` + +## Resource filters + +The `resources` field controls which changes are checked against the rule. Each entry uses a prefix to specify what to match. + +| Prefix | Example | Matches | +|---|---|---| +| `"*"` | `"*"` | All changes across the catalog | +| `"message:"` | `"message:OrderCreated"` | Changes involving a specific message | +| `"service:"` | `"service:NotificationService"` | Changes where the named service is the consumer or producer | +| `"produces:"` | `"produces:OrdersService"` | Changes involving any message that the named service sends | +| `"consumes:"` | `"consumes:PaymentService"` | Changes involving any message that the named service receives. For `message_deprecated` and `schema_changed`, matches messages consumed by the named service | + +You can combine multiple filters in one rule. A change matches if it matches **any** entry: + +```yaml +resources: + - message:OrderCreated + - message:OrderConfirmed + - service:NotificationService +``` + +## Actions + +Each rule can trigger one or more actions when its conditions are met. + +### Console + +Prints a summary of matched changes to the terminal. No additional configuration required. + +```yaml +actions: + - type: console +``` + +### Webhook + +Sends a POST request in [CloudEvents 1.0](https://cloudevents.io/) format to the specified URL. Custom headers are supported. + +_You can use [Webhook Sites](https://webhook.site) to test your webhook URLs. Don't share any sensitive information in the payload._ + +```yaml +actions: + - type: webhook + url: $SLACK_WEBHOOK_URL + headers: + Authorization: Bearer $API_TOKEN +``` + +See [Webhook Payload](/docs/development/governance/architecture-change-detection/webhooks) for the full payload format. + +### Fail + +Exits the CLI with code `1`, failing the CI/CD step and blocking the pipeline. An optional `message` field provides a human-readable reason shown in the terminal output. + +```yaml +actions: + - type: fail + message: "Schema change impacts payment services. Requires sign-off from @payments-team." +``` + +The `message` field supports `$ENV_VAR` interpolation. A rule can include multiple `fail` actions; all messages are collected and printed together before the process exits. + +If no triggered rule has a `fail` action, the CLI exits with code `0` regardless of how many rules fired. This means adding `type: fail` to specific rules is a non-breaking, opt-in change. + +See [Pipeline gates](/docs/development/governance/architecture-change-detection/pipeline-gates) for full examples of blocking CI/CD with governance rules. + +## Environment variables + +Webhook URLs and header values can reference environment variables using the `$VAR_NAME` syntax. The CLI loads your catalog's `.env` file automatically, so you can keep secrets out of `governance.yaml`. + +```yaml +actions: + - type: webhook + url: $SLACK_WEBHOOK_URL + headers: + Authorization: Bearer $API_TOKEN +``` + +If a referenced variable is not set at runtime, the command exits with an error. diff --git a/packages/core/docs/development/governance/architecture-change-detection/03-recipes.md b/packages/core/docs/development/governance/architecture-change-detection/03-recipes.md new file mode 100644 index 000000000..2ac05d8c7 --- /dev/null +++ b/packages/core/docs/development/governance/architecture-change-detection/03-recipes.md @@ -0,0 +1,309 @@ +--- +sidebar_position: 3 +sidebar_label: Recipes +title: Recipes +description: Common governance configurations you can copy and adapt +--- + +## Alert when anyone consumes a message your service produces + +Use the `produces:` prefix to scope a rule to all messages sent by a given service. This is the recommended pattern for service owners who want visibility into downstream adoption. + +```yaml title="governance.yaml" +rules: + - name: orders-service-consumer-changes + when: + - consumer_added + - consumer_removed + resources: + - produces:OrdersService + actions: + - type: webhook + url: $SLACK_WEBHOOK_URL + headers: + Authorization: Bearer $API_TOKEN +``` + +## Alert when a producer stops producing a message your service depends on + +Use `consumes:` to watch all messages a service receives, then trigger on `producer_removed`. This catches upstream breaking changes before they affect your service. + +```yaml title="governance.yaml" +rules: + - name: payment-service-upstream-removed + when: + - producer_removed + resources: + - consumes:PaymentService + actions: + - type: console + - type: webhook + url: $PAGERDUTY_WEBHOOK_URL +``` + +A collection of examples of common governance configurations you can copy and adapt. + +## Alert when a message gets a new consumer + +Useful when you own a message and want to know when other teams start depending on it. + +```yaml title="governance.yaml" +rules: + - name: new-consumer-for-payment-processed + when: + - consumer_added + resources: + - message:PaymentProcessed + actions: + - type: webhook + url: $SLACK_WEBHOOK_URL +``` + +## Alert when a message gets a new producer + +Useful for messages that should only ever have one authoritative producer. + +```yaml title="governance.yaml" +rules: + - name: new-producer-for-order-created + when: + - producer_added + resources: + - message:OrderCreated + actions: + - type: console + - type: webhook + url: $SLACK_WEBHOOK_URL +``` + + + +## Track all changes across the catalog + +The `"*"` wildcard matches every consumer and producer change. Useful for audit logs or a central ops channel. + +```yaml title="governance.yaml" +rules: + - name: catalog-wide-relationship-changes + when: + - consumer_added + - consumer_removed + - producer_added + - producer_removed + resources: + - "*" + actions: + - type: webhook + url: $AUDIT_WEBHOOK_URL +``` + +## Monitor all changes for a specific service + +Use the `service:` prefix to watch any consuming or producing change that involves a named service, regardless of which message is affected. + +```yaml title="governance.yaml" +rules: + - name: notification-service-changes + when: + - consumer_added + - consumer_removed + - producer_added + - producer_removed + resources: + - service:NotificationService + actions: + - type: console + - type: webhook + url: $SLACK_WEBHOOK_URL +``` + +## Get notified when a message your service consumes is deprecated + +Use `consumes:` with `message_deprecated` to alert your team when an upstream producer deprecates a message you depend on. This is the most useful deprecation pattern for consumer teams. + +```yaml title="governance.yaml" +rules: + - name: alert-deprecated-messages + when: + - message_deprecated + resources: + - "consumes:InventoryService" + actions: + - type: console + - type: webhook + url: $SLACK_WEBHOOK_URL +``` + +## Watch a specific message for deprecation + +Use `message:` to monitor a single message across any of its producers. + +```yaml title="governance.yaml" +rules: + - name: watch-order-placed-deprecation + when: + - message_deprecated + resources: + - "message:OrderPlaced" + actions: + - type: webhook + url: $TEAM_WEBHOOK_URL +``` + +## Track all deprecations across the catalog + +The `"*"` wildcard catches every deprecation in the catalog. Useful for a central ops channel or audit log. + +```yaml title="governance.yaml" +rules: + - name: catalog-wide-deprecation-alerts + when: + - message_deprecated + resources: + - "*" + actions: + - type: console + - type: webhook + url: $AUDIT_WEBHOOK_URL +``` + +## Alert when a message schema changes + +Use `schema_changed` to detect when the schema file for any message is added, modified, or replaced. This is useful for catching breaking changes before they propagate to consumers. + +```yaml title="governance.yaml" +rules: + - name: schema-change-alerts + when: + - schema_changed + resources: + - "*" + actions: + - type: console + - type: webhook + url: $SLACK_WEBHOOK_URL +``` + +## Alert when a schema changes for messages your service consumes + +Use `consumes:` with `schema_changed` to scope alerts to only the messages your service depends on. + +```yaml title="governance.yaml" +rules: + - name: payment-service-schema-changes + when: + - schema_changed + resources: + - consumes:PaymentService + actions: + - type: webhook + url: $PAGERDUTY_WEBHOOK_URL +``` + +## Watch a specific message for schema changes + +Use `message:` to monitor schema changes for a single message across any of its producers. + +```yaml title="governance.yaml" +rules: + - name: order-created-schema-watch + when: + - schema_changed + resources: + - message:OrderCreated + actions: + - type: console + - type: webhook + url: $TEAM_WEBHOOK_URL +``` + +## Block CI when a payment-critical schema changes + +Combine `type: fail` with `schema_changed` and service-scoped resource filters to require sign-off before a schema change can merge. + +```yaml title="governance.yaml" +rules: + - name: payment-schema-guard + when: + - schema_changed + resources: + - consumes:PaymentService + - consumes:BillingService + - message:PaymentProcessed + - message:OrderCreated + actions: + - type: console + - type: webhook + url: $SLACK_ALERTS_WEBHOOK + headers: + Authorization: Bearer $SLACK_TOKEN + - type: fail + message: "Schema change impacts payment services. Requires sign-off from @payments-team." +``` + +## Block CI when a consumer is silently removed + +Use `consumer_removed` with `type: fail` to ensure consumer removals are never merged without a migration ticket. + +```yaml title="governance.yaml" +rules: + - name: consumer-removal-guard + when: + - consumer_removed + resources: + - "*" + actions: + - type: console + - type: fail + message: "Removing a consumer is a breaking change. Open a migration ticket first." +``` + +## Block CI on deprecation without a replacement + +Pair `message_deprecated` with `type: fail` to enforce that deprecations are reviewed before merging. + +```yaml title="governance.yaml" +rules: + - name: deprecation-review-gate + when: + - message_deprecated + resources: + - "*" + actions: + - type: console + - type: fail + message: "Deprecations require a migration guide before merging. See CONTRIBUTING.md." +``` + +## Notify on some changes, block on others + +Rules without `type: fail` always exit `0`, so you can mix notification-only rules with blocking rules in the same file. + +```yaml title="governance.yaml" +rules: + # Blocks CI — schema changes to payment-critical messages + - name: payment-schema-guard + when: + - schema_changed + resources: + - consumes:PaymentService + actions: + - type: console + - type: fail + message: "Schema change impacts payment services. Requires sign-off from @payments-team." + + # Notifies only — new consumers are welcome but the team wants visibility + - name: notify-new-consumers + when: + - consumer_added + resources: + - "*" + actions: + - type: console + - type: webhook + url: $SLACK_NOTIFICATIONS_WEBHOOK +``` + +## Future changes + +We are working on adding new change detection features to EventCatalog, if you have any ideas or feedback please let us know on [Discord](https://eventcatalog.dev/discord) or raise an issue on [GitHub](https://github.com/event-catalog/eventcatalog/issues). \ No newline at end of file diff --git a/packages/core/docs/development/governance/architecture-change-detection/04-webhooks.md b/packages/core/docs/development/governance/architecture-change-detection/04-webhooks.md new file mode 100644 index 000000000..d4e07c7e4 --- /dev/null +++ b/packages/core/docs/development/governance/architecture-change-detection/04-webhooks.md @@ -0,0 +1,187 @@ +--- +sidebar_position: 4 +sidebar_label: Webhook Payload +title: Webhook Payload +description: CloudEvents 1.0 payload format sent by governance webhook actions +--- + +## Payload format + +Webhook actions send a POST request in [CloudEvents 1.0](https://cloudevents.io/) format. Each matched change produces a separate request. + +```json +{ + "specversion": "1.0", + "type": "eventcatalog.governance.consumer_added", + "source": "eventcatalog/governance", + "id": "uuid", + "time": "2026-03-04T12:00:00.000Z", + "datacontenttype": "application/json", + "data": { + "schemaVersion": 1, + "summary": "NotificationService is now consuming the event OrderConfirmed", + "consumer": { + "id": "NotificationService", + "version": "0.0.2" + }, + "message": { + "id": "OrderConfirmed", + "version": "0.0.1", + "type": "event" + } + } +} +``` + +## CloudEvents envelope + +| Field | Value | +|---|---| +| `specversion` | Always `"1.0"` | +| `type` | `eventcatalog.governance.`, one of `consumer_added`, `consumer_removed`, `producer_added`, `producer_removed`, `message_deprecated`, `schema_changed` | +| `source` | `eventcatalog/governance` | +| `id` | Unique UUID per request | +| `time` | ISO 8601 timestamp | +| `datacontenttype` | `application/json` | + +## Data object + +| Field | Description | +|---|---| +| `schemaVersion` | Always `1` (reserved for future format changes) | +| `status` | Only present when `--status` is passed to the CLI | +| `summary` | Human-readable description of the change | +| `consumer` or `producer` | The service involved. `consumer` for consumer triggers, `producer` for producer and `message_deprecated` triggers | +| `message` | The message involved, including its `type` (`event`, `command`, or `query`) | +| `schema` | Only present for `schema_changed`. Contains `beforeHash`, `afterHash`, `beforePath`, and `afterPath` | +| `consumers` | Only present for `schema_changed`. Array of services that consume the changed message | +| `producers` | Only present for `schema_changed`. Array of services that produce the changed message | +| `refs` | Only present for `schema_changed`. Contains `base` and `target` branch labels | + +## Producer triggers + +For `producer_added` and `producer_removed`, the `consumer` field is replaced with `producer`: + +```json +{ + "data": { + "schemaVersion": 1, + "summary": "OrdersService is now producing the event OrderCreated", + "producer": { + "id": "OrdersService", + "version": "1.0.0" + }, + "message": { + "id": "OrderCreated", + "version": "1.0.0", + "type": "event" + } + } +} +``` + +## Deprecation trigger + +For `message_deprecated`, the payload identifies the producing service and the deprecated message. One webhook fires per producing service, so if a message has multiple producers, each producer sends a separate request. + +```json +{ + "specversion": "1.0", + "type": "eventcatalog.governance.message_deprecated", + "source": "eventcatalog/governance", + "id": "550e8400-e29b-41d4-a716-446655440000", + "time": "2026-03-05T14:30:00.000Z", + "datacontenttype": "application/json", + "data": { + "schemaVersion": 1, + "summary": "OrderCreated (event) has been deprecated by OrdersService", + "producer": { + "id": "OrdersService", + "version": "2.0.0", + "owners": ["team-orders"] + }, + "message": { + "id": "OrderCreated", + "version": "1.0.0", + "type": "event" + } + } +} +``` + +The `owners` field is only present when the producing service has owners defined. The trigger fires only for newly deprecated messages: existing deprecations and un-deprecations do not fire. + +## Schema changed trigger + +For `schema_changed`, the payload includes a `schema` object with hashes and paths for the before and after versions, plus arrays of the affected consumer and producer services. One webhook fires per changed message. + +```json +{ + "specversion": "1.0", + "type": "eventcatalog.governance.schema_changed", + "source": "eventcatalog/governance", + "id": "550e8400-e29b-41d4-a716-446655440000", + "time": "2026-03-05T14:30:00.000Z", + "datacontenttype": "application/json", + "data": { + "schemaVersion": 1, + "summary": "Schema changed for event OrderCreated", + "message": { + "id": "OrderCreated", + "version": "1.0.0", + "type": "event" + }, + "schema": { + "beforeHash": "abc123", + "afterHash": "def456", + "beforePath": "schemas/order-created.v1.json", + "afterPath": "schemas/order-created.v2.json" + }, + "refs": { + "base": "main", + "target": "feature/update-schema" + }, + "producers": [ + { "id": "OrdersService", "version": "1.0.0", "owners": ["team-orders"] } + ], + "consumers": [ + { "id": "PaymentService", "version": "1.0.0", "owners": ["team-payments"] } + ] + } +} +``` + +The `schema.beforeHash` and `schema.afterHash` are SHA-256 hashes of the schema file content. Either side can be `null` when a schema is first added or removed. The `owners` field on each service is only present when owners are defined. + +## Status field + +When you pass `--status` to the CLI, the value is included in the payload. This is useful for distinguishing between stages in a pull request lifecycle. + +```bash +eventcatalog governance check --status proposed +``` + +```json +{ + "data": { + "schemaVersion": 1, + "status": "proposed", + "summary": "NotificationService is now consuming the event OrderConfirmed", + "consumer": { "id": "NotificationService", "version": "0.0.2" }, + "message": { "id": "OrderConfirmed", "version": "0.0.1", "type": "event" } + } +} +``` + +## Custom headers + +Headers support environment variable substitution using `$VAR_NAME` syntax. + +```yaml +actions: + - type: webhook + url: $SLACK_WEBHOOK_URL + headers: + Authorization: Bearer $API_TOKEN + X-Custom-Header: my-value +``` diff --git a/packages/core/docs/development/governance/architecture-change-detection/05-ci-cd.md b/packages/core/docs/development/governance/architecture-change-detection/05-ci-cd.md new file mode 100644 index 000000000..e19800714 --- /dev/null +++ b/packages/core/docs/development/governance/architecture-change-detection/05-ci-cd.md @@ -0,0 +1,121 @@ +--- +sidebar_position: 5 +sidebar_label: CLI & CI/CD +title: CLI & CI/CD +description: Run governance checks locally and in your CI/CD pipeline +--- + +## Run the check + +Run the check from inside your catalog directory. By default it compares the current working directory against the `main` branch. + +```bash +eventcatalog governance check +``` + +#### Testing locally + +To test locally, make changes to your catalog locally and then run the governance check command. + +```sh +eventcatalog governance check --target main +``` + +This will compare the current working directory against the specified branch. + +## CLI options + +| Option | Description | Default | +|---|---|---| +| `--base ` | Base branch to compare against | `main` | +| `--target ` | Target branch to compare (instead of working directory) | working directory | +| `--format json` | Output results as JSON | text | +| `--status