Skip to content

Commit

Permalink
401 generalize db schema and support relations more directly (#402)
Browse files Browse the repository at this point in the history
* Migrated schema from Table-per-class to Single-Table-Inheritance

* Created relations table
  • Loading branch information
mlhaufe authored Oct 15, 2024
1 parent 7d6f810 commit 48eb81b
Show file tree
Hide file tree
Showing 43 changed files with 1,044 additions and 3,573 deletions.
3,864 changes: 347 additions & 3,517 deletions migrations/.snapshot-cathedral.json

Large diffs are not rendered by default.

424 changes: 424 additions & 0 deletions migrations/Migration20241015194316.ts

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions migrations/Migration20241015215709.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Migration } from '@mikro-orm/migrations';

export class Migration20241015215709 extends Migration {

override async up(): Promise<void> {
this.addSql(`create table "requirement_relation" ("id" uuid not null, "left_id" uuid not null, "right_id" uuid not null, "rel_type" text check ("rel_type" in ('belongs', 'characterizes', 'constrains', 'contradicts', 'details', 'disjoins', 'duplicates', 'excepts', 'explains', 'extends', 'follows', 'repeats', 'shares')) not null, "left_1_id" uuid null, constraint "requirement_relation_pkey" primary key ("id"));`);
this.addSql(`alter table "requirement_relation" add constraint "requirement_relation_left_id_unique" unique ("left_id");`);
this.addSql(`alter table "requirement_relation" add constraint "requirement_relation_right_id_unique" unique ("right_id");`);
this.addSql(`create index "requirement_relation_rel_type_index" on "requirement_relation" ("rel_type");`);
this.addSql(`alter table "requirement_relation" add constraint "requirement_relation_left_1_id_unique" unique ("left_1_id");`);

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_left_1_id_foreign" foreign key ("left_1_id") references "requirement" ("id") on update cascade on delete set null;`);

this.addSql(`alter table "requirement" drop constraint if exists "requirement_req_type_check";`);

this.addSql(`alter table "requirement" add constraint "requirement_req_type_check" check("req_type" in ('assumption', 'constraint', 'effect', 'environment_component', 'functional_behavior', 'glossary_term', 'hint', 'invariant', 'justification', 'limit', 'meta_requirement', 'noise', 'non_functional_behavior', 'obstacle', 'outcome', 'parsed_requirement', 'person', 'product', 'responsibility', 'role', 'silence', 'stakeholder', 'system_component', 'task', 'test_case', 'use_case', 'user_story'));`);
}

override async down(): Promise<void> {
this.addSql(`drop table if exists "requirement_relation" cascade;`);

this.addSql(`alter table "requirement" drop constraint if exists "requirement_req_type_check";`);

this.addSql(`alter table "requirement" add constraint "requirement_req_type_check" check("req_type" in ('assumption', 'constraint', 'effect', 'environment_component', 'functional_behavior', 'glossary_term', 'hint', 'invariant', 'justification', 'limit', 'noise', 'non_functional_behavior', 'obstacle', 'outcome', 'parsed_requirement', 'person', 'product', 'responsibility', 'role', 'silence', 'stakeholder', 'system_component', 'task', 'test_case', 'use_case', 'user_story'));`);
}

}
7 changes: 5 additions & 2 deletions mikro-orm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { type Options, PostgreSqlDriver } from '@mikro-orm/postgresql';
import { TsMorphMetadataProvider } from '@mikro-orm/reflection';
import { Migrator } from '@mikro-orm/migrations';
import * as entities from "./server/domain/index.js";
import * as relations from "./server/domain/relations/index.js";
import AuditSubscriber from "./server/data/subscribers/AuditSubscriber.js";

