From 48d6dbd700a1fb992db207d26949d06ba7c014e7 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Sun, 16 Jun 2024 11:44:48 +0200 Subject: [PATCH] Refactor to use string IDs for executables #262 This commit unifies the concepts of executables having same ID structure. It paves the way for more complex ID structure and using IDs in collection files as part of new ID solution (#262). Using string IDs also leads to more expressive test code. This commit also refactors the rest of the code to adopt to the changes. This commit: - Separate concerns from entities for data access (in repositories) and executables. Executables use `Identifiable` meanwhile repositories use `RepositoryEntity`. - Refactor unnecessary generic parameters for enttities and ids, enforcing string gtype everwyhere. - Changes numeric IDs to string IDs for categories to unify the retrieval and construction for executables, using pseudo-ids (their names) just like scripts. - Remove `BaseEntity` for simplicity. - Simplify usage and construction of executable objects. Move factories responsible for creation of category/scripts to domain layer. Do not longer export `CollectionCategorY` and `CollectionScript`. - Use named typed for string IDs for better differentation of different ID contexts in code. --- docs/collection-files.md | 2 + src/application/Context/ApplicationContext.ts | 2 +- .../Context/State/CategoryCollectionState.ts | 2 +- .../State/Code/Event/CodeChangedEvent.ts | 4 +- .../State/Filter/AdaptiveFilterContext.ts | 2 +- .../State/Filter/Strategy/FilterStrategy.ts | 2 +- .../Filter/Strategy/LinearFilterStrategy.ts | 2 +- .../Context/State/ICategoryCollectionState.ts | 2 +- .../Category/CategorySelectionChange.ts | 4 +- .../ScriptToCategorySelectionMapper.ts | 6 +- .../Script/DebouncedScriptSelection.ts | 24 +- .../State/Selection/Script/SelectedScript.ts | 6 +- .../Selection/Script/UserSelectedScript.ts | 11 +- .../State/Selection/UserSelectionFacade.ts | 2 +- .../Parser/CategoryCollectionParser.ts | 4 +- .../Parser/Executable/CategoryParser.ts | 12 +- .../Parser/Executable/Script/ScriptParser.ts | 11 +- src/application/Repository/Repository.ts | 18 +- .../Repository/RepositoryEntity.ts | 6 + src/domain/Application.ts | 2 +- .../{ => Collection}/CategoryCollection.ts | 43 +- .../{ => Collection}/ICategoryCollection.ts | 5 +- src/domain/Executables/Category/Category.ts | 5 +- ...llectionCategory.ts => CategoryFactory.ts} | 53 ++- src/domain/Executables/Executable.ts | 6 +- src/domain/Executables/Identifiable.ts | 5 + src/domain/Executables/Script/Script.ts | 2 +- .../{CollectionScript.ts => ScriptFactory.ts} | 32 +- src/domain/IApplication.ts | 2 +- src/infrastructure/Entity/BaseEntity.ts | 14 - src/infrastructure/Entity/IEntity.ts | 5 - .../Repository/InMemoryRepository.ts | 19 +- .../RecommendationStatusHandler.ts | 4 +- .../TheRecommendationSelector.vue | 3 +- .../Scripts/View/Cards/CardList.vue | 9 +- .../Scripts/View/Cards/CardListItem.vue | 6 +- .../View/Cards/CardSelectionIndicator.vue | 8 +- .../View/Tree/NodeContent/NodeMetadata.ts | 4 +- .../View/Tree/NodeContent/RevertToggle.vue | 3 +- .../NodeContent/Reverter/CategoryReverter.ts | 13 +- .../NodeContent/Reverter/ReverterFactory.ts | 2 +- .../NodeContent/Reverter/ScriptReverter.ts | 4 +- .../Scripts/View/Tree/ScriptsTree.vue | 5 +- .../TreeView/Bindings/TreeInputNodeData.ts | 4 +- .../TreeView/Node/HierarchicalTreeNode.vue | 4 +- .../Tree/TreeView/Node/InteractableNode.vue | 4 +- .../View/Tree/TreeView/Node/LeafTreeNode.vue | 4 +- .../View/Tree/TreeView/Node/NodeCheckbox.vue | 4 +- .../View/Tree/TreeView/Node/TreeNode.ts | 4 +- .../Tree/TreeView/Node/TreeNodeManager.ts | 4 +- .../View/Tree/TreeView/TreeRoot/TreeRoot.vue | 3 +- .../Scripts/View/Tree/TreeView/TreeView.vue | 3 +- .../CategoryNodeMetadataConverter.ts | 27 +- .../UseExecutableFromTreeNodeId.ts | 3 + .../UseSelectedScriptNodeIds.ts | 9 +- .../TreeViewAdapter/UseTreeViewFilterEvent.ts | 31 +- .../TreeViewAdapter/UseTreeViewNodeInput.ts | 7 +- .../CompositeMarkdownRenderer.spec.ts | 2 +- .../Context/ApplicationContextFactory.spec.ts | 2 +- .../State/CategoryCollectionState.spec.ts | 2 +- .../Filter/AdaptiveFilterContext.spec.ts | 2 +- .../Filter/Result/AppliedFilterResult.spec.ts | 14 +- .../Strategy/LinearFilterStrategy.spec.ts | 7 +- .../ScriptToCategorySelectionMapper.spec.ts | 73 ++- .../Script/DebouncedScriptSelection.spec.ts | 38 +- .../Selection/UserSelectionFacade.spec.ts | 2 +- .../Parser/ApplicationParser.spec.ts | 2 +- .../Parser/Executable/CategoryParser.spec.ts | 377 +++++++++------- .../Executable/Script/ScriptParser.spec.ts | 424 +++++++++--------- tests/unit/domain/Application.spec.ts | 2 +- .../CategoryCollection.spec.ts | 2 +- .../Category/CategoryFactory.spec.ts | 316 +++++++++++++ .../Category/CollectionCategory.spec.ts | 217 --------- ...onScript.spec.ts => ScriptFactory.spec.ts} | 79 ++-- .../infrastructure/InMemoryRepository.spec.ts | 217 +++++---- .../RecommendationStatusHandler.spec.ts | 2 +- .../Reverter/ReverterFactory.spec.ts | 4 +- .../Reverter/ScriptReverter.spec.ts | 8 +- .../TreeView/Node/TreeNodeManager.spec.ts | 6 +- .../CategoryNodeMetadataConverter.spec.ts | 89 ++-- .../UseSelectedScriptNodeIds.spec.ts | 18 +- .../UseTreeViewFilterEvent.spec.ts | 24 +- .../UseTreeViewNodeInput.spec.ts | 2 +- tests/unit/shared/Stubs/ApplicationStub.ts | 2 +- .../Stubs/CategoryCollectionFactoryStub.ts | 4 +- .../Stubs/CategoryCollectionParserStub.ts | 2 +- .../Stubs/CategoryCollectionStateStub.ts | 2 +- .../shared/Stubs/CategoryCollectionStub.ts | 19 +- .../unit/shared/Stubs/CategoryFactoryStub.ts | 5 +- tests/unit/shared/Stubs/CategoryStub.ts | 22 +- tests/unit/shared/Stubs/FilterStrategyStub.ts | 2 +- tests/unit/shared/Stubs/NumericEntityStub.ts | 14 - .../unit/shared/Stubs/RepositoryEntityStub.ts | 14 + tests/unit/shared/Stubs/ScriptFactoryStub.ts | 3 +- tests/unit/shared/Stubs/ScriptStub.ts | 14 +- tests/unit/shared/Stubs/SelectedScriptStub.ts | 5 +- 96 files changed, 1417 insertions(+), 1109 deletions(-) create mode 100644 src/application/Repository/RepositoryEntity.ts rename src/domain/{ => Collection}/CategoryCollection.ts (75%) rename src/domain/{ => Collection}/ICategoryCollection.ts (82%) rename src/domain/Executables/Category/{CollectionCategory.ts => CategoryFactory.ts} (57%) create mode 100644 src/domain/Executables/Identifiable.ts rename src/domain/Executables/Script/{CollectionScript.ts => ScriptFactory.ts} (73%) delete mode 100644 src/infrastructure/Entity/BaseEntity.ts delete mode 100644 src/infrastructure/Entity/IEntity.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseExecutableFromTreeNodeId.ts rename tests/unit/domain/{ => Collection}/CategoryCollection.spec.ts (99%) create mode 100644 tests/unit/domain/Executables/Category/CategoryFactory.spec.ts delete mode 100644 tests/unit/domain/Executables/Category/CollectionCategory.spec.ts rename tests/unit/domain/Executables/Script/{CollectionScript.spec.ts => ScriptFactory.spec.ts} (60%) delete mode 100644 tests/unit/shared/Stubs/NumericEntityStub.ts create mode 100644 tests/unit/shared/Stubs/RepositoryEntityStub.ts diff --git a/docs/collection-files.md b/docs/collection-files.md index 414fd8ea..96b3ff65 100644 --- a/docs/collection-files.md +++ b/docs/collection-files.md @@ -30,6 +30,8 @@ Related documentation: ### Executables +They represent independently executable tweaks with documentation and reversibility. + An Executable is a logical entity that can - execute once compiled, diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts index 7829737f..4828947a 100644 --- a/src/application/Context/ApplicationContext.ts +++ b/src/application/Context/ApplicationContext.ts @@ -1,6 +1,6 @@ import type { IApplication } from '@/domain/IApplication'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import { EventSource } from '@/infrastructure/Events/EventSource'; import { assertInRange } from '@/application/Common/Enum'; import { CategoryCollectionState } from './State/CategoryCollectionState'; diff --git a/src/application/Context/State/CategoryCollectionState.ts b/src/application/Context/State/CategoryCollectionState.ts index 253a7133..6d37ed8f 100644 --- a/src/application/Context/State/CategoryCollectionState.ts +++ b/src/application/Context/State/CategoryCollectionState.ts @@ -1,4 +1,4 @@ -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext'; import { ApplicationCode } from './Code/ApplicationCode'; diff --git a/src/application/Context/State/Code/Event/CodeChangedEvent.ts b/src/application/Context/State/Code/Event/CodeChangedEvent.ts index aa99a78e..685faafe 100644 --- a/src/application/Context/State/Code/Event/CodeChangedEvent.ts +++ b/src/application/Context/State/Code/Event/CodeChangedEvent.ts @@ -36,12 +36,12 @@ export class CodeChangedEvent implements ICodeChangedEvent { } public getScriptPositionInCode(script: Script): ICodePosition { - return this.getPositionById(script.id); + return this.getPositionById(script.executableId); } private getPositionById(scriptId: string): ICodePosition { const position = [...this.scripts.entries()] - .filter(([s]) => s.id === scriptId) + .filter(([s]) => s.executableId === scriptId) .map(([, pos]) => pos) .at(0); if (!position) { diff --git a/src/application/Context/State/Filter/AdaptiveFilterContext.ts b/src/application/Context/State/Filter/AdaptiveFilterContext.ts index 902ec523..6ecba461 100644 --- a/src/application/Context/State/Filter/AdaptiveFilterContext.ts +++ b/src/application/Context/State/Filter/AdaptiveFilterContext.ts @@ -1,5 +1,5 @@ import { EventSource } from '@/infrastructure/Events/EventSource'; -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import { FilterChange } from './Event/FilterChange'; import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy'; import type { FilterResult } from './Result/FilterResult'; diff --git a/src/application/Context/State/Filter/Strategy/FilterStrategy.ts b/src/application/Context/State/Filter/Strategy/FilterStrategy.ts index 2bda8de2..2a4a7862 100644 --- a/src/application/Context/State/Filter/Strategy/FilterStrategy.ts +++ b/src/application/Context/State/Filter/Strategy/FilterStrategy.ts @@ -1,4 +1,4 @@ -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { FilterResult } from '../Result/FilterResult'; export interface FilterStrategy { diff --git a/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts b/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts index bb3bfb04..327bdf80 100644 --- a/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts +++ b/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts @@ -1,7 +1,7 @@ import type { Category } from '@/domain/Executables/Category/Category'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import type { Documentable } from '@/domain/Executables/Documentable'; -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { Script } from '@/domain/Executables/Script/Script'; import { AppliedFilterResult } from '../Result/AppliedFilterResult'; import type { FilterStrategy } from './FilterStrategy'; diff --git a/src/application/Context/State/ICategoryCollectionState.ts b/src/application/Context/State/ICategoryCollectionState.ts index 8a485679..3d9bdaa5 100644 --- a/src/application/Context/State/ICategoryCollectionState.ts +++ b/src/application/Context/State/ICategoryCollectionState.ts @@ -1,4 +1,4 @@ -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import { OperatingSystem } from '@/domain/OperatingSystem'; import type { IApplicationCode } from './Code/IApplicationCode'; import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext'; diff --git a/src/application/Context/State/Selection/Category/CategorySelectionChange.ts b/src/application/Context/State/Selection/Category/CategorySelectionChange.ts index a944a3f6..36b7de90 100644 --- a/src/application/Context/State/Selection/Category/CategorySelectionChange.ts +++ b/src/application/Context/State/Selection/Category/CategorySelectionChange.ts @@ -1,3 +1,5 @@ +import type { ExecutableId } from '@/domain/Executables/Identifiable'; + type CategorySelectionStatus = { readonly isSelected: true; readonly isReverted: boolean; @@ -6,7 +8,7 @@ type CategorySelectionStatus = { }; export interface CategorySelectionChange { - readonly categoryId: number; + readonly categoryId: ExecutableId; readonly newStatus: CategorySelectionStatus; } diff --git a/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts b/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts index a856b0ea..a2916926 100644 --- a/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts +++ b/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts @@ -1,5 +1,5 @@ import type { Category } from '@/domain/Executables/Category/Category'; -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelection } from './CategorySelection'; import type { ScriptSelection } from '../Script/ScriptSelection'; @@ -23,7 +23,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection { return false; } return scripts.every( - (script) => selectedScripts.some((selected) => selected.id === script.id), + (script) => selectedScripts.some((selected) => selected.id === script.executableId), ); } @@ -50,7 +50,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection { const scripts = category.getAllScriptsRecursively(); const scriptsChangesInCategory = scripts .map((script): ScriptSelectionChange => ({ - scriptId: script.id, + scriptId: script.executableId, newStatus: { ...change.newStatus, }, diff --git a/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts b/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts index 79a7dd46..23184f20 100644 --- a/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts +++ b/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts @@ -2,7 +2,7 @@ import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryReposito import type { Script } from '@/domain/Executables/Script/Script'; import { EventSource } from '@/infrastructure/Events/EventSource'; import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository'; -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce'; import { UserSelectedScript } from './UserSelectedScript'; import type { ScriptSelection } from './ScriptSelection'; @@ -16,7 +16,7 @@ export type DebounceFunction = typeof batchedDebounce>(); - private readonly scripts: Repository; + private readonly scripts: Repository; public readonly processChanges: ScriptSelection['processChanges']; @@ -25,7 +25,7 @@ export class DebouncedScriptSelection implements ScriptSelection { selectedScripts: ReadonlyArray, debounce: DebounceFunction = batchedDebounce, ) { - this.scripts = new InMemoryRepository(); + this.scripts = new InMemoryRepository(); for (const script of selectedScripts) { this.scripts.addItem(script); } @@ -49,7 +49,7 @@ export class DebouncedScriptSelection implements ScriptSelection { public selectAll(): void { const scriptsToSelect = this.collection .getAllScripts() - .filter((script) => !this.scripts.exists(script.id)) + .filter((script) => !this.scripts.exists(script.executableId)) .map((script) => new UserSelectedScript(script, false)); if (scriptsToSelect.length === 0) { return; @@ -116,9 +116,9 @@ export class DebouncedScriptSelection implements ScriptSelection { private applyChange(change: ScriptSelectionChange): number { const script = this.collection.getScript(change.scriptId); if (change.newStatus.isSelected) { - return this.addOrUpdateScript(script.id, change.newStatus.isReverted); + return this.addOrUpdateScript(script.executableId, change.newStatus.isReverted); } - return this.removeScript(script.id); + return this.removeScript(script.executableId); } private addOrUpdateScript(scriptId: string, revert: boolean): number { @@ -152,24 +152,24 @@ function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) { } function getScriptIdsToBeSelected( - existingItems: ReadonlyRepository, + existingItems: ReadonlyRepository, desiredScripts: readonly Script[], ): string[] { return desiredScripts - .filter((script) => !existingItems.exists(script.id)) - .map((script) => script.id); + .filter((script) => !existingItems.exists(script.executableId)) + .map((script) => script.executableId); } function getScriptIdsToBeDeselected( - existingItems: ReadonlyRepository, + existingItems: ReadonlyRepository, desiredScripts: readonly Script[], ): string[] { return existingItems .getItems() - .filter((existing) => !desiredScripts.some((script) => existing.id === script.id)) + .filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId)) .map((script) => script.id); } function equals(a: SelectedScript, b: SelectedScript): boolean { - return a.script.equals(b.script.id) && a.revert === b.revert; + return a.script.executableId === b.script.executableId && a.revert === b.revert; } diff --git a/src/application/Context/State/Selection/Script/SelectedScript.ts b/src/application/Context/State/Selection/Script/SelectedScript.ts index c0059f6e..858dbbbb 100644 --- a/src/application/Context/State/Selection/Script/SelectedScript.ts +++ b/src/application/Context/State/Selection/Script/SelectedScript.ts @@ -1,9 +1,7 @@ -import type { IEntity } from '@/infrastructure/Entity/IEntity'; import type { Script } from '@/domain/Executables/Script/Script'; +import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity'; -type ScriptId = Script['id']; - -export interface SelectedScript extends IEntity { +export interface SelectedScript extends RepositoryEntity { readonly script: Script; readonly revert: boolean; } diff --git a/src/application/Context/State/Selection/Script/UserSelectedScript.ts b/src/application/Context/State/Selection/Script/UserSelectedScript.ts index 315da002..01b40b38 100644 --- a/src/application/Context/State/Selection/Script/UserSelectedScript.ts +++ b/src/application/Context/State/Selection/Script/UserSelectedScript.ts @@ -1,17 +1,16 @@ -import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import type { Script } from '@/domain/Executables/Script/Script'; -import type { SelectedScript } from './SelectedScript'; +import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity'; -type SelectedScriptId = SelectedScript['id']; +export class UserSelectedScript implements RepositoryEntity { + public readonly id: string; -export class UserSelectedScript extends BaseEntity { constructor( public readonly script: Script, public readonly revert: boolean, ) { - super(script.id); + this.id = script.executableId; if (revert && !script.canRevert()) { - throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`); + throw new Error(`The script with ID '${script.executableId}' is not reversible and cannot be reverted.`); } } } diff --git a/src/application/Context/State/Selection/UserSelectionFacade.ts b/src/application/Context/State/Selection/UserSelectionFacade.ts index 7667d983..6fbc12bc 100644 --- a/src/application/Context/State/Selection/UserSelectionFacade.ts +++ b/src/application/Context/State/Selection/UserSelectionFacade.ts @@ -1,4 +1,4 @@ -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper'; import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection'; import type { CategorySelection } from './Category/CategorySelection'; diff --git a/src/application/Parser/CategoryCollectionParser.ts b/src/application/Parser/CategoryCollectionParser.ts index 7c92c7d3..08e07a79 100644 --- a/src/application/Parser/CategoryCollectionParser.ts +++ b/src/application/Parser/CategoryCollectionParser.ts @@ -1,7 +1,7 @@ import type { CollectionData } from '@/application/collections/'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; -import { CategoryCollection } from '@/domain/CategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import { createEnumParser, type EnumParser } from '../Common/Enum'; import { parseCategory, type CategoryParser } from './Executable/CategoryParser'; diff --git a/src/application/Parser/Executable/CategoryParser.ts b/src/application/Parser/Executable/CategoryParser.ts index 39d77e51..21279e54 100644 --- a/src/application/Parser/Executable/CategoryParser.ts +++ b/src/application/Parser/Executable/CategoryParser.ts @@ -3,16 +3,14 @@ import type { } from '@/application/collections/'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; import type { Category } from '@/domain/Executables/Category/Category'; -import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory'; import type { Script } from '@/domain/Executables/Script/Script'; +import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory'; import { parseDocs, type DocsParser } from './DocumentationParser'; import { parseScript, type ScriptParser } from './Script/ScriptParser'; import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator'; import { ExecutableType } from './Validation/ExecutableType'; import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities'; -let categoryIdCounter = 0; - export const parseCategory: CategoryParser = ( category: CategoryData, collectionUtilities: CategoryCollectionSpecificUtilities, @@ -59,7 +57,7 @@ function parseCategoryRecursively( } try { return context.categoryUtilities.createCategory({ - id: categoryIdCounter++, + executableId: context.categoryData.category, // arbitrary ID name: context.categoryData.category, docs: context.categoryUtilities.parseDocs(context.categoryData), subcategories: children.subcategories, @@ -166,10 +164,6 @@ function hasProperty( return Object.prototype.hasOwnProperty.call(object, propertyName); } -export type CategoryFactory = ( - ...parameters: ConstructorParameters -) => Category; - interface CategoryParserUtilities { readonly createCategory: CategoryFactory; readonly wrapError: ErrorWithContextWrapper; @@ -179,7 +173,7 @@ interface CategoryParserUtilities { } const DefaultCategoryParserUtilities: CategoryParserUtilities = { - createCategory: (...parameters) => new CollectionCategory(...parameters), + createCategory, wrapError: wrapErrorWithAdditionalContext, createValidator: createExecutableDataValidator, parseScript, diff --git a/src/application/Parser/Executable/Script/ScriptParser.ts b/src/application/Parser/Executable/Script/ScriptParser.ts index e248bde1..44923733 100644 --- a/src/application/Parser/Executable/Script/ScriptParser.ts +++ b/src/application/Parser/Executable/Script/ScriptParser.ts @@ -1,7 +1,6 @@ import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/'; import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax'; -import { CollectionScript } from '@/domain/Executables/Script/CollectionScript'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator'; @@ -10,6 +9,7 @@ import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptC import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory'; import type { Script } from '@/domain/Executables/Script/Script'; import { createEnumParser, type EnumParser } from '@/application/Common/Enum'; +import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory'; import { parseDocs, type DocsParser } from '../DocumentationParser'; import { ExecutableType } from '../Validation/ExecutableType'; import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator'; @@ -37,6 +37,7 @@ export const parseScript: ScriptParser = ( validateScript(data, validator); try { const script = scriptUtilities.createScript({ + executableId: data.name, // arbitrary ID name: data.name, code: parseCode( data, @@ -132,14 +133,6 @@ interface ScriptParserUtilities { readonly parseDocs: DocsParser; } -export type ScriptFactory = ( - ...parameters: ConstructorParameters -) => Script; - -const createScript: ScriptFactory = (...parameters) => { - return new CollectionScript(...parameters); -}; - const DefaultUtilities: ScriptParserUtilities = { levelParser: createEnumParser(RecommendationLevel), createScript, diff --git a/src/application/Repository/Repository.ts b/src/application/Repository/Repository.ts index 6232598a..0b2d83d7 100644 --- a/src/application/Repository/Repository.ts +++ b/src/application/Repository/Repository.ts @@ -1,17 +1,19 @@ -import type { IEntity } from '@/infrastructure/Entity/IEntity'; +import type { RepositoryEntity } from './RepositoryEntity'; -export interface ReadonlyRepository> { +type EntityId = RepositoryEntity['id']; + +export interface ReadonlyRepository { readonly length: number; getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[]; - getById(id: TKey): TEntity; - exists(id: TKey): boolean; + getById(id: EntityId): TEntity; + exists(id: EntityId): boolean; } -export interface MutableRepository> { +export interface MutableRepository { addItem(item: TEntity): void; addOrUpdateItem(item: TEntity): void; - removeItem(id: TKey): void; + removeItem(id: EntityId): void; } -export interface Repository> - extends ReadonlyRepository, MutableRepository { } +export interface Repository + extends ReadonlyRepository, MutableRepository { } diff --git a/src/application/Repository/RepositoryEntity.ts b/src/application/Repository/RepositoryEntity.ts new file mode 100644 index 00000000..1603cd97 --- /dev/null +++ b/src/application/Repository/RepositoryEntity.ts @@ -0,0 +1,6 @@ +/** Aggregate root */ +export type RepositoryEntityId = string; + +export interface RepositoryEntity { + readonly id: RepositoryEntityId; +} diff --git a/src/domain/Application.ts b/src/domain/Application.ts index 2fc1230d..77e85796 100644 --- a/src/domain/Application.ts +++ b/src/domain/Application.ts @@ -1,6 +1,6 @@ import { OperatingSystem } from './OperatingSystem'; import type { IApplication } from './IApplication'; -import type { ICategoryCollection } from './ICategoryCollection'; +import type { ICategoryCollection } from './Collection/ICategoryCollection'; import type { ProjectDetails } from './Project/ProjectDetails'; export class Application implements IApplication { diff --git a/src/domain/CategoryCollection.ts b/src/domain/Collection/CategoryCollection.ts similarity index 75% rename from src/domain/CategoryCollection.ts rename to src/domain/Collection/CategoryCollection.ts index 71d8c61e..064d5294 100644 --- a/src/domain/CategoryCollection.ts +++ b/src/domain/Collection/CategoryCollection.ts @@ -1,10 +1,10 @@ import { getEnumValues, assertInRange } from '@/application/Common/Enum'; -import { RecommendationLevel } from './Executables/Script/RecommendationLevel'; -import { OperatingSystem } from './OperatingSystem'; -import type { IEntity } from '../infrastructure/Entity/IEntity'; -import type { Category } from './Executables/Category/Category'; -import type { Script } from './Executables/Script/Script'; -import type { IScriptingDefinition } from './IScriptingDefinition'; +import { RecommendationLevel } from '../Executables/Script/RecommendationLevel'; +import { OperatingSystem } from '../OperatingSystem'; +import type { ExecutableId, Identifiable } from '../Executables/Identifiable'; +import type { Category } from '../Executables/Category/Category'; +import type { Script } from '../Executables/Script/Script'; +import type { IScriptingDefinition } from '../IScriptingDefinition'; import type { ICategoryCollection } from './ICategoryCollection'; export class CategoryCollection implements ICategoryCollection { @@ -30,14 +30,14 @@ export class CategoryCollection implements ICategoryCollection { this.queryable = makeQueryable(this.actions); assertInRange(this.os, OperatingSystem); ensureValid(this.queryable); - ensureNoDuplicates(this.queryable.allCategories); - ensureNoDuplicates(this.queryable.allScripts); + ensureNoDuplicateIds(this.queryable.allCategories); + ensureNoDuplicateIds(this.queryable.allScripts); } - public getCategory(categoryId: number): Category { - const category = this.queryable.allCategories.find((c) => c.id === categoryId); + public getCategory(executableId: ExecutableId): Category { + const category = this.queryable.allCategories.find((c) => c.executableId === executableId); if (!category) { - throw new Error(`Missing category with ID: "${categoryId}"`); + throw new Error(`Missing category with ID: "${executableId}"`); } return category; } @@ -48,10 +48,10 @@ export class CategoryCollection implements ICategoryCollection { return scripts ?? []; } - public getScript(scriptId: string): Script { - const script = this.queryable.allScripts.find((s) => s.id === scriptId); + public getScript(executableId: string): Script { + const script = this.queryable.allScripts.find((s) => s.executableId === executableId); if (!script) { - throw new Error(`missing script: ${scriptId}`); + throw new Error(`missing script: ${executableId}`); } return script; } @@ -65,17 +65,14 @@ export class CategoryCollection implements ICategoryCollection { } } -function ensureNoDuplicates(entities: ReadonlyArray>) { - const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array - .findIndex((otherId) => otherId === id) !== index; - const duplicatedIds = entities - .map((entity) => entity.id) - .filter((id, index, array) => !isUniqueInArray(id, index, array)) - .filter(isUniqueInArray); +function ensureNoDuplicateIds(executables: ReadonlyArray) { // TODO: Unit test this + const duplicatedIds = executables + .map((e) => e.executableId) + .filter((id, index, array) => array.findIndex((otherId) => otherId === id) !== index); if (duplicatedIds.length > 0) { const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(','); throw new Error( - `Duplicate entities are detected with following id(s): ${duplicatedIdsText}`, + `Duplicate executables are detected with following id(s): ${duplicatedIdsText}`, ); } } @@ -120,7 +117,7 @@ function flattenApplication( ): [Category[], Script[]] { const [subCategories, subScripts] = (categories || []) // Parse children - .map((category) => flattenApplication(category.subCategories)) + .map((category) => flattenApplication(category.subcategories)) // Flatten results .reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => { return [ diff --git a/src/domain/ICategoryCollection.ts b/src/domain/Collection/ICategoryCollection.ts similarity index 82% rename from src/domain/ICategoryCollection.ts rename to src/domain/Collection/ICategoryCollection.ts index c7b7d799..ac7879fc 100644 --- a/src/domain/ICategoryCollection.ts +++ b/src/domain/Collection/ICategoryCollection.ts @@ -3,6 +3,7 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import type { Script } from '@/domain/Executables/Script/Script'; import type { Category } from '@/domain/Executables/Category/Category'; +import type { ExecutableId } from '../Executables/Identifiable'; export interface ICategoryCollection { readonly scripting: IScriptingDefinition; @@ -12,8 +13,8 @@ export interface ICategoryCollection { readonly actions: ReadonlyArray; getScriptsByLevel(level: RecommendationLevel): ReadonlyArray +@/domain/Collection/ICategoryCollection diff --git a/src/presentation/components/Scripts/View/Cards/CardList.vue b/src/presentation/components/Scripts/View/Cards/CardList.vue index dd831327..f054265e 100644 --- a/src/presentation/components/Scripts/View/Cards/CardList.vue +++ b/src/presentation/components/Scripts/View/Cards/CardList.vue @@ -44,6 +44,7 @@ import { } from 'vue'; import { injectKey } from '@/presentation/injectionSymbols'; import SizeObserver from '@/presentation/components/Shared/SizeObserver.vue'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; import { hasDirective } from './NonCollapsingDirective'; import CardListItem from './CardListItem.vue'; @@ -58,12 +59,12 @@ export default defineComponent({ const width = ref(); - const categoryIds = computed( - () => currentState.value.collection.actions.map((category) => category.id), + const categoryIds = computed( + () => currentState.value.collection.actions.map((category) => category.executableId), ); - const activeCategoryId = ref(undefined); + const activeCategoryId = ref(undefined); - function onSelected(categoryId: number, isExpanded: boolean) { + function onSelected(categoryId: ExecutableId, isExpanded: boolean) { activeCategoryId.value = isExpanded ? categoryId : undefined; } diff --git a/src/presentation/components/Scripts/View/Cards/CardListItem.vue b/src/presentation/components/Scripts/View/Cards/CardListItem.vue index 454167f0..edd92bcb 100644 --- a/src/presentation/components/Scripts/View/Cards/CardListItem.vue +++ b/src/presentation/components/Scripts/View/Cards/CardListItem.vue @@ -56,12 +56,14 @@ +@/domain/Collection/ICategoryCollection diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts index 8175b400..fe805c69 100644 --- a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/CategoryReverter.ts @@ -1,17 +1,18 @@ import type { UserSelection } from '@/application/Context/State/Selection/UserSelection'; -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; -import { getCategoryId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter'; +import type { ExecutableId } from '@/domain/Executables/Identifiable'; +import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter'; import { ScriptReverter } from './ScriptReverter'; import type { Reverter } from './Reverter'; export class CategoryReverter implements Reverter { - private readonly categoryId: number; + private readonly categoryId: ExecutableId; private readonly scriptReverters: ReadonlyArray; constructor(nodeId: string, collection: ICategoryCollection) { - this.categoryId = getCategoryId(nodeId); + this.categoryId = createExecutableIdFromNodeId(nodeId); this.scriptReverters = createScriptReverters(this.categoryId, collection); } @@ -37,12 +38,12 @@ export class CategoryReverter implements Reverter { } function createScriptReverters( - categoryId: number, + categoryId: ExecutableId, collection: ICategoryCollection, ): ScriptReverter[] { const category = collection.getCategory(categoryId); const scripts = category .getAllScriptsRecursively() .filter((script) => script.canRevert()); - return scripts.map((script) => new ScriptReverter(script.id)); + return scripts.map((script) => new ScriptReverter(script.executableId)); } diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts index 1ea96de0..c6bf4acd 100644 --- a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ReverterFactory.ts @@ -1,4 +1,4 @@ -import type { ICategoryCollection } from '@/domain/ICategoryCollection'; +import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; import { type NodeMetadata, NodeType } from '../NodeMetadata'; import { ScriptReverter } from './ScriptReverter'; import { CategoryReverter } from './CategoryReverter'; diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts index ae4a5efd..d6f619df 100644 --- a/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts +++ b/src/presentation/components/Scripts/View/Tree/NodeContent/Reverter/ScriptReverter.ts @@ -1,13 +1,13 @@ import type { UserSelection } from '@/application/Context/State/Selection/UserSelection'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; -import { getScriptId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter'; +import { createExecutableIdFromNodeId } from '../../TreeViewAdapter/CategoryNodeMetadataConverter'; import type { Reverter } from './Reverter'; export class ScriptReverter implements Reverter { private readonly scriptId: string; constructor(nodeId: string) { - this.scriptId = getScriptId(nodeId); + this.scriptId = createExecutableIdFromNodeId(nodeId); } public getState(selectedScripts: ReadonlyArray): boolean { diff --git a/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue b/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue index daf58d76..c082c829 100644 --- a/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue +++ b/src/presentation/components/Scripts/View/Tree/ScriptsTree.vue @@ -24,8 +24,9 @@