├── frontend-vue ├── .prettierrc.json ├── .npmrc ├── e2e │ ├── setup │ │ └── Dockerfile │ ├── util │ │ └── test.utils.ts │ ├── README.md │ ├── run.sh │ └── pages │ │ ├── domain-details.page.ts │ │ └── bounded-context-canvas.page.ts ├── src │ ├── types │ │ ├── activeFilter.ts │ │ ├── namespace-templates.ts │ │ ├── domain.ts │ │ ├── namespace.ts │ │ ├── event-log.ts │ │ ├── collaboration.ts │ │ └── boundedContext.ts │ ├── assets │ │ └── logo │ │ │ ├── dark │ │ │ ├── dark.png │ │ │ ├── logotype.png │ │ │ └── with_name.png │ │ │ └── light │ │ │ ├── light.png │ │ │ ├── logotype.png │ │ │ └── with_name.png │ ├── components │ │ ├── bounded-context │ │ │ ├── canvas │ │ │ │ ├── layouts │ │ │ │ │ └── version.ts │ │ │ │ ├── BCCInboundConnections.vue │ │ │ │ ├── BCCOutboundConnections.vue │ │ │ │ ├── ContextureBoundedContextCanvasElement.vue │ │ │ │ ├── ContextureEditBoundedContextForm.vue │ │ │ │ └── BCCDescription.vue │ │ │ ├── ContextureBoundedContextCardGrid.vue │ │ │ ├── namespace │ │ │ │ ├── NamespaceLabelAutocomplete.vue │ │ │ │ └── NamespaceValueAutocomplete.vue │ │ │ └── ContextureMoveBoundedContextModal.vue │ │ ├── core │ │ │ ├── header │ │ │ │ ├── ContextureHeroHeader.vue │ │ │ │ ├── ContextureBlankHeader.vue │ │ │ │ └── StructurizerDiscloser.vue │ │ │ ├── auth │ │ │ │ ├── SignInCallback.vue │ │ │ │ └── SignIn.vue │ │ │ ├── ContextureEntityNotFound.vue │ │ │ ├── change-short-name │ │ │ │ ├── ContextureChangeShortName.vue │ │ │ │ └── changeShortNameValidationSchema.ts │ │ │ ├── navbar │ │ │ │ └── ContextureNavbar.vue │ │ │ └── breadcrumbs │ │ │ │ ├── breadcrumbs.ts │ │ │ │ └── breadcrumbs.spec.ts │ │ ├── primitives │ │ │ ├── dynamic-form │ │ │ │ ├── dynamicForm.ts │ │ │ │ └── ContextureDynamicForm.vue │ │ │ ├── button │ │ │ │ ├── ContextureIconButton.vue │ │ │ │ ├── util │ │ │ │ │ ├── useActionWithLoading.ts │ │ │ │ │ └── LoadingWrapper.vue │ │ │ │ ├── ContextureTextLinkButton.vue │ │ │ │ ├── ContextureWhiteButton.vue │ │ │ │ ├── ContexturePrimaryButton.vue │ │ │ │ ├── ContextureSecondaryButton.vue │ │ │ │ └── ContextureRoundedButton.vue │ │ │ ├── alert │ │ │ │ ├── ContextureHelpfulErrorAlert.vue │ │ │ │ └── ContextureAlert.vue │ │ │ ├── list │ │ │ │ └── ContextureListItem.vue │ │ │ ├── modal │ │ │ │ ├── ContextureConfirmationModal.vue │ │ │ │ └── ContextureModal.vue │ │ │ ├── switch │ │ │ │ └── ContextureSwitch.vue │ │ │ ├── accordion │ │ │ │ └── ContextureAccordionItem.vue │ │ │ ├── input │ │ │ │ ├── ContextureSearch.vue │ │ │ │ ├── ContextureTextarea.vue │ │ │ │ └── ContextureInputText.vue │ │ │ ├── viewswitcher │ │ │ │ └── ContextureViewSwitcher.vue │ │ │ ├── popover │ │ │ │ └── ContexturePopover.vue │ │ │ ├── radio │ │ │ │ ├── ContextureRadioGroup.vue │ │ │ │ └── ContextureRadio.vue │ │ │ ├── tooltip │ │ │ │ └── ContextureTooltip.vue │ │ │ ├── checkbox │ │ │ │ └── ContextureCheckbox.vue │ │ │ ├── badge │ │ │ │ └── ContextureBadge.vue │ │ │ └── collapsable │ │ │ │ └── ContextureCollapsable.vue │ │ ├── event-log │ │ │ ├── PropertyValue.vue │ │ │ ├── ValueDiff.vue │ │ │ ├── FormattedDate.vue │ │ │ └── EventLogModal.vue │ │ ├── README.md │ │ ├── domains │ │ │ ├── ContextureDomainCardGrid.vue │ │ │ ├── ContextureDeleteDomainModalConfirmation.vue │ │ │ └── details │ │ │ │ └── ContextureEditDomainForm.vue │ │ └── analytics │ │ │ ├── ContextureActiveFilters.vue │ │ │ ├── BubbleView.vue │ │ │ └── ContextureAddFilter.vue │ ├── core │ │ ├── isLink.ts │ │ ├── index.ts │ │ ├── sort.ts │ │ ├── uniqueId.ts │ │ ├── uniqueId.spec.ts │ │ ├── validationRules.ts │ │ ├── arrayContentEqual.ts │ │ ├── isLink.spec.ts │ │ ├── arraysEqual.spec.ts │ │ ├── filter.ts │ │ ├── validation.ts │ │ └── filter.spec.ts │ ├── stores │ │ ├── eventLogs.ts │ │ ├── namespace-templates.ts │ │ ├── confirmationModal.ts │ │ └── collaborations.ts │ ├── composables │ │ ├── date-utls.ts │ │ └── useFetch.ts │ ├── pages │ │ ├── bounded-context │ │ │ └── BoundedContextNamespaces.vue │ │ └── domains │ │ │ └── Domains.vue │ ├── App.vue │ ├── styles │ │ └── main.css │ ├── main.ts │ ├── routes.ts │ ├── constants │ │ └── domainRoles.ts │ └── visualisations │ │ └── DataAccess.js ├── public │ ├── favicon │ │ ├── favicon.ico │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── site.webmanifest │ │ └── favicon.svg │ └── fonts │ │ └── Figtree │ │ ├── Figtree-Bold.woff2 │ │ ├── Figtree-Light.woff2 │ │ └── Figtree-Regular.woff2 ├── postcss.config.js ├── .gitignore ├── shims.d.ts ├── .vscode │ ├── extensions.json │ └── settings.json ├── tsconfig.json ├── index.html ├── .env ├── README.md ├── vite.config.ts ├── package.json ├── playwright.config.ts ├── .eslintrc.cjs └── components.d.ts ├── Sketch.jpg ├── entrypoint.sh ├── backend ├── Contexture.Api │ ├── wwwroot │ │ └── index.html │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Dockerfile │ ├── Apis │ │ └── EventLog.fs │ ├── Infrastructure │ │ └── Projections.fs │ ├── Properties │ │ └── launchSettings.json │ ├── AllEvents.fs │ ├── Configuration.fs │ ├── Utils.fs │ ├── Views │ │ └── Namespaces.fs │ └── Contexture.Api.fsproj ├── Contexture.Api.Tests │ ├── Assertions.fs │ ├── Tests.fs │ ├── ReadModels.Tests.fs │ ├── SqlServerFixture.fs │ ├── EnvironmentSimulation.fs │ └── Contexture.Api.Tests.fsproj ├── Contexture.sln └── README.md ├── example ├── DomainOverview.png ├── CanvasV3Overview.png ├── CanvasV4Overview.png └── DomainsOverview.png ├── docker-compose.yml ├── .github └── workflows │ ├── ci-backend.yml │ ├── CI-frontend.yml │ ├── deploy-image.yml │ ├── CI-frontend-e2e.yml │ └── deploy-azure.yml ├── LICENSE ├── Makefile └── concept.md /frontend-vue/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /Sketch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/Sketch.jpg -------------------------------------------------------------------------------- /frontend-vue/.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | legacy-peer-deps=true 3 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | update-ca-certificates 3 | dotnet Contexture.Api.App.dll -------------------------------------------------------------------------------- /backend/Contexture.Api/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/DomainOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/example/DomainOverview.png -------------------------------------------------------------------------------- /example/CanvasV3Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/example/CanvasV3Overview.png -------------------------------------------------------------------------------- /example/CanvasV4Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/example/CanvasV4Overview.png -------------------------------------------------------------------------------- /example/DomainsOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/example/DomainsOverview.png -------------------------------------------------------------------------------- /frontend-vue/e2e/setup/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM softwarepark/contexture:latest 2 | COPY ./test_data/db.json /data/db.json -------------------------------------------------------------------------------- /frontend-vue/src/types/activeFilter.ts: -------------------------------------------------------------------------------- 1 | export interface ActiveFilter { 2 | key: string; 3 | value: string; 4 | } 5 | -------------------------------------------------------------------------------- /frontend-vue/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/public/favicon/favicon.ico -------------------------------------------------------------------------------- /frontend-vue/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend-vue/src/assets/logo/dark/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/src/assets/logo/dark/dark.png -------------------------------------------------------------------------------- /frontend-vue/src/assets/logo/light/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/src/assets/logo/light/light.png -------------------------------------------------------------------------------- /frontend-vue/e2e/util/test.utils.ts: -------------------------------------------------------------------------------- 1 | export function randomString() { 2 | return Math.random().toString(36).substring(2, 7).toString(); 3 | } 4 | -------------------------------------------------------------------------------- /frontend-vue/src/assets/logo/dark/logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/src/assets/logo/dark/logotype.png -------------------------------------------------------------------------------- /frontend-vue/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend-vue/src/assets/logo/dark/with_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/src/assets/logo/dark/with_name.png -------------------------------------------------------------------------------- /frontend-vue/src/assets/logo/light/logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/src/assets/logo/light/logotype.png -------------------------------------------------------------------------------- /frontend-vue/src/assets/logo/light/with_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/src/assets/logo/light/with_name.png -------------------------------------------------------------------------------- /frontend-vue/src/components/bounded-context/canvas/layouts/version.ts: -------------------------------------------------------------------------------- 1 | export enum BoundedContextVersion { 2 | V3 = "V3", 3 | V4 = "V4", 4 | } 5 | -------------------------------------------------------------------------------- /frontend-vue/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend-vue/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend-vue/public/fonts/Figtree/Figtree-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/public/fonts/Figtree/Figtree-Bold.woff2 -------------------------------------------------------------------------------- /frontend-vue/public/fonts/Figtree/Figtree-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/public/fonts/Figtree/Figtree-Light.woff2 -------------------------------------------------------------------------------- /frontend-vue/public/fonts/Figtree/Figtree-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trustbit/Contexture/HEAD/frontend-vue/public/fonts/Figtree/Figtree-Regular.woff2 -------------------------------------------------------------------------------- /frontend-vue/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.local 3 | dist 4 | dist-ssr 5 | node_modules 6 | .idea/ 7 | *.log 8 | /test-results/ 9 | /playwright-report/ 10 | /playwright/.cache/ 11 | -------------------------------------------------------------------------------- /frontend-vue/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import type { DefineComponent } from "vue"; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /frontend-vue/src/components/core/header/ContextureHeroHeader.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /backend/Contexture.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "//SqlBased": { 3 | "ConnectionString": "Server=localhost;User Id=sa;Password=development(!)Password" 4 | }, 5 | "FileBased": { 6 | "Path": "data/db.json" 7 | } 8 | } -------------------------------------------------------------------------------- /backend/Contexture.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "//SqlBased": { 3 | "ConnectionString": "Server=localhost;User Id=sa;Password=development(!)Password" 4 | }, 5 | "FileBased": { 6 | "Path": "data/db.json" 7 | } 8 | } -------------------------------------------------------------------------------- /frontend-vue/src/core/isLink.ts: -------------------------------------------------------------------------------- 1 | export function isLink(text: string): boolean { 2 | let url; 3 | try { 4 | url = new URL(text); 5 | } catch (_) { 6 | return false; 7 | } 8 | return url.protocol === "http:" || url.protocol === "https:"; 9 | } 10 | -------------------------------------------------------------------------------- /frontend-vue/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { isLink } from "./isLink"; 2 | export { uniqueId } from "./uniqueId"; 3 | export { filter } from "./filter"; 4 | export { contains, endsWith, startsWith, startsWithNumber, isAlpha, isUniqueIn } from "./validation"; 5 | import "./sort"; 6 | -------------------------------------------------------------------------------- /frontend-vue/e2e/README.md: -------------------------------------------------------------------------------- 1 | # E2E Tests 2 | 3 | ## Setup 4 | 5 | ```shell 6 | cd setup 7 | docker run -d --rm -p 3000:3000 -e ASPNETCORE_hostBuilder__reloadConfigOnChange=false -it $(docker build -q .) 8 | ``` 9 | 10 | ## Run 11 | 12 | ```shell 13 | npm run test:e2e 14 | ``` 15 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/dynamic-form/dynamicForm.ts: -------------------------------------------------------------------------------- 1 | export interface DynamicFormSchema { 2 | fields: DynamicFormSchemaField[]; 3 | } 4 | 5 | export interface DynamicFormSchemaField { 6 | name: keyof T; 7 | label?: string; 8 | component: any; 9 | componentProps?: any; 10 | } 11 | -------------------------------------------------------------------------------- /frontend-vue/src/core/sort.ts: -------------------------------------------------------------------------------- 1 | interface Array { 2 | sortAlphabeticallyBy(selectProperty: (arg0: T) => string): Array; 3 | } 4 | 5 | Array.prototype.sortAlphabeticallyBy = function (selectProperty) { 6 | return this.toSorted((a, b) => selectProperty(a).toLowerCase().localeCompare(selectProperty(b))); 7 | }; 8 | -------------------------------------------------------------------------------- /frontend-vue/src/core/uniqueId.ts: -------------------------------------------------------------------------------- 1 | let lastId = 0; 2 | 3 | /** 4 | * Create a unique id with the given prefix for the global application. 5 | * 6 | * @param prefix an optional prefix 7 | */ 8 | export function uniqueId(prefix = "contexture_id_") { 9 | lastId++; 10 | return `${prefix}${lastId}`; 11 | } 12 | -------------------------------------------------------------------------------- /frontend-vue/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.vite", 4 | "antfu.iconify", 5 | "vue.volar", 6 | "dbaeumer.vscode-eslint", 7 | "EditorConfig.EditorConfig", 8 | "lokalise.i18n-ally", 9 | "bradlc.vscode-tailwindcss", 10 | "ms-playwright.playwright" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /frontend-vue/src/components/core/header/ContextureBlankHeader.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /frontend-vue/src/components/event-log/PropertyValue.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /backend/Contexture.Api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 2 | COPY ./ /app 3 | WORKDIR /app 4 | EXPOSE 3000 5 | ARG GIT_HASH=unspecified 6 | LABEL org.opencontainers.image.revision=$GIT_HASH 7 | ENV ASPNETCORE_URLS=http://*:3000 8 | ENV FileBased__Path=/data/db.json 9 | ENV GitHash=$GIT_HASH 10 | RUN ["chmod", "+x", "./entrypoint.sh"] 11 | ENTRYPOINT ["./entrypoint.sh"] 12 | -------------------------------------------------------------------------------- /frontend-vue/src/core/uniqueId.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { uniqueId } from "~/core/uniqueId"; 3 | 4 | describe("uniqueId", () => { 5 | it("unique id without prefix", () => { 6 | expect(uniqueId()).toBe("contexture_id_1"); 7 | }); 8 | it("unique id with prefix", () => { 9 | expect(uniqueId("prefix_")).toBe("prefix_2"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /frontend-vue/src/core/validationRules.ts: -------------------------------------------------------------------------------- 1 | import { toFieldValidator } from "@vee-validate/zod"; 2 | import * as zod from "zod"; 3 | import { i18n } from "~/main"; 4 | 5 | const { t } = i18n.global; 6 | 7 | export const requiredStringRule = toFieldValidator(zod.string().min(1)); 8 | 9 | export const requiredObjectRule = toFieldValidator(zod.custom((v) => !!v, { message: t("common.required") })); 10 | -------------------------------------------------------------------------------- /frontend-vue/e2e/run.sh: -------------------------------------------------------------------------------- 1 | # bash 2 | cd setup && docker run -d --rm -p 3000:3000 -e ASPNETCORE_hostBuilder__reloadConfigOnChange=false -it $(docker build -q .) 3 | printf '\nWaiting for server to accept requests...\n' 4 | until curl --output /dev/null --silent --fail http://localhost:3000/api/domains 5 | ; do 6 | printf '.' 7 | sleep 1 8 | done 9 | printf '\nServer is accepting requests. Starting tests.\n' 10 | npm run test:e2e -------------------------------------------------------------------------------- /frontend-vue/src/components/README.md: -------------------------------------------------------------------------------- 1 | ## Icons 2 | 3 | We use the icons from Material Symbols [Iconify](https://icon-sets.iconify.design/material-symbols/). 4 | 5 | They are prefixed with `Icon`. 6 | 7 | Usage: 8 | 9 | ``` html 10 | 11 | ``` 12 | 13 | It will only bundle the icons you use. Check out [`unplugin-icons`](https://github.com/antfu/unplugin-icons) for more 14 | details. 15 | -------------------------------------------------------------------------------- /frontend-vue/src/components/event-log/ValueDiff.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /frontend-vue/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | }, 6 | "files.associations": { 7 | "*.css": "postcss" 8 | }, 9 | "editor.formatOnSave": false, 10 | "i18n-ally.sourceLanguage": "en", 11 | "i18n-ally.keystyle": "nested", 12 | "i18n-ally.localesPaths": "locales", 13 | "i18n-ally.sortKeys": true, 14 | "i18n-ally.extract.autoDetect": true 15 | } 16 | -------------------------------------------------------------------------------- /frontend-vue/src/core/arrayContentEqual.ts: -------------------------------------------------------------------------------- 1 | export function arrayContentEqual(a: any[], b: any[]): boolean { 2 | if (a.length !== b.length) { 3 | return false; 4 | } 5 | 6 | const sortedA = a.slice().sort(); 7 | const sortedB = b.slice().sort(); 8 | 9 | for (let i = 0; i < sortedA.length; i++) { 10 | if (JSON.stringify(sortedA[i]) !== JSON.stringify(sortedB[i])) { 11 | return false; 12 | } 13 | } 14 | 15 | return true; 16 | } 17 | -------------------------------------------------------------------------------- /frontend-vue/src/types/namespace-templates.ts: -------------------------------------------------------------------------------- 1 | export type NamespaceTemplateId = string; 2 | export type NamespaceTemplateItemId = string; 3 | 4 | export interface NamespaceTemplate { 5 | id: NamespaceTemplateId; 6 | name: string; 7 | description: string; 8 | template: NamespaceTemplateItem[]; 9 | } 10 | 11 | export interface NamespaceTemplateItem { 12 | id: NamespaceTemplateItemId; 13 | name: string; 14 | description: string; 15 | placeholder: string; 16 | } 17 | -------------------------------------------------------------------------------- /frontend-vue/public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /frontend-vue/e2e/pages/domain-details.page.ts: -------------------------------------------------------------------------------- 1 | import { Locator, Page } from "@playwright/test"; 2 | 3 | export class DomainDetailsPage { 4 | readonly page: Page; 5 | readonly editDomainButton: Locator; 6 | readonly closeEditDomainButton: Locator; 7 | 8 | constructor(page: Page) { 9 | this.page = page; 10 | this.editDomainButton = page.getByRole("button", { name: "Edit Domain" }); 11 | this.closeEditDomainButton = page.getByRole("button", { name: "Close edit domain" }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | contexture-database: 4 | image: mcr.microsoft.com/mssql/server:2022-latest 5 | ports: 6 | - "1433:1433" 7 | environment: 8 | ACCEPT_EULA: Y 9 | MSSQL_SA_PASSWORD: development(!)Password 10 | 11 | contexture: 12 | depends_on: [contexture-database] 13 | image: softwarepark/contexture:latest 14 | environment: 15 | - SqlBased__ConnectionString=Server=contexture-contexture-database-1;User Id=sa;Password=development(!)Password 16 | ports: 17 | - "3000:3000" 18 | -------------------------------------------------------------------------------- /frontend-vue/src/types/domain.ts: -------------------------------------------------------------------------------- 1 | import { BoundedContext } from "~/types/boundedContext"; 2 | 3 | export type DomainId = string; 4 | 5 | export interface Domain { 6 | id: DomainId; 7 | parentDomainId?: DomainId; 8 | shortName?: string; 9 | name: string; 10 | vision?: string; 11 | subdomains: Domain[]; 12 | boundedContexts: BoundedContext[]; 13 | } 14 | 15 | export interface CreateDomain { 16 | name: String; 17 | shortName?: String; 18 | vision?: String; 19 | } 20 | 21 | export interface UpdateDomain { 22 | key?: string; 23 | name?: string; 24 | vision?: string; 25 | } 26 | -------------------------------------------------------------------------------- /frontend-vue/src/stores/eventLogs.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { useFetch } from "~/composables/useFetch"; 3 | import { EventLogEntry } from "~/types/event-log"; 4 | 5 | export const useEventLogsStore = defineStore("event-logs", () => { 6 | async function fetchEventLogs(id: string) { 7 | const { data, error } = await useFetch(`/api/event-log/${id}`).get(); 8 | if (error.value) { 9 | console.error("Error fetching event logs:", error.value); 10 | } 11 | 12 | return data.value || []; 13 | } 14 | 15 | return { 16 | fetchEventLogs, 17 | }; 18 | }); 19 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/button/ContextureIconButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /frontend-vue/src/composables/date-utls.ts: -------------------------------------------------------------------------------- 1 | import { formatDistanceToNow, format } from "date-fns"; 2 | import { enGB } from "date-fns/locale"; 3 | 4 | export const useDateUtils = (locale = enGB) => { 5 | const toRelativeTime = (isoDate: string) => { 6 | if (!isoDate) return ""; 7 | return formatDistanceToNow(new Date(isoDate), { addSuffix: true, locale }); 8 | }; 9 | 10 | const toFormattedDate = (isoDate: string, dateFormat = "dd MMM yyyy HH:mm") => { 11 | if (!isoDate) return ""; 12 | return format(new Date(isoDate), dateFormat, { locale }); 13 | }; 14 | 15 | return { 16 | toRelativeTime, 17 | toFormattedDate, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /backend/Contexture.Api.Tests/Assertions.fs: -------------------------------------------------------------------------------- 1 | module Contexture.Api.Tests.Assertions 2 | 3 | type Then = Xunit.Assert 4 | module Then = 5 | let expectOk (result: Async>) : Async = 6 | async { 7 | match! result with 8 | | Ok _ -> return () 9 | | Error e -> return failwithf "Expected an Ok result but got Error:\n%O" e 10 | } 11 | 12 | let resultOrFail (result: Async>) : Async<'r> = 13 | async { 14 | match! result with 15 | | Ok r -> return r 16 | | Error e -> return failwithf "Expected an Ok result but got Error:\n%O" e 17 | } 18 | -------------------------------------------------------------------------------- /frontend-vue/src/components/event-log/FormattedDate.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /frontend-vue/src/components/core/auth/SignInCallback.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/button/util/useActionWithLoading.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | 3 | export type Action = (values?: any) => PromiseLike; 4 | export interface ActionProps { 5 | action: Action; 6 | } 7 | 8 | export function useActionWithLoading({ action }: ActionProps) { 9 | const isLoading = ref(false); 10 | 11 | const handleAction = async (values: any) => { 12 | if (action) { 13 | isLoading.value = true; 14 | try { 15 | await action(values); 16 | } catch (error) { 17 | console.error(error); 18 | } finally { 19 | isLoading.value = false; 20 | } 21 | } 22 | }; 23 | 24 | return { isLoading, handleAction }; 25 | } 26 | -------------------------------------------------------------------------------- /frontend-vue/src/core/isLink.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { isLink } from "~/core/isLink"; 3 | 4 | describe("isLink", () => { 5 | it("is http link", () => { 6 | expect(isLink("http://localhost:8080")).toBeTruthy(); 7 | }); 8 | 9 | it("is https link", () => { 10 | expect(isLink("https://localhost:8080")).toBeTruthy(); 11 | }); 12 | 13 | it("is no link", () => { 14 | expect(isLink("no-link")).toBeFalsy(); 15 | }); 16 | 17 | it("valid url but no link", () => { 18 | expect(isLink("javascript:void(0)")).toBeFalsy(); 19 | }); 20 | 21 | it("valid url but no protocol", () => { 22 | expect(isLink("www.trustbit.tech")).toBeFalsy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /backend/Contexture.Api.Tests/Tests.fs: -------------------------------------------------------------------------------- 1 | module Tests 2 | 3 | open Contexture.Api 4 | open Xunit 5 | open FileBased.Database 6 | 7 | [] 8 | let ``Unversioned JSON deserialization`` () = task { 9 | let exampleInputPath = @"../../../../../example/restaurant-db.json" 10 | 11 | let! expectedJson = exampleInputPath |> Persistence.read 12 | 13 | let parsedRoot = expectedJson |> Serialization.deserialize 14 | 15 | Assert.NotEmpty(parsedRoot.Collaborations) 16 | Assert.NotEmpty(parsedRoot.Domains) 17 | Assert.NotEmpty(parsedRoot.BoundedContexts) 18 | Assert.True(parsedRoot.Version.IsSome) 19 | let resultJson = parsedRoot |> Serialization.serialize 20 | 21 | Assert.NotEmpty resultJson 22 | } -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/alert/ContextureHelpfulErrorAlert.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /frontend-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "es2016", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "jsx": "preserve", 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "allowJs": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "types": ["vite/client", "vite-plugin-pages/client", "unplugin-icons/types/vue", "@fuse-open/types"], 18 | "paths": { 19 | "~/*": ["src/*"] 20 | } 21 | }, 22 | "exclude": ["dist", "node_modules", "postcss.config.js", "tailwind.config.cjs"] 23 | } 24 | -------------------------------------------------------------------------------- /frontend-vue/src/components/core/auth/SignIn.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /frontend-vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Contexture - Managing your Domains & Contexts 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /frontend-vue/src/types/namespace.ts: -------------------------------------------------------------------------------- 1 | import { NamespaceTemplateId } from "~/types/namespace-templates"; 2 | 3 | export type NamespaceId = string; 4 | export type NamespaceLabelId = string; 5 | 6 | export interface CreateNamespace { 7 | name: NamespaceId; 8 | labels: CreateNamespaceLabel[]; 9 | template?: NamespaceTemplateId; 10 | } 11 | 12 | export interface CreateNamespaceLabel { 13 | name: string; 14 | value?: string; 15 | template?: string; 16 | } 17 | 18 | export interface Namespace { 19 | id: string; 20 | template: string; 21 | name: string; 22 | labels: NamespaceLabel[]; 23 | } 24 | 25 | export interface NamespaceLabel { 26 | id: NamespaceLabelId; 27 | name: string; 28 | value: string; 29 | template: string; 30 | } 31 | 32 | export interface LabelChange { 33 | id?: string; 34 | name: string; 35 | value?: string; 36 | } 37 | -------------------------------------------------------------------------------- /backend/Contexture.Api.Tests/ReadModels.Tests.fs: -------------------------------------------------------------------------------- 1 | module Contexture.Api.ReadModels.Tests 2 | 3 | open System 4 | open System.Threading.Tasks 5 | open Xunit 6 | 7 | open Contexture.Api.Infrastructure.ReadModels 8 | open Contexture.Api.Infrastructure 9 | 10 | module State = 11 | let Initial = "initialstate" 12 | 13 | let timeoutms = (TimeSpan.FromSeconds 1).TotalMilliseconds |> int 14 | let testReadModel: ReadModel = 15 | readModel (fun state _events -> state) State.Initial (Some timeoutms) 16 | 17 | [] 18 | let ``fetching state when requested position is ahead of processed should throw after timeout``() = task{ 19 | let getState = fun () -> testReadModel.State(Some(Position.from 1)) :> Task 20 | do! Assert.ThrowsAsync(fun () -> Task.Run(getState, (new Threading.CancellationTokenSource(TimeSpan.FromSeconds(2))).Token)) :> Task 21 | } -------------------------------------------------------------------------------- /backend/Contexture.Api.Tests/SqlServerFixture.fs: -------------------------------------------------------------------------------- 1 | namespace Contexture.Api.Tests 2 | 3 | open System 4 | open System.Threading 5 | open Testcontainers.MsSql 6 | open Xunit 7 | 8 | type MsSqlFixture() = 9 | static let mutable counter = ref 0 10 | let container = 11 | let containerConfiguration = 12 | MsSqlBuilder() 13 | .WithName($"MS-SQL-Integration-Tests-{Interlocked.Increment counter}") 14 | .WithCleanUp(false) 15 | .WithAutoRemove(true) 16 | 17 | containerConfiguration.Build() 18 | 19 | member _.Container = container 20 | 21 | interface IAsyncLifetime with 22 | member this.DisposeAsync() = container.StopAsync() 23 | member this.InitializeAsync() = container.StartAsync() 24 | 25 | interface IAsyncDisposable with 26 | member this.DisposeAsync() = container.DisposeAsync() -------------------------------------------------------------------------------- /backend/Contexture.Api/Apis/EventLog.fs: -------------------------------------------------------------------------------- 1 | namespace Contexture.Api.Apis 2 | 3 | open System 4 | open Contexture.Api.Infrastructure.ReadModels 5 | open Contexture.Api.ReadModels 6 | open Giraffe 7 | 8 | module EventLog = 9 | 10 | let getEventsForEntity (entityId: Guid) : HttpHandler = fun next ctx -> task { 11 | let! state = ctx |> State.fetch State.fromReadModel 12 | let events = EventLog.eventsForEntity state entityId 13 | 14 | return! json events next ctx 15 | } 16 | 17 | let routes : HttpHandler = 18 | subRouteCi "/event-log" 19 | (choose [ 20 | subRoutef "/%O" 21 | (fun (entityId:Guid) -> 22 | GET >=> getEventsForEntity entityId 23 | ) 24 | RequestErrors.NOT_FOUND "Not found" 25 | ]) 26 | -------------------------------------------------------------------------------- /frontend-vue/src/pages/bounded-context/BoundedContextNamespaces.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /frontend-vue/.env: -------------------------------------------------------------------------------- 1 | # any backend url can be used 2 | # VITE_CONTEXTURE_API_BASE_URL=http://localhost:5000 3 | # however this will use the reverse proxy (and allows you to use other devices which do not run the backend) 4 | VITE_CONTEXTURE_API_BASE_URL= 5 | # This sets the max nesting level of subdomains to 1. 6 | # This means that any subdomain of a subdomain will not be allowed. 7 | # When its not set the fallback value will be 1 8 | CONTEXTURE_MAX_SUBDOMAINS_NESTING_LEVEL=1 9 | # This sets the path/url to the structurizr (https://structurizr.com/) mappings file 10 | # Its optional and the format of the file should be like this: 11 | #{ 12 | # "rootDomainKey": { 13 | # "id": 85806, # this is the id of the workspace 14 | # "key": "db79db2f-bc78-4f26-9910-97943c229113" # this is the key of the workspace 15 | # } 16 | # } 17 | # example value: /src/assets/structurizr.json 18 | VITE_CONTEXTURE_STRUCTURIZR_MAPPINGS_URL= 19 | -------------------------------------------------------------------------------- /frontend-vue/src/types/event-log.ts: -------------------------------------------------------------------------------- 1 | type EventType = 2 | | "ShortNameAssigned" 3 | | "BoundedContextRenamed" 4 | | "BoundedContextReclassified" 5 | | "BoundedContextCreated" 6 | | "BoundedContextImported" 7 | | "BoundedContextRemoved" 8 | | "BoundedContextMovedToDomain" 9 | | "DescriptionChanged" 10 | | "BusinessDecisionsUpdated" 11 | | "UbiquitousLanguageUpdated" 12 | | "DomainRolesUpdated" 13 | | "MessagesUpdated" 14 | | "DomainImported" 15 | | "DomainCreated" 16 | | "SubDomainCreated" 17 | | "DomainRenamed" 18 | | "CategorizedAsSubdomain" 19 | | "PromotedToDomain" 20 | | "VisionRefined" 21 | | "DomainRemoved" 22 | | "NamespaceImported" 23 | | "NamespaceAdded" 24 | | "NamespaceRemoved" 25 | | "LabelAdded" 26 | | "LabelRemoved" 27 | | "LabelUpdated"; 28 | 29 | export interface EventLogEntry { 30 | eventType: EventType; 31 | timestamp: string; 32 | eventData: any; 33 | } 34 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/list/ContextureListItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | -------------------------------------------------------------------------------- /frontend-vue/src/components/bounded-context/canvas/BCCInboundConnections.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /frontend-vue/src/components/bounded-context/canvas/BCCOutboundConnections.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /backend/Contexture.Api/Infrastructure/Projections.fs: -------------------------------------------------------------------------------- 1 | module Contexture.Api.Infrastructure.Projections 2 | 3 | 4 | type Projection<'State, 'Event> = 5 | { Init: 'State 6 | Update: 'State -> 'Event -> 'State } 7 | 8 | let projectIntoMap selectId projection = 9 | fun state (eventEnvelope: EventEnvelope<_>) -> 10 | let selectedId = selectId eventEnvelope 11 | 12 | state 13 | |> Map.tryFind selectedId 14 | |> Option.defaultValue projection.Init 15 | |> fun projectionState -> eventEnvelope.Event |> projection.Update projectionState 16 | |> fun newState -> state |> Map.add selectedId newState 17 | 18 | let projectIntoMapBySourceId projection = 19 | projectIntoMap (fun eventEnvelope -> eventEnvelope.Metadata.Source) projection 20 | 21 | let project projection (events: EventEnvelope<_> list) = 22 | events 23 | |> List.map (fun e -> e.Event) 24 | |> List.fold projection.Update projection.Init 25 | -------------------------------------------------------------------------------- /frontend-vue/src/components/core/ContextureEntityNotFound.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | -------------------------------------------------------------------------------- /frontend-vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 33 | -------------------------------------------------------------------------------- /frontend-vue/src/styles/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @font-face { 6 | font-family: "Figtree"; 7 | font-style: normal; 8 | font-weight: 300; 9 | font-display: swap; 10 | src: url("/fonts/Figtree/Figtree-Light.woff2"); 11 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 12 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 13 | } 14 | 15 | @font-face { 16 | font-family: "Figtree"; 17 | font-style: normal; 18 | font-weight: 400; 19 | font-display: swap; 20 | src: url("/fonts/Figtree/Figtree-Regular.woff2"); 21 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 22 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 23 | } 24 | 25 | @font-face { 26 | font-family: "Figtree"; 27 | font-style: normal; 28 | font-weight: 700; 29 | font-display: swap; 30 | src: url("/fonts/Figtree/Figtree-Bold.woff2"); 31 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 32 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 33 | } 34 | -------------------------------------------------------------------------------- /frontend-vue/src/core/arraysEqual.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { arrayContentEqual } from "~/core/arrayContentEqual"; 3 | 4 | describe("arraysEqual", () => { 5 | it("arraysEqual empty objects are equal", () => { 6 | const equal = arrayContentEqual([], []); 7 | 8 | expect(equal).toBeTruthy(); 9 | }); 10 | 11 | it("arraysEqual string objects arq equal", () => { 12 | const equal = arrayContentEqual(["a"], ["a"]); 13 | 14 | expect(equal).toBeTruthy(); 15 | }); 16 | 17 | it("arraysEqual string objects are not equal", () => { 18 | const equal = arrayContentEqual(["a"], ["b"]); 19 | 20 | expect(equal).toBeFalsy(); 21 | }); 22 | 23 | it("arraysEqual complex objects are equal", () => { 24 | const equal = arrayContentEqual([{ a: "a", b: "b" }], [{ a: "a", b: "b" }]); 25 | 26 | expect(equal).toBeTruthy(); 27 | }); 28 | 29 | it("arraysEqual complex objects are not equal", () => { 30 | const equal = arrayContentEqual([{ a: "a", b: "b" }], [{ a: "c", b: "d" }]); 31 | 32 | expect(equal).toBeFalsy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /frontend-vue/README.md: -------------------------------------------------------------------------------- 1 | # Contexture Frontend 2 | 3 | The Contexture frontend is written in [Vue.js](https://vuejs.org/). 4 | 5 | ## Requirements 6 | 7 | - Contexture API on port 5000 8 | - npm > 8.x 9 | 10 | ## Development 11 | 12 | Recommended editor is VSCode. For the optimal development experience install all plugins 13 | in [.vscode/extensions.json](.vscode/extensions.json) 14 | 15 | ### Start the application 16 | 17 | ```bash 18 | npm i 19 | npm run dev 20 | ``` 21 | 22 | ### Build the application 23 | 24 | ```bash 25 | npm i 26 | npm run build 27 | ``` 28 | 29 | ## Configuration 30 | 31 | See [Env variables](.env) 32 | 33 | | Key | Default Value | Description | 34 | |------------------------------|---------------|----------------------------------------------------------------------------| 35 | | VITE_CONTEXTURE_API_BASE_URL | "" | The base url of the API. Defaults to blank string to use the reverse proxy | 36 | 37 | ### Helpful links 38 | 39 | [Tailwind Cheat Sheet](https://nerdcave.com/tailwind-cheat-sheet) 40 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/button/ContextureTextLinkButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 43 | -------------------------------------------------------------------------------- /frontend-vue/src/stores/namespace-templates.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { Ref } from "vue"; 3 | import { onMounted, ref } from "vue"; 4 | import { useFetch } from "~/composables/useFetch"; 5 | import { NamespaceTemplate } from "~/types/namespace-templates"; 6 | 7 | export const useNamespaceTemplatesStore = defineStore("namespace-templates", () => { 8 | const namespaceTemplates: Ref = ref([]); 9 | const loading = ref(false); 10 | const error = ref(); 11 | 12 | async function fetchNamespaceTemplates(): Promise { 13 | loading.value = true; 14 | const error = await fetch(); 15 | loading.value = false; 16 | error.value = error; 17 | } 18 | 19 | async function fetch() { 20 | const { data, error } = await useFetch("/api/namespaces/templates").get(); 21 | 22 | namespaceTemplates.value = data.value ? data.value : []; 23 | return error; 24 | } 25 | 26 | onMounted(fetchNamespaceTemplates); 27 | 28 | return { 29 | fetchNamespaceTemplates, 30 | namespaceTemplates, 31 | loading, 32 | error, 33 | }; 34 | }); 35 | -------------------------------------------------------------------------------- /.github/workflows/ci-backend.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Backend CI 5 | 6 | on: 7 | push: 8 | paths: 9 | - 'backend/**' 10 | 11 | jobs: 12 | build-backend: 13 | defaults: 14 | run: 15 | working-directory: ./ 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-dotnet@v2 20 | with: 21 | dotnet-version: '7.0.x' 22 | - run: make publish-backend 23 | name: Build 24 | - run: make test-backend 25 | - name: Publish Test Report 26 | if: github.actor != 'dependabot[bot]' && always() 27 | uses: dorny/test-reporter@v1 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | path: "backend/**/TestResults.trx" 31 | name: "Test Report - Contexture Backend" 32 | reporter: "dotnet-trx" 33 | - name: Publish 34 | run: make publish-backend 35 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/button/ContextureWhiteButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 44 | -------------------------------------------------------------------------------- /backend/Contexture.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:52757/", 7 | "sslPort": 44344 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Contexture.Api": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:5000" 25 | }, 26 | "Contexture.Api SQL-SERVER": { 27 | "commandName": "Project", 28 | "launchBrowser": true, 29 | "environmentVariables": { 30 | "ASPNETCORE_ENVIRONMENT": "Development", 31 | "SqlBased__ConnectionString":"Server=localhost;User Id=sa;Password=development(!)Password" 32 | }, 33 | "applicationUrl": "http://localhost:5000" 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/modal/ContextureConfirmationModal.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | -------------------------------------------------------------------------------- /.github/workflows/CI-frontend.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI-Frontend 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master and main branch 7 | on: 8 | push: 9 | paths: 10 | - '.github/workflows/CI-frontend.yml' 11 | - 'frontend-vue/**' 12 | 13 | defaults: 14 | run: 15 | shell: bash 16 | working-directory: frontend-vue 17 | 18 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 19 | jobs: 20 | build-frontend: 21 | # The type of runner that the job will run on 22 | runs-on: ubuntu-latest 23 | 24 | # Steps represent a sequence of tasks that will be executed as part of the job 25 | steps: 26 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: '18.x' 31 | - run: npm ci 32 | - run: npm run lint 33 | - run: npm run test:unit 34 | - run: make build-app 35 | working-directory: ./ 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christian Folie and SWP Softwarepark GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend-vue/src/composables/useFetch.ts: -------------------------------------------------------------------------------- 1 | import { AfterFetchContext, BeforeFetchContext, createFetch } from "@vueuse/core"; 2 | import { useAuthStore } from "~/stores/auth"; 3 | 4 | export const useFetch = createFetch({ 5 | baseUrl: import.meta.env.VITE_CONTEXTURE_API_BASE_URL, 6 | fetchOptions: { 7 | redirect: "follow", 8 | }, 9 | 10 | options: { 11 | async beforeFetch(ctx: BeforeFetchContext) { 12 | const authStore = useAuthStore(); 13 | if (authStore.enabled) { 14 | const accessToken = await authStore.getAccessToken(); 15 | const headers = { 16 | ...ctx.options.headers, 17 | Authorization: `Bearer ${accessToken}`, 18 | }; 19 | 20 | ctx.options.headers = headers; 21 | } 22 | 23 | return ctx; 24 | }, 25 | async afterFetch(ctx: AfterFetchContext) { 26 | return { 27 | response: ctx.response, 28 | data: typeof ctx.data === "object" ? ctx.data : JSON.parse(ctx.data), 29 | }; 30 | }, 31 | async onFetchError(ctx: { data: any; response: Response | null; error: any }) { 32 | console.warn(ctx); 33 | return ctx; 34 | }, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /frontend-vue/src/components/core/change-short-name/ContextureChangeShortName.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy-image.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | jobs: 10 | build-and-deploy: 11 | name: Build and Deploy 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@master 15 | - uses: actions/setup-dotnet@v2 16 | with: 17 | dotnet-version: '7.0.x' 18 | - run: make build-backend 19 | name: Build 20 | - run: make test-backend 21 | name: Test 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '18.x' 26 | - run: make prepare-image 27 | name: Prepares backend & frontend into an image 28 | - name: Publish to Registry 29 | uses: elgohr/Publish-Docker-Github-Action@main 30 | env: 31 | GIT_HASH: ${{ github.sha }} 32 | with: 33 | name: softwarepark/contexture 34 | workdir: artifacts/image 35 | username: ${{ secrets.DOCKER_USERNAME }} 36 | password: ${{ secrets.DOCKER_PASSWORD }} 37 | dockerfile: ../backend/Dockerfile 38 | buildargs: GIT_HASH 39 | tag_semver: true 40 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/button/ContexturePrimaryButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 44 | -------------------------------------------------------------------------------- /.github/workflows/CI-frontend-e2e.yml: -------------------------------------------------------------------------------- 1 | name: Frontend E2E Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths: 7 | - '.github/workflows/CI-frontend-e2e.yml' 8 | - 'frontend-vue/**' 9 | 10 | defaults: 11 | run: 12 | shell: bash 13 | working-directory: frontend-vue 14 | 15 | jobs: 16 | frontend-e2e: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: '18.x' 23 | - run: npm ci 24 | - name: Prepare backend 25 | run: docker run -d --rm -p 3000:3000 -e ASPNETCORE_hostBuilder__reloadConfigOnChange=false -it $(docker build -q .) 26 | working-directory: frontend-vue/e2e/setup 27 | - name: Install Playwright Browsers 28 | run: npx playwright install --with-deps 29 | - name: Run Playwright tests 30 | run: npx playwright test 31 | - uses: actions/upload-artifact@v3 32 | if: always() 33 | with: 34 | name: playwright-report 35 | # the upload-artifact action does not use the working-directory setting :-( 36 | path: frontend-vue/playwright-report/ 37 | retention-days: 3 38 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/switch/ContextureSwitch.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 40 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/button/ContextureSecondaryButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 43 | -------------------------------------------------------------------------------- /frontend-vue/src/components/event-log/EventLogModal.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 38 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/button/ContextureRoundedButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build-image 2 | 3 | build-backend: 4 | cd backend && dotnet build 5 | 6 | test-backend: 7 | cd backend && dotnet test \ 8 | --logger "trx;LogFileName=TestResults.trx" 9 | 10 | publish-backend: 11 | dotnet publish backend/Contexture.Api/Contexture.Api.fsproj \ 12 | -c Release \ 13 | -o artifacts/backend 14 | 15 | publish-docker: 16 | dotnet publish backend/Contexture.Api/Contexture.Api.fsproj \ 17 | -c Release \ 18 | --os linux \ 19 | -o artifacts/backend 20 | 21 | build-app: 22 | cd frontend-vue && npm ci && npm run build 23 | 24 | publish-app: build-app 25 | mkdir -p artifacts/frontend 26 | cp -r frontend-vue/dist/** artifacts/frontend 27 | 28 | prepare-image: publish-docker publish-app 29 | mkdir -p artifacts/image/wwwroot 30 | cp -r artifacts/backend/*.* artifacts/image/ 31 | cp entrypoint.sh artifacts/image/entrypoint.sh 32 | cp -r artifacts/frontend/** artifacts/image/wwwroot/ 33 | 34 | build-image: prepare-image 35 | cd artifacts/image && docker build -t softwarepark/contexture -f ../backend/Dockerfile . 36 | 37 | run-app: 38 | docker run -it -p 3000:3000 -v contexture_data:/data softwarepark/contexture 39 | 40 | run-image: build-image run-app 41 | 42 | clean: 43 | rm -rf artifacts/ 44 | -------------------------------------------------------------------------------- /frontend-vue/e2e/pages/bounded-context-canvas.page.ts: -------------------------------------------------------------------------------- 1 | import { Locator, Page } from "@playwright/test"; 2 | 3 | export class BoundedContextCanvasPage { 4 | readonly page: Page; 5 | readonly editBoundedContextButton: Locator; 6 | readonly addNewInboundCollaborator: Locator; 7 | readonly addNewOutboundCollaborator: Locator; 8 | readonly addNewBusinessDecision: Locator; 9 | readonly addNewTerm: Locator; 10 | readonly addNewDomainRole: Locator; 11 | readonly addDomainRoleFromTemplate: Locator; 12 | 13 | constructor(page: Page) { 14 | this.page = page; 15 | this.editBoundedContextButton = page.getByRole("button", { 16 | name: "Edit bounded context", 17 | }); 18 | this.addNewInboundCollaborator = page.getByRole("button", { name: "add new collaborator" }).first(); 19 | this.addNewOutboundCollaborator = page.getByRole("button", { name: "add new collaborator" }).last(); 20 | this.addNewBusinessDecision = page.getByRole("button", { name: "add business decision" }); 21 | this.addNewTerm = page.getByRole("button", { name: "add new term" }); 22 | this.addNewDomainRole = page.getByRole("button", { name: "add new domain role" }); 23 | this.addDomainRoleFromTemplate = page.getByRole("button", { name: "choose domain role from pre-defined list" }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/button/util/LoadingWrapper.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 23 | 58 | -------------------------------------------------------------------------------- /frontend-vue/src/core/filter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filter a given object by searching for a specific query in its properties. 3 | * 4 | * @param obj The object to search 5 | * @param query The string to search for 6 | * @param key An optional property key to limit the search to 7 | * @returns Returns a boolean indicating if the query was found in any of the object's properties. 8 | */ 9 | export function filter(obj: any, query: string, key?: string | undefined): boolean { 10 | for (const prop in obj) { 11 | if (key && prop !== key) { 12 | continue; 13 | } 14 | const propValue = obj[prop]; 15 | if (!propValue) return false; 16 | if (Array.isArray(propValue)) { 17 | if (propValue.includes(query)) { 18 | return true; 19 | } 20 | const nestedResults = filterInArray(propValue as any[], query); 21 | if (nestedResults.length) { 22 | return true; 23 | } 24 | } else if (typeof propValue === "object") { 25 | if (filter(propValue, query)) { 26 | return true; 27 | } 28 | } else if (typeof propValue === "string") { 29 | if (propValue.toLowerCase().includes(query?.toLowerCase())) { 30 | return true; 31 | } 32 | } 33 | } 34 | return false; 35 | } 36 | 37 | const filterInArray = (array: any[], query: string, key?: string): any[] => { 38 | return array.filter((o) => filter(o, query, key)); 39 | }; 40 | -------------------------------------------------------------------------------- /frontend-vue/src/components/bounded-context/ContextureBoundedContextCardGrid.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 43 | -------------------------------------------------------------------------------- /frontend-vue/src/core/validation.ts: -------------------------------------------------------------------------------- 1 | import { RefinementCtx } from "zod"; 2 | import * as zod from "zod"; 3 | 4 | export const contains = (term: string, arg: string) => term.includes(arg); 5 | export const startsWith = (term: string, arg: string) => term.startsWith(arg); 6 | export const endsWith = (term: string, arg: string) => term.endsWith(arg); 7 | export const startsWithNumber = (term: string) => term.match(/^\d/); 8 | export const isAlpha = (term: string) => term.match(/^[a-zA-Z]+$/); 9 | 10 | export const isUniqueIn = ( 11 | term: string, 12 | ctx: RefinementCtx, 13 | params: { field?: keyof T; in: T | T[] | undefined; errorMessage: string } 14 | ) => { 15 | const alreadyExists = checkExistenceIn(term, params); 16 | 17 | if (alreadyExists) { 18 | ctx.addIssue({ 19 | code: zod.ZodIssueCode.custom, 20 | message: params.errorMessage, 21 | }); 22 | return false; 23 | } 24 | 25 | return true; 26 | }; 27 | 28 | function checkExistenceIn(arg: string, params: { field?: any; in: any | any[] }): boolean { 29 | if (!params.in) { 30 | return false; 31 | } 32 | if (Array.isArray(params.in)) { 33 | if (params.field) { 34 | return !!params.in.find((toFind) => toFind[params.field]?.toLowerCase() === arg.toLowerCase()); 35 | } else { 36 | return !!params.in.find((toFind) => toFind.toLowerCase() === arg.toLowerCase()); 37 | } 38 | } 39 | return !!Object.keys(params.in).find((key) => key.toLowerCase() === arg.toLowerCase()); 40 | } 41 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/dynamic-form/ContextureDynamicForm.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 34 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/alert/ContextureAlert.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 58 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/accordion/ContextureAccordionItem.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 43 | -------------------------------------------------------------------------------- /backend/Contexture.Api.Tests/EnvironmentSimulation.fs: -------------------------------------------------------------------------------- 1 | namespace Contexture.Api.Tests.EnvironmentSimulation 2 | 3 | open System 4 | open System.Threading 5 | 6 | type Clock = unit -> DateTimeOffset 7 | 8 | module Clock = 9 | 10 | let systemClock = fun () -> DateTimeOffset.UtcNow 11 | 12 | let currentInstant (clock: Clock) = clock () 13 | 14 | let dateTimeUtc (clock: Clock) = 15 | clock 16 | |> currentInstant 17 | |> fun t -> t.ToUniversalTime() 18 | 19 | type ISimulateEnvironment = 20 | abstract Time : unit -> System.DateTimeOffset 21 | abstract NextId : unit -> int 22 | 23 | type FixedTimeEnvironment(now: DateTimeOffset, seed: int) = 24 | let ids = ref seed 25 | 26 | let nextId () = Interlocked.Increment(ids) 27 | 28 | static member FromInstance(now: DateTimeOffset) = 29 | FixedTimeEnvironment(now, 0) :> ISimulateEnvironment 30 | 31 | static member FromClock(clock: Clock) = 32 | FixedTimeEnvironment.FromInstance(Clock.currentInstant clock) 33 | 34 | static member FromSystemClock() = 35 | FixedTimeEnvironment.FromClock(Clock.systemClock) 36 | 37 | interface ISimulateEnvironment with 38 | member this.NextId() = nextId () 39 | member __.Time() = now 40 | 41 | 42 | [] 43 | module PseudoRandom = 44 | let sequentialGuidString number = sprintf "00000000-6c78-4f2e-0000-%012i" number 45 | let sequentialGuid = sequentialGuidString >> Guid 46 | let guid (env: ISimulateEnvironment) = env.NextId() |> sequentialGuid 47 | let nameWithGuid prefix (env: ISimulateEnvironment) = 48 | $"%s{prefix}-%O{guid env}" -------------------------------------------------------------------------------- /frontend-vue/src/components/domains/ContextureDomainCardGrid.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 47 | -------------------------------------------------------------------------------- /frontend-vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from "pinia"; 2 | import { createApp } from "vue"; 3 | import { createI18n, I18nOptions } from "vue-i18n"; 4 | import { createRouter, createWebHistory } from "vue-router"; 5 | import App from "./App.vue"; 6 | import routes from "./routes"; 7 | import "./styles/main.css"; 8 | import { Bubble } from "~/visualisations/Bubble"; 9 | import { HierarchicalEdge } from "~/visualisations/HierarchicalEdge"; 10 | import { Sunburst } from "~/visualisations/Sunburst"; 11 | import { getSecurityConfiguration } from "~/stores/auth"; 12 | 13 | const messages = Object.fromEntries( 14 | Object.entries(import.meta.glob<{ default: any }>("../locales/*.json", { eager: true })).map(([key, value]) => { 15 | return [key.slice(11, -5), value.default]; 16 | }) 17 | ) as I18nOptions["messages"]; 18 | 19 | export const i18n = createI18n({ 20 | legacy: false, 21 | locale: "en", 22 | messages, 23 | }); 24 | 25 | getSecurityConfiguration().then((securityConfiguration) => { 26 | const pinia = createPinia(); 27 | const app = createApp(App); 28 | app.provide("securityConfiguration", securityConfiguration); 29 | 30 | const router = createRouter({ 31 | history: createWebHistory(import.meta.env.BASE_URL), 32 | routes, 33 | scrollBehavior() { 34 | // always scroll to top 35 | return { top: 0 }; 36 | }, 37 | }); 38 | 39 | customElements.define("bubble-visualization", Bubble); 40 | customElements.define("hierarchical-edge", HierarchicalEdge); 41 | customElements.define("visualization-sunburst", Sunburst); 42 | 43 | app.use(router); 44 | app.use(i18n); 45 | app.use(pinia); 46 | app.mount("#app"); 47 | }); 48 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/input/ContextureSearch.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 53 | -------------------------------------------------------------------------------- /frontend-vue/src/core/filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { filter } from "~/core/filter"; 3 | 4 | describe("filter", () => { 5 | it("search in object property string", () => { 6 | const actualFound = filter({ a: "hit", b: "nothing" }, "hit"); 7 | 8 | expect(actualFound).toBeTruthy(); 9 | }); 10 | 11 | it("search in object property array", () => { 12 | const actualFound = filter({ a: [{ a: "hit" }], b: [{ a: "nothing" }] }, "hit"); 13 | 14 | expect(actualFound).toBeTruthy(); 15 | }); 16 | 17 | it("search in object property string not found", () => { 18 | const actualFound = filter({ a: "nothing", b: "nothing" }, "hit"); 19 | 20 | expect(actualFound).toBeFalsy(); 21 | }); 22 | 23 | it("search in object property array not found", () => { 24 | const actualFound = filter({ a: [{ a: "nothing" }], b: [{ a: "nothing" }] }, "hit"); 25 | 26 | expect(actualFound).toBeFalsy(); 27 | }); 28 | 29 | it("search in object array limit by key", () => { 30 | const actualFound = filter({ a: [{ a: "hit" }], b: [{ a: "nothing" }] }, "hit", "b"); 31 | 32 | expect(actualFound).toBeFalsy(); 33 | }); 34 | 35 | it("search in object string limit by key", () => { 36 | const actualFound = filter({ a: "hit", b: "nothing" }, "hit", "b"); 37 | 38 | expect(actualFound).toBeFalsy(); 39 | }); 40 | 41 | it("should search in nested object array", () => { 42 | const nestedObject = { 43 | a: [ 44 | { 45 | b: [ 46 | { 47 | c: "hit", 48 | }, 49 | ], 50 | }, 51 | ], 52 | }; 53 | 54 | const result = filter(nestedObject, "hit"); 55 | expect(result).toBeTruthy(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /frontend-vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import VueI18n from "@intlify/unplugin-vue-i18n/vite"; 4 | import path from "path"; 5 | import IconsResolver from "unplugin-icons/resolver"; 6 | import Icons from "unplugin-icons/vite"; 7 | import Components from "unplugin-vue-components/vite"; 8 | import { defineConfig } from "vite"; 9 | import vue from "@vitejs/plugin-vue"; 10 | 11 | export default defineConfig({ 12 | server: { 13 | proxy: { 14 | "/api": { 15 | target: "http://localhost:3000", 16 | changeOrigin: true, 17 | secure: false, 18 | }, 19 | }, 20 | }, 21 | resolve: { 22 | alias: { 23 | "~/": `${path.resolve(__dirname, "src")}/`, 24 | }, 25 | }, 26 | plugins: [ 27 | vue({ 28 | reactivityTransform: true, 29 | template: { 30 | compilerOptions: { 31 | isCustomElement: (tag) => 32 | ["bubble-visualization", "hierarchical-edge", "visualization-sunburst"].includes(tag), 33 | }, 34 | }, 35 | }), 36 | 37 | // https://github.com/antfu/vite-plugin-components 38 | Components({ 39 | dts: true, 40 | dirs: [], 41 | resolvers: [ 42 | IconsResolver({ 43 | prefix: "icon", 44 | }), 45 | ], 46 | }), 47 | 48 | // https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n 49 | VueI18n({ 50 | runtimeOnly: true, 51 | compositionOnly: true, 52 | fullInstall: true, 53 | include: [path.resolve(__dirname, "locales/**")], 54 | }), 55 | 56 | Icons({ 57 | compiler: "vue3", 58 | }), 59 | ], 60 | 61 | // https://github.com/vitest-dev/vitest 62 | test: { 63 | environment: "jsdom", 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /.github/workflows/deploy-azure.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Contexture to Azure Web App 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | # CONFIGURATION 10 | # For help, go to https://github.com/Azure/Actions 11 | # 12 | # 1. Set up the following secrets in your repository: 13 | # AZURE_WEBAPP_PUBLISH_PROFILE 14 | # 15 | # 2. Change these variables for your configuration: 16 | env: 17 | AZURE_WEBAPP_NAME: contexture # set this to your application's name 18 | AZURE_WEBAPP_PACKAGE_PATH: './artifacts/image/' # set this to the path to your web app project, defaults to the repository root 19 | DOTNET_VERSION: '7.0.X' 20 | 21 | jobs: 22 | build-and-deploy: 23 | name: Build and Deploy 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@master 27 | - uses: actions/setup-dotnet@v2 28 | with: 29 | dotnet-version: '7.0.x' 30 | - run: make build-backend 31 | name: Build 32 | - run: make test-backend 33 | name: Test 34 | - name: Setup Node.js 35 | uses: actions/setup-node@v3 36 | with: 37 | node-version: '18.x' 38 | - run: make prepare-image 39 | - name: prepare scenario 40 | run: cp ./example/restaurant-db.json ./artifacts/image/db.json 41 | - name: 'Deploy to Azure WebApp' 42 | uses: azure/webapps-deploy@v2 43 | with: 44 | app-name: ${{ env.AZURE_WEBAPP_NAME }} 45 | publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }} 46 | package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} 47 | 48 | # For more information on GitHub Actions for Azure, refer to https://github.com/Azure/Actions 49 | # For more samples to get started with GitHub Action workflows to deploy to Azure, refer to https://github.com/Azure/actions-workflow-samples 50 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/modal/ContextureModal.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 56 | -------------------------------------------------------------------------------- /frontend-vue/public/favicon/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend-vue/src/components/analytics/ContextureActiveFilters.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 55 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/viewswitcher/ContextureViewSwitcher.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 67 | -------------------------------------------------------------------------------- /frontend-vue/src/components/bounded-context/namespace/NamespaceLabelAutocomplete.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 62 | -------------------------------------------------------------------------------- /backend/Contexture.Api/AllEvents.fs: -------------------------------------------------------------------------------- 1 | module Contexture.Api.AllEvents 2 | 3 | open Contexture.Api.Aggregates 4 | open Contexture.Api.Infrastructure 5 | 6 | type AllEvents = 7 | | BoundedContexts of BoundedContext.Event 8 | | Domains of Domain.Event 9 | | Namespaces of Namespace.Event 10 | | NamespaceTemplates of NamespaceTemplate.Event 11 | | Collaboration of Collaboration.Event 12 | 13 | static member fromEnvelope(event: EventEnvelope) = 14 | match event.StreamKind with 15 | | kind when kind = StreamKind.Of() -> 16 | event |> EventEnvelope.unbox |> EventEnvelope.map BoundedContexts |> Some 17 | | kind when kind = StreamKind.Of() -> 18 | event |> EventEnvelope.unbox |> EventEnvelope.map Domains |> Some 19 | | kind when kind = StreamKind.Of() -> 20 | event |> EventEnvelope.unbox |> EventEnvelope.map Namespaces |> Some 21 | | kind when kind = StreamKind.Of() -> 22 | event |> EventEnvelope.unbox |> EventEnvelope.map NamespaceTemplates |> Some 23 | | kind when kind = StreamKind.Of() -> 24 | event |> EventEnvelope.unbox |> EventEnvelope.map Collaboration |> Some 25 | | _ -> None 26 | 27 | static member select<'E> (event: AllEvents) = 28 | match event with 29 | | e when typeof<'E> = typeof -> e |> unbox<'E> 30 | | BoundedContexts e when typeof<'E> = typeof -> e |> unbox<'E> 31 | | Domains e when typeof<'E> = typeof-> e |> unbox<'E> 32 | | Namespaces e when typeof<'E> = typeof-> e |> unbox<'E> 33 | | NamespaceTemplates e when typeof<'E> = typeof-> e |> unbox<'E> 34 | | Collaboration e when typeof<'E> = typeof-> e |> unbox<'E> 35 | | other -> failwithf "Unable to match %s from %O" typeof<'E>.FullName other 36 | -------------------------------------------------------------------------------- /frontend-vue/src/components/core/navbar/ContextureNavbar.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 67 | 68 | 73 | -------------------------------------------------------------------------------- /frontend-vue/src/types/collaboration.ts: -------------------------------------------------------------------------------- 1 | import { BoundedContextId } from "~/types/boundedContext"; 2 | import { DomainId } from "~/types/domain"; 3 | 4 | export type CollaborationId = string; 5 | export type CollaboratorKeys = keyof Collaborator; 6 | export type RelationshipTypes = keyof RelationshipType & "unknown"; 7 | 8 | export interface Collaboration { 9 | id: CollaborationId; 10 | description?: string; 11 | initiator: Collaborator; 12 | recipient: Collaborator; 13 | relationshipType: RelationshipType | "unknown"; 14 | } 15 | 16 | export interface CreateCollaborator { 17 | description?: string; 18 | initiator: Collaborator; 19 | recipient: Collaborator; 20 | } 21 | 22 | export interface RelationshipType { 23 | symmetric?: SymmetricRelationship; 24 | upstreamDownstream?: { 25 | role?: InitiatorCustomerSupplierRole; 26 | downstreamType?: DownstreamRelationship; 27 | upstreamType?: UpstreamRelationship; 28 | initiatorRole?: InitiatorRole; 29 | }; 30 | } 31 | 32 | export enum InitiatorRole { 33 | Upstream = "Upstream", 34 | Downstream = "Downstream", 35 | } 36 | 37 | export enum InitiatorCustomerSupplierRole { 38 | Supplier = "Supplier", 39 | Customer = "Customer", 40 | } 41 | 42 | export enum SymmetricRelationship { 43 | SharedKernel = "SharedKernel", 44 | Partnership = "Partnership", 45 | SeparateWays = "SeparateWays", 46 | BigBallOfMud = "BigBallOfMud", 47 | } 48 | 49 | export enum UpstreamRelationship { 50 | Upstream = "Upstream", 51 | PublishedLanguage = "PublishedLanguage", 52 | OpenHost = "OpenHost", 53 | } 54 | 55 | export enum DownstreamRelationship { 56 | Downstream = "Downstream", 57 | AntiCorruptionLayer = "AntiCorruptionLayer", 58 | Conformist = "Conformist", 59 | } 60 | 61 | export interface Collaborator { 62 | boundedContext?: BoundedContextId; 63 | domain?: DomainId; 64 | externalSystem?: string; 65 | frontend?: string; 66 | } 67 | 68 | export interface RelationshipBetweenCollaborators { 69 | value: DownstreamRelationship | RelationshipType | UpstreamRelationship; 70 | label: string; 71 | description: string; 72 | } 73 | -------------------------------------------------------------------------------- /backend/Contexture.Api/Configuration.fs: -------------------------------------------------------------------------------- 1 | namespace Contexture.Api 2 | 3 | open System 4 | open Contexture.Api.Infrastructure 5 | 6 | type ContextureConfiguration = 7 | { GitHash: string 8 | Engine: ContextureStorageEngine 9 | Security: Security.SecurityConfiguration} 10 | 11 | and ContextureStorageEngine = 12 | | FileBased of path: string 13 | | SqlServerBased of connectionString: string 14 | 15 | module Options = 16 | [] 17 | type FileBased = { Path: string } 18 | 19 | [] 20 | type SqlBased = { ConnectionString: string } 21 | 22 | [] 23 | type ContextureOptions = 24 | { FileBased: FileBased 25 | SqlBased: SqlBased 26 | [] 27 | DatabasePath: string 28 | GitHash: string 29 | Security: Security.Options.SecurityOptions } 30 | 31 | let buildConfiguration (options: ContextureOptions) = 32 | let securityConfiguration = Security.Options.buildSecurityConfiguration options.Security 33 | match tryUnbox options.SqlBased with 34 | | Some sql when not (String.IsNullOrEmpty sql.ConnectionString) -> 35 | { GitHash = options.GitHash 36 | Engine = SqlServerBased sql.ConnectionString 37 | Security = securityConfiguration 38 | } 39 | | _ -> 40 | match tryUnbox options.FileBased with 41 | | Some file when not (String.IsNullOrEmpty file.Path) -> 42 | { GitHash = options.GitHash 43 | Engine = FileBased file.Path 44 | Security = securityConfiguration 45 | } 46 | | _ when not (String.IsNullOrEmpty options.DatabasePath) -> 47 | { GitHash = options.GitHash 48 | Engine = FileBased options.DatabasePath 49 | Security = securityConfiguration 50 | } 51 | | _ -> failwith "Unable to initialize a correct Contexture configuration. Configure either SqlBased_ConnectionString or FileBased_Path" 52 | -------------------------------------------------------------------------------- /frontend-vue/src/stores/confirmationModal.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { shallowRef } from "vue"; 3 | 4 | export interface ConfirmationModal { 5 | isOpen: boolean; 6 | title: string; 7 | body: string; 8 | component?: any; 9 | componentProps?: any; 10 | confirmButtonText: string; 11 | onConfirm: (props?: any) => void; 12 | onCancel: (props?: any) => void; 13 | } 14 | 15 | export const useConfirmationModalStore = defineStore("confirmation-modal", { 16 | state: (): ConfirmationModal => ({ 17 | isOpen: false, 18 | title: "", 19 | body: "", 20 | confirmButtonText: "", 21 | componentProps: "", 22 | component: shallowRef(), 23 | onConfirm: () => {}, 24 | onCancel: () => {}, 25 | }), 26 | actions: { 27 | open( 28 | title: string, 29 | body: string, 30 | confirmButtonText: string, 31 | onConfirm: (props?: any) => void, 32 | onCancel?: (props?: any) => void 33 | ) { 34 | this.isOpen = true; 35 | this.title = title; 36 | this.body = body; 37 | this.confirmButtonText = confirmButtonText; 38 | this.onConfirm = onConfirm; 39 | this.onCancel = onCancel || (() => {}); 40 | }, 41 | openWithComponent( 42 | title: string, 43 | component: any, 44 | componentProps: any, 45 | confirmButtonText: string, 46 | onConfirm: (props?: any) => void, 47 | onCancel?: (props?: any) => void 48 | ) { 49 | this.isOpen = true; 50 | this.title = title; 51 | this.component = component; 52 | this.confirmButtonText = confirmButtonText; 53 | this.componentProps = componentProps; 54 | this.onConfirm = onConfirm; 55 | this.onCancel = onCancel || (() => {}); 56 | }, 57 | cancel() { 58 | this.onCancel(); 59 | this.isOpen = false; 60 | setTimeout(() => { 61 | this.$reset(); 62 | }, 500); 63 | }, 64 | confirm() { 65 | this.onConfirm(); 66 | this.isOpen = false; 67 | setTimeout(() => { 68 | this.$reset(); 69 | }, 500); 70 | }, 71 | }, 72 | }); 73 | 74 | export default useConfirmationModalStore; 75 | -------------------------------------------------------------------------------- /frontend-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contexture", 3 | "version": "0.3.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "vite build", 7 | "dev": "vite --port 4200 --host", 8 | "lint": "eslint .", 9 | "lint:fix": "eslint . --fix", 10 | "typecheck": "vue-tsc --noEmit", 11 | "preview": "vite preview", 12 | "test:unit": "vitest --environment jsdom --root src/", 13 | "test:e2e": "playwright test" 14 | }, 15 | "dependencies": { 16 | "@floating-ui/vue": "^1.0.3", 17 | "@headlessui/tailwindcss": "^0.2.0", 18 | "@headlessui/vue": "^1.7.17", 19 | "@vee-validate/zod": "^4.12.4", 20 | "@vueuse/core": "^10.7.2", 21 | "@vueuse/router": "^10.7.2", 22 | "d3": "^7.8.5", 23 | "date-fns": "^4.1.0", 24 | "fuse.js": "^7.0.0", 25 | "oidc-client-ts": "^2.4.0", 26 | "pinia": "^2.1.7", 27 | "vee-validate": "^4.12.4", 28 | "vite-plugin-pages": "^0.32.2", 29 | "vue": "^3.4.29", 30 | "vue-i18n": "^9.9.0", 31 | "vue-router": "^4.2.5", 32 | "zod": "^3.22.4" 33 | }, 34 | "devDependencies": { 35 | "@babel/core": "^7.23.7", 36 | "@fuse-open/types": "^2.7.1", 37 | "@iconify-json/material-symbols": "^1.1.70", 38 | "@intlify/unplugin-vue-i18n": "^0.8.2", 39 | "@playwright/test": "^1.30.0", 40 | "@rushstack/eslint-patch": "^1.1.4", 41 | "@tailwindcss/forms": "^0.5.3", 42 | "@types/node": "^18.14.2", 43 | "@vitejs/plugin-vue": "^4.0.0", 44 | "@vue/compiler-sfc": "^3.2.47", 45 | "@vue/eslint-config-prettier": "^7.0.0", 46 | "@vue/eslint-config-typescript": "^11.0.2", 47 | "@vue/test-utils": "^2.2.10", 48 | "autoprefixer": "^10.4.13", 49 | "babel-loader": "^8.3.0", 50 | "eslint": "^8.35.0", 51 | "eslint-plugin-vue": "^9.20.1", 52 | "jsdom": "^20.0.3", 53 | "postcss": "^8.4.21", 54 | "prettier": "^2.8.4", 55 | "prettier-plugin-tailwindcss": "^0.2.3", 56 | "tailwindcss": "^3.2.7", 57 | "typescript": "^4.9.5", 58 | "unplugin-icons": "^0.15.3", 59 | "unplugin-vue-components": "^0.24.0", 60 | "vite": "^4.5.1", 61 | "vitest": "^0.29.1", 62 | "vue-loader": "^17.4.2", 63 | "vue-tsc": "^1.8.27" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend-vue/src/components/primitives/input/ContextureTextarea.vue: -------------------------------------------------------------------------------- 1 |