dotenv.config();
Expand All @@ -21,8 +22,10 @@ const config: Options = {
driverOptions: {
connection: { ssl: true }
},
entities: Object.values(entities)
.filter((entity) => typeof entity === 'function'),
entities: [
...Object.values(entities),
...Object.values(relations)
].filter((entity) => typeof entity === 'function'),
discovery: { disableDynamicFileAccess: true },
seeder: {},
subscribers: [new AuditSubscriber()],
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@final-hill/cathedral",
"version": "0.16.0",
"version": "0.17.0",
"description": "Requirements management system",
"keywords": [],
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion server/domain/Assumption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export class Assumption extends Requirement {
/**
* Requirement that this assumption follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement
}
5 changes: 2 additions & 3 deletions server/domain/AuditLog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { v7 as uuidv7 } from 'uuid';
import { ChangeSetType, Entity, Enum, ManyToOne, Property } from "@mikro-orm/core";
import { Solution } from './Solution.js';
import { ChangeSetType, Entity, Enum, Property } from "@mikro-orm/core";

/**
* The AuditLog class is responsible for tracking changes to entities in the database.
Expand Down Expand Up @@ -41,7 +40,7 @@ export class AuditLog {
/**
* The entity that was changed
*/
@Property({ type: 'json', nullable: true })
@Property({ type: 'json' })
entity: string

/**
Expand Down
2 changes: 1 addition & 1 deletion server/domain/Behavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export abstract class Behavior extends Requirement {
/**
* The priority of the behavior.
*/
@Enum({ items: () => MoscowPriority, nullable: false })
@Enum({ items: () => MoscowPriority })
priority: MoscowPriority;
}
4 changes: 2 additions & 2 deletions server/domain/Constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ export class Constraint extends Requirement {
/**
* Category of the constraint
*/
@Enum({ items: () => ConstraintCategory, nullable: true })
@Enum({ items: () => ConstraintCategory })
category?: ConstraintCategory;

/**
* Requirement that this constraint follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
2 changes: 1 addition & 1 deletion server/domain/Effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export class Effect extends Requirement {
/**
* Requirement that this effect follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
4 changes: 2 additions & 2 deletions server/domain/EnvironmentComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ export class EnvironmentComponent extends Component {
/**
* The parent component of the current environment component if any
*/
@ManyToOne({ entity: () => EnvironmentComponent, nullable: true })
@ManyToOne({ entity: () => EnvironmentComponent })
parentComponent?: EnvironmentComponent;

/**
* Requirement that this environment component follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
2 changes: 1 addition & 1 deletion server/domain/FunctionalBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export class FunctionalBehavior extends Functionality {
/**
* Requirement that this functional behavior follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
4 changes: 2 additions & 2 deletions server/domain/GlossaryTerm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ export class GlossaryTerm extends Component {
/**
* The parent term of the glossary term, if any.
*/
@ManyToOne({ entity: () => GlossaryTerm, nullable: true })
@ManyToOne({ entity: () => GlossaryTerm })
parentComponent?: GlossaryTerm;

/**
* Requirement that this glossary term follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
2 changes: 1 addition & 1 deletion server/domain/Invariant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export class Invariant extends Requirement {
/**
* Requirement that this invariant follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
2 changes: 1 addition & 1 deletion server/domain/Justification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export class Justification extends MetaRequirement {
/**
* Requirement that this justification follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
2 changes: 1 addition & 1 deletion server/domain/Limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export class Limit extends Requirement {
/**
* Requirement that this limit follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
4 changes: 2 additions & 2 deletions server/domain/MetaRequirement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ import { Requirement } from "./Requirement.js";
/**
* Property of requirements themselves (not of the Project, Environment, Goals, or System)
*/
@Entity({ abstract: true })
export abstract class MetaRequirement extends Requirement { }
@Entity()
export class MetaRequirement extends Requirement { }
2 changes: 1 addition & 1 deletion server/domain/NonFunctionalBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export class NonFunctionalBehavior extends Functionality {
/**
* Requirement that this non-functional behavior follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
2 changes: 1 addition & 1 deletion server/domain/Obstacle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export class Obstacle extends Goal {
/**
* Requirement that this obstacle follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
2 changes: 1 addition & 1 deletion server/domain/Outcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export class Outcome extends Goal {
/**
* Requirement that this outcome follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
4 changes: 2 additions & 2 deletions server/domain/Person.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ export class Person extends Actor {
* Email address of the person
*/
// email address: https://stackoverflow.com/a/574698
@Property({ type: 'string', length: 254, nullable: true })
@Property({ type: 'string', length: 254 })
email?: string;

/**
* Requirement that this person follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;
}
17 changes: 10 additions & 7 deletions server/domain/Requirement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { Solution } from './Solution.js';
/**
* A Requirement is a statement that specifies a property.
*/
@Entity({ abstract: true })
@Entity({
discriminatorColumn: 'req_type',
abstract: true
})
export abstract class Requirement {
constructor(props: Omit<Requirement, 'id'>) {
this.id = uuidv7();
Expand All @@ -31,40 +34,40 @@ export abstract class Requirement {
/**
* A short name for the requirement
*/
@Property({ type: 'string', nullable: false })
@Property({ type: 'string' })
name: string;

/**
* A human-readable description of a property
* @throws {Error} if the statement is longer than 1000 characters
*/
@Property({ type: 'string', length: 1000, nullable: false })
@Property({ type: 'string', length: 1000 })
statement: string;

/**
* The solution that owns this requirement
*/
@ManyToOne({ entity: () => Solution, nullable: false })
@ManyToOne({ entity: () => Solution })
solution: Solution;

/**
* The date and time when the requirement was last modified
*/
@Property({ type: 'datetime', nullable: false, onCreate: () => new Date(), onUpdate: () => new Date(), defaultRaw: 'now()' })
@Property({ type: 'datetime', onCreate: () => new Date(), onUpdate: () => new Date(), defaultRaw: 'now()' })
lastModified: Date;

/**
* The user who last modified the requirement
*/
// System Admin is the default user for the initial migration
// This can be removed in v0.14.0 or later
@ManyToOne({ entity: () => AppUser, nullable: false, default: 'ac594919-50e3-438a-b9bc-efb8a8654243' })
@ManyToOne({ entity: () => AppUser, default: 'ac594919-50e3-438a-b9bc-efb8a8654243' })
modifiedBy: AppUser;

/**
* Whether the requirement is a silence requirement.
* (i.e. a requirement that is not included in the solution)
*/
@Property({ type: 'boolean', nullable: false, default: false })
@Property({ type: 'boolean', default: false })
isSilence: boolean;
}
2 changes: 1 addition & 1 deletion server/domain/Scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ export abstract class Scenario extends Example {
/**
* Primary actor involved in the scenario
*/
@ManyToOne({ entity: () => Stakeholder, nullable: true })
@ManyToOne({ entity: () => Stakeholder })
primaryActor?: Stakeholder;
}
12 changes: 6 additions & 6 deletions server/domain/Stakeholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,36 @@ export class Stakeholder extends Component {
/**
* Requirement that this stakeholder follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;

/**
* The parent component of the stakeholder, if any.
*/
@ManyToOne({ entity: () => Stakeholder, nullable: true })
@ManyToOne({ entity: () => Stakeholder })
parentComponent?: Stakeholder;

/**
* The segmentation of the stakeholder.
*/
@Enum({ items: () => StakeholderSegmentation, nullable: true })
@Enum({ items: () => StakeholderSegmentation })
segmentation?: StakeholderSegmentation;

/**
* The category of the stakeholder.
*/
@Enum({ items: () => StakeholderCategory, nullable: true })
@Enum({ items: () => StakeholderCategory })
category?: StakeholderCategory;

/**
* The availability of the stakeholder.
*/
@Property({ type: 'number', nullable: false, check: 'availability >= 0 AND availability <= 100' })
@Property({ type: 'number', check: 'availability >= 0 AND availability <= 100' })
availability: number;

/**
* The influence of the stakeholder.
*/
@Property({ type: 'number', nullable: false, check: 'influence >= 0 AND influence <= 100' })
@Property({ type: 'number', check: 'influence >= 0 AND influence <= 100' })
influence: number;
}
4 changes: 2 additions & 2 deletions server/domain/SystemComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ export class SystemComponent extends Component {
/**
* Requirement that this system component follows from
*/
@ManyToOne({ entity: () => ParsedRequirement, nullable: true })
@ManyToOne({ entity: () => ParsedRequirement })
follows?: ParsedRequirement;

/**
* Parent component of the current system component
*/
@ManyToOne({ entity: () => SystemComponent, nullable: true })
@ManyToOne({ entity: () => SystemComponent })
parentComponent?: SystemComponent;
}
Loading

0 comments on commit 48eb81b

Please sign in to comment.