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 @@
-
-
-
- Obstacles are the challenges that prevent the goals from being achieved.
-
-
-
-
-
\ 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 @@
@@ -53,4 +98,15 @@ watch(situationDescription, debounce(() => {
+
+ Obstacles
+
+ Obstacles are the challenges that prevent the goals from being achieved.
+
+
+
\ No newline at end of file
diff --git a/pages/o/[organization-slug]/[solution-slug]/goals/stakeholders.client.vue b/pages/o/[organization-slug]/[solution-slug]/goals/stakeholders.client.vue
index 63e9cdaa..74575c71 100644
--- a/pages/o/[organization-slug]/[solution-slug]/goals/stakeholders.client.vue
+++ b/pages/o/[organization-slug]/[solution-slug]/goals/stakeholders.client.vue
@@ -5,6 +5,7 @@ import { StakeholderSegmentation } from '~/domain/requirements/StakeholderSegmen
interface StakeholderViewModel {
id: string;
+ reqId: string;
name: string;
description: string;
category: string;
@@ -138,6 +139,7 @@ const onDelete = async (id: string) => {
refreshStakeholders()">
+ :to="{ name: link.name, params: { solutionslug, organizationslug } }" class="col-fixed w-2 mr-4"
+ v-badge="link.reqId">
@@ -154,4 +155,11 @@ const resetRawRequirement = () => {
-->
-
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/pages/o/[organization-slug]/[solution-slug]/project/index.client.vue b/pages/o/[organization-slug]/[solution-slug]/project/index.client.vue
index 67e11512..fa623e60 100644
--- a/pages/o/[organization-slug]/[solution-slug]/project/index.client.vue
+++ b/pages/o/[organization-slug]/[solution-slug]/project/index.client.vue
@@ -4,7 +4,7 @@ definePageMeta({ name: 'Project' })
const { solutionslug, organizationslug } = useRoute('Project').params,
links = [
- { name: 'Roles & Personnel' as const, icon: 'pi-users', label: 'Roles & Personnel' }
+ { name: 'Roles & Personnel' as const, icon: 'pi-users', label: 'Roles & Personnel', reqId: 'P.1' }
]
@@ -15,9 +15,17 @@ const { solutionslug, organizationslug } = useRoute('Project').params,
+ :to="{ name: link.name, params: { solutionslug, organizationslug } }" 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]/project/roles-personnel.client.vue b/pages/o/[organization-slug]/[solution-slug]/project/roles-personnel.client.vue
index 4ec9c7d1..a931b838 100644
--- a/pages/o/[organization-slug]/[solution-slug]/project/roles-personnel.client.vue
+++ b/pages/o/[organization-slug]/[solution-slug]/project/roles-personnel.client.vue
@@ -1,6 +1,7 @@
@@ -16,9 +16,17 @@ const links = [
+ :to="{ name: link.name, params: { solutionslug, organizationslug } }" 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]/system/scenarios.client.vue b/pages/o/[organization-slug]/[solution-slug]/system/scenarios.client.vue
index 7d1a37b0..3c195545 100644
--- a/pages/o/[organization-slug]/[solution-slug]/system/scenarios.client.vue
+++ b/pages/o/[organization-slug]/[solution-slug]/system/scenarios.client.vue
@@ -3,6 +3,7 @@ import { MoscowPriority } from '~/domain/requirements/MoscowPriority.js';
type AssumptionViewModel = {
id: string;
+ reqId: string;
name: string;
description: string;
lastModified: Date;
@@ -10,6 +11,7 @@ type AssumptionViewModel = {
type EffectViewModel = {
id: string;
+ reqId: string;
name: string;
description: string;
lastModified: Date;
@@ -17,6 +19,7 @@ type EffectViewModel = {
type FunctionalBehaviorViewModel = {
id: string;
+ reqId: string;
name: string;
description: string;
lastModified: Date;
@@ -24,6 +27,7 @@ type FunctionalBehaviorViewModel = {
type OutcomeViewModel = {
id: string;
+ reqId: string;
name: string;
description: string;
lastModified: Date;
@@ -31,6 +35,7 @@ type OutcomeViewModel = {
type StakeholderViewModel = {
id: string;
+ reqId: string;
name: string;
description: string;
lastModified: Date;
@@ -38,12 +43,13 @@ type StakeholderViewModel = {
type UseCaseViewModel = {
id: string;
+ reqId: string;
name: string;
priority: MoscowPriority;
scope: string;
level: string;
primaryActor: string;
- goalInContext: string;
+ outcome: string;
precondition: string;
triggerId: string;
mainSuccessScenario: string;
@@ -54,6 +60,7 @@ type UseCaseViewModel = {
type UserStoryViewModel = {
id: string;
+ reqId: string;
name: string;
primaryActor: string;
functionalBehavior: string;
@@ -62,7 +69,6 @@ type UserStoryViewModel = {
lastModified: Date;
};
-
useHead({ title: 'Scenarios' })
definePageMeta({ name: 'Scenarios' })
@@ -155,7 +161,7 @@ const onUseCaseCreate = async (useCase: UseCaseViewModel) => {
scope: useCase.scope,
level: useCase.level,
primaryActor: useCase.primaryActor,
- goalInContext: useCase.goalInContext,
+ outcome: useCase.outcome,
precondition: useCase.precondition,
triggerId: useCase.triggerId,
mainSuccessScenario: useCase.mainSuccessScenario,
@@ -178,7 +184,7 @@ const onUseCaseUpdate = async (useCase: UseCaseViewModel) => {
scope: useCase.scope,
level: useCase.level,
primaryActor: useCase.primaryActor,
- goalInContext: useCase.goalInContext,
+ outcome: useCase.outcome,
precondition: useCase.precondition,
triggerId: useCase.triggerId,
mainSuccessScenario: useCase.mainSuccessScenario,
@@ -224,6 +230,7 @@ const onUseCaseDelete = async (id: string) => {
{
level: 'text',
priority: Object.values(MoscowPriority),
primaryActor: { type: 'requirement', options: roles ?? [] },
- goalInContext: 'text',
+ outcome: { type: 'requirement', options: outcomes ?? [] },
precondition: { type: 'requirement', options: assumptions ?? [] },
triggerId: 'text',
mainSuccessScenario: 'text',
@@ -283,7 +291,7 @@ const onUseCaseDelete = async (id: string) => {
level: 'text',
priority: Object.values(MoscowPriority),
primaryActor: { type: 'requirement', options: roles ?? [] },
- goalInContext: 'text',
+ outcome: { type: 'requirement', options: outcomes ?? [] },
precondition: { type: 'requirement', options: assumptions ?? [] },
triggerId: 'text',
mainSuccessScenario: 'text',
diff --git a/server/api/assumption/[id].delete.ts b/server/api/assumption/[id].delete.ts
index 5634bb6f..71f74ec1 100644
--- a/server/api/assumption/[id].delete.ts
+++ b/server/api/assumption/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
assumption = await assertReqBelongsToSolution(em, Assumption, id, solution)
- await em.removeAndFlush(assumption)
+ await deleteSolutionRequirement(em, assumption, solution)
})
\ No newline at end of file
diff --git a/server/api/assumption/[id].put.ts b/server/api/assumption/[id].put.ts
index d81f3676..e774847f 100644
--- a/server/api/assumption/[id].put.ts
+++ b/server/api/assumption/[id].put.ts
@@ -1,6 +1,6 @@
import { fork } from "~/server/data/orm.js"
import { z } from "zod"
-import { Assumption } from "~/domain/requirements/index.js"
+import { Assumption, assumptionReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -31,6 +31,11 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !assumption.reqId)
+ assumption.reqId = await getNextReqId(assumptionReqIdPrefix, em, solution) as Assumption['reqId']
+
await em.persistAndFlush(assumption)
})
diff --git a/server/api/assumption/index.post.ts b/server/api/assumption/index.post.ts
index ff37c292..15a7ff62 100644
--- a/server/api/assumption/index.post.ts
+++ b/server/api/assumption/index.post.ts
@@ -1,6 +1,6 @@
import { fork } from "~/server/data/orm.js"
import { z } from "zod"
-import { Assumption } from "~/domain/requirements/index.js"
+import { Assumption, assumptionReqIdPrefix } from "~/domain/requirements/index.js"
import { Belongs } from "~/domain/relations"
const bodySchema = z.object({
@@ -18,17 +18,25 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newAssumption = em.create(Assumption, {
- name,
- description,
- modifiedBy: sessionUser,
- lastModified: new Date(),
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newAssumption = em.create(Assumption, {
+ reqId: await getNextReqId(assumptionReqIdPrefix, em, solution) as Assumption['reqId'],
+ name,
+ description,
+ modifiedBy: sessionUser,
+ lastModified: new Date(),
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newAssumption,
+ right: solution
+ })
- em.create(Belongs, { left: newAssumption, right: solution })
+ await em.flush()
- await em.flush()
+ return newAssumption.id
+ })
- return newAssumption.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/constraint/[id].delete.ts b/server/api/constraint/[id].delete.ts
index 406e79b3..d70d671a 100644
--- a/server/api/constraint/[id].delete.ts
+++ b/server/api/constraint/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
constraint = await assertReqBelongsToSolution(em, Constraint, id, solution)
- await em.removeAndFlush(constraint)
+ await deleteSolutionRequirement(em, constraint, solution)
})
\ No newline at end of file
diff --git a/server/api/constraint/[id].put.ts b/server/api/constraint/[id].put.ts
index 839544b8..5fe62a0d 100644
--- a/server/api/constraint/[id].put.ts
+++ b/server/api/constraint/[id].put.ts
@@ -1,5 +1,5 @@
import { z } from "zod"
-import { Constraint, ConstraintCategory } from "~/domain/requirements/index.js"
+import { Constraint, ConstraintCategory, constraintReqIdPrefix } from "~/domain/requirements/index.js"
import { fork } from "~/server/data/orm.js"
const paramSchema = z.object({
@@ -33,5 +33,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !constraint.reqId)
+ constraint.reqId = await getNextReqId(constraintReqIdPrefix, em, solution) as Constraint['reqId']
+
await em.persistAndFlush(constraint)
})
\ No newline at end of file
diff --git a/server/api/constraint/index.post.ts b/server/api/constraint/index.post.ts
index c959d1d6..b0b18be7 100644
--- a/server/api/constraint/index.post.ts
+++ b/server/api/constraint/index.post.ts
@@ -19,18 +19,26 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newConstraint = em.create(Constraint, {
- name,
- description,
- category,
- lastModified: new Date(),
- modifiedBy: sessionUser,
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newConstraint = em.create(Constraint, {
+ reqId: await getNextReqId('E.3.', em, solution) as Constraint['reqId'],
+ name,
+ description,
+ category,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newConstraint,
+ right: solution
+ })
- em.create(Belongs, { left: newConstraint, right: solution })
+ await em.flush()
- await em.flush()
+ return newConstraint.id
+ })
- return newConstraint.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/effect/[id].delete.ts b/server/api/effect/[id].delete.ts
index 6a0b54e6..d39af0bb 100644
--- a/server/api/effect/[id].delete.ts
+++ b/server/api/effect/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
effect = await assertReqBelongsToSolution(em, Effect, id, solution)
- await em.removeAndFlush(effect)
+ await deleteSolutionRequirement(em, effect, solution)
})
\ No newline at end of file
diff --git a/server/api/effect/[id].put.ts b/server/api/effect/[id].put.ts
index 6f2d9fe7..0cd40251 100644
--- a/server/api/effect/[id].put.ts
+++ b/server/api/effect/[id].put.ts
@@ -1,6 +1,6 @@
import { fork } from "~/server/data/orm.js"
import { z } from "zod"
-import { Effect } from "~/domain/requirements/index.js"
+import { Effect, effectReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -31,5 +31,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date(),
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !effect.reqId)
+ effect.reqId = await getNextReqId(effectReqIdPrefix, em, solution) as Effect['reqId']
+
await em.persistAndFlush(effect)
})
\ No newline at end of file
diff --git a/server/api/effect/index.post.ts b/server/api/effect/index.post.ts
index 6a9e347e..9a2ce34c 100644
--- a/server/api/effect/index.post.ts
+++ b/server/api/effect/index.post.ts
@@ -18,17 +18,25 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newEffect = em.create(Effect, {
- name,
- description,
- modifiedBy: sessionUser,
- lastModified: new Date(),
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newEffect = em.create(Effect, {
+ reqId: await getNextReqId('E.5.', em, solution) as Effect['reqId'],
+ name,
+ description,
+ modifiedBy: sessionUser,
+ lastModified: new Date(),
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newEffect,
+ right: solution
+ })
- em.create(Belongs, { left: newEffect, right: solution })
+ await em.flush()
- await em.flush()
+ return newEffect.id
+ })
- return newEffect.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/environment-component/[id].delete.ts b/server/api/environment-component/[id].delete.ts
index e6d2e217..4ed85172 100644
--- a/server/api/environment-component/[id].delete.ts
+++ b/server/api/environment-component/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
environmentComponent = await assertReqBelongsToSolution(em, EnvironmentComponent, id, solution)
- await em.removeAndFlush(environmentComponent)
+ await deleteSolutionRequirement(em, environmentComponent, solution)
})
\ No newline at end of file
diff --git a/server/api/environment-component/[id].put.ts b/server/api/environment-component/[id].put.ts
index 08ccd54f..b819a0c9 100644
--- a/server/api/environment-component/[id].put.ts
+++ b/server/api/environment-component/[id].put.ts
@@ -1,7 +1,7 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
import { Belongs } from "~/domain/relations"
-import { EnvironmentComponent } from "~/domain/requirements/index.js"
+import { EnvironmentComponent, environmentComponentReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -50,5 +50,10 @@ export default defineEventHandler(async (event) => {
// Do nothing
}
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !environmentComponent.reqId)
+ environmentComponent.reqId = await getNextReqId(environmentComponentReqIdPrefix, em, solution) as EnvironmentComponent['reqId']
+
await em.persistAndFlush(environmentComponent)
})
\ No newline at end of file
diff --git a/server/api/environment-component/index.post.ts b/server/api/environment-component/index.post.ts
index 90abea9c..380bf40e 100644
--- a/server/api/environment-component/index.post.ts
+++ b/server/api/environment-component/index.post.ts
@@ -19,20 +19,28 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newEnvironmentComponent = em.create(EnvironmentComponent, {
- name,
- description,
- lastModified: new Date(),
- modifiedBy: sessionUser,
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newEnvironmentComponent = em.create(EnvironmentComponent, {
+ reqId: await getNextReqId('E.2.', em, solution) as EnvironmentComponent['reqId'],
+ name,
+ description,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newEnvironmentComponent,
+ right: solution
+ })
- em.create(Belongs, { left: newEnvironmentComponent, right: solution })
+ if (parentComponentId)
+ em.create(Belongs, { left: newEnvironmentComponent, right: parentComponentId })
- if (parentComponentId)
- em.create(Belongs, { left: newEnvironmentComponent, right: parentComponentId })
+ await em.flush()
- await em.flush()
+ return newEnvironmentComponent.id
+ })
- return newEnvironmentComponent.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/goal/[id].delete.ts b/server/api/epic/[id].delete.ts
similarity index 71%
rename from server/api/goal/[id].delete.ts
rename to server/api/epic/[id].delete.ts
index fcdea146..597bf97d 100644
--- a/server/api/goal/[id].delete.ts
+++ b/server/api/epic/[id].delete.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Goal } from "~/domain/requirements/index.js"
+import { Epic } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -11,14 +11,14 @@ const bodySchema = z.object({
})
/**
- * Delete a goal by id.
+ * Delete an epic by id.
*/
export default defineEventHandler(async (event) => {
const { id } = await validateEventParams(event, paramSchema),
{ solutionId } = await validateEventBody(event, bodySchema),
{ solution } = await assertSolutionContributor(event, solutionId),
em = fork(),
- goal = await assertReqBelongsToSolution(em, Goal, id, solution)
+ epic = await assertReqBelongsToSolution(em, Epic, id, solution)
- await em.removeAndFlush(goal)
+ await deleteSolutionRequirement(em, epic, solution)
})
\ No newline at end of file
diff --git a/server/api/goal/[id].get.ts b/server/api/epic/[id].get.ts
similarity index 70%
rename from server/api/goal/[id].get.ts
rename to server/api/epic/[id].get.ts
index a6d96736..a7736ac4 100644
--- a/server/api/goal/[id].get.ts
+++ b/server/api/epic/[id].get.ts
@@ -1,24 +1,24 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Goal } from "~/domain/requirements/index.js"
+import { Epic } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
})
const querySchema = z.object({
- solutionId: z.string().uuid()
+ solutionId: z.string().uuid(),
})
/**
- * Returns a Goal by id
+ * Returns an epic by id
*/
export default defineEventHandler(async (event) => {
const { id } = await validateEventParams(event, paramSchema),
{ solutionId } = await validateEventQuery(event, querySchema),
{ solution } = await assertSolutionReader(event, solutionId),
em = fork(),
- goal = await assertReqBelongsToSolution(em, Goal, id, solution)
+ epic = await assertReqBelongsToSolution(em, Epic, id, solution)
- return goal
+ return epic
})
\ No newline at end of file
diff --git a/server/api/epic/[id].put.ts b/server/api/epic/[id].put.ts
new file mode 100644
index 00000000..b70d8be4
--- /dev/null
+++ b/server/api/epic/[id].put.ts
@@ -0,0 +1,42 @@
+import { fork } from "~/server/data/orm.js"
+import { z } from "zod"
+import { Epic, epicReqIdPrefix, MoscowPriority } from "~/domain/requirements/index.js"
+
+const paramSchema = z.object({
+ id: z.string().uuid()
+})
+
+const bodySchema = z.object({
+ solutionId: z.string().uuid(),
+ name: z.string().optional(),
+ priority: z.nativeEnum(MoscowPriority).optional(),
+ description: z.string().optional(),
+ isSilence: z.boolean().optional()
+})
+
+/**
+ * Updates an epic by id.
+ */
+export default defineEventHandler(async (event) => {
+ const { id } = await validateEventParams(event, paramSchema),
+ { name, description, solutionId, isSilence, priority } = await validateEventBody(event, bodySchema),
+ { sessionUser, solution } = await assertSolutionContributor(event, solutionId),
+ em = fork(),
+ epic = await assertReqBelongsToSolution(em, Epic, id, solution)
+
+ epic.assign({
+ name: name ?? epic.name,
+ isSilence: isSilence ?? epic.isSilence,
+ description: description ?? epic.description,
+ priority: priority ?? epic.priority,
+ modifiedBy: sessionUser,
+ lastModified: new Date(),
+ })
+
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !epic.reqId)
+ epic.reqId = await getNextReqId(epicReqIdPrefix, em, solution) as Epic['reqId']
+
+ await em.persistAndFlush(epic)
+})
\ No newline at end of file
diff --git a/server/api/goal/index.get.ts b/server/api/epic/index.get.ts
similarity index 72%
rename from server/api/goal/index.get.ts
rename to server/api/epic/index.get.ts
index d0d982c0..108c3fe8 100644
--- a/server/api/goal/index.get.ts
+++ b/server/api/epic/index.get.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Goal, ReqType } from "~/domain/requirements/index.js"
+import { Epic, ReqType } from "~/domain/requirements/index.js"
const querySchema = z.object({
solutionId: z.string().uuid(),
@@ -10,7 +10,7 @@ const querySchema = z.object({
})
/**
- * Returns all goals that match the query parameters
+ * Returns all epics that match the query parameters
*/
export default defineEventHandler(async (event) => {
const query = await validateEventQuery(event, querySchema),
@@ -18,5 +18,7 @@ export default defineEventHandler(async (event) => {
await assertSolutionReader(event, query.solutionId)
- return await findAllSolutionRequirements(ReqType.GOAL, em, query)
-})
\ No newline at end of file
+ return await findAllSolutionRequirements(ReqType.EPIC, em, query)
+})
+
+
diff --git a/server/api/epic/index.post.ts b/server/api/epic/index.post.ts
new file mode 100644
index 00000000..1b47c8c1
--- /dev/null
+++ b/server/api/epic/index.post.ts
@@ -0,0 +1,44 @@
+import { fork } from "~/server/data/orm.js"
+import { z } from "zod"
+import { Epic, MoscowPriority, ReqType } from "~/domain/requirements/index.js"
+import { Belongs } from "~/domain/relations"
+
+const bodySchema = z.object({
+ solutionId: z.string().uuid(),
+ name: z.string().default("{Untitled Epic}"),
+ priority: z.nativeEnum(MoscowPriority).default(MoscowPriority.MUST),
+ description: z.string().default(""),
+ isSilence: z.boolean().default(false)
+})
+
+/**
+ * Creates a new epic and returns its id
+ */
+export default defineEventHandler(async (event) => {
+ const { name, description, solutionId, isSilence, priority } = await validateEventBody(event, bodySchema),
+ { solution, sessionUser } = await assertSolutionContributor(event, solutionId),
+ em = fork()
+
+ const newId = await em.transactional(async (em) => {
+ const newEpic = em.create(Epic, {
+ reqId: await getNextReqId('G.5.', em, solution) as Epic['reqId'],
+ name,
+ description,
+ modifiedBy: sessionUser,
+ lastModified: new Date(),
+ priority,
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newEpic,
+ right: solution
+ })
+
+ await em.flush()
+
+ return newEpic.id
+ })
+
+ return newId
+})
\ No newline at end of file
diff --git a/server/api/functional-behavior/[id].delete.ts b/server/api/functional-behavior/[id].delete.ts
index 94c28b1b..dd965c25 100644
--- a/server/api/functional-behavior/[id].delete.ts
+++ b/server/api/functional-behavior/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
functionalBehavior = await assertReqBelongsToSolution(em, FunctionalBehavior, id, solution)
- await em.removeAndFlush(functionalBehavior)
+ await deleteSolutionRequirement(em, functionalBehavior, solution)
})
\ No newline at end of file
diff --git a/server/api/functional-behavior/[id].put.ts b/server/api/functional-behavior/[id].put.ts
index d5d3fe9e..56555b1c 100644
--- a/server/api/functional-behavior/[id].put.ts
+++ b/server/api/functional-behavior/[id].put.ts
@@ -1,5 +1,5 @@
import { z } from "zod"
-import { FunctionalBehavior, MoscowPriority } from "~/domain/requirements/index.js"
+import { FunctionalBehavior, functionalBehaviorReqIdPrefix, MoscowPriority } from "~/domain/requirements/index.js"
import { fork } from "~/server/data/orm.js"
const paramSchema = z.object({
@@ -33,5 +33,10 @@ export default defineEventHandler(async (event) => {
...(isSilence !== undefined && { isSilence })
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !functionalBehavior.reqId)
+ functionalBehavior.reqId = await getNextReqId(functionalBehaviorReqIdPrefix, em, solution) as FunctionalBehavior['reqId']
+
await em.persistAndFlush(functionalBehavior)
})
\ No newline at end of file
diff --git a/server/api/functional-behavior/index.post.ts b/server/api/functional-behavior/index.post.ts
index 885ea534..74bd21f3 100644
--- a/server/api/functional-behavior/index.post.ts
+++ b/server/api/functional-behavior/index.post.ts
@@ -19,18 +19,26 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newFunctionalBehavior = em.create(FunctionalBehavior, {
- name,
- description,
- priority,
- lastModified: new Date(),
- modifiedBy: sessionUser,
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newFunctionalBehavior = em.create(FunctionalBehavior, {
+ reqId: await getNextReqId('S.2.', em, solution) as FunctionalBehavior['reqId'],
+ name,
+ description,
+ priority,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newFunctionalBehavior,
+ right: solution
+ })
- em.create(Belongs, { left: newFunctionalBehavior, right: solution })
+ await em.flush()
- await em.flush()
+ return newFunctionalBehavior.id
+ })
- return newFunctionalBehavior.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/glossary-term/[id].delete.ts b/server/api/glossary-term/[id].delete.ts
index dca1341c..c5d832a8 100644
--- a/server/api/glossary-term/[id].delete.ts
+++ b/server/api/glossary-term/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
glossaryTerm = await assertReqBelongsToSolution(em, GlossaryTerm, id, solution)
- await em.removeAndFlush(glossaryTerm)
+ await deleteSolutionRequirement(em, glossaryTerm, solution)
})
\ No newline at end of file
diff --git a/server/api/glossary-term/[id].put.ts b/server/api/glossary-term/[id].put.ts
index 66157ac4..4dd96a3b 100644
--- a/server/api/glossary-term/[id].put.ts
+++ b/server/api/glossary-term/[id].put.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { GlossaryTerm } from "~/domain/requirements/index.js"
+import { GlossaryTerm, glossaryTermReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -33,5 +33,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !glossaryTerm.reqId)
+ glossaryTerm.reqId = await getNextReqId(glossaryTermReqIdPrefix, em, solution) as GlossaryTerm['reqId']
+
await em.persistAndFlush(glossaryTerm)
})
\ No newline at end of file
diff --git a/server/api/glossary-term/index.post.ts b/server/api/glossary-term/index.post.ts
index 219108d4..9c00b6f8 100644
--- a/server/api/glossary-term/index.post.ts
+++ b/server/api/glossary-term/index.post.ts
@@ -19,20 +19,28 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const glossaryTerm = em.create(GlossaryTerm, {
- name,
- description,
- lastModified: new Date(),
- modifiedBy: sessionUser,
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const glossaryTerm = em.create(GlossaryTerm, {
+ reqId: await getNextReqId('E.1.', em, solution) as GlossaryTerm['reqId'],
+ name,
+ description,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: glossaryTerm,
+ right: solution
+ })
- em.create(Belongs, { left: glossaryTerm, right: solution })
+ if (parentComponentId)
+ em.create(Belongs, { left: glossaryTerm, right: parentComponentId })
- if (parentComponentId)
- em.create(Belongs, { left: glossaryTerm, right: parentComponentId })
+ await em.flush()
- await em.flush()
+ return glossaryTerm.id
+ })
- return glossaryTerm.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/goal/[id].put.ts b/server/api/goal/[id].put.ts
deleted file mode 100644
index fb41120b..00000000
--- a/server/api/goal/[id].put.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { fork } from "~/server/data/orm.js"
-import { z } from "zod"
-import { Goal } from "~/domain/requirements/index.js"
-
-const paramSchema = z.object({
- id: z.string().uuid()
-})
-
-const bodySchema = z.object({
- solutionId: z.string().uuid(),
- name: z.string().optional(),
- description: z.string().optional(),
- isSilence: z.boolean().optional()
-})
-
-/**
- * Updates a Goal by id.
- */
-export default defineEventHandler(async (event) => {
- const { id } = await validateEventParams(event, paramSchema),
- { solutionId } = await validateEventBody(event, bodySchema),
- { sessionUser, solution } = await assertSolutionContributor(event, solutionId),
- { name, description, isSilence } = await validateEventBody(event, bodySchema),
- em = fork(),
- goal = await assertReqBelongsToSolution(em, Goal, id, solution)
-
- goal.assign({
- name: name ?? goal.name,
- description: description ?? goal.description,
- isSilence: isSilence ?? goal.isSilence,
- modifiedBy: sessionUser,
- lastModified: new Date()
- })
-
- await em.persistAndFlush(goal)
-})
\ No newline at end of file
diff --git a/server/api/goal/index.post.ts b/server/api/goal/index.post.ts
deleted file mode 100644
index c75d9259..00000000
--- a/server/api/goal/index.post.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { fork } from "~/server/data/orm.js"
-import { z } from "zod"
-import { Goal } from "~/domain/requirements/index.js"
-import { Belongs } from "~/domain/relations"
-
-const bodySchema = z.object({
- solutionId: z.string().uuid(),
- name: z.string().default("{Untitled Goal}"),
- description: z.string().default(""),
- isSilence: z.boolean().default(false)
-})
-
-/**
- * Creates a new goal and returns its id
- */
-export default defineEventHandler(async (event) => {
- const { name, description, solutionId, isSilence } = await validateEventBody(event, bodySchema),
- { solution, sessionUser } = await assertSolutionContributor(event, solutionId),
- em = fork()
-
- const newGoal = em.create(Goal, {
- name,
- description,
- modifiedBy: sessionUser,
- lastModified: new Date(),
- isSilence
- })
-
- em.create(Belongs, { left: newGoal, right: solution })
-
- await em.flush()
-
- return newGoal.id
-})
\ No newline at end of file
diff --git a/server/api/invariant/[id].delete.ts b/server/api/invariant/[id].delete.ts
index c4d74775..c0a594a9 100644
--- a/server/api/invariant/[id].delete.ts
+++ b/server/api/invariant/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
invariant = await assertReqBelongsToSolution(em, Invariant, id, solution)
- await em.removeAndFlush(invariant)
+ await deleteSolutionRequirement(em, invariant, solution)
})
\ No newline at end of file
diff --git a/server/api/invariant/[id].put.ts b/server/api/invariant/[id].put.ts
index 98ada4ac..379373c7 100644
--- a/server/api/invariant/[id].put.ts
+++ b/server/api/invariant/[id].put.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Invariant } from "~/domain/requirements/index.js"
+import { Invariant, invariantReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -31,5 +31,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !invariant.reqId)
+ invariant.reqId = await getNextReqId(invariantReqIdPrefix, em, solution) as Invariant['reqId']
+
await em.persistAndFlush(invariant)
})
\ No newline at end of file
diff --git a/server/api/invariant/index.post.ts b/server/api/invariant/index.post.ts
index 009321d8..dcaa8e85 100644
--- a/server/api/invariant/index.post.ts
+++ b/server/api/invariant/index.post.ts
@@ -18,17 +18,25 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const invariant = em.create(Invariant, {
- name,
- description,
- modifiedBy: sessionUser,
- lastModified: new Date(),
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newInvariant = em.create(Invariant, {
+ reqId: await getNextReqId('E.6.', em, solution) as Invariant['reqId'],
+ name,
+ description,
+ modifiedBy: sessionUser,
+ lastModified: new Date(),
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newInvariant,
+ right: solution
+ })
- em.create(Belongs, { left: invariant, right: solution })
+ await em.flush()
- await em.flush()
+ return newInvariant.id
+ })
- return invariant.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/justification/[id].delete.ts b/server/api/justification/[id].delete.ts
index 42a15c26..9c637d30 100644
--- a/server/api/justification/[id].delete.ts
+++ b/server/api/justification/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
justification = await assertReqBelongsToSolution(em, Justification, id, solution)
- await em.removeAndFlush(justification)
+ await deleteSolutionRequirement(em, justification, solution)
})
\ No newline at end of file
diff --git a/server/api/justification/index.post.ts b/server/api/justification/index.post.ts
index 4207b6b0..88cadb23 100644
--- a/server/api/justification/index.post.ts
+++ b/server/api/justification/index.post.ts
@@ -19,6 +19,7 @@ export default defineEventHandler(async (event) => {
em = fork()
const newJustification = em.create(Justification, {
+ reqId: undefined, // Should there be a Documentation category (D)?
name,
description,
modifiedBy: sessionUser,
@@ -26,7 +27,10 @@ export default defineEventHandler(async (event) => {
isSilence
})
- em.create(Belongs, { left: newJustification, right: solution })
+ em.create(Belongs, {
+ left: newJustification,
+ right: solution
+ })
await em.flush()
diff --git a/server/api/limit/[id].delete.ts b/server/api/limit/[id].delete.ts
index c95ff58c..3198cc14 100644
--- a/server/api/limit/[id].delete.ts
+++ b/server/api/limit/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
limit = await assertReqBelongsToSolution(em, Limit, id, solution)
- await em.removeAndFlush(limit)
+ await deleteSolutionRequirement(em, limit, solution)
})
\ No newline at end of file
diff --git a/server/api/limit/[id].put.ts b/server/api/limit/[id].put.ts
index 4735a5cd..df418ebc 100644
--- a/server/api/limit/[id].put.ts
+++ b/server/api/limit/[id].put.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Limit } from "~/domain/requirements/index.js"
+import { Limit, limitReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -31,5 +31,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !limit.reqId)
+ limit.reqId = await getNextReqId(limitReqIdPrefix, em, solution) as Limit['reqId']
+
await em.persistAndFlush(limit)
})
\ No newline at end of file
diff --git a/server/api/limit/index.post.ts b/server/api/limit/index.post.ts
index aa234093..1584f419 100644
--- a/server/api/limit/index.post.ts
+++ b/server/api/limit/index.post.ts
@@ -18,17 +18,25 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newLimit = em.create(Limit, {
- name,
- description,
- modifiedBy: sessionUser,
- lastModified: new Date(),
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newLimit = em.create(Limit, {
+ reqId: await getNextReqId('G.6.', em, solution) as Limit['reqId'],
+ name,
+ description,
+ modifiedBy: sessionUser,
+ lastModified: new Date(),
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newLimit,
+ right: solution
+ })
- em.create(Belongs, { left: newLimit, right: solution })
+ await em.flush()
- await em.flush()
+ return newLimit.id
+ })
- return newLimit.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/non-functional-behavior/[id].delete.ts b/server/api/non-functional-behavior/[id].delete.ts
index b72c9299..fbd435df 100644
--- a/server/api/non-functional-behavior/[id].delete.ts
+++ b/server/api/non-functional-behavior/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
nonFunctionalBehavior = await assertReqBelongsToSolution(em, NonFunctionalBehavior, id, solution)
- await em.removeAndFlush(nonFunctionalBehavior)
+ await deleteSolutionRequirement(em, nonFunctionalBehavior, solution)
})
\ No newline at end of file
diff --git a/server/api/non-functional-behavior/[id].put.ts b/server/api/non-functional-behavior/[id].put.ts
index e63b7a42..8a64da1b 100644
--- a/server/api/non-functional-behavior/[id].put.ts
+++ b/server/api/non-functional-behavior/[id].put.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { NonFunctionalBehavior, MoscowPriority } from "~/domain/requirements/index.js"
+import { NonFunctionalBehavior, MoscowPriority, nonFunctionalBehaviorReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -33,5 +33,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !nonFunctionalBehavior.reqId)
+ nonFunctionalBehavior.reqId = await getNextReqId(nonFunctionalBehaviorReqIdPrefix, em, solution) as NonFunctionalBehavior['reqId']
+
await em.persistAndFlush(nonFunctionalBehavior)
})
\ No newline at end of file
diff --git a/server/api/non-functional-behavior/index.post.ts b/server/api/non-functional-behavior/index.post.ts
index f4ea21a1..9a9d8f6d 100644
--- a/server/api/non-functional-behavior/index.post.ts
+++ b/server/api/non-functional-behavior/index.post.ts
@@ -19,18 +19,26 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newNonFunctionalBehavior = em.create(NonFunctionalBehavior, {
- name,
- description,
- priority,
- lastModified: new Date(),
- modifiedBy: sessionUser,
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newNonFunctionalBehavior = em.create(NonFunctionalBehavior, {
+ reqId: await getNextReqId('S.2.', em, solution) as NonFunctionalBehavior['reqId'],
+ name,
+ description,
+ priority,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newNonFunctionalBehavior,
+ right: solution
+ })
- em.create(Belongs, { left: newNonFunctionalBehavior, right: solution })
+ await em.flush()
- await em.flush()
+ return newNonFunctionalBehavior.id
+ })
- return newNonFunctionalBehavior.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/obstacle/[id].delete.ts b/server/api/obstacle/[id].delete.ts
index 0ff56f08..dac1fd11 100644
--- a/server/api/obstacle/[id].delete.ts
+++ b/server/api/obstacle/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
obstacle = await assertReqBelongsToSolution(em, Obstacle, id, solution)
- await em.removeAndFlush(obstacle)
+ await deleteSolutionRequirement(em, obstacle, solution)
})
\ No newline at end of file
diff --git a/server/api/obstacle/[id].put.ts b/server/api/obstacle/[id].put.ts
index c262b132..24c90e9e 100644
--- a/server/api/obstacle/[id].put.ts
+++ b/server/api/obstacle/[id].put.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Obstacle } from "~/domain/requirements/index.js"
+import { Obstacle, obstacleReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -31,5 +31,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !obstacle.reqId)
+ obstacle.reqId = await getNextReqId(obstacleReqIdPrefix, em, solution) as Obstacle['reqId']
+
await em.persistAndFlush(obstacle)
})
\ No newline at end of file
diff --git a/server/api/obstacle/index.post.ts b/server/api/obstacle/index.post.ts
index e7047f76..e45640e3 100644
--- a/server/api/obstacle/index.post.ts
+++ b/server/api/obstacle/index.post.ts
@@ -18,17 +18,25 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newObstacle = em.create(Obstacle, {
- name,
- description,
- modifiedBy: sessionUser,
- lastModified: new Date(),
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newObstacle = em.create(Obstacle, {
+ reqId: await getNextReqId('G.2.', em, solution) as Obstacle['reqId'],
+ name,
+ description,
+ modifiedBy: sessionUser,
+ lastModified: new Date(),
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newObstacle,
+ right: solution
+ })
- em.create(Belongs, { left: newObstacle, right: solution })
+ await em.flush()
- await em.flush()
+ return newObstacle.id
+ })
- return newObstacle.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/outcome/[id].delete.ts b/server/api/outcome/[id].delete.ts
index 3d2b6758..94248345 100644
--- a/server/api/outcome/[id].delete.ts
+++ b/server/api/outcome/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
outcome = await assertReqBelongsToSolution(em, Outcome, id, solution)
- await em.removeAndFlush(outcome)
+ await deleteSolutionRequirement(em, outcome, solution)
})
\ No newline at end of file
diff --git a/server/api/outcome/[id].put.ts b/server/api/outcome/[id].put.ts
index 998b3ec4..1ada374c 100644
--- a/server/api/outcome/[id].put.ts
+++ b/server/api/outcome/[id].put.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Outcome } from "~/domain/requirements/index.js"
+import { Outcome, outcomeReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -31,5 +31,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !outcome.reqId)
+ outcome.reqId = await getNextReqId(outcomeReqIdPrefix, em, solution) as Outcome['reqId']
+
await em.persistAndFlush(outcome)
})
\ No newline at end of file
diff --git a/server/api/outcome/index.post.ts b/server/api/outcome/index.post.ts
index 8e08ada7..56d5cbdb 100644
--- a/server/api/outcome/index.post.ts
+++ b/server/api/outcome/index.post.ts
@@ -18,17 +18,25 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newOutcome = em.create(Outcome, {
- name,
- description,
- modifiedBy: sessionUser,
- lastModified: new Date(),
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newOutcome = em.create(Outcome, {
+ reqId: await getNextReqId('G.1.', em, solution) as Outcome['reqId'],
+ name,
+ description,
+ modifiedBy: sessionUser,
+ lastModified: new Date(),
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newOutcome,
+ right: solution
+ })
- em.create(Belongs, { left: newOutcome, right: solution })
+ await em.flush()
- await em.flush()
+ return newOutcome.id
+ })
- return newOutcome.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/parse-requirement/index.post.ts b/server/api/parse-requirement/index.post.ts
index 7095a014..7fa08d4f 100644
--- a/server/api/parse-requirement/index.post.ts
+++ b/server/api/parse-requirement/index.post.ts
@@ -262,7 +262,13 @@ export default defineEventHandler(async (event) => {
const useCase = em.create(UseCase, {
isSilence: true,
extensions: item.extensions,
- goalInContext: item.goalInContext,
+ outcome: em.create(Outcome, {
+ isSilence: true,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ name: item.name,
+ description: item.outcome
+ }),
level: item.level,
mainSuccessScenario: item.mainSuccessScenario,
scope: item.scope,
diff --git a/server/api/person/[id].delete.ts b/server/api/person/[id].delete.ts
index 7718d9e5..0d39fee8 100644
--- a/server/api/person/[id].delete.ts
+++ b/server/api/person/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
person = await assertReqBelongsToSolution(em, Person, id, solution)
- await em.removeAndFlush(person)
+ await deleteSolutionRequirement(em, person, solution)
})
\ No newline at end of file
diff --git a/server/api/person/[id].put.ts b/server/api/person/[id].put.ts
index f618ca15..0dccd936 100644
--- a/server/api/person/[id].put.ts
+++ b/server/api/person/[id].put.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Person } from "~/domain/requirements/index.js"
+import { Person, personReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -33,5 +33,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !person.reqId)
+ person.reqId = await getNextReqId(personReqIdPrefix, em, solution) as Person['reqId']
+
await em.persistAndFlush(person)
})
\ No newline at end of file
diff --git a/server/api/person/index.post.ts b/server/api/person/index.post.ts
index de25efb7..759ed106 100644
--- a/server/api/person/index.post.ts
+++ b/server/api/person/index.post.ts
@@ -19,18 +19,26 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newPerson = em.create(Person, {
- name,
- description,
- email,
- modifiedBy: sessionUser,
- lastModified: new Date(),
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newPerson = em.create(Person, {
+ reqId: await getNextReqId('P.1.', em, solution) as Person['reqId'],
+ name,
+ description,
+ email,
+ modifiedBy: sessionUser,
+ lastModified: new Date(),
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newPerson,
+ right: solution
+ })
- em.create(Belongs, { left: newPerson, right: solution })
+ await em.flush()
- await em.flush()
+ return newPerson.id
+ })
- return newPerson.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/solution/index.post.ts b/server/api/solution/index.post.ts
index 86a87702..4e3d5902 100644
--- a/server/api/solution/index.post.ts
+++ b/server/api/solution/index.post.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Obstacle, Solution, Goal } from "~/domain/requirements/index.js"
+import { Obstacle, Solution, Outcome } from "~/domain/requirements/index.js"
import { Belongs } from "~/domain/relations"
import slugify from "~/utils/slugify"
@@ -29,8 +29,9 @@ export default defineEventHandler(async (event) => {
em.create(Belongs, { left: newSolution, right: organization });
- [[Goal, 'G.1'] as const, [Obstacle, 'G.2'] as const].forEach(([Entity, name]) => {
+ [[Outcome, 'G.1'] as const, [Obstacle, 'G.2'] as const].forEach(([Entity, name]) => {
const entity = em.create(Entity, {
+ reqId: `${name}.1` as any,
name,
description: '',
lastModified: new Date(),
@@ -38,7 +39,10 @@ export default defineEventHandler(async (event) => {
isSilence: false
})
- em.create(Belongs, { left: entity, right: newSolution })
+ em.create(Belongs, {
+ left: entity,
+ right: newSolution
+ })
})
await em.flush()
diff --git a/server/api/stakeholder/[id].delete.ts b/server/api/stakeholder/[id].delete.ts
index 12c6602f..8c25268f 100644
--- a/server/api/stakeholder/[id].delete.ts
+++ b/server/api/stakeholder/[id].delete.ts
@@ -20,8 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
stakeholder = await assertReqBelongsToSolution(em, Stakeholder, id, solution)
- console.log('removing stakeholder', stakeholder)
-
- await em.removeAndFlush(stakeholder)
- console.log('stakeholder removed')
+ await deleteSolutionRequirement(em, stakeholder, solution)
})
\ No newline at end of file
diff --git a/server/api/stakeholder/[id].put.ts b/server/api/stakeholder/[id].put.ts
index 2e6d229e..d9c60fa1 100644
--- a/server/api/stakeholder/[id].put.ts
+++ b/server/api/stakeholder/[id].put.ts
@@ -1,7 +1,7 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
import { Belongs } from "~/domain/relations"
-import { Stakeholder, StakeholderSegmentation, StakeholderCategory } from "~/domain/requirements/index.js"
+import { Stakeholder, StakeholderSegmentation, StakeholderCategory, stakeholderReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -59,5 +59,10 @@ export default defineEventHandler(async (event) => {
// Do nothing
}
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !stakeholder.reqId)
+ stakeholder.reqId = await getNextReqId(stakeholderReqIdPrefix, em, solution) as Stakeholder['reqId']
+
await em.persistAndFlush(stakeholder)
})
\ No newline at end of file
diff --git a/server/api/stakeholder/index.post.ts b/server/api/stakeholder/index.post.ts
index 743882ef..0c236e9d 100644
--- a/server/api/stakeholder/index.post.ts
+++ b/server/api/stakeholder/index.post.ts
@@ -24,24 +24,32 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newStakeholder = em.create(Stakeholder, {
- name,
- description,
- availability,
- influence,
- segmentation,
- category,
- lastModified: new Date(),
- modifiedBy: sessionUser,
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newStakeholder = em.create(Stakeholder, {
+ reqId: await getNextReqId('G.7.', em, solution) as Stakeholder['reqId'],
+ name,
+ description,
+ availability,
+ influence,
+ segmentation,
+ category,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newStakeholder,
+ right: solution
+ })
- em.create(Belongs, { left: newStakeholder, right: solution })
+ if (parentComponentId)
+ em.create(Belongs, { left: newStakeholder, right: parentComponentId })
- if (parentComponentId)
- em.create(Belongs, { left: newStakeholder, right: parentComponentId })
+ await em.flush()
- await em.flush()
+ return newStakeholder.id
+ })
- return newStakeholder.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/system-component/[id].delete.ts b/server/api/system-component/[id].delete.ts
index 4f2ff7f9..314fd6a0 100644
--- a/server/api/system-component/[id].delete.ts
+++ b/server/api/system-component/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
systemComponent = await assertReqBelongsToSolution(em, SystemComponent, id, solution)
- await em.removeAndFlush(systemComponent)
+ await deleteSolutionRequirement(em, systemComponent, solution)
})
\ No newline at end of file
diff --git a/server/api/system-component/[id].put.ts b/server/api/system-component/[id].put.ts
index 2cf1a24e..6be7197b 100644
--- a/server/api/system-component/[id].put.ts
+++ b/server/api/system-component/[id].put.ts
@@ -1,7 +1,7 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
import { Belongs } from "~/domain/relations"
-import { SystemComponent } from "~/domain/requirements/index.js"
+import { SystemComponent, systemComponentReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -50,5 +50,10 @@ export default defineEventHandler(async (event) => {
// Do nothing
}
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (isSilence !== undefined && isSilence == false && !systemComponent.reqId)
+ systemComponent.reqId = await getNextReqId(systemComponentReqIdPrefix, em, solution) as SystemComponent['reqId']
+
await em.persistAndFlush(systemComponent)
})
\ No newline at end of file
diff --git a/server/api/system-component/index.post.ts b/server/api/system-component/index.post.ts
index 0c81622b..2ce169b9 100644
--- a/server/api/system-component/index.post.ts
+++ b/server/api/system-component/index.post.ts
@@ -19,20 +19,28 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, solutionId),
em = fork()
- const newSystemComponent = new SystemComponent({
- name,
- description,
- lastModified: new Date(),
- modifiedBy: sessionUser,
- isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newSystemComponent = new SystemComponent({
+ reqId: await getNextReqId('S.1.', em, solution) as SystemComponent['reqId'],
+ name,
+ description,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ isSilence
+ })
+
+ em.create(Belongs, {
+ left: newSystemComponent,
+ right: solution
+ })
- em.create(Belongs, { left: newSystemComponent, right: solution })
+ if (parentComponent)
+ em.create(Belongs, { left: newSystemComponent, right: parentComponent })
- if (parentComponent)
- em.create(Belongs, { left: newSystemComponent, right: parentComponent })
+ await em.flush()
- await em.flush()
+ return newSystemComponent.id
+ })
- return newSystemComponent.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/use-case/[id].delete.ts b/server/api/use-case/[id].delete.ts
index 6878ba79..6942faee 100644
--- a/server/api/use-case/[id].delete.ts
+++ b/server/api/use-case/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
useCase = await assertReqBelongsToSolution(em, UseCase, id, solution)
- await em.removeAndFlush(useCase)
+ await deleteSolutionRequirement(em, useCase, solution)
})
\ No newline at end of file
diff --git a/server/api/use-case/[id].put.ts b/server/api/use-case/[id].put.ts
index a9f536ed..8946c005 100644
--- a/server/api/use-case/[id].put.ts
+++ b/server/api/use-case/[id].put.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { Assumption, Effect, MoscowPriority, Stakeholder, UseCase } from "~/domain/requirements/index.js"
+import { Assumption, Effect, MoscowPriority, Outcome, Stakeholder, UseCase, useCaseReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -14,7 +14,7 @@ const bodySchema = z.object({
priority: z.nativeEnum(MoscowPriority).optional(),
scope: z.string().optional(),
level: z.string().optional(),
- goalInContext: z.string().optional(),
+ outcome: z.string().uuid().optional(),
precondition: z.string().uuid().optional(),
triggerId: z.string().uuid().optional(),
mainSuccessScenario: z.string().optional(),
@@ -39,6 +39,8 @@ export default defineEventHandler(async (event) => {
useCase.precondition = await assertReqBelongsToSolution(em, Assumption, body.precondition, solution)
if (body.successGuarantee)
useCase.successGuarantee = await assertReqBelongsToSolution(em, Effect, body.successGuarantee, solution)
+ if (body.outcome)
+ useCase.outcome = await assertReqBelongsToSolution(em, Outcome, body.outcome, solution)
useCase.assign({
name: body.name ?? useCase.name,
@@ -46,7 +48,7 @@ export default defineEventHandler(async (event) => {
priority: body.priority ?? useCase.priority,
scope: body.scope ?? useCase.scope,
level: body.level ?? useCase.level,
- goalInContext: body.goalInContext ?? useCase.goalInContext,
+ outcome: body.outcome ?? useCase.outcome,
triggerId: body.triggerId ?? useCase.triggerId,
mainSuccessScenario: body.mainSuccessScenario ?? useCase.mainSuccessScenario,
extensions: body.extensions ?? useCase.extensions,
@@ -55,5 +57,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (body.isSilence !== undefined && body.isSilence == false && !useCase.reqId)
+ useCase.reqId = await getNextReqId(useCaseReqIdPrefix, em, solution) as UseCase['reqId']
+
await em.persistAndFlush(useCase)
})
\ No newline at end of file
diff --git a/server/api/use-case/index.get.ts b/server/api/use-case/index.get.ts
index 4fce2eee..22f3219a 100644
--- a/server/api/use-case/index.get.ts
+++ b/server/api/use-case/index.get.ts
@@ -11,7 +11,7 @@ const querySchema = z.object({
priority: z.nativeEnum(MoscowPriority).optional(),
scope: z.string().optional(),
level: z.string().optional(),
- goalInContext: z.string().optional(),
+ outcome: z.string().uuid().optional(),
precondition: z.string().uuid().optional(),
triggerId: z.literal(emptyUuid).optional(),
mainSuccessScenario: z.string().optional(),
@@ -29,5 +29,5 @@ export default defineEventHandler(async (event) => {
await assertSolutionReader(event, query.solutionId)
- return await findAllSolutionRequirements(ReqType.USE_CASE, em, query, ['primaryActor', 'precondition', 'successGuarantee'])
+ return await findAllSolutionRequirements(ReqType.USE_CASE, em, query, ['primaryActor', 'precondition', 'successGuarantee', 'outcome'])
})
\ No newline at end of file
diff --git a/server/api/use-case/index.post.ts b/server/api/use-case/index.post.ts
index d6264a50..fe2bfb0a 100644
--- a/server/api/use-case/index.post.ts
+++ b/server/api/use-case/index.post.ts
@@ -1,6 +1,6 @@
import { NIL as emptyUuid } from "uuid"
import { z } from "zod"
-import { MoscowPriority, Stakeholder, Assumption, Effect, UseCase } from "~/domain/requirements/index.js"
+import { MoscowPriority, Stakeholder, Assumption, Effect, UseCase, Outcome } from "~/domain/requirements/index.js"
import { fork } from "~/server/data/orm.js"
import { Belongs } from "~/domain/relations"
@@ -12,7 +12,7 @@ const bodySchema = z.object({
priority: z.nativeEnum(MoscowPriority),
scope: z.string(),
level: z.string(),
- goalInContext: z.string(),
+ outcome: z.string().uuid(),
precondition: z.string().uuid(),
triggerId: z.literal(emptyUuid),
mainSuccessScenario: z.string(),
@@ -29,27 +29,35 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, body.solutionId),
em = fork()
- const newUseCase = new UseCase({
- name: body.name,
- description: body.description,
- primaryActor: body.primaryActor ? em.getReference(Stakeholder, body.primaryActor) : undefined,
- priority: body.priority,
- scope: body.scope,
- level: body.level,
- goalInContext: body.goalInContext,
- precondition: body.precondition ? em.getReference(Assumption, body.precondition) : undefined,
- triggerId: body.triggerId,
- mainSuccessScenario: body.mainSuccessScenario,
- successGuarantee: body.successGuarantee ? em.getReference(Effect, body.successGuarantee) : undefined,
- extensions: body.extensions,
- lastModified: new Date(),
- modifiedBy: sessionUser,
- isSilence: body.isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newUseCase = new UseCase({
+ reqId: await getNextReqId('S.4.', em, solution) as UseCase['reqId'],
+ name: body.name,
+ description: body.description,
+ primaryActor: body.primaryActor ? em.getReference(Stakeholder, body.primaryActor) : undefined,
+ priority: body.priority,
+ scope: body.scope,
+ level: body.level,
+ outcome: body.outcome ? em.getReference(Outcome, body.outcome) : undefined,
+ precondition: body.precondition ? em.getReference(Assumption, body.precondition) : undefined,
+ triggerId: body.triggerId,
+ mainSuccessScenario: body.mainSuccessScenario,
+ successGuarantee: body.successGuarantee ? em.getReference(Effect, body.successGuarantee) : undefined,
+ extensions: body.extensions,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ isSilence: body.isSilence
+ })
+
+ em.create(Belongs, {
+ left: newUseCase,
+ right: solution
+ })
- em.create(Belongs, { left: newUseCase, right: solution })
+ await em.flush()
- await em.flush()
+ return newUseCase.id
+ })
- return newUseCase.id
+ return newId
})
\ No newline at end of file
diff --git a/server/api/user-story/[id].delete.ts b/server/api/user-story/[id].delete.ts
index 606b72dc..9a945eba 100644
--- a/server/api/user-story/[id].delete.ts
+++ b/server/api/user-story/[id].delete.ts
@@ -20,5 +20,5 @@ export default defineEventHandler(async (event) => {
em = fork(),
userStory = await assertReqBelongsToSolution(em, UserStory, id, solution)
- await em.removeAndFlush(userStory)
+ await deleteSolutionRequirement(em, userStory, solution)
})
\ No newline at end of file
diff --git a/server/api/user-story/[id].put.ts b/server/api/user-story/[id].put.ts
index e063c18e..7efd7f91 100644
--- a/server/api/user-story/[id].put.ts
+++ b/server/api/user-story/[id].put.ts
@@ -1,6 +1,6 @@
import { z } from "zod"
import { fork } from "~/server/data/orm.js"
-import { FunctionalBehavior, MoscowPriority, Outcome, Stakeholder, UserStory } from "~/domain/requirements/index.js"
+import { FunctionalBehavior, MoscowPriority, Outcome, Stakeholder, UserStory, userStoryReqIdPrefix } from "~/domain/requirements/index.js"
const paramSchema = z.object({
id: z.string().uuid()
@@ -43,5 +43,10 @@ export default defineEventHandler(async (event) => {
lastModified: new Date()
})
+ // If the entity is no longer silent and has no reqId, assume
+ // that it is a new requirement from the workbox
+ if (body.isSilence !== undefined && body.isSilence == false && !userStory.reqId)
+ userStory.reqId = await getNextReqId(userStoryReqIdPrefix, em, solution) as UserStory['reqId']
+
await em.persistAndFlush(userStory)
})
\ No newline at end of file
diff --git a/server/api/user-story/index.post.ts b/server/api/user-story/index.post.ts
index 1d9a34bb..70b41b33 100644
--- a/server/api/user-story/index.post.ts
+++ b/server/api/user-story/index.post.ts
@@ -22,21 +22,29 @@ export default defineEventHandler(async (event) => {
{ solution, sessionUser } = await assertSolutionContributor(event, body.solutionId),
em = fork()
- const newUserStory = new UserStory({
- functionalBehavior: body.functionalBehavior ? em.getReference(FunctionalBehavior, body.functionalBehavior) : undefined,
- outcome: body.outcome ? em.getReference(Outcome, body.outcome) : undefined,
- name: body.name,
- description: body.description,
- primaryActor: body.primaryActor ? em.getReference(Stakeholder, body.primaryActor) : undefined,
- priority: body.priority,
- lastModified: new Date(),
- modifiedBy: sessionUser,
- isSilence: body.isSilence
- })
+ const newId = await em.transactional(async (em) => {
+ const newUserStory = new UserStory({
+ reqId: await getNextReqId('S.4.', em, solution) as UserStory['reqId'],
+ functionalBehavior: body.functionalBehavior ? em.getReference(FunctionalBehavior, body.functionalBehavior) : undefined,
+ outcome: body.outcome ? em.getReference(Outcome, body.outcome) : undefined,
+ name: body.name,
+ description: body.description,
+ primaryActor: body.primaryActor ? em.getReference(Stakeholder, body.primaryActor) : undefined,
+ priority: body.priority,
+ lastModified: new Date(),
+ modifiedBy: sessionUser,
+ isSilence: body.isSilence
+ })
+
+ em.create(Belongs, {
+ left: newUserStory,
+ right: solution
+ })
- em.create(Belongs, { left: newUserStory, right: solution })
+ await em.flush()
- await em.flush()
+ return newUserStory.id
+ })
- return newUserStory.id
+ return newId
})
\ No newline at end of file
diff --git a/server/data/llm-zod-schemas/UseCaseSchema.ts b/server/data/llm-zod-schemas/UseCaseSchema.ts
index 465b6d93..8187cfbf 100644
--- a/server/data/llm-zod-schemas/UseCaseSchema.ts
+++ b/server/data/llm-zod-schemas/UseCaseSchema.ts
@@ -6,7 +6,7 @@ export default z.object({
primaryActor: z.string().describe('The primary actor involved in the Use Case. Use N/A if there is no primary actor.'),
scope: z.string().describe('The scope of the Use Case. Use N/A if there is no scope.'),
level: z.string().describe('The level of the Use Case. Use N/A if there is no level.'),
- goalInContext: z.string().describe('The goal in context of the Use Case. Use N/A if there is no goal in context.'),
+ outcome: z.string().describe('The goal/outcome in context of the Use Case. Use N/A if there is no goal in context.'),
precondition: z.string().describe('The precondition is an Assumption that must be true before the Use Case can start. Use N/A if there is no precondition.'),
mainSuccessScenario: z.string().describe('The main success scenario is the most common path through the system. It takes the form of a sequence of steps that describe the interaction. Use N/A if there is no main success scenario.'),
successGuarantee: z.string().describe('The success guarantee is the guarantee that the system will provide to the user. Use N/A if there is no success guarantee.'),
diff --git a/server/utils/deleteSolutionRequirement.ts b/server/utils/deleteSolutionRequirement.ts
new file mode 100644
index 00000000..738f54d9
--- /dev/null
+++ b/server/utils/deleteSolutionRequirement.ts
@@ -0,0 +1,44 @@
+import { SqlEntityManager } from "@mikro-orm/postgresql"
+import { Belongs } from "~/domain/relations"
+import { Requirement, Solution } from "~/domain/requirements/index.js"
+
+/**
+ * Delete a requirement associated with a solution and decrement the numbers of subsequent requirements
+ *
+ * @param em - The entity manager
+ * @param entity - The requirement to delete
+ * @param solution - The solution to delete the requirement from
+ */
+const deleteSolutionRequirement = async (em: SqlEntityManager, entity: Requirement, solution: Solution) => {
+ return em.transactional(async (em) => {
+ if (!entity.reqId)
+ return await em.removeAndFlush(entity)
+
+ const reReqId = /^([PEGS]\.\d+\.)(\d+)$/,
+ [, prefixMatch, numMatch] = entity.reqId.match(reReqId)!
+
+ // Find all reqIds with the same prefix and a higher number than the deleted one
+ const rowsToUpdate = (await em.find(Belongs, {
+ left: {
+ reqId: { $like: `${prefixMatch}%` }
+ },
+ right: solution
+ }, { populate: ['left'] })).filter(({ left }) => {
+ const [, , num] = left.reqId!.match(reReqId)!
+ return parseInt(num) > parseInt(numMatch)
+ }).map(({ left }) => left)
+
+ em.remove(entity)
+
+ // Decrement the number part of each subsequent reqId
+ for (const row of rowsToUpdate) {
+ const [, prefix, num] = row.reqId!.match(reReqId)!
+ row.reqId = `${prefix}${parseInt(num) - 1}` as Requirement['reqId']
+ em.persist(row)
+ }
+
+ await em.flush()
+ })
+}
+
+export default deleteSolutionRequirement
\ No newline at end of file
diff --git a/server/utils/findAllSolutionRequirements.ts b/server/utils/findAllSolutionRequirements.ts
index d1eefea8..25e8c662 100644
--- a/server/utils/findAllSolutionRequirements.ts
+++ b/server/utils/findAllSolutionRequirements.ts
@@ -5,6 +5,7 @@ import { ReqType } from "../../domain/requirements/ReqType.js"
import { type ReqRelModel } from "../../domain/types/index.js"
/**
+ * Find all requirements associated with a solution
*
* @param req_type - The type of requirement to find
* @param em - The entity manager
@@ -37,6 +38,28 @@ export default async function findAllSolutionRequirements
})
return solutionItems.map(result =>
- Object.assign(result.left, { solutionId: query.solutionId }) as unknown as ReqRelModel
+ Object.assign(result.left, {
+ solutionId: query.solutionId
+ }) as unknown as ReqRelModel
)
+ // Sort by reqId ascending
+ // reqId could be null or a string of the form: X.#.# where X is a letter and # is a number.
+ // If reqId is null, it should be treated as the string 'X.0.0'
+ // Dictionary sorting willwork for this if each number is zero-padded to a fixed length
+ .sort((a, b) => {
+ let [aLetter, aMajor, aMinor] = (a.reqId ?? '0.0.0').split('.'),
+ [bLetter, bMajor, bMinor] = (b.reqId ?? '0.0.0').split('.'),
+ majorLength = Math.max(aMajor.length, bMajor.length),
+ minorLength = Math.max(aMinor.length, bMinor.length)
+
+ aMajor = aMajor.padStart(majorLength, '0')
+ bMajor = bMajor.padStart(majorLength, '0')
+ aMinor = aMinor.padStart(minorLength, '0')
+ bMinor = bMinor.padStart(minorLength, '0')
+
+ const aId = `${aLetter}.${aMajor}.${aMinor}`,
+ bId = `${bLetter}.${bMajor}.${bMinor}`
+
+ return aId.localeCompare(bId)
+ })
}
\ No newline at end of file
diff --git a/server/utils/getNextReqId.ts b/server/utils/getNextReqId.ts
new file mode 100644
index 00000000..a7776abc
--- /dev/null
+++ b/server/utils/getNextReqId.ts
@@ -0,0 +1,23 @@
+import { SqlEntityManager } from "@mikro-orm/postgresql"
+import { Belongs } from "~/domain/relations"
+import { type ReqId, type ReqIdPrefix, Solution } from "~/domain/requirements"
+
+/**
+ * Gets the next requirement id for the given solution and requirement type
+ *
+ * @param prefix - The prefix for the requirement id. Ex: 'P.1.'
+ * @param em - The entity manager
+ * @param solution - The owning solution of the requirement
+ */
+const getNextReqId = async (prefix: T, em: SqlEntityManager, solution: Solution): Promise => {
+ const entityCount = await em.count(Belongs, {
+ left: {
+ reqId: { $like: `${prefix}%` },
+ },
+ right: solution
+ })
+
+ return `${prefix}${entityCount + 1}` as U
+}
+
+export default getNextReqId
\ No newline at end of file