diff --git a/_old/domain/entities/Invariant.mts b/_old/domain/entities/Invariant.mts deleted file mode 100644 index 82f03a4b..00000000 --- a/_old/domain/entities/Invariant.mts +++ /dev/null @@ -1,3 +0,0 @@ -import Constraint from './Constraint.mjs'; - -export default class Invariant extends Constraint { } \ No newline at end of file diff --git a/src/data/InvariantRepository.mts b/src/data/InvariantRepository.mts new file mode 100644 index 00000000..6dcea67d --- /dev/null +++ b/src/data/InvariantRepository.mts @@ -0,0 +1,11 @@ +import type Invariant from '~/domain/Invariant.mjs'; +import StorageRepository from './StorageRepository.mjs'; +import type { SemVerString } from '~/lib/SemVer.mjs'; +import InvariantToJsonMapper from '~/mappers/InvariantToJsonMapper.mjs'; +import pkg from '~/../package.json' with { type: 'json' }; + +export default class InvariantRepository extends StorageRepository { + constructor(storage: Storage) { + super('invariants', storage, new InvariantToJsonMapper(pkg.version as SemVerString)); + } +} \ No newline at end of file diff --git a/src/domain/Environment.mts b/src/domain/Environment.mts index 8be76fec..75f01a53 100644 --- a/src/domain/Environment.mts +++ b/src/domain/Environment.mts @@ -12,10 +12,12 @@ import type { Properties } from '~/types/Properties.mjs'; export default class Environment extends Entity { glossaryIds: Uuid[]; constraintIds: Uuid[]; + invariantIds: Uuid[]; constructor(options: Properties) { super(options); this.glossaryIds = options.glossaryIds; this.constraintIds = options.constraintIds; + this.invariantIds = options.invariantIds; } } \ No newline at end of file diff --git a/src/domain/Invariant.mts b/src/domain/Invariant.mts new file mode 100644 index 00000000..615aad1c --- /dev/null +++ b/src/domain/Invariant.mts @@ -0,0 +1,6 @@ +import Requirement from './Requirement.mjs'; + +/** + * An invariant is an environment property that must be maintained + */ +export default class Invariant extends Requirement { } \ No newline at end of file diff --git a/src/mappers/EnvironmentToJsonMapper.mts b/src/mappers/EnvironmentToJsonMapper.mts index c5b425b6..746dc768 100644 --- a/src/mappers/EnvironmentToJsonMapper.mts +++ b/src/mappers/EnvironmentToJsonMapper.mts @@ -6,6 +6,7 @@ import SemVer from '~/lib/SemVer.mjs'; export interface EnvironmentJson extends EntityJson { glossaryIds: Uuid[]; constraintIds: Uuid[]; + invariantIds: Uuid[]; } export default class EnvironmentToJsonMapper extends EntityToJsonMapper { @@ -15,7 +16,8 @@ export default class EnvironmentToJsonMapper extends EntityToJsonMapper { if (version.gte('0.3.0')) return new Environment({ ...target, - constraintIds: target.constraintIds ?? [] + constraintIds: target.constraintIds ?? [], + invariantIds: target.invariantIds ?? [] }); throw new Error(`Unsupported serialization version: ${version}`); @@ -25,7 +27,8 @@ export default class EnvironmentToJsonMapper extends EntityToJsonMapper { return { ...super.mapTo(source), glossaryIds: source.glossaryIds, - constraintIds: source.constraintIds + constraintIds: source.constraintIds, + invariantIds: source.invariantIds }; } } \ No newline at end of file diff --git a/src/mappers/InvariantToJsonMapper.mts b/src/mappers/InvariantToJsonMapper.mts new file mode 100644 index 00000000..c796764f --- /dev/null +++ b/src/mappers/InvariantToJsonMapper.mts @@ -0,0 +1,20 @@ +import Invariant from '~/domain/Invariant.mjs'; +import RequirementToJsonMapper, { type RequirementJson } from './RequirementToJsonMapper.mjs'; +import SemVer from '~/lib/SemVer.mjs'; + +export interface InvariantJson extends RequirementJson { } + +export default class InvariantToJsonMapper extends RequirementToJsonMapper { + override mapFrom(target: InvariantJson): Invariant { + const version = new SemVer(target.serializationVersion); + + if (version.gte('0.4.0')) + return new Invariant(target); + + throw new Error(`Unsupported serialization version: ${version}`); + } + + override mapTo(source: Invariant): InvariantJson { + return super.mapTo(source); + } +} \ No newline at end of file diff --git a/src/presentation/Application.mts b/src/presentation/Application.mts index 15082eb9..46948b67 100644 --- a/src/presentation/Application.mts +++ b/src/presentation/Application.mts @@ -68,6 +68,7 @@ export default class Application extends Container { (await import('./pages/solution/environment/EnvironmentsIndexPage.mjs')).default, (await import('./pages/solution/environment/GlossaryPage.mjs')).default, (await import('./pages/solution/environment/ConstraintsPage.mjs')).default, + (await import('./pages/solution/environment/InvariantsPage.mjs')).default, (await import('./pages/solution/goals/GoalsIndexPage.mjs')).default, (await import('./pages/solution/goals/RationalePage.mjs')).default, (await import('./pages/solution/goals/FunctionalityPage.mjs')).default, diff --git a/src/presentation/pages/solution/NewSolutionPage.mts b/src/presentation/pages/solution/NewSolutionPage.mts index 71ba1a06..1de19146 100644 --- a/src/presentation/pages/solution/NewSolutionPage.mts +++ b/src/presentation/pages/solution/NewSolutionPage.mts @@ -109,7 +109,8 @@ export default class NewSolutionPage extends Page { environment = new Environment({ id: solution.environmentId, glossaryIds: [], - constraintIds: [] + constraintIds: [], + invariantIds: [], }), goals = new Goals({ id: solution.goalsId, diff --git a/src/presentation/pages/solution/environment/EnvironmentsIndexPage.mts b/src/presentation/pages/solution/environment/EnvironmentsIndexPage.mts index c7ac6204..bbff6531 100644 --- a/src/presentation/pages/solution/environment/EnvironmentsIndexPage.mts +++ b/src/presentation/pages/solution/environment/EnvironmentsIndexPage.mts @@ -30,6 +30,11 @@ export default class EnvironmentsIndexPage extends Page { title: 'Constraints', icon: 'anchor', href: `${location.pathname}/constraints` + }), + new MiniCard({ + title: 'Invariants', + icon: 'lock', + href: `${location.pathname}/invariants` }) ]) ); diff --git a/src/presentation/pages/solution/environment/InvariantsPage.mts b/src/presentation/pages/solution/environment/InvariantsPage.mts new file mode 100644 index 00000000..226c8b60 --- /dev/null +++ b/src/presentation/pages/solution/environment/InvariantsPage.mts @@ -0,0 +1,78 @@ +import type { Uuid } from '~/types/Uuid.mjs'; +import type Environment from '~/domain/Environment.mjs'; +import Invariant from '~/domain/Invariant.mjs'; +import SolutionRepository from '~/data/SolutionRepository.mjs'; +import EnvironmentRepository from '~/data/EnvironmentRepository.mjs'; +import InvariantRepository from '~/data/InvariantRepository.mjs'; +import Page from '~/presentation/pages/Page.mjs'; +import { DataTable } from '~/presentation/components/DataTable.mjs'; +import html from '~/presentation/lib/html.mjs'; + +const { p } = html; + +export default class InvariantsPage extends Page { + static override route = '/:solution/environment/invariants'; + static { + customElements.define('x-page-invariants', this); + } + + #solutionRepository = new SolutionRepository(localStorage); + #environmentRepository = new EnvironmentRepository(localStorage); + #invariantRepository = new InvariantRepository(localStorage); + #environment?: Environment; + + constructor() { + super({ title: 'Constraints' }, []); + + const dataTable = new DataTable({ + columns: { + id: { headerText: 'ID', readonly: true, formType: 'hidden', unique: true }, + statement: { headerText: 'Statement', required: true, formType: 'text', unique: true } + }, + select: async () => { + if (!this.#environment) + return []; + + return await this.#invariantRepository.getAll(t => this.#environment!.invariantIds.includes(t.id)); + }, + onCreate: async item => { + const invariant = new Invariant({ ...item, id: self.crypto.randomUUID() }); + this.#environment!.invariantIds.push(invariant.id); + await Promise.all([ + this.#invariantRepository.add(invariant), + this.#environmentRepository.update(this.#environment!) + ]); + }, + onUpdate: async item => { + await this.#invariantRepository.update(new Invariant({ + ...item + })); + }, + onDelete: async id => { + this.#environment!.invariantIds = this.#environment!.invariantIds.filter(x => x !== id); + await Promise.all([ + this.#invariantRepository.delete(id), + this.#environmentRepository.update(this.#environment!) + ]); + } + }); + + this.append( + p(` + Invariants are properties that must always be true. They are used to + constrain the possible states of a system. + `), + dataTable + ); + + this.#environmentRepository.addEventListener('update', () => dataTable.renderData()); + this.#invariantRepository.addEventListener('update', () => dataTable.renderData()); + const solutionId = this.urlParams['solution'] as Uuid; + this.#solutionRepository.getBySlug(solutionId).then(solution => { + this.#environmentRepository.get(solution!.environmentId).then(environment => { + this.#environment = environment; + dataTable.renderData(); + }); + }); + } +} \ No newline at end of file