From 1ca1726697e22bf5732787c1a4b8864546e09d0f Mon Sep 17 00:00:00 2001 From: Michael Haufe Date: Wed, 27 Nov 2024 19:55:34 +0000 Subject: [PATCH 1/3] Initial refactoring. --- application/OrganizationInteractor.ts | 100 +++++------ domain/relations/RequirementRelation.ts | 21 +-- domain/requirements/Actor.ts | 6 +- domain/requirements/Behavior.ts | 5 +- domain/requirements/Component.ts | 4 +- domain/requirements/Constraint.ts | 3 +- domain/requirements/Epic.ts | 3 +- domain/requirements/Example.ts | 6 +- domain/requirements/Functionality.ts | 4 +- domain/requirements/Goal.ts | 4 +- domain/requirements/Invariant.ts | 3 +- domain/requirements/Organization.ts | 3 +- domain/requirements/Person.ts | 3 +- domain/requirements/Requirement.ts | 212 +++++++++++++++++++++++- domain/requirements/Scenario.ts | 3 +- domain/requirements/Silence.ts | 5 +- domain/requirements/Solution.ts | 3 +- domain/requirements/Stakeholder.ts | 3 +- domain/requirements/TestCase.ts | 2 +- domain/requirements/UseCase.ts | 3 +- domain/requirements/UserStory.ts | 7 +- domain/types/index.ts | 12 +- 22 files changed, 293 insertions(+), 122 deletions(-) diff --git a/application/OrganizationInteractor.ts b/application/OrganizationInteractor.ts index eef61c13..6485ee19 100644 --- a/application/OrganizationInteractor.ts +++ b/application/OrganizationInteractor.ts @@ -1,9 +1,9 @@ import { Assumption, Constraint, Effect, EnvironmentComponent, FunctionalBehavior, GlossaryTerm, Invariant, Justification, Limit, MoscowPriority, NonFunctionalBehavior, Obstacle, Organization, Outcome, ParsedRequirement, Person, ReqType, Requirement, Solution, Stakeholder, StakeholderCategory, StakeholderSegmentation, SystemComponent, UseCase, UserStory } from "~/domain/requirements"; import { QueryOrder, type ChangeSetType, type EntityManager } from "@mikro-orm/core"; import { AppUserOrganizationRole, AppRole, AppUser, AuditLog } from "~/domain/application"; -import { Belongs } from "~/domain/relations/Belongs.js"; +// import { Belongs } from "~/domain/relations/Belongs.js"; import { validate } from 'uuid' -import { Follows } from "~/domain/relations"; +// import { Follows } from "~/domain/relations"; import type NaturalLanguageToRequirementService from "~/server/data/services/NaturalLanguageToRequirementService"; import { groupBy, slugify } from "#shared/utils"; @@ -53,17 +53,16 @@ export class OrganizationInteractor { * @param prefix - The prefix for the requirement id. Ex: 'P.1.' */ private async _getNextReqId(solution: Solution, prefix: R['reqIdPrefix']): Promise['reqId']> { - const em = this.getEntityManager(), - entityCount = await em.count(Belongs, { - left: { reqId: { $like: `${prefix}%` } }, - right: solution - }) + const entityCount = await solution.contains.loadCount({ + where: { reqId: { $like: `${prefix}%` } } + }); return `${prefix}${entityCount + 1}` } /** * Create a new entity manager fork + * * @returns The entity manager */ // TODO: this will likely be moved into a Repository class in the future @@ -78,8 +77,8 @@ export class OrganizationInteractor { * @param props.ReqClass - The Constructor of the requirement to add * @param props.reqProps - The requirement data to add * @returns The id of the new requirement + * @throws {Error} If the organization does not exist * @throws {Error} If the solution does not exist - * @throws {Error} If the solution does not belong to the organization * @throws {Error} If the user is not a reader of the organization or better * @throws {Error} If the user is not a contributor of the solution or better * @throws {Error} If a referenced requirement does not belong to the solution @@ -94,34 +93,38 @@ export class OrganizationInteractor { if (!this.isOrganizationContributor()) throw new Error('Forbidden: You do not have permission to perform this action') + const organization = await this.getOrganization(), + solution = (await organization.contains.loadItems({ + where: { id: solutionId, req_type: ReqType.SOLUTION } + }))[0] + + if (!solution) + throw new Error('Not Found: The solution does not exist.') + const em = this.getEntityManager(), appUser = await this._user, - { left: solution } = await em.findOneOrFail(Belongs, { - left: { id: solutionId, req_type: ReqType.SOLUTION }, - right: await this._organization - }) as unknown as { left: Solution }, // If the entity is silent, do not assign a reqId reqId = reqProps.isSilence ? undefined : await this._getNextReqId(solution, ReqClass.reqIdPrefix); // if a property is a uuid, assert that it belongs to the solution - for (const [key, value] of Object.entries(reqProps)) { - if (validate(value) && !['id', 'createdBy', 'modifiedBy'].includes(key)) - await em.findOneOrFail(Belongs, { left: value, right: solution }) + for (const [key, value] of Object.entries(reqProps) as [keyof typeof reqProps, string][]) { + if (validate(value) && !['id', 'createdBy', 'modifiedBy'].includes(key as string)) { + const reqExists = await solution.contains.loadCount({ where: { id: value } }) + if (reqExists === 0) + throw new Error(`Not Found: The referenced requirement with id ${value} does not exist in the solution.`) + } } - console.log('reqProps', reqProps) - const newRequirement = em.create(ReqClass, { ...reqProps, reqId, lastModified: new Date(), createdBy: appUser, - modifiedBy: appUser + modifiedBy: appUser, + belongs: [solution] }) as InstanceType - em.create(Belongs, { left: newRequirement, right: solution }) - await em.flush() return newRequirement @@ -282,14 +285,11 @@ export class OrganizationInteractor { const em = this.getEntityManager(), appUser = await em.findOne(AppUser, { email }), - organization = await this._organization + organization = await this.getOrganization() if (!appUser) throw new Error('Not Found: The app user with the given email does not exist.') - if (!organization) - throw new Error('Not Found: The organization does not exist.') - const existingOrgAppUserRole = await em.findOne(AppUserOrganizationRole, { appUser, organization @@ -313,7 +313,7 @@ export class OrganizationInteractor { * @throws {Error} If the user is trying to get an app user that is not in the same organization */ async getAppUserById(id: AppUser['id']): Promise { - const organization = await this._organization, + const organization = await this.getOrganization(), em = this.getEntityManager(), { appUser, role } = (await em.findOneOrFail(AppUserOrganizationRole, { appUser: id, @@ -339,7 +339,7 @@ export class OrganizationInteractor { */ async deleteAppUser(id: AppUser['id']): Promise { const em = this.getEntityManager(), - organization = await this._organization, + organization = await this.getOrganization(), appUser = await this._user, [targetAppUserRole, orgAdminCount] = await Promise.all([ em.findOne(AppUserOrganizationRole, { @@ -377,7 +377,7 @@ export class OrganizationInteractor { */ async updateAppUserRole(id: AppUser['id'], role: AppRole): Promise { const em = this.getEntityManager(), - organization = await this._organization, + organization = await this.getOrganization(), appUser = await this._user, [targetAppUserRole, orgAdminCount] = await Promise.all([ em.findOne(AppUserOrganizationRole, { @@ -419,7 +419,7 @@ export class OrganizationInteractor { throw new Error('Forbidden: You do not have permission to perform this action') const em = this.getEntityManager(), - organization = await this._organization, + organization = await this.getOrganization(), appUserOrganizationRoles = await em.findAll(AppUserOrganizationRole, { where: { organization }, populate: ['appUser'] @@ -440,7 +440,7 @@ export class OrganizationInteractor { */ async isOrganizationAdmin(): Promise { const appUser = await this._user, - organization = await this._organization + organization = await this.getOrganization() if (appUser.isSystemAdmin) return true @@ -458,7 +458,7 @@ export class OrganizationInteractor { */ async isOrganizationContributor(): Promise { const appUser = await this._user, - organization = await this._organization + organization = await this.getOrganization() if (appUser.isSystemAdmin) return true @@ -475,7 +475,7 @@ export class OrganizationInteractor { * Check if the current user is a reader of the organization or a system admin */ async isOrganizationReader(): Promise { - const organization = await this._organization, + const organization = await this.getOrganization(), appUser = await this._user if (appUser.isSystemAdmin) return true @@ -528,12 +528,9 @@ export class OrganizationInteractor { * @throws {Error} If the organization does not exist */ async addSolution({ name, description }: Pick): Promise { - const organization = await this._organization, + const organization = await this.getOrganization(), createdBy = await this._user - if (!organization) - throw new Error('Not Found: The organization does not exist.') - if (!await this.isOrganizationAdmin()) throw new Error('Forbidden: You do not have permission to perform this action') @@ -554,13 +551,13 @@ export class OrganizationInteractor { await this.addRequirement({ solutionId: newSolution.id, ReqClass: Outcome, - reqProps: { name: 'G.1', description: 'Context and Objective', isSilence: false } + reqProps: { name: 'G.1', description: 'Context and Objective', isSilence: false } as ConstructorParameters[0] }) await this.addRequirement({ solutionId: newSolution.id, ReqClass: Obstacle, - reqProps: { name: 'G.2', description: 'Situation', isSilence: false } + reqProps: { name: 'G.2', description: 'Situation', isSilence: false } as ConstructorParameters[0] }) return newSolution @@ -575,10 +572,7 @@ export class OrganizationInteractor { * @throws {Error} If the organization does not exist */ async findSolutions(query: Partial = {}): Promise { - const organization = await this._organization - - if (!organization) - throw new Error('Not Found: The organization does not exist.') + const organization = await this.getOrganization() if (!await this.isOrganizationReader()) throw new Error('Forbidden: You do not have permission to perform this action') @@ -618,7 +612,7 @@ export class OrganizationInteractor { const em = this.getEntityManager(), { left: solution } = await em.findOneOrFail(Belongs, { left: { id: solutionId, req_type: ReqType.SOLUTION }, - right: await this._organization + right: await this.getOrganization() }, { populate: ['left'] }) as unknown as { left: Solution } return solution @@ -640,7 +634,7 @@ export class OrganizationInteractor { const em = this.getEntityManager(), { left: solution } = await em.findOneOrFail(Belongs, { left: { slug, req_type: ReqType.SOLUTION } as Solution, - right: await this._organization + right: await this.getOrganization() }, { populate: ['left'] }) as unknown as { left: Solution } return solution @@ -661,7 +655,7 @@ export class OrganizationInteractor { const em = this.getEntityManager(), { left: solution } = await em.findOneOrFail(Belongs, { left: { slug, req_type: ReqType.SOLUTION } as Solution, - right: await this._organization + right: await this.getOrganization() }) as unknown as { left: Solution } await em.removeAndFlush(solution) @@ -683,7 +677,7 @@ export class OrganizationInteractor { const em = this.getEntityManager(), { left: solution } = await em.findOneOrFail(Belongs, { left: { slug, req_type: ReqType.SOLUTION } as Solution, - right: await this._organization + right: await this.getOrganization() }) as unknown as { left: Solution } solution.assign({ @@ -735,7 +729,7 @@ export class OrganizationInteractor { * Returns the organization that the user is associated with * * @returns The organization - * @throws {Error} If the user is not associated with an organization + * @throws {Error} If the organization does not exist */ async getOrganization(): Promise { if (!this._organization) @@ -751,10 +745,7 @@ export class OrganizationInteractor { */ async deleteOrganization(): Promise { const em = this.getEntityManager(), - organization = await this._organization - - if (!organization) - throw new Error('Not Found: The organization does not exist.') + organization = await this.getOrganization() if (!await this.isOrganizationAdmin()) throw new Error('Forbidden: You do not have permission to perform this action') @@ -781,10 +772,7 @@ export class OrganizationInteractor { */ async updateOrganization(props: Pick, 'name' | 'description'>): Promise { const em = this.getEntityManager(), - organization = await this._organization - - if (!organization) - throw new Error('Not Found: The organization does not exist.') + organization = await this.getOrganization() if (!await this.isOrganizationContributor()) throw new Error('Forbidden: You do not have permission to perform this action') @@ -915,7 +903,7 @@ export class OrganizationInteractor { // Check if the Solution belongs to the organization { left: solution } = (await em.findOneOrFail(Belongs, { left: { id: solutionId, req_type: ReqType.SOLUTION }, - right: await this._organization + right: await this.getOrganization() }, { populate: ['left'] })) as unknown as { left: Solution }, parsedRequirements = (await em.find(Belongs, { left: { req_type: ReqType.PARSED_REQUIREMENT }, @@ -948,7 +936,7 @@ export class OrganizationInteractor { em = this.getEntityManager(), { left: solution } = (await em.findOneOrFail(Belongs, { left: { id: solutionId, req_type: ReqType.SOLUTION }, - right: await this._organization + right: await this.getOrganization() }, { populate: ['left'] })) as unknown as { left: Solution } const groupedResult = await parsingService.parse(statement) diff --git a/domain/relations/RequirementRelation.ts b/domain/relations/RequirementRelation.ts index b6f59f5d..a1ce25e5 100644 --- a/domain/relations/RequirementRelation.ts +++ b/domain/relations/RequirementRelation.ts @@ -1,35 +1,26 @@ -import { v7 as uuidv7 } from 'uuid'; -import { BaseEntity, Cascade, Entity, ManyToOne, Property } from "@mikro-orm/core"; +import { BaseEntity, Entity, ManyToOne } from "@mikro-orm/core"; import { Requirement } from '../requirements/Requirement.js' -import { type Properties } from '../types/index.js'; /** * Relations between requirements */ @Entity({ abstract: true, discriminatorColumn: 'rel_type' }) export abstract class RequirementRelation extends BaseEntity { - constructor(props: Properties>) { + constructor({ left, right }: { left: Requirement, right: Requirement }) { super() - this.id = uuidv7(); - this.left = props.left; - this.right = props.right; + this.left = left; + this.right = right; } - /** - * The unique identifier of the RequirementRelation - */ - @Property({ type: 'uuid', primary: true }) - id: string; - /** * The left-hand side of the relation */ - @ManyToOne({ entity: () => Requirement, cascade: [Cascade.REMOVE] }) + @ManyToOne({ primary: true }) left: Requirement /** * The right-hand side of the relation */ - @ManyToOne({ entity: () => Requirement, cascade: [Cascade.REMOVE] }) + @ManyToOne({ primary: true }) right: Requirement } \ No newline at end of file diff --git a/domain/requirements/Actor.ts b/domain/requirements/Actor.ts index 7a1b6876..51f19f4b 100644 --- a/domain/requirements/Actor.ts +++ b/domain/requirements/Actor.ts @@ -5,7 +5,7 @@ import { ReqType } from "./ReqType.js"; /** * A part of a Project, Environment, System, or Goals that may affect or be affected by the associated entities */ -@Entity({ abstract: true, discriminatorValue: ReqType.ACTOR }) -export abstract class Actor extends Requirement { +@Entity({ discriminatorValue: ReqType.ACTOR }) +export class Actor extends Requirement { static override req_type: ReqType = ReqType.ACTOR; -} +} \ No newline at end of file diff --git a/domain/requirements/Behavior.ts b/domain/requirements/Behavior.ts index c09401d5..fd9feffa 100644 --- a/domain/requirements/Behavior.ts +++ b/domain/requirements/Behavior.ts @@ -1,17 +1,16 @@ import { Entity, Enum } from "@mikro-orm/core"; import { Requirement } from "./Requirement.js"; import { MoscowPriority } from "./MoscowPriority.js"; -import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; /** * Property of the operation of the system */ -@Entity({ abstract: true, discriminatorValue: ReqType.BEHAVIOR }) +@Entity({ discriminatorValue: ReqType.BEHAVIOR }) export class Behavior extends Requirement { static override req_type: ReqType = ReqType.BEHAVIOR; - constructor({ priority, ...rest }: Properties>) { + constructor({ priority, ...rest }: ConstructorParameters[0] & Pick) { super(rest); this.priority = priority; } diff --git a/domain/requirements/Component.ts b/domain/requirements/Component.ts index 2b37f8d4..0fb24a72 100644 --- a/domain/requirements/Component.ts +++ b/domain/requirements/Component.ts @@ -5,7 +5,7 @@ import { ReqType } from "./ReqType.js"; /** * Idenfitication of a part (of the Project, Environment, Goals, or System) */ -@Entity({ abstract: true, discriminatorValue: ReqType.COMPONENT }) -export abstract class Component extends Actor { +@Entity({ discriminatorValue: ReqType.COMPONENT }) +export class Component extends Actor { static override req_type: ReqType = ReqType.COMPONENT; } diff --git a/domain/requirements/Constraint.ts b/domain/requirements/Constraint.ts index 01e5e45f..6294d5bc 100644 --- a/domain/requirements/Constraint.ts +++ b/domain/requirements/Constraint.ts @@ -1,7 +1,6 @@ import { Entity, Enum } from '@mikro-orm/core'; import { Requirement } from './Requirement.js'; import { ConstraintCategory } from './ConstraintCategory.js'; -import { type Properties } from '../types/index.js'; import { ReqType } from './ReqType.js'; /** @@ -12,7 +11,7 @@ export class Constraint extends Requirement { static override reqIdPrefix = 'E.3.' as const; static override req_type = ReqType.CONSTRAINT; - constructor({ category, ...rest }: Properties>) { + constructor({ category, ...rest }: ConstructorParameters[0] & Pick) { super(rest); this.category = category; } diff --git a/domain/requirements/Epic.ts b/domain/requirements/Epic.ts index 623dd240..31db5665 100644 --- a/domain/requirements/Epic.ts +++ b/domain/requirements/Epic.ts @@ -1,7 +1,6 @@ import { Entity, ManyToOne } from "@mikro-orm/core"; import { Scenario } from "./Scenario.js"; import { ReqType } from "./ReqType.js"; -import type { Properties } from "../types/index.js"; import { FunctionalBehavior } from "./FunctionalBehavior.js"; /** @@ -13,7 +12,7 @@ export class Epic extends Scenario { static override reqIdPrefix = 'G.5.' as const; static override req_type = ReqType.EPIC; - constructor(props: Properties>) { + constructor(props: ConstructorParameters[0] & Pick) { super(props); this.functionalBehavior = props.functionalBehavior; } diff --git a/domain/requirements/Example.ts b/domain/requirements/Example.ts index 6e4c058f..98d95d80 100644 --- a/domain/requirements/Example.ts +++ b/domain/requirements/Example.ts @@ -5,7 +5,7 @@ import { ReqType } from "./ReqType.js"; /** * Illustration of behavior through a usage scenario */ -@Entity({ abstract: true, discriminatorValue: ReqType.EXAMPLE }) -export abstract class Example extends Behavior { +@Entity({ discriminatorValue: ReqType.EXAMPLE }) +export class Example extends Behavior { static override req_type: ReqType = ReqType.EXAMPLE; -} +} \ No newline at end of file diff --git a/domain/requirements/Functionality.ts b/domain/requirements/Functionality.ts index 14991964..50a006f0 100644 --- a/domain/requirements/Functionality.ts +++ b/domain/requirements/Functionality.ts @@ -5,7 +5,7 @@ import { ReqType } from "./ReqType.js"; /** * Functionality describes what system will do and how it will do it. */ -@Entity({ abstract: true, discriminatorValue: ReqType.FUNCTIONALITY }) -export abstract class Functionality extends Behavior { +@Entity({ discriminatorValue: ReqType.FUNCTIONALITY }) +export class Functionality extends Behavior { static override req_type: ReqType = ReqType.FUNCTIONALITY; } \ No newline at end of file diff --git a/domain/requirements/Goal.ts b/domain/requirements/Goal.ts index 5bd4273e..803f6c54 100644 --- a/domain/requirements/Goal.ts +++ b/domain/requirements/Goal.ts @@ -7,7 +7,7 @@ import { ReqType } from "./ReqType.js"; * an objective of the project or system, in terms * of their desired effect on the environment */ -@Entity({ abstract: true, discriminatorValue: ReqType.GOAL }) -export abstract class Goal extends Requirement { +@Entity({ discriminatorValue: ReqType.GOAL }) +export class Goal extends Requirement { static override req_type: ReqType = ReqType.GOAL; } \ No newline at end of file diff --git a/domain/requirements/Invariant.ts b/domain/requirements/Invariant.ts index 6de2729f..e872ade1 100644 --- a/domain/requirements/Invariant.ts +++ b/domain/requirements/Invariant.ts @@ -1,6 +1,5 @@ import { Entity } from "@mikro-orm/core"; import { Requirement } from "./Requirement.js"; -import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; /** @@ -13,7 +12,7 @@ export class Invariant extends Requirement { static override reqIdPrefix = 'E.6.' as const; static override req_type: ReqType = ReqType.INVARIANT; - constructor(props: Properties>) { + constructor(props: ConstructorParameters[0]) { super(props); this.req_type = ReqType.INVARIANT; } diff --git a/domain/requirements/Organization.ts b/domain/requirements/Organization.ts index 8984a92d..cc330a52 100644 --- a/domain/requirements/Organization.ts +++ b/domain/requirements/Organization.ts @@ -1,7 +1,6 @@ import { Entity, Property } from "@mikro-orm/core"; import { slugify } from "../../shared/utils/slugify.js"; import { Requirement } from "./Requirement.js"; -import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; /** @@ -11,7 +10,7 @@ import { ReqType } from "./ReqType.js"; export class Organization extends Requirement { static override req_type: ReqType = ReqType.ORGANIZATION - constructor(props: Properties>) { + constructor(props: ConstructorParameters[0]) { super(props) this._slug = slugify(props.name); } diff --git a/domain/requirements/Person.ts b/domain/requirements/Person.ts index 02b5bb6c..57332dc0 100644 --- a/domain/requirements/Person.ts +++ b/domain/requirements/Person.ts @@ -1,6 +1,5 @@ import { Entity, Property } from "@mikro-orm/core"; import { Actor } from "./Actor.js"; -import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; /** @@ -11,7 +10,7 @@ export class Person extends Actor { static override req_type: ReqType = ReqType.PERSON; static override reqIdPrefix = 'P.1.' as const; - constructor({ email, ...rest }: Properties>) { + constructor({ email, ...rest }: ConstructorParameters[0] & Pick) { super(rest); this.email = email; } diff --git a/domain/requirements/Requirement.ts b/domain/requirements/Requirement.ts index 4a4db074..8f821c0d 100644 --- a/domain/requirements/Requirement.ts +++ b/domain/requirements/Requirement.ts @@ -1,8 +1,9 @@ import { v7 as uuidv7 } from 'uuid'; -import { BaseEntity, Entity, Enum, ManyToOne, OptionalProps, Property } from '@mikro-orm/core'; -import { type Properties } from '../types/index.js'; +import { BaseEntity, Collection, Entity, Enum, ManyToMany, ManyToOne, OptionalProps, Property } from '@mikro-orm/core'; +import { type CollectionPropsToOptionalArrays, type Properties } from '../types/index.js'; import { ReqType } from './ReqType.js'; import { AppUser } from '../application/AppUser.js'; +import { Belongs, Characterizes, Constrains, Contradicts, Details, Disjoins, Duplicates, Excepts, Explains, Extends, Follows, Repeats, Shares } from '../relations/index.js'; /** * A Requirement is a statement that specifies a property. @@ -12,7 +13,7 @@ export abstract class Requirement extends BaseEntity { static req_type: ReqType = ReqType.REQUIREMENT; static reqIdPrefix: `${'P' | 'E' | 'G' | 'S' | '0'}.${number}.` = '0.0.'; - constructor(props: Properties>) { + constructor(props: CollectionPropsToOptionalArrays>>) { super() this.id = uuidv7(); this.req_type = (this.constructor as typeof Requirement).req_type; @@ -23,6 +24,11 @@ export abstract class Requirement extends BaseEntity { this.isSilence = props.isSilence; this.createdBy = props.createdBy; this._reqId = props.reqId; + + for (const [key, value] of Object.entries(props)) { + if (Array.isArray(value)) + (this[key as keyof this] as Collection) = new Collection(this, value); + } } // This fixes the issue with em.create not honoring the constructor signature @@ -103,4 +109,204 @@ export abstract class Requirement extends BaseEntity { */ @Property({ type: 'boolean', default: false }) isSilence: boolean; + + /////////////////////////////////////////////////////////////////////////// + ////////////////////// Relations ///////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////// + + /** + * this ⊆ other + * + * this is a sub-requirement of other; textually included + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Belongs, inversedBy: (r) => r.contains }) + belongs = new Collection(this); + + /** + * The inverse of {@link belongs} + * + * This is the set of requirements that are sub-requirements of this requirement + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Belongs, mappedBy: (r) => r.belongs }) + contains = new Collection(this); + + /** + * this → other + * + * Meta-requirement this applies to other + */ + // TODO: need to enforce that this is a MetaRequirement + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Characterizes, inversedBy: (r) => r.characterizedBy }) + characterizes = new Collection(this); + + /** + * The inverse of {@link characterizes} + */ + // TODO: need to enforce that other is a MetaRequirement + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Characterizes, mappedBy: (r) => r.characterizes }) + characterizedBy = new Collection(this); + + /** + * this ▸ other + * + * Constraint this applies to other + */ + // TODO: need to enforce that this is a Constraint + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Constrains, inversedBy: (r) => r.constrainedBy }) + constrains = new Collection(this); + + /** + * The inverse of {@link constrains} + */ + // TODO: need to enforce that other is a Constraint + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Constrains, mappedBy: (r) => r.constrains }) + constrainedBy = new Collection(this); + + /** + * this ⊕ other + * + * Properties specified by this and other cannot both hold + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Contradicts, inversedBy: (r) => r.contradictedBy }) + contradicts = new Collection(this); + + /** + * The inverse of {@link contradicts} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Contradicts, mappedBy: (r) => r.contradicts }) + contradictedBy = new Collection(this); + + /** + * this » other + * + * this adds detail to properties of other + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Details, inversedBy: (r) => r.detailedBy }) + details = new Collection(this); + + /** + * The inverse of {@link details} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Details, mappedBy: (r) => r.details }) + detailedBy = new Collection(this); + + /** + * this || other + * + * this and other are unrelated + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Disjoins, inversedBy: (r) => r.disjoinedBy }) + disjoins = new Collection(this); + + /** + * The inverse of {@link disjoins} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Disjoins, mappedBy: (r) => r.disjoins }) + disjoinedBy = new Collection(this); + + /** + * this ≡ other + * + * this ⇔ other, and this has the same type as other. + * In other words, this and other are redundant. + * Same properties, same type (notation-wise, this ≡ other) + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Duplicates, inversedBy: (r) => r.duplicatedBy }) + duplicates = new Collection(this); + + /** + * The inverse of {@link duplicates} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Duplicates, mappedBy: (r) => r.duplicates }) + duplicatedBy = new Collection(this); + + /** + * this \\ other + * + * this specifies an exception to the property specified by other. + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Excepts, inversedBy: (r) => r.exceptedBy }) + excepts = new Collection(this); + + /** + * The inverse of {@link excepts} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Excepts, mappedBy: (r) => r.excepts }) + exceptedBy = new Collection(this); + + /** + * X ≅ Y + * + * X ⇔ Y, and X has a different type from Y. + * In other words, Y introduces no new property but helps understand X better. + * Same properties, different type (notation-wise) + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Explains, inversedBy: (r) => r.explainedBy }) + explains = new Collection(this); + + /** + * The inverse of {@link explains} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Explains, mappedBy: (r) => r.explains }) + explainedBy = new Collection(this); + + /** + * X > Y + * + * aka "refines". + * X assumes Y and specifies a property that Y does not. + * X adds to properties of Y + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Extends, inversedBy: (r) => r.extendedBy }) + extends = new Collection(this); + + /** + * The inverse of {@link extends} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Extends, mappedBy: (r) => r.extends }) + extendedBy = new Collection(this); + + /** + * X ⊣ Y + * + * X is a consequence of the property specified by Y + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Follows, inversedBy: (r) => r.followedBy }) + follows = new Collection(this); + + /** + * The inverse of {@link follows} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Follows, mappedBy: (r) => r.follows }) + followedBy = new Collection(this); + + /** + * X ⇔ Y + * + * X specifies the same property as Y + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Repeats, inversedBy: (r) => r.repeatedBy }) + repeats = new Collection(this); + + /** + * The inverse of {@link repeats} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Repeats, mappedBy: (r) => r.repeats }) + repeatedBy = new Collection(this); + + /** + * X ∩ Y + * + * X' ⇔ Y' for some sub-requirements X' and Y' of X and Y. + * (Involve Repeats). + * Some subrequirement is in common between X and Y. + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Shares, inversedBy: (r) => r.sharedBy }) + shares = new Collection(this); + + /** + * The inverse of {@link shares} + */ + @ManyToMany({ entity: () => Requirement, pivotEntity: () => Shares, mappedBy: (r) => r.shares }) + sharedBy = new Collection(this); } \ No newline at end of file diff --git a/domain/requirements/Scenario.ts b/domain/requirements/Scenario.ts index 5481c99c..d3577e55 100644 --- a/domain/requirements/Scenario.ts +++ b/domain/requirements/Scenario.ts @@ -1,7 +1,6 @@ import { Entity, ManyToOne } from "@mikro-orm/core"; 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"; @@ -13,7 +12,7 @@ import { Outcome } from "./Outcome.js"; export abstract class Scenario extends Example { static override req_type: ReqType = ReqType.SCENARIO; - constructor({ primaryActor, outcome, ...rest }: Properties>) { + constructor({ primaryActor, outcome, ...rest }: ConstructorParameters[0] & Pick) { super(rest); this.primaryActor = primaryActor; this.outcome = outcome; diff --git a/domain/requirements/Silence.ts b/domain/requirements/Silence.ts index f254fe5b..1ccb7d91 100644 --- a/domain/requirements/Silence.ts +++ b/domain/requirements/Silence.ts @@ -1,16 +1,15 @@ import { Entity } from "@mikro-orm/core"; import { Requirement } from "./Requirement.js"; -import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; /** - * Propery that is not in requirements but should be + * Property that is not in requirements but should be */ @Entity({ discriminatorValue: ReqType.SILENCE }) export class Silence extends Requirement { static override req_type: ReqType = ReqType.SILENCE; - constructor(props: Properties>) { + constructor(props: Omit[0], 'isSilence'>) { super({ ...props, isSilence: true }); } } \ No newline at end of file diff --git a/domain/requirements/Solution.ts b/domain/requirements/Solution.ts index 44da9a31..f8d9a699 100644 --- a/domain/requirements/Solution.ts +++ b/domain/requirements/Solution.ts @@ -1,5 +1,4 @@ import { Entity, Property } from '@mikro-orm/core'; -import { type Properties } from '../types/index.js'; import { slugify } from '../../shared/utils/slugify.js'; import { Requirement } from './Requirement.js'; import { ReqType } from './ReqType.js'; @@ -11,7 +10,7 @@ import { ReqType } from './ReqType.js'; export class Solution extends Requirement { static override req_type: ReqType = ReqType.SOLUTION; - constructor(props: Properties>) { + constructor(props: ConstructorParameters[0]) { super(props); this._slug = slugify(props.name); } diff --git a/domain/requirements/Stakeholder.ts b/domain/requirements/Stakeholder.ts index c7fc1d16..cbd39a02 100644 --- a/domain/requirements/Stakeholder.ts +++ b/domain/requirements/Stakeholder.ts @@ -2,7 +2,6 @@ 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"; /** @@ -13,7 +12,7 @@ export class Stakeholder extends Component { static override reqIdPrefix = 'G.7.' as const; static override req_type: ReqType = ReqType.STAKEHOLDER; - constructor(props: Properties>) { + constructor(props: ConstructorParameters[0] & Pick) { super(props); this.influence = props.influence; this.availability = props.availability; diff --git a/domain/requirements/TestCase.ts b/domain/requirements/TestCase.ts index a783215b..a0aa6094 100644 --- a/domain/requirements/TestCase.ts +++ b/domain/requirements/TestCase.ts @@ -5,7 +5,7 @@ import { ReqType } from "./ReqType.js"; /** * A TestCase is a specification of the inputs, execution conditions, * testing procedure, and expected results that define a single test to - * be executed to achieve a particular goal., + * be executed to achieve a particular goal. */ @Entity({ discriminatorValue: ReqType.TEST_CASE }) export class TestCase extends Example { diff --git a/domain/requirements/UseCase.ts b/domain/requirements/UseCase.ts index a470274d..4ba73bdf 100644 --- a/domain/requirements/UseCase.ts +++ b/domain/requirements/UseCase.ts @@ -2,7 +2,6 @@ import { Entity, ManyToOne, Property } from "@mikro-orm/core"; import { Assumption } from "./Assumption.js"; import { Effect } from "./Effect.js"; import { Scenario } from "./Scenario.js"; -import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; /** @@ -14,7 +13,7 @@ export class UseCase extends Scenario { static override reqIdPrefix = 'S.4.' as const; static override req_type = ReqType.USE_CASE; - constructor(props: Properties>) { + constructor(props: ConstructorParameters[0] & Pick) { super(props); this.scope = props.scope; this.level = props.level; diff --git a/domain/requirements/UserStory.ts b/domain/requirements/UserStory.ts index c0978f18..e4b1e20e 100644 --- a/domain/requirements/UserStory.ts +++ b/domain/requirements/UserStory.ts @@ -1,7 +1,6 @@ import { Entity, ManyToOne } from "@mikro-orm/core"; import { FunctionalBehavior } from "./FunctionalBehavior.js"; import { Scenario } from "./Scenario.js"; -import { type Properties } from "../types/index.js"; import { ReqType } from "./ReqType.js"; /** @@ -18,9 +17,9 @@ export class UserStory extends Scenario { static override reqIdPrefix = 'S.4.' as const; static override req_type = ReqType.USER_STORY; - constructor(props: Properties>) { - super(props); - this.functionalBehavior = props.functionalBehavior; + constructor({ functionalBehavior, ...rest }: ConstructorParameters[0] & Pick) { + super(rest); + this.functionalBehavior = functionalBehavior; } override get reqId() { return super.reqId as `${typeof UserStory.reqIdPrefix}${number}` | undefined; } diff --git a/domain/types/index.ts b/domain/types/index.ts index af88c1f4..5d43ff52 100644 --- a/domain/types/index.ts +++ b/domain/types/index.ts @@ -1,4 +1,4 @@ -import { Requirement } from "../requirements/Requirement.js"; +import type { Collection } from "@mikro-orm/core"; export type Constructor = (new (...args: any[]) => T) | (abstract new (...args: any[]) => T); @@ -9,12 +9,10 @@ export type Properties = Pick any ? never : K }[keyof T]>; + /** - * Represents a requirement model with relations + * A type that converts all the Collection properties of a type T to optional array properties */ -// TODO: move to a base DTO object -// possibly related to the following work: https://github.com/final-hill/cathedral/issues/164#issuecomment-2381004280 -export type ReqRelModel = R & { - parentComponent?: string, - solutionId: string +export type CollectionPropsToOptionalArrays = { + [K in keyof T]: T[K] extends Collection ? U[] | undefined : T[K] } \ No newline at end of file From 39081bce5365938f22d13ea80a655322ac4731b4 Mon Sep 17 00:00:00 2001 From: Michael Haufe Date: Thu, 28 Nov 2024 16:59:42 +0000 Subject: [PATCH 2/3] Updated Interactor --- application/OrganizationInteractor.ts | 191 +++++++++------------- domain/requirements/Requirement.ts | 26 +-- server/api/parse-requirement/index.get.ts | 28 +--- 3 files changed, 102 insertions(+), 143 deletions(-) diff --git a/application/OrganizationInteractor.ts b/application/OrganizationInteractor.ts index 6485ee19..3f270c75 100644 --- a/application/OrganizationInteractor.ts +++ b/application/OrganizationInteractor.ts @@ -1,9 +1,7 @@ import { Assumption, Constraint, Effect, EnvironmentComponent, FunctionalBehavior, GlossaryTerm, Invariant, Justification, Limit, MoscowPriority, NonFunctionalBehavior, Obstacle, Organization, Outcome, ParsedRequirement, Person, ReqType, Requirement, Solution, Stakeholder, StakeholderCategory, StakeholderSegmentation, SystemComponent, UseCase, UserStory } from "~/domain/requirements"; import { QueryOrder, type ChangeSetType, type EntityManager } from "@mikro-orm/core"; import { AppUserOrganizationRole, AppRole, AppUser, AuditLog } from "~/domain/application"; -// import { Belongs } from "~/domain/relations/Belongs.js"; import { validate } from 'uuid' -// import { Follows } from "~/domain/relations"; import type NaturalLanguageToRequirementService from "~/server/data/services/NaturalLanguageToRequirementService"; import { groupBy, slugify } from "#shared/utils"; @@ -151,13 +149,12 @@ export class OrganizationInteractor { const em = this.getEntityManager(), solution = await this.getSolutionById(props.solutionId), - { left: requirement } = await em.findOneOrFail(Belongs, { - left: { - id: props.id, - req_type: props.ReqClass.req_type - }, - right: solution - }, { populate: ['left'] }) as unknown as { left: InstanceType } + requirement = (await solution.contains.loadItems({ + where: { id: props.id, req_type: props.ReqClass.req_type } + }))[0] + + if (!requirement) + throw new Error('Not Found: The requirement does not exist.') // TODO: decrement the reqId of all requirements that have a reqId greater than the deleted requirement @@ -184,12 +181,13 @@ export class OrganizationInteractor { if (!this.isOrganizationReader()) throw new Error('Forbidden: You do not have permission to perform this action') - const em = this.getEntityManager(), - solution = await this.getSolutionById(props.solutionId), - { left: requirement } = await em.findOneOrFail(Belongs, { - left: { id: props.id, req_type: props.ReqClass.req_type }, - right: solution - }, { populate: ['left'] }) as unknown as { left: InstanceType } + const solution = await this.getSolutionById(props.solutionId), + requirement = (await solution.contains.loadItems({ + where: { id: props.id, req_type: props.ReqClass.req_type } + }))[0] as InstanceType + + if (!requirement) + throw new Error('Not Found: The requirement does not exist.') return requirement } @@ -211,15 +209,13 @@ export class OrganizationInteractor { if (!this.isOrganizationReader()) throw new Error('Forbidden: You do not have permission to perform this action') - const em = this.getEntityManager(), - solution = await this.getSolutionById(props.solutionId), - requirements = (await em.find(Belongs, { - left: { + const solution = await this.getSolutionById(props.solutionId), + requirements = (await solution.contains.loadItems>({ + where: { req_type: props.ReqClass.req_type, ...props.query - }, - right: solution - }, { populate: ['left'] })).map((req) => req.left as InstanceType) + } + })) as InstanceType[] return requirements } @@ -250,9 +246,12 @@ export class OrganizationInteractor { const em = this.getEntityManager(), solution = await this.getSolutionById(props.solutionId), - { left: requirement } = await em.findOneOrFail(Belongs, { - left: props.id, right: solution - }, { populate: ['left'] }) as unknown as { left: InstanceType } + requirement = (await solution.contains.loadItems({ + where: { id: props.id, req_type: props.ReqClass.req_type } + }))[0] as InstanceType + + if (!requirement) + throw new Error('Not Found: The requirement does not exist.') // if a property is a uuid, assert that it belongs to the solution requirement.assign({ @@ -541,11 +540,10 @@ export class OrganizationInteractor { lastModified: new Date(), createdBy, modifiedBy: createdBy, - isSilence: false + isSilence: false, + belongs: [organization] }); - em.create(Belongs, { left: newSolution, right: organization }) - await em.flush() await this.addRequirement({ @@ -577,21 +575,16 @@ export class OrganizationInteractor { if (!await this.isOrganizationReader()) throw new Error('Forbidden: You do not have permission to perform this action') - const em = this.getEntityManager(), - solutions = (await em.find(Belongs, { - left: { - req_type: ReqType.SOLUTION, - // remove null entries from the query - ...Object.entries(query).reduce((acc, [key, value]) => { - if (value != null && value != undefined) acc[key] = value - return acc - }, {} as Record) - }, - right: { id: organization.id } - }, { populate: ['left'] })).map((sol) => sol.left as Solution) - - if (solutions.length === 0) - return [] + const solutions = await organization.contains.loadItems({ + where: { + // remove null entries from the query + ...Object.entries(query).reduce((acc, [key, value]) => { + if (value !== null && value !== undefined) acc[key] = value + return acc + }, {} as Record), + req_type: ReqType.SOLUTION + } + }) return solutions } @@ -609,11 +602,13 @@ export class OrganizationInteractor { if (!await this.isOrganizationReader()) throw new Error('Forbidden: You do not have permission to perform this action') - const em = this.getEntityManager(), - { left: solution } = await em.findOneOrFail(Belongs, { - left: { id: solutionId, req_type: ReqType.SOLUTION }, - right: await this.getOrganization() - }, { populate: ['left'] }) as unknown as { left: Solution } + const organization = await this.getOrganization(), + solution = (await organization.contains.loadItems({ + where: { id: solutionId, req_type: ReqType.SOLUTION } + }))[0] + + if (!solution) + throw new Error('Not Found: The solution does not exist.') return solution } @@ -631,11 +626,13 @@ export class OrganizationInteractor { if (!await this.isOrganizationReader()) throw new Error('Forbidden: You do not have permission to perform this action') - const em = this.getEntityManager(), - { left: solution } = await em.findOneOrFail(Belongs, { - left: { slug, req_type: ReqType.SOLUTION } as Solution, - right: await this.getOrganization() - }, { populate: ['left'] }) as unknown as { left: Solution } + const organization = await this.getOrganization(), + solution = (await organization.contains.loadItems({ + where: { slug, req_type: ReqType.SOLUTION } + }))[0] + + if (!solution) + throw new Error('Not Found: The solution does not exist.') return solution } @@ -653,10 +650,13 @@ export class OrganizationInteractor { throw new Error('Forbidden: You do not have permission to perform this action') const em = this.getEntityManager(), - { left: solution } = await em.findOneOrFail(Belongs, { - left: { slug, req_type: ReqType.SOLUTION } as Solution, - right: await this.getOrganization() - }) as unknown as { left: Solution } + organization = await this.getOrganization(), + solution = (await organization.contains.loadItems({ + where: { slug, req_type: ReqType.SOLUTION } + }))[0] + + if (!solution) + throw new Error('Not Found: The solution does not exist.') await em.removeAndFlush(solution) } @@ -675,10 +675,13 @@ export class OrganizationInteractor { throw new Error('Forbidden: You do not have permission to perform this action') const em = this.getEntityManager(), - { left: solution } = await em.findOneOrFail(Belongs, { - left: { slug, req_type: ReqType.SOLUTION } as Solution, - right: await this.getOrganization() - }) as unknown as { left: Solution } + organization = await this.getOrganization(), + solution = (await organization.contains.loadItems({ + where: { slug, req_type: ReqType.SOLUTION } + }))[0] + + if (!solution) + throw new Error('Not Found: The solution does not exist.') solution.assign({ name: props.name ?? solution.name, @@ -870,47 +873,18 @@ export class OrganizationInteractor { if (!this.isOrganizationReader()) throw new Error('Forbidden: You do not have permission to perform this action') - const em = this.getEntityManager(), - solution = await this.getSolutionById(solutionId), - // Check if the ParsedRequirement belongs to the solution - { left: parsedRequirement } = (await em.findOneOrFail(Belongs, { - left: { id: id, req_type: ReqType.PARSED_REQUIREMENT }, - right: solution - }, { populate: ['left'] })) as { left: ParsedRequirement }, - // Get all unapproved requirements that follow from the specified ParsedRequirement - requirements = (await em.find(Follows, { - right: parsedRequirement, - left: { isSilence: true } - }, { populate: ['left'] })).map(f => f.left as Requirement), - // Group the results by the requirement type - groupedResult = groupBy(requirements, ({ req_type }) => req_type) - - return groupedResult - } + const parsedRequirement = await this.getRequirementById({ solutionId, ReqClass: ParsedRequirement, id }) - /** - * Return all ParsedRequirements for a Solution. - * @param solutionId The id of the solution to get the ParsedRequirements for - * @returns The ParsedRequirements for the Solution - * @throws {Error} If the user is not a reader of the organization or better - * @throws {Error} If the Solution does not belong to the organization - */ - async getParsedRequirements(solutionId: Solution['id']): Promise { - if (!this.isOrganizationReader()) - throw new Error('Forbidden: You do not have permission to perform this action') + if (!parsedRequirement) + throw new Error('Not Found: The ParsedRequirement does not exist.') - const em = this.getEntityManager(), - // Check if the Solution belongs to the organization - { left: solution } = (await em.findOneOrFail(Belongs, { - left: { id: solutionId, req_type: ReqType.SOLUTION }, - right: await this.getOrganization() - }, { populate: ['left'] })) as unknown as { left: Solution }, - parsedRequirements = (await em.find(Belongs, { - left: { req_type: ReqType.PARSED_REQUIREMENT }, - right: solution - }, { populate: ['left'] })).map((pr) => pr.left as ParsedRequirement) - - return parsedRequirements + // Get all unapproved requirements that follow from the specified ParsedRequirement + const requirements = await parsedRequirement.followedBy.loadItems({ + where: { isSilence: true } + }) + + // Group the results by the requirement type + return groupBy(requirements, ({ req_type }) => req_type) } /** @@ -934,10 +908,7 @@ export class OrganizationInteractor { const appUser = await this._user, em = this.getEntityManager(), - { left: solution } = (await em.findOneOrFail(Belongs, { - left: { id: solutionId, req_type: ReqType.SOLUTION }, - right: await this.getOrganization() - }, { populate: ['left'] })) as unknown as { left: Solution } + solution = await this.getSolutionById(solutionId) const groupedResult = await parsingService.parse(statement) @@ -947,11 +918,10 @@ export class OrganizationInteractor { createdBy: appUser, modifiedBy: appUser, lastModified: new Date(), - isSilence: true + isSilence: true, + belongs: [solution] }) - em.create(Belongs, { left: parsedRequirement, right: solution }); - // FIXME: need a better type than 'any' const addSolReq = (ReqClass: typeof Requirement, props: any) => { const req = em.create(ReqClass, { @@ -959,18 +929,17 @@ export class OrganizationInteractor { isSilence: true, lastModified: new Date(), modifiedBy: appUser, - createdBy: appUser + createdBy: appUser, + belongs: [solution] }) - em.create(Belongs, { left: req, right: solution }); - return req } // FIXME: need a better type than 'any' const addParsedReq = (ReqClass: typeof Requirement, props: any) => { const req = addSolReq(ReqClass, props) - em.create(Follows, { left: req, right: parsedRequirement }); + req.follows.add(parsedRequirement) return req }; diff --git a/domain/requirements/Requirement.ts b/domain/requirements/Requirement.ts index 8f821c0d..04895c07 100644 --- a/domain/requirements/Requirement.ts +++ b/domain/requirements/Requirement.ts @@ -235,10 +235,10 @@ export abstract class Requirement extends BaseEntity { exceptedBy = new Collection(this); /** - * X ≅ Y + * this ≅ other * - * X ⇔ Y, and X has a different type from Y. - * In other words, Y introduces no new property but helps understand X better. + * this ⇔ other, and this has a different type from other. + * In other words, other introduces no new property but helps understand this better. * Same properties, different type (notation-wise) */ @ManyToMany({ entity: () => Requirement, pivotEntity: () => Explains, inversedBy: (r) => r.explainedBy }) @@ -251,11 +251,11 @@ export abstract class Requirement extends BaseEntity { explainedBy = new Collection(this); /** - * X > Y + * this > other * * aka "refines". - * X assumes Y and specifies a property that Y does not. - * X adds to properties of Y + * this assumes other and specifies a property that other does not. + * this adds to properties of other */ @ManyToMany({ entity: () => Requirement, pivotEntity: () => Extends, inversedBy: (r) => r.extendedBy }) extends = new Collection(this); @@ -267,9 +267,9 @@ export abstract class Requirement extends BaseEntity { extendedBy = new Collection(this); /** - * X ⊣ Y + * this ⊣ other * - * X is a consequence of the property specified by Y + * this is a consequence of the property specified by other */ @ManyToMany({ entity: () => Requirement, pivotEntity: () => Follows, inversedBy: (r) => r.followedBy }) follows = new Collection(this); @@ -281,9 +281,9 @@ export abstract class Requirement extends BaseEntity { followedBy = new Collection(this); /** - * X ⇔ Y + * this ⇔ other * - * X specifies the same property as Y + * this specifies the same property as other */ @ManyToMany({ entity: () => Requirement, pivotEntity: () => Repeats, inversedBy: (r) => r.repeatedBy }) repeats = new Collection(this); @@ -295,11 +295,11 @@ export abstract class Requirement extends BaseEntity { repeatedBy = new Collection(this); /** - * X ∩ Y + * this ∩ other * - * X' ⇔ Y' for some sub-requirements X' and Y' of X and Y. + * this' ⇔ other' for some sub-requirements this' and other' of this and other. * (Involve Repeats). - * Some subrequirement is in common between X and Y. + * Some subrequirement is in common between this and other. */ @ManyToMany({ entity: () => Requirement, pivotEntity: () => Shares, inversedBy: (r) => r.sharedBy }) shares = new Collection(this); diff --git a/server/api/parse-requirement/index.get.ts b/server/api/parse-requirement/index.get.ts index f1f7692b..a71e4290 100644 --- a/server/api/parse-requirement/index.get.ts +++ b/server/api/parse-requirement/index.get.ts @@ -1,24 +1,14 @@ import { z } from "zod"; -import { fork } from "~/server/data/orm.js" -import { getServerSession } from '#auth' -import { OrganizationInteractor } from "~/application"; - -const querySchema = z.object({ - solutionId: z.string().uuid(), - organizationSlug: z.string() -}) +import { ParsedRequirement } from "~/domain/requirements"; /** - * Return all ParsedRequirements for a Solution. + * Returns all ParsedRequirements that match the query parameters */ -export default defineEventHandler(async (event) => { - const { organizationSlug, solutionId } = await validateEventQuery(event, querySchema), - session = (await getServerSession(event))!, - organizationInteractor = new OrganizationInteractor({ - userId: session.id, - organizationSlug, - entityManager: fork() - }) - - return organizationInteractor.getParsedRequirements(solutionId) +export default findRequirementsHttpHandler({ + ReqClass: ParsedRequirement, + querySchema: z.object({ + name: z.string().optional(), + description: z.string().optional(), + isSilence: z.boolean().optional().default(false) + }) }) \ No newline at end of file From 1a0a6f10747c9ea74fc1fa1287744475cfbffb53 Mon Sep 17 00:00:00 2001 From: Michael Haufe Date: Fri, 29 Nov 2024 20:45:27 +0000 Subject: [PATCH 3/3] Fixed CRUD operations with relations --- application/OrganizationInteractor.ts | 51 ++++++++++--------- domain/relations/RequirementRelation.ts | 4 +- domain/requirements/Requirement.ts | 66 ++++++++++++------------- domain/requirements/index.ts | 2 +- domain/types/index.ts | 14 ++---- migrations/.snapshot-cathedral.json | 22 +++------ migrations/Migration20241129150626.ts | 41 +++++++++++++++ 7 files changed, 115 insertions(+), 85 deletions(-) create mode 100644 migrations/Migration20241129150626.ts diff --git a/application/OrganizationInteractor.ts b/application/OrganizationInteractor.ts index 3f270c75..15e362e8 100644 --- a/application/OrganizationInteractor.ts +++ b/application/OrganizationInteractor.ts @@ -1,9 +1,10 @@ import { Assumption, Constraint, Effect, EnvironmentComponent, FunctionalBehavior, GlossaryTerm, Invariant, Justification, Limit, MoscowPriority, NonFunctionalBehavior, Obstacle, Organization, Outcome, ParsedRequirement, Person, ReqType, Requirement, Solution, Stakeholder, StakeholderCategory, StakeholderSegmentation, SystemComponent, UseCase, UserStory } from "~/domain/requirements"; -import { QueryOrder, type ChangeSetType, type EntityManager } from "@mikro-orm/core"; +import { Collection, QueryOrder, type ChangeSetType, type EntityManager } from "@mikro-orm/core"; import { AppUserOrganizationRole, AppRole, AppUser, AuditLog } from "~/domain/application"; import { validate } from 'uuid' import type NaturalLanguageToRequirementService from "~/server/data/services/NaturalLanguageToRequirementService"; import { groupBy, slugify } from "#shared/utils"; +import { Belongs, Follows } from "~/domain/relations"; type OrganizationInteractorConstructor = { entityManager: EntityManager, @@ -91,10 +92,7 @@ export class OrganizationInteractor { if (!this.isOrganizationContributor()) throw new Error('Forbidden: You do not have permission to perform this action') - const organization = await this.getOrganization(), - solution = (await organization.contains.loadItems({ - where: { id: solutionId, req_type: ReqType.SOLUTION } - }))[0] + const solution = await this.getSolutionById(solutionId) if (!solution) throw new Error('Not Found: The solution does not exist.') @@ -119,10 +117,11 @@ export class OrganizationInteractor { reqId, lastModified: new Date(), createdBy: appUser, - modifiedBy: appUser, - belongs: [solution] + modifiedBy: appUser }) as InstanceType + em.create(Belongs, { left: newRequirement, right: solution }) + await em.flush() return newRequirement @@ -156,6 +155,14 @@ export class OrganizationInteractor { if (!requirement) throw new Error('Not Found: The requirement does not exist.') + // remove all relationships to the requirement + const conn = this.getEntityManager().getConnection() + await conn.execute(` + DELETE + FROM requirement_relation + WHERE left_id = ? OR right_id = ?; + `, [requirement.id, requirement.id]) + // TODO: decrement the reqId of all requirements that have a reqId greater than the deleted requirement await em.removeAndFlush(requirement) @@ -540,10 +547,11 @@ export class OrganizationInteractor { lastModified: new Date(), createdBy, modifiedBy: createdBy, - isSilence: false, - belongs: [organization] + isSilence: false }); + em.create(Belongs, { left: newSolution, right: organization }) + await em.flush() await this.addRequirement({ @@ -650,10 +658,7 @@ export class OrganizationInteractor { throw new Error('Forbidden: You do not have permission to perform this action') const em = this.getEntityManager(), - organization = await this.getOrganization(), - solution = (await organization.contains.loadItems({ - where: { slug, req_type: ReqType.SOLUTION } - }))[0] + solution = await this.getSolutionBySlug(slug) if (!solution) throw new Error('Not Found: The solution does not exist.') @@ -675,10 +680,7 @@ export class OrganizationInteractor { throw new Error('Forbidden: You do not have permission to perform this action') const em = this.getEntityManager(), - organization = await this.getOrganization(), - solution = (await organization.contains.loadItems({ - where: { slug, req_type: ReqType.SOLUTION } - }))[0] + solution = await this.getSolutionBySlug(slug) if (!solution) throw new Error('Not Found: The solution does not exist.') @@ -832,7 +834,7 @@ export class OrganizationInteractor { entity: string } - const conn = this._entityManager.getConnection(), + const conn = this.getEntityManager().getConnection(), res: RowType[] = await conn.execute(` SELECT d.id, d.type, d.created_at, a.entity_id, a.entity_name, a.entity FROM audit_log AS d @@ -918,10 +920,11 @@ export class OrganizationInteractor { createdBy: appUser, modifiedBy: appUser, lastModified: new Date(), - isSilence: true, - belongs: [solution] + isSilence: true }) + em.create(Belongs, { left: parsedRequirement, right: solution }) + // FIXME: need a better type than 'any' const addSolReq = (ReqClass: typeof Requirement, props: any) => { const req = em.create(ReqClass, { @@ -929,17 +932,19 @@ export class OrganizationInteractor { isSilence: true, lastModified: new Date(), modifiedBy: appUser, - createdBy: appUser, - belongs: [solution] + createdBy: appUser }) + em.create(Belongs, { left: req, right: solution }) + return req } // FIXME: need a better type than 'any' const addParsedReq = (ReqClass: typeof Requirement, props: any) => { const req = addSolReq(ReqClass, props) - req.follows.add(parsedRequirement) + em.create(Follows, { left: req, right: parsedRequirement }) + return req }; diff --git a/domain/relations/RequirementRelation.ts b/domain/relations/RequirementRelation.ts index a1ce25e5..29a1b821 100644 --- a/domain/relations/RequirementRelation.ts +++ b/domain/relations/RequirementRelation.ts @@ -15,12 +15,12 @@ export abstract class RequirementRelation extends BaseEntity { /** * The left-hand side of the relation */ - @ManyToOne({ primary: true }) + @ManyToOne({ primary: true, entity: () => Requirement }) left: Requirement /** * The right-hand side of the relation */ - @ManyToOne({ primary: true }) + @ManyToOne({ primary: true, entity: () => Requirement }) right: Requirement } \ No newline at end of file diff --git a/domain/requirements/Requirement.ts b/domain/requirements/Requirement.ts index 04895c07..f85af6db 100644 --- a/domain/requirements/Requirement.ts +++ b/domain/requirements/Requirement.ts @@ -1,9 +1,8 @@ import { v7 as uuidv7 } from 'uuid'; -import { BaseEntity, Collection, Entity, Enum, ManyToMany, ManyToOne, OptionalProps, Property } from '@mikro-orm/core'; -import { type CollectionPropsToOptionalArrays, type Properties } from '../types/index.js'; +import { BaseEntity, Cascade, Collection, Entity, Enum, ManyToMany, ManyToOne, OptionalProps, Property } from '@mikro-orm/core'; +import { type Properties } from '../types/index.js'; import { ReqType } from './ReqType.js'; import { AppUser } from '../application/AppUser.js'; -import { Belongs, Characterizes, Constrains, Contradicts, Details, Disjoins, Duplicates, Excepts, Explains, Extends, Follows, Repeats, Shares } from '../relations/index.js'; /** * A Requirement is a statement that specifies a property. @@ -13,7 +12,7 @@ export abstract class Requirement extends BaseEntity { static req_type: ReqType = ReqType.REQUIREMENT; static reqIdPrefix: `${'P' | 'E' | 'G' | 'S' | '0'}.${number}.` = '0.0.'; - constructor(props: CollectionPropsToOptionalArrays>>) { + constructor(props: Properties>) { super() this.id = uuidv7(); this.req_type = (this.constructor as typeof Requirement).req_type; @@ -25,10 +24,9 @@ export abstract class Requirement extends BaseEntity { this.createdBy = props.createdBy; this._reqId = props.reqId; - for (const [key, value] of Object.entries(props)) { - if (Array.isArray(value)) - (this[key as keyof this] as Collection) = new Collection(this, value); - } + // The ORM can pass extra properties to the constructor + if (Object.values(props).some((value) => (value as any) instanceof Collection)) + throw new Error('Collections cannot be passed to the Requirement constructor') } // This fixes the issue with em.create not honoring the constructor signature @@ -119,7 +117,7 @@ export abstract class Requirement extends BaseEntity { * * this is a sub-requirement of other; textually included */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Belongs, inversedBy: (r) => r.contains }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Belongs', inversedBy: (r) => r.contains }) belongs = new Collection(this); /** @@ -127,7 +125,7 @@ export abstract class Requirement extends BaseEntity { * * This is the set of requirements that are sub-requirements of this requirement */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Belongs, mappedBy: (r) => r.belongs }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Belongs', mappedBy: (r) => r.belongs }) contains = new Collection(this); /** @@ -136,14 +134,14 @@ export abstract class Requirement extends BaseEntity { * Meta-requirement this applies to other */ // TODO: need to enforce that this is a MetaRequirement - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Characterizes, inversedBy: (r) => r.characterizedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Characterizes', inversedBy: (r) => r.characterizedBy }) characterizes = new Collection(this); /** * The inverse of {@link characterizes} */ // TODO: need to enforce that other is a MetaRequirement - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Characterizes, mappedBy: (r) => r.characterizes }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Characterizes', mappedBy: (r) => r.characterizes }) characterizedBy = new Collection(this); /** @@ -152,14 +150,14 @@ export abstract class Requirement extends BaseEntity { * Constraint this applies to other */ // TODO: need to enforce that this is a Constraint - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Constrains, inversedBy: (r) => r.constrainedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Constrains', inversedBy: (r) => r.constrainedBy }) constrains = new Collection(this); /** * The inverse of {@link constrains} */ // TODO: need to enforce that other is a Constraint - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Constrains, mappedBy: (r) => r.constrains }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Constrains', mappedBy: (r) => r.constrains }) constrainedBy = new Collection(this); /** @@ -167,13 +165,13 @@ export abstract class Requirement extends BaseEntity { * * Properties specified by this and other cannot both hold */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Contradicts, inversedBy: (r) => r.contradictedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Contradicts', inversedBy: (r) => r.contradictedBy }) contradicts = new Collection(this); /** * The inverse of {@link contradicts} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Contradicts, mappedBy: (r) => r.contradicts }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Contradicts', mappedBy: (r) => r.contradicts }) contradictedBy = new Collection(this); /** @@ -181,13 +179,13 @@ export abstract class Requirement extends BaseEntity { * * this adds detail to properties of other */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Details, inversedBy: (r) => r.detailedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Details', inversedBy: (r) => r.detailedBy }) details = new Collection(this); /** * The inverse of {@link details} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Details, mappedBy: (r) => r.details }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Details', mappedBy: (r) => r.details }) detailedBy = new Collection(this); /** @@ -195,13 +193,13 @@ export abstract class Requirement extends BaseEntity { * * this and other are unrelated */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Disjoins, inversedBy: (r) => r.disjoinedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Disjoins', inversedBy: (r) => r.disjoinedBy }) disjoins = new Collection(this); /** * The inverse of {@link disjoins} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Disjoins, mappedBy: (r) => r.disjoins }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Disjoins', mappedBy: (r) => r.disjoins }) disjoinedBy = new Collection(this); /** @@ -211,13 +209,13 @@ export abstract class Requirement extends BaseEntity { * In other words, this and other are redundant. * Same properties, same type (notation-wise, this ≡ other) */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Duplicates, inversedBy: (r) => r.duplicatedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Duplicates', inversedBy: (r) => r.duplicatedBy }) duplicates = new Collection(this); /** * The inverse of {@link duplicates} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Duplicates, mappedBy: (r) => r.duplicates }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Duplicates', mappedBy: (r) => r.duplicates }) duplicatedBy = new Collection(this); /** @@ -225,13 +223,13 @@ export abstract class Requirement extends BaseEntity { * * this specifies an exception to the property specified by other. */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Excepts, inversedBy: (r) => r.exceptedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Excepts', inversedBy: (r) => r.exceptedBy }) excepts = new Collection(this); /** * The inverse of {@link excepts} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Excepts, mappedBy: (r) => r.excepts }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Excepts', mappedBy: (r) => r.excepts }) exceptedBy = new Collection(this); /** @@ -241,13 +239,13 @@ export abstract class Requirement extends BaseEntity { * In other words, other introduces no new property but helps understand this better. * Same properties, different type (notation-wise) */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Explains, inversedBy: (r) => r.explainedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Explains', inversedBy: (r) => r.explainedBy }) explains = new Collection(this); /** * The inverse of {@link explains} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Explains, mappedBy: (r) => r.explains }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Explains', mappedBy: (r) => r.explains }) explainedBy = new Collection(this); /** @@ -257,13 +255,13 @@ export abstract class Requirement extends BaseEntity { * this assumes other and specifies a property that other does not. * this adds to properties of other */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Extends, inversedBy: (r) => r.extendedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Extends', inversedBy: (r) => r.extendedBy }) extends = new Collection(this); /** * The inverse of {@link extends} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Extends, mappedBy: (r) => r.extends }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Extends', mappedBy: (r) => r.extends }) extendedBy = new Collection(this); /** @@ -271,13 +269,13 @@ export abstract class Requirement extends BaseEntity { * * this is a consequence of the property specified by other */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Follows, inversedBy: (r) => r.followedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Follows', inversedBy: (r) => r.followedBy }) follows = new Collection(this); /** * The inverse of {@link follows} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Follows, mappedBy: (r) => r.follows }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Follows', mappedBy: (r) => r.follows }) followedBy = new Collection(this); /** @@ -285,13 +283,13 @@ export abstract class Requirement extends BaseEntity { * * this specifies the same property as other */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Repeats, inversedBy: (r) => r.repeatedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Repeats', inversedBy: (r) => r.repeatedBy }) repeats = new Collection(this); /** * The inverse of {@link repeats} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Repeats, mappedBy: (r) => r.repeats }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Repeats', mappedBy: (r) => r.repeats }) repeatedBy = new Collection(this); /** @@ -301,12 +299,12 @@ export abstract class Requirement extends BaseEntity { * (Involve Repeats). * Some subrequirement is in common between this and other. */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Shares, inversedBy: (r) => r.sharedBy }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Shares', inversedBy: (r) => r.sharedBy }) shares = new Collection(this); /** * The inverse of {@link shares} */ - @ManyToMany({ entity: () => Requirement, pivotEntity: () => Shares, mappedBy: (r) => r.shares }) + @ManyToMany({ entity: () => Requirement, pivotEntity: 'Shares', mappedBy: (r) => r.shares }) sharedBy = new Collection(this); } \ No newline at end of file diff --git a/domain/requirements/index.ts b/domain/requirements/index.ts index 963afc9b..81b43d25 100644 --- a/domain/requirements/index.ts +++ b/domain/requirements/index.ts @@ -1,10 +1,10 @@ +export * from './Requirement.js'; export * from './Assumption.js'; export * from './ConstraintCategory.js'; export * from './Constraint.js'; export * from './Organization.js'; export * from './Solution.js'; export * from './ParsedRequirement.js'; -export * from './Requirement.js'; export * from './MetaRequirement.js'; export * from './Actor.js'; export * from './Component.js'; diff --git a/domain/types/index.ts b/domain/types/index.ts index 5d43ff52..d5cb1308 100644 --- a/domain/types/index.ts +++ b/domain/types/index.ts @@ -4,15 +4,9 @@ export type Constructor = (new (...args: any[]) => T) | (abstract new (...arg /** * A type that represents all the members of a type T that are not functions + * and are not collections */ export type Properties = Pick any ? never : K -}[keyof T]>; - - -/** - * A type that converts all the Collection properties of a type T to optional array properties - */ -export type CollectionPropsToOptionalArrays = { - [K in keyof T]: T[K] extends Collection ? U[] | undefined : T[K] -} \ No newline at end of file + [K in keyof T]: T[K] extends (...args: any[]) => any ? never : + T[K] extends Collection ? never : K +}[keyof T]>; \ No newline at end of file diff --git a/migrations/.snapshot-cathedral.json b/migrations/.snapshot-cathedral.json index b9c9a78d..319401dd 100644 --- a/migrations/.snapshot-cathedral.json +++ b/migrations/.snapshot-cathedral.json @@ -684,22 +684,13 @@ }, { "columns": { - "id": { - "name": "id", - "type": "uuid", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "uuid" - }, "left_id": { "name": "left_id", "type": "uuid", "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "mappedType": "uuid" }, "right_id": { @@ -708,7 +699,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "mappedType": "uuid" }, "rel_type": { @@ -752,9 +743,10 @@ { "keyName": "requirement_relation_pkey", "columnNames": [ - "id" + "left_id", + "right_id" ], - "composite": false, + "composite": true, "constraint": true, "primary": true, "unique": true @@ -772,7 +764,7 @@ "id" ], "referencedTableName": "public.requirement", - "deleteRule": "cascade" + "updateRule": "cascade" }, "requirement_relation_right_id_foreign": { "constraintName": "requirement_relation_right_id_foreign", @@ -784,7 +776,7 @@ "id" ], "referencedTableName": "public.requirement", - "deleteRule": "cascade" + "updateRule": "cascade" } }, "nativeEnums": {} diff --git a/migrations/Migration20241129150626.ts b/migrations/Migration20241129150626.ts new file mode 100644 index 00000000..39783512 --- /dev/null +++ b/migrations/Migration20241129150626.ts @@ -0,0 +1,41 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20241129150626 extends Migration { + + override async up(): Promise { + this.addSql(`alter table "requirement_relation" drop constraint "requirement_relation_left_id_foreign";`); + this.addSql(`alter table "requirement_relation" drop constraint "requirement_relation_right_id_foreign";`); + + this.addSql(`alter table "requirement_relation" drop constraint "requirement_relation_pkey";`); + this.addSql(`alter table "requirement_relation" drop column "id";`); + + this.addSql(`alter table "requirement_relation" alter column "left_id" drop default;`); + this.addSql(`alter table "requirement_relation" alter column "left_id" type uuid using ("left_id"::text::uuid);`); + this.addSql(`alter table "requirement_relation" alter column "left_id" set not null;`); + this.addSql(`alter table "requirement_relation" alter column "right_id" drop default;`); + this.addSql(`alter table "requirement_relation" alter column "right_id" type uuid using ("right_id"::text::uuid);`); + this.addSql(`alter table "requirement_relation" alter column "right_id" set not null;`); + this.addSql(`alter table "requirement_relation" add constraint "requirement_relation_left_id_foreign" foreign key ("left_id") references "requirement" ("id") on update cascade;`); + this.addSql(`alter table "requirement_relation" add constraint "requirement_relation_right_id_foreign" foreign key ("right_id") references "requirement" ("id") on update cascade;`); + this.addSql(`alter table "requirement_relation" add constraint "requirement_relation_pkey" primary key ("left_id", "right_id");`); + } + + override async down(): Promise { + this.addSql(`alter table "requirement_relation" drop constraint "requirement_relation_left_id_foreign";`); + this.addSql(`alter table "requirement_relation" drop constraint "requirement_relation_right_id_foreign";`); + + this.addSql(`alter table "requirement_relation" drop constraint "requirement_relation_pkey";`); + + this.addSql(`alter table "requirement_relation" add column "id" uuid not null;`); + this.addSql(`alter table "requirement_relation" alter column "left_id" drop default;`); + this.addSql(`alter table "requirement_relation" alter column "left_id" type uuid using ("left_id"::text::uuid);`); + this.addSql(`alter table "requirement_relation" alter column "left_id" drop not null;`); + this.addSql(`alter table "requirement_relation" alter column "right_id" drop default;`); + this.addSql(`alter table "requirement_relation" alter column "right_id" type uuid using ("right_id"::text::uuid);`); + this.addSql(`alter table "requirement_relation" alter column "right_id" drop not null;`); + this.addSql(`alter table "requirement_relation" add constraint "requirement_relation_left_id_foreign" foreign key ("left_id") references "requirement" ("id") on delete cascade;`); + this.addSql(`alter table "requirement_relation" add constraint "requirement_relation_right_id_foreign" foreign key ("right_id") references "requirement" ("id") on delete cascade;`); + this.addSql(`alter table "requirement_relation" add constraint "requirement_relation_pkey" primary key ("id");`); + } + +}