diff --git a/components/XDataTable.vue b/components/XDataTable.vue index 6416518c..e8aa5333 100644 --- a/components/XDataTable.vue +++ b/components/XDataTable.vue @@ -34,7 +34,6 @@ const props = defineProps<{ const dataTable = ref(), createDisabled = ref(false), - sortField = ref('name'), confirm = useConfirm(), createDialog = ref(), createDialogVisible = ref(false), @@ -190,8 +189,7 @@ const onEditDialogCancel = () => { + :globalFilterFields="Object.keys(props.datasource?.[0] ?? {})" :loading="props.loading" stripedRows> diff --git a/domain/relations/RequirementRelation.ts b/domain/relations/RequirementRelation.ts index 18de2c3a..b6f59f5d 100644 --- a/domain/relations/RequirementRelation.ts +++ b/domain/relations/RequirementRelation.ts @@ -21,9 +21,15 @@ export abstract class RequirementRelation extends BaseEntity { @Property({ type: 'uuid', primary: true }) id: string; + /** + * The left-hand side of the relation + */ @ManyToOne({ entity: () => Requirement, cascade: [Cascade.REMOVE] }) left: Requirement + /** + * The right-hand side of the relation + */ @ManyToOne({ entity: () => Requirement, cascade: [Cascade.REMOVE] }) right: Requirement } \ No newline at end of file diff --git a/domain/requirements/Assumption.ts b/domain/requirements/Assumption.ts index e8e87b5f..322ed113 100644 --- a/domain/requirements/Assumption.ts +++ b/domain/requirements/Assumption.ts @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const assumptionReqIdPrefix = 'E.4.' as const; +export type AssumptionReqId = `${typeof assumptionReqIdPrefix}${number}`; + /** * Posited property of the environment */ @@ -12,4 +15,7 @@ export class Assumption extends Requirement { super(props); this.req_type = ReqType.ASSUMPTION; } + + override get reqId(): AssumptionReqId | undefined { return super.reqId as AssumptionReqId | undefined } + override set reqId(value: AssumptionReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/Constraint.ts b/domain/requirements/Constraint.ts index 6bee0795..ebd68db7 100644 --- a/domain/requirements/Constraint.ts +++ b/domain/requirements/Constraint.ts @@ -4,6 +4,9 @@ import { ConstraintCategory } from './ConstraintCategory.js'; import { type Properties } from '../types/index.js'; import { ReqType } from './ReqType.js'; +export const constraintReqIdPrefix = 'E.3.' as const; +export type ConstraintReqId = `${typeof constraintReqIdPrefix}${number}`; + /** * A Constraint is a property imposed by the environment */ @@ -15,6 +18,9 @@ export class Constraint extends Requirement { this.req_type = ReqType.CONSTRAINT; } + override get reqId(): ConstraintReqId | undefined { return super.reqId as ConstraintReqId | undefined } + override set reqId(value: ConstraintReqId | undefined) { super.reqId = value } + /** * Category of the constraint */ diff --git a/domain/requirements/Effect.ts b/domain/requirements/Effect.ts index a131693a..26774f54 100644 --- a/domain/requirements/Effect.ts +++ b/domain/requirements/Effect.ts @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const effectReqIdPrefix = 'E.5.' as const; +export type EffectReqId = `${typeof effectReqIdPrefix}${number}`; + /** * Environment property affected by the system */ @@ -12,4 +15,7 @@ export class Effect extends Requirement { super(props); this.req_type = ReqType.EFFECT; } + + override get reqId(): EffectReqId | undefined { return super.reqId as EffectReqId | undefined } + override set reqId(value: EffectReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/EnvironmentComponent.ts b/domain/requirements/EnvironmentComponent.ts index f6ea5547..1469557a 100644 --- a/domain/requirements/EnvironmentComponent.ts +++ b/domain/requirements/EnvironmentComponent.ts @@ -1,8 +1,11 @@ -import { Entity, ManyToOne } from "@mikro-orm/core"; +import { Entity } from "@mikro-orm/core"; import { Component } from "./Component.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const environmentComponentReqIdPrefix = 'E.2.' as const; +export type EnvironmentComponentReqId = `${typeof environmentComponentReqIdPrefix}${number}`; + /** * Represents a component that is part of an environment. */ @@ -12,4 +15,7 @@ export class EnvironmentComponent extends Component { super(props); this.req_type = ReqType.ENVIRONMENT_COMPONENT; } + + override get reqId(): EnvironmentComponentReqId | undefined { return super.reqId as EnvironmentComponentReqId | undefined } + override set reqId(value: EnvironmentComponentReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/Epic.ts b/domain/requirements/Epic.ts new file mode 100644 index 00000000..3cc487f9 --- /dev/null +++ b/domain/requirements/Epic.ts @@ -0,0 +1,21 @@ +import { Entity } from "@mikro-orm/core"; +import { Scenario } from "./Scenario.js"; +import { ReqType } from "./ReqType.js"; + +export const epicReqIdPrefix = 'G.5.' as const; +export type EpicReqId = `${typeof epicReqIdPrefix}${number}`; + +/** + * An Epic is a collection of Use Cases and User Stories all directed towards a common goal. + * Ex: "decrease the percentage of of fraudulent sellers by 20%" + */ +@Entity({ discriminatorValue: ReqType.EPIC }) +export class Epic extends Scenario { + constructor(props: Omit) { + super(props); + this.req_type = ReqType.EPIC; + } + + override get reqId(): EpicReqId | undefined { return super.reqId as EpicReqId | undefined } + override set reqId(value: EpicReqId | undefined) { super.reqId = value } +} \ No newline at end of file diff --git a/domain/requirements/FunctionalBehavior.ts b/domain/requirements/FunctionalBehavior.ts index f6788b26..f5bcca06 100644 --- a/domain/requirements/FunctionalBehavior.ts +++ b/domain/requirements/FunctionalBehavior.ts @@ -3,6 +3,9 @@ import { Functionality } from "./Functionality.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const functionalBehaviorReqIdPrefix = 'S.2.' as const; +export type FunctionalBehaviorReqId = `${typeof functionalBehaviorReqIdPrefix}${number}`; + /** * FunctionalBehavior specifies **what** behavior the system should exhibit, i.e., * the results or effects of the system's operation. @@ -14,4 +17,7 @@ export class FunctionalBehavior extends Functionality { super(props); this.req_type = ReqType.FUNCTIONAL_BEHAVIOR; } + + override get reqId(): FunctionalBehaviorReqId | undefined { return super.reqId as FunctionalBehaviorReqId | undefined } + override set reqId(value: FunctionalBehaviorReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/GlossaryTerm.ts b/domain/requirements/GlossaryTerm.ts index f0ec86a6..49a1d45f 100644 --- a/domain/requirements/GlossaryTerm.ts +++ b/domain/requirements/GlossaryTerm.ts @@ -1,8 +1,11 @@ -import { Entity, ManyToOne } from "@mikro-orm/core"; +import { Entity } from "@mikro-orm/core"; import { Component } from "./Component.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const glossaryTermReqIdPrefix = 'E.1.' as const; +export type GlossaryTermReqId = `${typeof glossaryTermReqIdPrefix}${number}`; + /** * A word or phrase that is part of a glossary. Provides a definition for the term */ @@ -12,4 +15,7 @@ export class GlossaryTerm extends Component { super(props); this.req_type = ReqType.GLOSSARY_TERM; } + + override get reqId(): GlossaryTermReqId | undefined { return super.reqId as GlossaryTermReqId | undefined } + override set reqId(value: GlossaryTermReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/Invariant.ts b/domain/requirements/Invariant.ts index 5cf18dbe..f763fc01 100644 --- a/domain/requirements/Invariant.ts +++ b/domain/requirements/Invariant.ts @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const invariantReqIdPrefix = 'E.6.' as const; +export type InvariantReqId = `${typeof invariantReqIdPrefix}${number}`; + /** * Environment property that must be maintained. * It exists as both an assumption and an effect. @@ -14,4 +17,7 @@ export class Invariant extends Requirement { super(props); this.req_type = ReqType.INVARIANT; } + + override get reqId(): InvariantReqId | undefined { return super.reqId as InvariantReqId | undefined } + override set reqId(value: InvariantReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/Justification.ts b/domain/requirements/Justification.ts index 4965b92f..1e863fbd 100644 --- a/domain/requirements/Justification.ts +++ b/domain/requirements/Justification.ts @@ -4,7 +4,8 @@ import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; /** - * Explanation of a project or system property in reference to a goal or environment property + * Explanation of a project or system property in reference to a goal or environment property. + * A requirement is justified if it helps to achieve a goal or to satisfy an environment property (constraint). */ @Entity({ discriminatorValue: ReqType.JUSTIFICATION }) export class Justification extends MetaRequirement { diff --git a/domain/requirements/Limit.ts b/domain/requirements/Limit.ts index c711d355..0fb03828 100644 --- a/domain/requirements/Limit.ts +++ b/domain/requirements/Limit.ts @@ -3,6 +3,9 @@ import { Requirement } from "./Requirement.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const limitReqIdPrefix = 'G.6.' as const; +export type LimitReqId = `${typeof limitReqIdPrefix}${number}`; + /** * An Exclusion from the scope of requirements */ @@ -12,4 +15,7 @@ export class Limit extends Requirement { super(props); this.req_type = ReqType.LIMIT; } + + override get reqId(): LimitReqId | undefined { return super.reqId as LimitReqId | undefined } + override set reqId(value: LimitReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/NonFunctionalBehavior.ts b/domain/requirements/NonFunctionalBehavior.ts index bb9109bd..5194e41a 100644 --- a/domain/requirements/NonFunctionalBehavior.ts +++ b/domain/requirements/NonFunctionalBehavior.ts @@ -3,6 +3,9 @@ import { Functionality } from "./Functionality.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const nonFunctionalBehaviorReqIdPrefix = 'S.2.' as const; +export type NonFunctionalBehaviorReqId = `${typeof nonFunctionalBehaviorReqIdPrefix}${number}`; + /** * NonFunctionalBehavior is a type of Behavior that is not directly related to the functionality of a system. * It specifies **how** the system should behave, i.e., the qualities that the system must exhibit. @@ -14,4 +17,7 @@ export class NonFunctionalBehavior extends Functionality { super(props); this.req_type = ReqType.NON_FUNCTIONAL_BEHAVIOR; } + + override get reqId(): NonFunctionalBehaviorReqId | undefined { return super.reqId as NonFunctionalBehaviorReqId | undefined } + override set reqId(value: NonFunctionalBehaviorReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/Obstacle.ts b/domain/requirements/Obstacle.ts index 5b30bd68..9b9cb58d 100644 --- a/domain/requirements/Obstacle.ts +++ b/domain/requirements/Obstacle.ts @@ -3,6 +3,9 @@ import { Goal } from "./Goal.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const obstacleReqIdPrefix = 'G.2.' as const; +export type ObstacleReqId = `${typeof obstacleReqIdPrefix}${number}`; + /** * Obstacles are the challenges that prevent the goals from being achieved. */ @@ -12,4 +15,7 @@ export class Obstacle extends Goal { super(props); this.req_type = ReqType.OBSTACLE; } + + override get reqId(): ObstacleReqId | undefined { return super.reqId as ObstacleReqId | undefined } + override set reqId(value: ObstacleReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/Outcome.ts b/domain/requirements/Outcome.ts index 308a1442..450588e6 100644 --- a/domain/requirements/Outcome.ts +++ b/domain/requirements/Outcome.ts @@ -3,6 +3,10 @@ import { Goal } from "./Goal.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +// FIXME: The Context and overall objective entry is an Outcome, but the req_id is G.1.0 +export const outcomeReqIdPrefix = 'G.3.' as const; +export type OutcomeReqId = `${typeof outcomeReqIdPrefix}${number}`; + /** * A result desired by an organization */ @@ -12,4 +16,7 @@ export class Outcome extends Goal { super(props); this.req_type = ReqType.OUTCOME; } + + override get reqId(): OutcomeReqId | undefined { return super.reqId as OutcomeReqId | undefined } + override set reqId(value: OutcomeReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/Person.ts b/domain/requirements/Person.ts index 96729133..5ac60555 100644 --- a/domain/requirements/Person.ts +++ b/domain/requirements/Person.ts @@ -3,6 +3,9 @@ import { Actor } from "./Actor.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const personReqIdPrefix = 'P.1.' as const; +export type PersonReqId = `${typeof personReqIdPrefix}${number}`; + /** * A person is a member of the Project staff */ @@ -14,6 +17,9 @@ export class Person extends Actor { this.email = email; } + override get reqId(): PersonReqId | undefined { return super.reqId as PersonReqId | undefined } + override set reqId(value: PersonReqId | undefined) { super.reqId = value } + /** * Email address of the person */ diff --git a/domain/requirements/ReqType.ts b/domain/requirements/ReqType.ts index 9b5276ec..455f02ad 100644 --- a/domain/requirements/ReqType.ts +++ b/domain/requirements/ReqType.ts @@ -10,6 +10,7 @@ export enum ReqType { CONSTRAINT = 'constraint', EFFECT = 'effect', ENVIRONMENT_COMPONENT = 'environment_component', + EPIC = 'epic', EXAMPLE = 'example', FUNCTIONAL_BEHAVIOR = 'functional_behavior', FUNCTIONALITY = 'functionality', diff --git a/domain/requirements/Requirement.ts b/domain/requirements/Requirement.ts index 2b651bf1..0a5a7328 100644 --- a/domain/requirements/Requirement.ts +++ b/domain/requirements/Requirement.ts @@ -4,6 +4,9 @@ import { type Properties } from '../types/index.js'; import { ReqType } from './ReqType.js'; import { AppUser } from '../application/AppUser.js'; +export type ReqIdPrefix = `${'P' | 'E' | 'G' | 'S'}.${number}.` +export type ReqId = `${ReqIdPrefix}${number}` + /** * A Requirement is a statement that specifies a property. */ @@ -33,6 +36,16 @@ export abstract class Requirement extends BaseEntity { @Property({ type: 'uuid', primary: true }) id: string; + private _reqId?: ReqId + + /** + * The user-friendly identifier of the requirement that is unique within its parent + */ + // This is nullable because MetaRequirements, Silence, and Noise do not have a reqId + @Property({ type: 'text', nullable: true }) + get reqId(): ReqId | undefined { return this._reqId } + set reqId(value: ReqId | undefined) { this._reqId = value } + // A property is a Predicate formalizing its associated statement. // see: https://github.com/final-hill/cathedral/issues/368 // property!: string diff --git a/domain/requirements/Scenario.ts b/domain/requirements/Scenario.ts index 9bf3de9f..3801af1b 100644 --- a/domain/requirements/Scenario.ts +++ b/domain/requirements/Scenario.ts @@ -3,6 +3,7 @@ import { Example } from "./Example.js"; import { Stakeholder } from "./Stakeholder.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +import { Outcome } from "./Outcome.js"; /** * A Scenario specifies system behavior by describing paths @@ -10,9 +11,10 @@ import { ReqType } from "./ReqType.js"; */ @Entity({ abstract: true, discriminatorValue: ReqType.SCENARIO }) export abstract class Scenario extends Example { - constructor({ primaryActor, ...rest }: Properties>) { + constructor({ primaryActor, outcome, ...rest }: Properties>) { super(rest); this.primaryActor = primaryActor; + this.outcome = outcome; this.req_type = ReqType.SCENARIO; } @@ -21,4 +23,10 @@ export abstract class Scenario extends Example { */ @ManyToOne({ entity: () => Stakeholder }) primaryActor?: Stakeholder; + + /** + * The outcome (goal) that the scenario is aiming to achieve. + */ + @ManyToOne({ entity: () => Outcome }) + outcome?: Outcome; } \ No newline at end of file diff --git a/domain/requirements/Stakeholder.ts b/domain/requirements/Stakeholder.ts index ee7f8974..4462eaf7 100644 --- a/domain/requirements/Stakeholder.ts +++ b/domain/requirements/Stakeholder.ts @@ -1,10 +1,13 @@ -import { Entity, Enum, ManyToOne, Property } from "@mikro-orm/core"; +import { Entity, Enum, Property } from "@mikro-orm/core"; import { Component } from "./Component.js"; import { StakeholderCategory } from "./StakeholderCategory.js"; import { StakeholderSegmentation } from "./StakeholderSegmentation.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const stakeholderReqIdPrefix = 'G.7.' as const; +export type StakeholderReqId = `${typeof stakeholderReqIdPrefix}${number}`; + /** * A human actor who may affect or be affected by a project or its associated system */ @@ -19,6 +22,9 @@ export class Stakeholder extends Component { this.category = props.category; } + override get reqId(): StakeholderReqId | undefined { return super.reqId as StakeholderReqId | undefined } + override set reqId(value: StakeholderReqId | undefined) { super.reqId = value } + /** * The segmentation of the stakeholder. */ diff --git a/domain/requirements/SystemComponent.ts b/domain/requirements/SystemComponent.ts index c0b68a82..fe9669c1 100644 --- a/domain/requirements/SystemComponent.ts +++ b/domain/requirements/SystemComponent.ts @@ -1,8 +1,11 @@ -import { Entity, ManyToOne } from "@mikro-orm/core"; +import { Entity } from "@mikro-orm/core"; import { Component } from "./Component.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const systemComponentReqIdPrefix = 'S.1.' as const; +export type SystemComponentReqId = `${typeof systemComponentReqIdPrefix}${number}`; + /** * A component of a system */ @@ -12,4 +15,7 @@ export class SystemComponent extends Component { super(props); this.req_type = ReqType.SYSTEM_COMPONENT; } + + override get reqId(): SystemComponentReqId | undefined { return super.reqId as SystemComponentReqId | undefined } + override set reqId(value: SystemComponentReqId | undefined) { super.reqId = value } } \ No newline at end of file diff --git a/domain/requirements/UseCase.ts b/domain/requirements/UseCase.ts index cd081707..83cb9f3a 100644 --- a/domain/requirements/UseCase.ts +++ b/domain/requirements/UseCase.ts @@ -5,6 +5,9 @@ import { Scenario } from "./Scenario.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const useCaseReqIdPrefix = 'S.4.' as const; +export type UseCaseReqId = `${typeof useCaseReqIdPrefix}${number}`; + /** * A Use Case specifies the scenario of a complete * interaction of a user through a system. @@ -16,7 +19,6 @@ export class UseCase extends Scenario { this.req_type = ReqType.USE_CASE; this.scope = props.scope; this.level = props.level; - this.goalInContext = props.goalInContext; this.precondition = props.precondition; this.triggerId = props.triggerId; this.mainSuccessScenario = props.mainSuccessScenario; @@ -25,6 +27,9 @@ export class UseCase extends Scenario { // this.stakeHoldersAndInterests = props.stakeHoldersAndInterests; } + override get reqId(): UseCaseReqId | undefined { return super.reqId as UseCaseReqId | undefined } + override set reqId(value: UseCaseReqId | undefined) { super.reqId = value } + /** * The scope of the use case. */ @@ -39,13 +44,6 @@ export class UseCase extends Scenario { @Property({ type: 'string' }) level: string; - /** - * The goal in context of the use case. - */ - // TODO: is this just the Goal.description? - @Property({ type: 'string' }) - goalInContext: string; - /** * The precondition is an Assumption that must be true before the use case can start. */ diff --git a/domain/requirements/UserStory.ts b/domain/requirements/UserStory.ts index c196bb6b..fde1141a 100644 --- a/domain/requirements/UserStory.ts +++ b/domain/requirements/UserStory.ts @@ -1,10 +1,12 @@ import { Entity, ManyToOne } from "@mikro-orm/core"; import { FunctionalBehavior } from "./FunctionalBehavior.js"; -import { Outcome } from "./Outcome.js"; import { Scenario } from "./Scenario.js"; import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; +export const userStoryReqIdPrefix = 'S.4.' as const; +export type UserStoryReqId = `${typeof userStoryReqIdPrefix}${number}`; + /** * A User Story specifies the handling of a specific user need. * @@ -19,19 +21,15 @@ export class UserStory extends Scenario { constructor(props: Properties>) { super(props); this.req_type = ReqType.USER_STORY; - this.outcome = props.outcome; this.functionalBehavior = props.functionalBehavior; } + override get reqId(): UserStoryReqId | undefined { return super.reqId as UserStoryReqId | undefined } + override set reqId(value: UserStoryReqId | undefined) { super.reqId = value } + /** * The action that the user wants to perform. */ @ManyToOne({ entity: () => FunctionalBehavior }) functionalBehavior?: FunctionalBehavior; - - /** - * The outcome that the story is aiming to achieve. - */ - @ManyToOne({ entity: () => Outcome }) - outcome?: Outcome; } \ No newline at end of file diff --git a/domain/requirements/index.ts b/domain/requirements/index.ts index 6f0ef572..963afc9b 100644 --- a/domain/requirements/index.ts +++ b/domain/requirements/index.ts @@ -34,6 +34,7 @@ export * from './Responsibility.js'; export * from './ReqType.js'; export * from './Role.js'; export * from './Scenario.js'; +export * from './Epic.js'; export * from './Silence.js'; export * from './SystemComponent.js'; export * from './Task.js'; diff --git a/domain/types/index.ts b/domain/types/index.ts index 4a4a9425..b372aa3c 100644 --- a/domain/types/index.ts +++ b/domain/types/index.ts @@ -10,6 +10,8 @@ export type Properties = Pick = R & { parentComponent?: string, solutionId: string diff --git a/e2e/identification-principle.test.disabled b/e2e/identification-principle.test.disabled new file mode 100644 index 00000000..f92b8ea4 --- /dev/null +++ b/e2e/identification-principle.test.disabled @@ -0,0 +1,12 @@ +/* + TODO: Implement testing + + Identification Principle: + Every element appearing in requirements must have a unique number or key allowing unambiguous identification. + + This is currently accomplished by assigning a reqId property to the `belongs` relationship + with a uniqueness constraint on (reqId, right) in the database. + + THis should enforce uniqueness of reqId within a solution. + +*/ \ No newline at end of file diff --git a/migrations/.snapshot-cathedral.json b/migrations/.snapshot-cathedral.json index ce4f994f..810d91e6 100644 --- a/migrations/.snapshot-cathedral.json +++ b/migrations/.snapshot-cathedral.json @@ -193,6 +193,7 @@ "constraint", "effect", "environment_component", + "epic", "example", "functional_behavior", "functionality", @@ -226,6 +227,15 @@ ], "mappedType": "enum" }, + "req_id": { + "name": "req_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, "name": { "name": "name", "type": "varchar(100)", @@ -311,6 +321,15 @@ "nullable": true, "mappedType": "uuid" }, + "outcome_id": { + "name": "outcome_id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "uuid" + }, "slug": { "name": "slug", "type": "varchar(255)", @@ -390,16 +409,6 @@ "length": 255, "mappedType": "string" }, - "goal_in_context": { - "name": "goal_in_context", - "type": "varchar(255)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 255, - "mappedType": "string" - }, "precondition_id": { "name": "precondition_id", "type": "uuid", @@ -455,15 +464,6 @@ "primary": false, "nullable": true, "mappedType": "uuid" - }, - "outcome_id": { - "name": "outcome_id", - "type": "uuid", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "uuid" } }, "name": "requirement", @@ -527,10 +527,10 @@ "deleteRule": "set null", "updateRule": "cascade" }, - "requirement_precondition_id_foreign": { - "constraintName": "requirement_precondition_id_foreign", + "requirement_outcome_id_foreign": { + "constraintName": "requirement_outcome_id_foreign", "columnNames": [ - "precondition_id" + "outcome_id" ], "localTableName": "public.requirement", "referencedColumnNames": [ @@ -540,10 +540,10 @@ "deleteRule": "set null", "updateRule": "cascade" }, - "requirement_success_guarantee_id_foreign": { - "constraintName": "requirement_success_guarantee_id_foreign", + "requirement_precondition_id_foreign": { + "constraintName": "requirement_precondition_id_foreign", "columnNames": [ - "success_guarantee_id" + "precondition_id" ], "localTableName": "public.requirement", "referencedColumnNames": [ @@ -553,10 +553,10 @@ "deleteRule": "set null", "updateRule": "cascade" }, - "requirement_functional_behavior_id_foreign": { - "constraintName": "requirement_functional_behavior_id_foreign", + "requirement_success_guarantee_id_foreign": { + "constraintName": "requirement_success_guarantee_id_foreign", "columnNames": [ - "functional_behavior_id" + "success_guarantee_id" ], "localTableName": "public.requirement", "referencedColumnNames": [ @@ -566,10 +566,10 @@ "deleteRule": "set null", "updateRule": "cascade" }, - "requirement_outcome_id_foreign": { - "constraintName": "requirement_outcome_id_foreign", + "requirement_functional_behavior_id_foreign": { + "constraintName": "requirement_functional_behavior_id_foreign", "columnNames": [ - "outcome_id" + "functional_behavior_id" ], "localTableName": "public.requirement", "referencedColumnNames": [ diff --git a/migrations/Migration20241101165305.ts b/migrations/Migration20241101165305.ts new file mode 100644 index 00000000..e01afdb3 --- /dev/null +++ b/migrations/Migration20241101165305.ts @@ -0,0 +1,148 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20241101165305 extends Migration { + + override async up(): Promise { + this.addSql(`alter table "requirement" drop constraint if exists "requirement_req_type_check";`); + + this.addSql(`alter table "requirement" drop column "goal_in_context";`); + + this.addSql(`alter table "requirement" add column "req_id" text null;`); + this.addSql(`alter table "requirement" add constraint "requirement_req_type_check" check("req_type" in ('actor', 'assumption', 'behavior', 'component', 'constraint', 'effect', 'environment_component', 'epic', 'example', 'functional_behavior', 'functionality', 'glossary_term', 'goal', 'hint', 'invariant', 'justification', 'limit', 'meta_requirement', 'noise', 'non_functional_behavior', 'obstacle', 'organization', 'outcome', 'parsed_requirement', 'person', 'product', 'requirement', 'responsibility', 'role', 'scenario', 'silence', 'solution', 'stakeholder', 'system_component', 'task', 'test_case', 'use_case', 'user_story'));`); + + // change the justification requirements to outcome requirements + this.addSql(` + UPDATE requirement + SET req_type = 'outcome' + WHERE req_type = 'justification' + AND name = 'G.1'; + `) + + // Temporary SQL function for generating req_id's based on the solution_id and prefix + this.addSql(` + CREATE OR REPLACE FUNCTION generate_req_id(solution_id UUID, prefix TEXT) + RETURNS TEXT AS $$ + DECLARE + max_suffix INTEGER; + new_req_id TEXT; + BEGIN + -- Find the maximum numeric suffix for the given prefix within the specified solution + SELECT COALESCE(MAX((substring(r.req_id from length(prefix) + 1 for 10))::INTEGER), 0) + INTO max_suffix + FROM requirement r + JOIN requirement_relation rr ON rr.left_id = r.id + WHERE r.req_id LIKE prefix || '%' + AND rr.right_id = solution_id + AND rr.rel_type = 'belongs' + AND r.is_silence = false; + + -- Generate the new reqId by incrementing the suffix + new_req_id := prefix || (max_suffix + 1)::TEXT; + + RETURN new_req_id; + END; + $$ LANGUAGE plpgsql; + `) + + // Generate req_id's for existing requirements + this.addSql(` + DO $$ + DECLARE + solution RECORD; + req RECORD; + reqIdPrefix TEXT; + new_req_id TEXT; + BEGIN + -- Loop over each solution in the requirements table + FOR solution IN + SELECT * FROM requirement WHERE req_type = 'solution' + LOOP + -- For each solution, find all non-silence requirements that belong to it + FOR req IN + SELECT r.* FROM requirement r + JOIN requirement_relation rr ON rr.left_id = r.id + WHERE rr.right_id = solution.id + AND r.req_id IS NULL -- Only generate for null req_id + AND r.is_silence = false -- Only generate for non-silence requirements + AND r.req_type in ('person', 'glossary_term', 'environment_component', 'constraint', 'assumption', 'effect', 'invariant', 'outcome', 'obstacle', 'epic', 'limit', 'stakeholder', 'system_component', 'functional_behavior', 'non_functional_behavior', 'use_case', 'user_story') + LOOP + -- Determine the prefix based on req_type + CASE req.req_type + WHEN 'person' THEN reqIdPrefix := 'P.1.'; + WHEN 'glossary_term' THEN reqIdPrefix := 'E.1.'; + WHEN 'environment_component' THEN reqIdPrefix := 'E.2.'; + WHEN 'constraint' THEN reqIdPrefix := 'E.3.'; + WHEN 'assumption' THEN reqIdPrefix := 'E.4.'; + WHEN 'effect' THEN reqIdPrefix := 'E.5.'; + WHEN 'invariant' THEN reqIdPrefix := 'E.6.'; + WHEN 'obstacle' THEN reqIdPrefix := 'G.2.'; + WHEN 'outcome' THEN reqIdPrefix := 'G.3.'; + WHEN 'epic' THEN reqIdPrefix := 'G.5.'; + WHEN 'limit' THEN reqIdPrefix := 'G.6.'; + WHEN 'stakeholder' THEN reqIdPrefix := 'G.7.'; + WHEN 'system_component' THEN reqIdPrefix := 'S.1.'; + WHEN 'functional_behavior' THEN reqIdPrefix := 'S.2.'; + WHEN 'non_functional_behavior' THEN reqIdPrefix := 'S.2.'; + WHEN 'use_case' THEN reqIdPrefix := 'S.4.'; + WHEN 'user_story' THEN reqIdPrefix := 'S.4.'; + END CASE; + + -- Generate new req_id using the prefix, passing the solution ID + new_req_id := generate_req_id(solution.id, reqIdPrefix); + + -- Update the requirement with the new req_id + UPDATE requirement + SET req_id = new_req_id + WHERE id = req.id; + END LOOP; + END LOOP; + END $$; + `) + + // delete the generate_req_id function + this.addSql(`DROP FUNCTION generate_req_id(UUID, TEXT);`); + + // Set the current Goal situation Obstacle to the new req_id G.2.0 + this.addSql(` + UPDATE requirement + SET req_id = 'G.2.0' + WHERE req_type = 'obstacle' + AND name = 'G.2'; + `) + + // Change the 'G.1' goal to req_type = 'outcome', + // Set its req_id to 'G.1.0' + this.addSql(` + UPDATE requirement + SET req_type = 'outcome', req_id = 'G.1.0' + WHERE req_type = 'goal' + AND name = 'G.1'; + `) + } + + override async down(): Promise { + this.addSql(`alter table "requirement" drop constraint if exists "requirement_req_type_check";`); + + // Change the 'G.1.0' outcome to req_type = 'goal' + this.addSql(` + UPDATE requirement + SET req_type = 'goal' + WHERE req_type = 'outcome' + AND req_id = 'G.1.0'; + `) + + this.addSql(`alter table "requirement" drop column "req_id";`); + + this.addSql(`alter table "requirement" add column "goal_in_context" varchar(255) null;`); + this.addSql(`alter table "requirement" add constraint "requirement_req_type_check" check("req_type" in ('actor', 'assumption', 'behavior', 'component', 'constraint', 'effect', 'environment_component', 'example', 'functional_behavior', 'functionality', 'glossary_term', 'goal', 'hint', 'invariant', 'justification', 'limit', 'meta_requirement', 'noise', 'non_functional_behavior', 'obstacle', 'organization', 'outcome', 'parsed_requirement', 'person', 'product', 'requirement', 'responsibility', 'role', 'scenario', 'silence', 'solution', 'stakeholder', 'system_component', 'task', 'test_case', 'use_case', 'user_story'));`); + + // change the outcome requirements to justification requirements + this.addSql(` + UPDATE requirement + SET req_type = 'justification' + WHERE req_type = 'outcome' + AND name = 'G.1'; + `) + } + +} diff --git a/pages/o/[organization-slug]/[solution-slug]/environment/assumptions.client.vue b/pages/o/[organization-slug]/[solution-slug]/environment/assumptions.client.vue index 014f88c8..ecdaf00f 100644 --- a/pages/o/[organization-slug]/[solution-slug]/environment/assumptions.client.vue +++ b/pages/o/[organization-slug]/[solution-slug]/environment/assumptions.client.vue @@ -19,6 +19,7 @@ if (getSolutionError.value) interface AssumptionViewModel { id: string, + reqId: string, name: string, description: string, lastModified: Date @@ -52,8 +53,7 @@ const onDelete = async (id: string) => { await $fetch(`/api/assumption/${id}`, { method: 'delete', body: { solutionId } - }) - .catch((e) => $eventBus.$emit('page-error', e)) + }).catch((e) => $eventBus.$emit('page-error', e)) refresh() } @@ -79,7 +79,8 @@ const onUpdate = async (data: AssumptionViewModel) => { An example of an assumption would be: "Screen resolution will not change during the execution of the program".

- diff --git a/pages/o/[organization-slug]/[solution-slug]/environment/components.client.vue b/pages/o/[organization-slug]/[solution-slug]/environment/components.client.vue index 466fffcb..3e539537 100644 --- a/pages/o/[organization-slug]/[solution-slug]/environment/components.client.vue +++ b/pages/o/[organization-slug]/[solution-slug]/environment/components.client.vue @@ -6,6 +6,7 @@ definePageMeta({ name: 'Environment Components' }) interface EnvironmentComponentViewModel { id: string; + reqId: string; name: string; description: string; lastModified: Date; @@ -73,7 +74,8 @@ const onUpdate = async (data: EnvironmentComponentViewModel) => { Environment components are the EXTERNAL elements that the system interacts with. These external components expose interfaces that the system uses to communicate with.

- diff --git a/pages/o/[organization-slug]/[solution-slug]/environment/constraints.client.vue b/pages/o/[organization-slug]/[solution-slug]/environment/constraints.client.vue index fb1931ef..d19d252e 100644 --- a/pages/o/[organization-slug]/[solution-slug]/environment/constraints.client.vue +++ b/pages/o/[organization-slug]/[solution-slug]/environment/constraints.client.vue @@ -7,6 +7,7 @@ definePageMeta({ name: 'Constraints' }) interface ConstraintViewModel { id: string; + reqId: string; name: string; description: string; category: ConstraintCategory; @@ -78,7 +79,7 @@ const onUpdate = async (data: ConstraintViewModel) => { Environmental constraints are the limitations and obligations that the environment imposes on the project and system.

- { An Effect is an environment property affected by a System. Example: "The running system will cause the temperature of the room to increase."

- diff --git a/pages/o/[organization-slug]/[solution-slug]/environment/glossary.client.vue b/pages/o/[organization-slug]/[solution-slug]/environment/glossary.client.vue index 5e0ce422..d05a83c2 100644 --- a/pages/o/[organization-slug]/[solution-slug]/environment/glossary.client.vue +++ b/pages/o/[organization-slug]/[solution-slug]/environment/glossary.client.vue @@ -1,6 +1,7 @@ @@ -22,9 +22,17 @@ const links = [
+ :to="{ name: link.name, params: { organizationslug, solutionslug } }" class="col-fixed w-2 mr-4" + v-badge="link.reqId">
- \ No newline at end of file + + \ No newline at end of file diff --git a/pages/o/[organization-slug]/[solution-slug]/environment/invariants.client.vue b/pages/o/[organization-slug]/[solution-slug]/environment/invariants.client.vue index 12e8794d..9930b761 100644 --- a/pages/o/[organization-slug]/[solution-slug]/environment/invariants.client.vue +++ b/pages/o/[organization-slug]/[solution-slug]/environment/invariants.client.vue @@ -1,6 +1,7 @@ @@ -24,9 +23,17 @@ const links = [
+ :to="{ name: link.name, params: { organizationslug, solutionslug } }" class="col-fixed w-2 mr-4" + v-badge="link.reqId">
- \ No newline at end of file + + \ No newline at end of file diff --git a/pages/o/[organization-slug]/[solution-slug]/goals/limitations.client.vue b/pages/o/[organization-slug]/[solution-slug]/goals/limitations.client.vue index 388f0349..8b0f2aae 100644 --- a/pages/o/[organization-slug]/[solution-slug]/goals/limitations.client.vue +++ b/pages/o/[organization-slug]/[solution-slug]/goals/limitations.client.vue @@ -1,6 +1,7 @@ - - \ No newline at end of file diff --git a/pages/o/[organization-slug]/[solution-slug]/goals/outcomes.client.vue b/pages/o/[organization-slug]/[solution-slug]/goals/outcomes.client.vue index e14beaa2..08d07c82 100644 --- a/pages/o/[organization-slug]/[solution-slug]/goals/outcomes.client.vue +++ b/pages/o/[organization-slug]/[solution-slug]/goals/outcomes.client.vue @@ -17,6 +17,7 @@ if (getSolutionError.value) interface OutcomeViewModel { id: string; + reqId: string; name: string; description: string; lastModified: Date; @@ -27,7 +28,7 @@ const { data: outcomes, refresh, status, error: getOutcomesError } = await useFe transform: (data) => data.map((item) => { item.lastModified = new Date(item.lastModified) return item - }) + }).filter((item) => item.reqId !== 'G.1.0') }) if (getOutcomesError.value) @@ -75,7 +76,8 @@ const onDelete = async (id: string) => { of the system that will be achieved by the associated project.

- diff --git a/pages/o/[organization-slug]/[solution-slug]/goals/scenarios.client.vue b/pages/o/[organization-slug]/[solution-slug]/goals/scenarios.client.vue index 1f8f30c6..d06d686b 100644 --- a/pages/o/[organization-slug]/[solution-slug]/goals/scenarios.client.vue +++ b/pages/o/[organization-slug]/[solution-slug]/goals/scenarios.client.vue @@ -17,8 +17,9 @@ const { $eventBus } = useNuxtApp(), if (getSolutionError.value) $eventBus.$emit('page-error', getSolutionError.value); -interface UserStoryViewModel { +interface EpicViewModel { id: string; + reqId: string; name: string; primaryActor: StakeholderViewModel; functionalBehavior: FunctionalBehaviorViewModel; @@ -42,12 +43,12 @@ interface OutcomeViewModel { } const [ - { data: userStories, refresh, status, error: getUserStoriesError }, + { data: epics, refresh, status, error: getEpicsError }, { data: roles, error: getRolesError }, { data: functionalBehaviors, error: getFunctionalBehaviorsError }, { data: outcomes, error: getOutcomesError }, ] = await Promise.all([ - useFetch(`/api/user-story`, { + useFetch(`/api/epic`, { query: { solutionId }, transform: (data) => data.map((item) => { item.lastModified = new Date(item.lastModified) @@ -59,8 +60,8 @@ const [ useFetch(`/api/outcome`, { query: { solutionId } }) ]) -if (getUserStoriesError.value) - $eventBus.$emit('page-error', getUserStoriesError.value); +if (getEpicsError.value) + $eventBus.$emit('page-error', getEpicsError.value); if (getRolesError.value) $eventBus.$emit('page-error', getRolesError.value); if (getFunctionalBehaviorsError.value) @@ -68,11 +69,11 @@ if (getFunctionalBehaviorsError.value) if (getOutcomesError.value) $eventBus.$emit('page-error', getOutcomesError.value); -const onUserStoryCreate = async (userStory: UserStoryViewModel) => { - await $fetch(`/api/user-story`, { +const onEpicCreate = async (epic: EpicViewModel) => { + await $fetch(`/api/epic`, { method: 'POST', body: { - ...userStory, + ...epic, solutionId, description: '', priority: MoscowPriority.MUST @@ -82,11 +83,11 @@ const onUserStoryCreate = async (userStory: UserStoryViewModel) => { refresh(); } -const onUserStoryUpdate = async (userStory: UserStoryViewModel) => { - await $fetch(`/api/user-story/${userStory.id}`, { +const onEpicUpdate = async (epic: EpicViewModel) => { + await $fetch(`/api/epic/${epic.id}`, { method: 'PUT', body: { - ...userStory, + ...epic, solutionId, description: '', priority: MoscowPriority.MUST @@ -96,8 +97,8 @@ const onUserStoryUpdate = async (userStory: UserStoryViewModel) => { refresh(); } -const onUserStoryDelete = async (id: string) => { - await $fetch(`/api/user-story/${id}`, { +const onEpicDelete = async (id: string) => { + await $fetch(`/api/epic/${id}`, { method: 'DELETE', body: { solutionId } }).catch((e) => $eventBus.$emit('page-error', e)); @@ -123,6 +124,7 @@ const onUserStoryDelete = async (id: string) => {

+ }" :datasource="epics" :onCreate="onEpicCreate" :onUpdate="onEpicUpdate" :onDelete="onEpicDelete" + :loading="status === 'pending'" :organizationSlug="organizationslug" entityName="UserStory" + :showRecycleBin="true"> \ No newline at end of file diff --git a/pages/o/[organization-slug]/[solution-slug]/goals/situation.client.vue b/pages/o/[organization-slug]/[solution-slug]/goals/situation.client.vue index 4daaca62..d0d92ee7 100644 --- a/pages/o/[organization-slug]/[solution-slug]/goals/situation.client.vue +++ b/pages/o/[organization-slug]/[solution-slug]/goals/situation.client.vue @@ -1,8 +1,10 @@