From 49c5e8e108215f9c2291459623df359fb5f63d63 Mon Sep 17 00:00:00 2001 From: K-Dud Date: Tue, 16 Jan 2024 12:35:28 +0100 Subject: [PATCH 1/6] Make domain/subdomain short name and vision required --- frontend-vue/locales/en.json | 2 ++ .../domains/ContextureCreateDomainModal.vue | 18 ++++++++++++++++++ .../details/ContextureCreateSubdomainModal.vue | 18 ++++++++++++++++++ .../details/ContextureEditDomainForm.vue | 8 ++++++-- frontend-vue/src/types/domain.ts | 2 ++ 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/frontend-vue/locales/en.json b/frontend-vue/locales/en.json index 21b1913e..2d9925ec 100644 --- a/frontend-vue/locales/en.json +++ b/frontend-vue/locales/en.json @@ -261,6 +261,8 @@ "domains.search.title": "All Domains Search", "domains.modal.create.error.submit": "Could not create new domain", "domains.modal.create.form.fields.name.label": "Name of the Domain", + "domains.modal.create.form.fields.short_name.label": "Short name", + "domains.modal.create.form.fields.vision.label": "Vision", "domains.modal.create.form.submit": "create domain", "domains.modal.create.title": "Create a new Domain", "domains.modal.create_bounded_context.error.submit": "Could not create new bounded context", diff --git a/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue b/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue index 48743765..7835a527 100644 --- a/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue +++ b/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue @@ -54,6 +54,24 @@ const form: DynamicFormSchema = { rules: toFieldValidator(zod.string().min(1)), }, }, + { + name: "shortName", + component: ContextureInputText, + componentProps: { + label: t("domains.modal.create.form.fields.short_name.label"), + required: true, + rules: toFieldValidator(zod.string().min(1)), + }, + }, + { + name: "vision", + component: ContextureInputText, + componentProps: { + label: t("domains.modal.create.form.fields.vision.label"), + required: true, + rules: toFieldValidator(zod.string().min(1)), + } + }, ], }; diff --git a/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue b/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue index e7cd2eef..de412141 100644 --- a/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue +++ b/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue @@ -57,6 +57,24 @@ const form: DynamicFormSchema = { rules: toFieldValidator(zod.string().min(1)), }, }, + { + name: "shortName", + component: ContextureInputText, + componentProps: { + label: t("domains.modal.create.form.fields.short_name.label"), + required: true, + rules: toFieldValidator(zod.string().min(1)), + }, + }, + { + name: "vision", + component: ContextureInputText, + componentProps: { + label: t("domains.modal.create.form.fields.vision.label"), + required: true, + rules: toFieldValidator(zod.string().min(1)), + } + }, ], }; diff --git a/frontend-vue/src/components/domains/details/ContextureEditDomainForm.vue b/frontend-vue/src/components/domains/details/ContextureEditDomainForm.vue index e2ef9da1..efff2640 100644 --- a/frontend-vue/src/components/domains/details/ContextureEditDomainForm.vue +++ b/frontend-vue/src/components/domains/details/ContextureEditDomainForm.vue @@ -5,6 +5,8 @@ name="key" :description="t('domains.details.edit.form.description.key')" :label="t('domains.details.edit.form.label.key')" + :rules="requiredString" + required /> @@ -21,6 +23,8 @@ name="vision" :description="t('domains.details.edit.form.description.vision')" :label="t('domains.details.edit.form.label.vision')" + :rules="requiredString" + required />
@@ -54,7 +58,7 @@ const emit = defineEmits(["submit"]); const { t } = useI18n(); const editModel: Ref = toRef(props, "domain"); -const nameValidation = toFieldValidator(zod.string().min(1, t("validation.required"))); +const requiredString = toFieldValidator(zod.string().min(1, t("validation.required"))); function submit(values: any) { emit("submit", values); diff --git a/frontend-vue/src/types/domain.ts b/frontend-vue/src/types/domain.ts index 1e0b2b19..4a7ed496 100644 --- a/frontend-vue/src/types/domain.ts +++ b/frontend-vue/src/types/domain.ts @@ -14,6 +14,8 @@ export interface Domain { export interface CreateDomain { name: String; + shortName?: String; + vision?: String; } export interface UpdateDomain { From 5649a57e9ad3c2ffc25e20233e5b210236a3c3f0 Mon Sep 17 00:00:00 2001 From: K-Dud Date: Tue, 16 Jan 2024 13:18:55 +0100 Subject: [PATCH 2/6] Required BC fields --- .../Contexture.Api/Entities/BoundedContext.fs | 35 +++++--- backend/Contexture.Api/Entities/Domain.fs | 81 ++++++++++--------- frontend-vue/locales/en.json | 3 + .../bounded-context/canvas/BCCDescription.vue | 7 +- .../ContextureEditBoundedContextForm.vue | 6 +- .../ContextureChangeShortName.vue | 1 + .../changeShortNameValidationSchema.ts | 6 +- .../ContextureCreateBoundedContextModal.vue | 21 +++++ frontend-vue/src/types/boundedContext.ts | 2 + 9 files changed, 107 insertions(+), 55 deletions(-) diff --git a/backend/Contexture.Api/Entities/BoundedContext.fs b/backend/Contexture.Api/Entities/BoundedContext.fs index 786c6d2b..5249ab39 100644 --- a/backend/Contexture.Api/Entities/BoundedContext.fs +++ b/backend/Contexture.Api/Entities/BoundedContext.fs @@ -84,7 +84,7 @@ type Command = | UpdateDomainRoles of BoundedContextId * UpdateDomainRoles | UpdateMessages of BoundedContextId * UpdateMessages -and CreateBoundedContext = { Name: string } +and CreateBoundedContext = { Name: string; ShortName: string option; Description: string option } and RenameBoundedContext = { Name: string } and AssignShortName = { ShortName: string } @@ -214,6 +214,20 @@ type State = let nameValidation name = if String.IsNullOrWhiteSpace name then Error EmptyName else Ok name +let assignShortNameToBoundedContext shortName boundedContextId = + ShortNameAssigned + { BoundedContextId = boundedContextId + ShortName = + shortName + |> Option.filter (String.IsNullOrWhiteSpace >> not) } + |> Ok + +let changeDescription descriptionText contextId = + DescriptionChanged + { Description = descriptionText + BoundedContextId = contextId } + |> Ok + let newBoundedContext id domainId name = name |> nameValidation @@ -223,6 +237,12 @@ let newBoundedContext id domainId name = DomainId = domainId Name = name }) +let createBoundedContext id domainId name shortName description = + FsToolkit.ErrorHandling.Result.map3 (fun a b c -> [a;b;c]) + (newBoundedContext id domainId name) + (assignShortNameToBoundedContext shortName id) + (changeDescription description id) + let renameBoundedContext potentialName boundedContextId = potentialName |> nameValidation @@ -231,22 +251,13 @@ let renameBoundedContext potentialName boundedContextId = { Name = name BoundedContextId = boundedContextId }) -let assignShortNameToBoundedContext shortName boundedContextId = - ShortNameAssigned - { BoundedContextId = boundedContextId - ShortName = - shortName - |> Option.ofObj - |> Option.filter (String.IsNullOrWhiteSpace >> not) } - |> Ok - let private asList item = item |> Result.map List.singleton let decide (command: Command) state = match command with - | CreateBoundedContext (id, domainId, createBc) -> newBoundedContext id domainId createBc.Name |> asList + | CreateBoundedContext (id, domainId, createBc) -> createBoundedContext id domainId createBc.Name createBc.ShortName createBc.Description | RenameBoundedContext (contextId, rename) -> renameBoundedContext rename.Name contextId |> asList - | AssignShortName (contextId, shortName) -> assignShortNameToBoundedContext shortName.ShortName contextId |> asList + | AssignShortName (contextId, shortName) -> assignShortNameToBoundedContext (shortName.ShortName |> Option.ofObj) contextId |> asList | RemoveBoundedContext contextId -> match state with | Existing domain -> diff --git a/backend/Contexture.Api/Entities/Domain.fs b/backend/Contexture.Api/Entities/Domain.fs index 5d3f4e9c..93163683 100644 --- a/backend/Contexture.Api/Entities/Domain.fs +++ b/backend/Contexture.Api/Entities/Domain.fs @@ -2,6 +2,8 @@ namespace Contexture.Api.Aggregates module Domain = open System + open FsToolkit.ErrorHandling + module ValueObjects = type DomainId = Guid @@ -15,7 +17,7 @@ module Domain = | AssignShortName of DomainId * AssignShortName | RemoveDomain of DomainId - and CreateDomain = { Name: string } + and CreateDomain = { Name: string; Vision: string option; ShortName: string option } and RenameDomain = { Name: string } @@ -43,7 +45,7 @@ module Domain = Name: string Vision: string option } - and DomainCreated = { DomainId: DomainId; Name: String } + and DomainCreated = { DomainId: DomainId; Name: String; } and SubDomainCreated = { DomainId: DomainId @@ -147,17 +149,42 @@ module Domain = Existing { domain with ShortName = c.ShortName } | _ -> state - let newDomain id name parentDomain = - name - |> nameValidation - |> Result.map (fun name -> - match parentDomain with - | Some parent -> - SubDomainCreated - { DomainId = id - ParentDomainId = parent - Name = name } - | None -> DomainCreated { DomainId = id; Name = name }) + let assignShortNameToDomain shortName domainId = + ShortNameAssigned + { DomainId = domainId + ShortName = + shortName + |> Option.filter (String.IsNullOrWhiteSpace >> not) } + |> Ok + + let refineVisionOfDomain vision domainId = + VisionRefined + { DomainId = domainId + Vision = + vision + |> Option.filter (String.IsNullOrWhiteSpace >> not) } + |> Ok + + let newDomain id name shortName vision parentDomain = result { + let! created = + name + |> nameValidation + |> Result.map (fun name -> + match parentDomain with + | Some parent -> + SubDomainCreated + { + DomainId = id + ParentDomainId = parent + Name = name + } + | None -> DomainCreated { DomainId = id; Name = name }) + + let! shortNameEvent = assignShortNameToDomain shortName id + let! visionEvent = refineVisionOfDomain vision id + + return [created; shortNameEvent; visionEvent] + } let moveDomain (state:State) parent domainId = match state with @@ -179,15 +206,6 @@ module Domain = | _ -> Ok [] - let refineVisionOfDomain vision domainId = - VisionRefined - { DomainId = domainId - Vision = - vision - |> Option.ofObj - |> Option.filter (String.IsNullOrWhiteSpace >> not) } - |> Ok - let renameDomain potentialName domainId state = match state with | Existing name -> @@ -201,16 +219,7 @@ module Domain = }) | _ -> Error DomainAlreadyDeleted - - let assignShortNameToDomain shortName domainId = - ShortNameAssigned - { DomainId = domainId - ShortName = - shortName - |> Option.ofObj - |> Option.filter (String.IsNullOrWhiteSpace >> not) } - |> Ok - + let removeDomain state domainId = match state with | Existing domain -> @@ -225,14 +234,14 @@ module Domain = let private asList item = item |> Result.map List.singleton let decide (command: Command) (state: State) = match command with - | CreateDomain (domainId, createDomain) -> newDomain domainId createDomain.Name None |> asList + | CreateDomain (domainId, createDomain) -> newDomain domainId createDomain.Name createDomain.ShortName createDomain.Vision None | CreateSubdomain (domainId, subdomainId, createDomain) -> - newDomain domainId createDomain.Name (Some subdomainId) |> asList + newDomain domainId createDomain.Name createDomain.ShortName createDomain.Vision (Some subdomainId) | RemoveDomain domainId -> removeDomain state domainId | MoveDomain (domainId, move) -> moveDomain state move.ParentDomainId domainId | RenameDomain (domainId, rename) -> renameDomain rename.Name domainId state |> asList - | RefineVision (domainId, refineVision) -> refineVisionOfDomain refineVision.Vision domainId |> asList - | AssignShortName (domainId, assignShortName) -> assignShortNameToDomain assignShortName.ShortName domainId |> asList + | RefineVision (domainId, refineVision) -> refineVisionOfDomain (refineVision.Vision |> Option.ofObj) domainId |> asList + | AssignShortName (domainId, assignShortName) -> assignShortNameToDomain (assignShortName.ShortName |> Option.ofObj) domainId |> asList diff --git a/frontend-vue/locales/en.json b/frontend-vue/locales/en.json index 2d9925ec..c6d738f6 100644 --- a/frontend-vue/locales/en.json +++ b/frontend-vue/locales/en.json @@ -58,6 +58,7 @@ "bounded_context_canvas.description.empty": "no description written yet", "bounded_context_canvas.description.error.update": "Could not update bounded context description", "bounded_context_canvas.description.title": "Description", + "bounded_context_canvas.description.description": "A few sentences describing the why and what of the context in business language. No technical details here.", "bounded_context_canvas.domain_roles.actions.collapsed.add": "add new domain role", "bounded_context_canvas.domain_roles.actions.collapsed.choose": "choose domain role from pre-defined list", "bounded_context_canvas.domain_roles.actions.open.add": "add domain role", @@ -267,6 +268,8 @@ "domains.modal.create.title": "Create a new Domain", "domains.modal.create_bounded_context.error.submit": "Could not create new bounded context", "domains.modal.create_bounded_context.form.fields.name.label": "Name of the Bounded Context", + "domains.modal.create_bounded_context.form.fields.short_name.label": "Short Key", + "domains.modal.create_bounded_context.form.fields.description.label": "Description", "domains.modal.create_bounded_context.form.submit": "create bounded context", "domains.modal.create_bounded_context.title": "Create Bounded Context", "domains.modal.create_subdomain.error.submit": "Could not create new subdomain", diff --git a/frontend-vue/src/components/bounded-context/canvas/BCCDescription.vue b/frontend-vue/src/components/bounded-context/canvas/BCCDescription.vue index c3d8c0b4..0e4151a9 100644 --- a/frontend-vue/src/components/bounded-context/canvas/BCCDescription.vue +++ b/frontend-vue/src/components/bounded-context/canvas/BCCDescription.vue @@ -12,7 +12,9 @@ diff --git a/frontend-vue/src/components/core/change-short-name/changeShortNameValidationSchema.ts b/frontend-vue/src/components/core/change-short-name/changeShortNameValidationSchema.ts index b3a05886..6d9cacca 100644 --- a/frontend-vue/src/components/core/change-short-name/changeShortNameValidationSchema.ts +++ b/frontend-vue/src/components/core/change-short-name/changeShortNameValidationSchema.ts @@ -13,6 +13,7 @@ export const shortNameValidationSchema = ( ) => zod .string() + .min(1) .max(16) .superRefine((arg: string, ctx: RefinementCtx) => { isUniqueIn(arg, ctx, { @@ -43,10 +44,7 @@ export const shortNameValidationSchema = ( .refine((term: string) => !endsWith(term, "-"), { message: "Must not end with hyphen" }) .refine((term: string) => term.split("").every((c) => allowedCharacters.includes(c)), { message: "Must only contain alphanumeric characters and hyphens", - }) - .nullable() - .optional() - .or(zod.literal("")); + }); function mapDomain(prop: string, domains: Domain[]): string { const domain = domains.find((d) => d.shortName?.toUpperCase() === prop.toUpperCase()); diff --git a/frontend-vue/src/components/domains/details/ContextureCreateBoundedContextModal.vue b/frontend-vue/src/components/domains/details/ContextureCreateBoundedContextModal.vue index 5797d66a..e8750a47 100644 --- a/frontend-vue/src/components/domains/details/ContextureCreateBoundedContextModal.vue +++ b/frontend-vue/src/components/domains/details/ContextureCreateBoundedContextModal.vue @@ -53,6 +53,27 @@ const form: DynamicFormSchema = { component: ContextureInputText, componentProps: { label: t("domains.modal.create_bounded_context.form.fields.name.label"), + description: t("bounded_context_canvas.edit.form.description.name"), + required: true, + rules: toFieldValidator(zod.string().min(1)), + }, + }, + { + name: "shortName", + component: ContextureInputText, + componentProps: { + label: t("domains.modal.create_bounded_context.form.fields.short_name.label"), + description: t("bounded_context_canvas.edit.form.description.key"), + required: true, + rules: toFieldValidator(zod.string().min(1)), + }, + }, + { + name: "description", + component: ContextureInputText, + componentProps: { + label: t("domains.modal.create_bounded_context.form.fields.description.label"), + description: t("bounded_context_canvas.description.description"), required: true, rules: toFieldValidator(zod.string().min(1)), }, diff --git a/frontend-vue/src/types/boundedContext.ts b/frontend-vue/src/types/boundedContext.ts index f92f8d5b..df89d69c 100644 --- a/frontend-vue/src/types/boundedContext.ts +++ b/frontend-vue/src/types/boundedContext.ts @@ -83,6 +83,8 @@ export interface DomainRole { export interface CreateBoundedContext { name: String; + shortName: string; + description: String; } export interface CreateMessage { From f3978647eabcd25b197d419f63440f8aa96b9bf5 Mon Sep 17 00:00:00 2001 From: K-Dud Date: Tue, 16 Jan 2024 13:23:51 +0100 Subject: [PATCH 3/6] lint --- .../src/components/domains/ContextureCreateDomainModal.vue | 2 +- .../domains/details/ContextureCreateSubdomainModal.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue b/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue index 7835a527..11073a95 100644 --- a/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue +++ b/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue @@ -70,7 +70,7 @@ const form: DynamicFormSchema = { label: t("domains.modal.create.form.fields.vision.label"), required: true, rules: toFieldValidator(zod.string().min(1)), - } + }, }, ], }; diff --git a/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue b/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue index de412141..80188db0 100644 --- a/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue +++ b/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue @@ -73,7 +73,7 @@ const form: DynamicFormSchema = { label: t("domains.modal.create.form.fields.vision.label"), required: true, rules: toFieldValidator(zod.string().min(1)), - } + }, }, ], }; From 866831498b10d024d704c289d77591340106cf04 Mon Sep 17 00:00:00 2001 From: K-Dud Date: Tue, 16 Jan 2024 13:30:32 +0100 Subject: [PATCH 4/6] Update short name validation tests --- .../change-short-name/changeShortNameValidationSchema.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend-vue/src/components/core/change-short-name/changeShortNameValidationSchema.spec.ts b/frontend-vue/src/components/core/change-short-name/changeShortNameValidationSchema.spec.ts index cdd1ecc3..406d92c0 100644 --- a/frontend-vue/src/components/core/change-short-name/changeShortNameValidationSchema.spec.ts +++ b/frontend-vue/src/components/core/change-short-name/changeShortNameValidationSchema.spec.ts @@ -2,12 +2,12 @@ import { describe, expect, test } from "vitest"; import { shortNameValidationSchema } from "~/components/core/change-short-name/changeShortNameValidationSchema"; describe("change short name validation rules", () => { - test.each(["short-name", "a1", "", null, undefined])("'%s' is valid short name", (shortName) => { + test.each(["", null, undefined])("'%s' can not be null or empty", (shortName) => { const validation = shortNameValidationSchema("", [], []); const { success } = validation.safeParse(shortName); - expect(success).toBeTruthy(); + expect(success).toBeFalsy(); }); test("cannot exceed 16 characters", () => { From c788613e960019c159add33c02befb729a317d7a Mon Sep 17 00:00:00 2001 From: K-Dud Date: Tue, 16 Jan 2024 13:43:27 +0100 Subject: [PATCH 5/6] Fix BC ShortKey validation --- .../canvas/ContextureEditBoundedContextForm.vue | 2 -- .../domains/details/ContextureCreateBoundedContextModal.vue | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend-vue/src/components/bounded-context/canvas/ContextureEditBoundedContextForm.vue b/frontend-vue/src/components/bounded-context/canvas/ContextureEditBoundedContextForm.vue index e0a3cbcd..3b077c41 100644 --- a/frontend-vue/src/components/bounded-context/canvas/ContextureEditBoundedContextForm.vue +++ b/frontend-vue/src/components/bounded-context/canvas/ContextureEditBoundedContextForm.vue @@ -5,8 +5,6 @@ :model-value="editModel.shortName" name="key" :label="t('bounded_context_canvas.edit.form.label.key')" - :rules="requiredString" - required /> = { }, { name: "shortName", - component: ContextureInputText, + component: ContextureChangeKey, componentProps: { label: t("domains.modal.create_bounded_context.form.fields.short_name.label"), description: t("bounded_context_canvas.edit.form.description.key"), - required: true, - rules: toFieldValidator(zod.string().min(1)), }, }, { From 931b94759c15aebe55ab63ddab5d5a4ec92a2906 Mon Sep 17 00:00:00 2001 From: K-Dud Date: Tue, 16 Jan 2024 13:47:28 +0100 Subject: [PATCH 6/6] Field descriptions for create domain/subdomain modals --- .../src/components/domains/ContextureCreateDomainModal.vue | 3 +++ .../domains/details/ContextureCreateSubdomainModal.vue | 3 +++ 2 files changed, 6 insertions(+) diff --git a/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue b/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue index 11073a95..f17b11fb 100644 --- a/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue +++ b/frontend-vue/src/components/domains/ContextureCreateDomainModal.vue @@ -50,6 +50,7 @@ const form: DynamicFormSchema = { component: ContextureInputText, componentProps: { label: t("domains.modal.create.form.fields.name.label"), + description: t("domains.details.edit.form.description.name"), required: true, rules: toFieldValidator(zod.string().min(1)), }, @@ -59,6 +60,7 @@ const form: DynamicFormSchema = { component: ContextureInputText, componentProps: { label: t("domains.modal.create.form.fields.short_name.label"), + description: t("domains.details.edit.form.description.key"), required: true, rules: toFieldValidator(zod.string().min(1)), }, @@ -68,6 +70,7 @@ const form: DynamicFormSchema = { component: ContextureInputText, componentProps: { label: t("domains.modal.create.form.fields.vision.label"), + description: t("domains.details.edit.form.description.vision"), required: true, rules: toFieldValidator(zod.string().min(1)), }, diff --git a/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue b/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue index 80188db0..20667ca8 100644 --- a/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue +++ b/frontend-vue/src/components/domains/details/ContextureCreateSubdomainModal.vue @@ -53,6 +53,7 @@ const form: DynamicFormSchema = { component: ContextureInputText, componentProps: { label: t("domains.modal.create_subdomain.form.fields.name.label"), + description: t("domains.details.edit.form.description.name"), required: true, rules: toFieldValidator(zod.string().min(1)), }, @@ -62,6 +63,7 @@ const form: DynamicFormSchema = { component: ContextureInputText, componentProps: { label: t("domains.modal.create.form.fields.short_name.label"), + description: t("domains.details.edit.form.description.key"), required: true, rules: toFieldValidator(zod.string().min(1)), }, @@ -71,6 +73,7 @@ const form: DynamicFormSchema = { component: ContextureInputText, componentProps: { label: t("domains.modal.create.form.fields.vision.label"), + description: t("domains.details.edit.form.description.vision"), required: true, rules: toFieldValidator(zod.string().min(1)), },