Refactor to unify scripts/categories as Executable

This commit consolidates scripts and categories under a unified
'Executable' concept. This simplifies the architecture and improves code
readability.

- Introduce subfolders within `src/domain` to segregate domain elements.
- Update class and interface names by removing the 'I' prefix in
  alignment with new coding standards.
- Replace 'Node' with 'Executable' to clarify usage; reserve 'Node'
  exclusively for the UI's tree component.
This commit is contained in:
undergroundwires
2024-06-12 12:36:40 +02:00
parent 8becc7dbc4
commit c138f74460
230 changed files with 1120 additions and 1039 deletions

View File

@@ -1,9 +1,9 @@
import { getEnumValues, assertInRange } from '@/application/Common/Enum';
import { RecommendationLevel } from './RecommendationLevel';
import { RecommendationLevel } from './Executables/Script/RecommendationLevel';
import { OperatingSystem } from './OperatingSystem';
import type { IEntity } from '../infrastructure/Entity/IEntity';
import type { ICategory } from './ICategory';
import type { IScript } from './IScript';
import type { Category } from './Executables/Category/Category';
import type { Script } from './Executables/Script/Script';
import type { IScriptingDefinition } from './IScriptingDefinition';
import type { ICategoryCollection } from './ICategoryCollection';
@@ -16,7 +16,7 @@ export class CategoryCollection implements ICategoryCollection {
constructor(
public readonly os: OperatingSystem,
public readonly actions: ReadonlyArray<ICategory>,
public readonly actions: ReadonlyArray<Category>,
public readonly scripting: IScriptingDefinition,
) {
this.queryable = makeQueryable(actions);
@@ -26,7 +26,7 @@ export class CategoryCollection implements ICategoryCollection {
ensureNoDuplicates(this.queryable.allScripts);
}
public getCategory(categoryId: number): ICategory {
public getCategory(categoryId: number): Category {
const category = this.queryable.allCategories.find((c) => c.id === categoryId);
if (!category) {
throw new Error(`Missing category with ID: "${categoryId}"`);
@@ -34,13 +34,13 @@ export class CategoryCollection implements ICategoryCollection {
return category;
}
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
public getScriptsByLevel(level: RecommendationLevel): readonly Script[] {
assertInRange(level, RecommendationLevel);
const scripts = this.queryable.scriptsByLevel.get(level);
return scripts ?? [];
}
public getScript(scriptId: string): IScript {
public getScript(scriptId: string): Script {
const script = this.queryable.allScripts.find((s) => s.id === scriptId);
if (!script) {
throw new Error(`missing script: ${scriptId}`);
@@ -48,11 +48,11 @@ export class CategoryCollection implements ICategoryCollection {
return script;
}
public getAllScripts(): IScript[] {
public getAllScripts(): Script[] {
return this.queryable.allScripts;
}
public getAllCategories(): ICategory[] {
public getAllCategories(): Category[] {
return this.queryable.allCategories;
}
}
@@ -73,9 +73,9 @@ function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
}
interface IQueryableCollection {
allCategories: ICategory[];
allScripts: IScript[];
scriptsByLevel: Map<RecommendationLevel, readonly IScript[]>;
allCategories: Category[];
allScripts: Script[];
scriptsByLevel: Map<RecommendationLevel, readonly Script[]>;
}
function ensureValid(application: IQueryableCollection) {
@@ -83,13 +83,13 @@ function ensureValid(application: IQueryableCollection) {
ensureValidScripts(application.allScripts);
}
function ensureValidCategories(allCategories: readonly ICategory[]) {
function ensureValidCategories(allCategories: readonly Category[]) {
if (!allCategories.length) {
throw new Error('must consist of at least one category');
}
}
function ensureValidScripts(allScripts: readonly IScript[]) {
function ensureValidScripts(allScripts: readonly Script[]) {
if (!allScripts.length) {
throw new Error('must consist of at least one script');
}
@@ -102,8 +102,8 @@ function ensureValidScripts(allScripts: readonly IScript[]) {
}
function flattenApplication(
categories: ReadonlyArray<ICategory>,
): [ICategory[], IScript[]] {
categories: ReadonlyArray<Category>,
): [Category[], Script[]] {
const [subCategories, subScripts] = (categories || [])
// Parse children
.map((category) => flattenApplication(category.subCategories))
@@ -113,7 +113,7 @@ function flattenApplication(
[...previousCategories, ...currentCategories],
[...previousScripts, ...currentScripts],
];
}, [new Array<ICategory>(), new Array<IScript>()]);
}, [new Array<Category>(), new Array<Script>()]);
return [
[
...(categories || []),
@@ -127,7 +127,7 @@ function flattenApplication(
}
function makeQueryable(
actions: ReadonlyArray<ICategory>,
actions: ReadonlyArray<Category>,
): IQueryableCollection {
const flattened = flattenApplication(actions);
return {
@@ -138,8 +138,8 @@ function makeQueryable(
}
function groupByLevel(
allScripts: readonly IScript[],
): Map<RecommendationLevel, readonly IScript[]> {
allScripts: readonly Script[],
): Map<RecommendationLevel, readonly Script[]> {
return getEnumValues(RecommendationLevel)
.map((level) => ({
level,
@@ -150,5 +150,5 @@ function groupByLevel(
.reduce((map, group) => {
map.set(group.level, group.scripts);
return map;
}, new Map<RecommendationLevel, readonly IScript[]>());
}, new Map<RecommendationLevel, readonly Script[]>());
}

View File

@@ -0,0 +1,11 @@
import type { Script } from '../Script/Script';
import type { Executable } from '../Executable';
export interface Category extends Executable<number> {
readonly id: number;
readonly name: string;
readonly subCategories: ReadonlyArray<Category>;
readonly scripts: ReadonlyArray<Script>;
includes(script: Script): boolean;
getAllScriptsRecursively(): ReadonlyArray<Script>;
}

View File

@@ -1,17 +1,17 @@
import { BaseEntity } from '../infrastructure/Entity/BaseEntity';
import type { ICategory } from './ICategory';
import type { IScript } from './IScript';
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import type { Category } from './Category';
import type { Script } from '../Script/Script';
export class Category extends BaseEntity<number> implements ICategory {
private allSubScripts?: ReadonlyArray<IScript> = undefined;
export class CollectionCategory extends BaseEntity<number> implements Category {
private allSubScripts?: ReadonlyArray<Script> = undefined;
public readonly name: string;
public readonly docs: ReadonlyArray<string>;
public readonly subCategories: ReadonlyArray<ICategory>;
public readonly subCategories: ReadonlyArray<Category>;
public readonly scripts: ReadonlyArray<IScript>;
public readonly scripts: ReadonlyArray<Script>;
constructor(parameters: CategoryInitParameters) {
super(parameters.id);
@@ -22,11 +22,11 @@ export class Category extends BaseEntity<number> implements ICategory {
this.scripts = parameters.scripts;
}
public includes(script: IScript): boolean {
public includes(script: Script): boolean {
return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id);
}
public getAllScriptsRecursively(): readonly IScript[] {
public getAllScriptsRecursively(): readonly Script[] {
if (!this.allSubScripts) {
this.allSubScripts = parseScriptsRecursively(this);
}
@@ -38,11 +38,11 @@ export interface CategoryInitParameters {
readonly id: number;
readonly name: string;
readonly docs: ReadonlyArray<string>;
readonly subcategories: ReadonlyArray<ICategory>;
readonly scripts: ReadonlyArray<IScript>;
readonly subcategories: ReadonlyArray<Category>;
readonly scripts: ReadonlyArray<Script>;
}
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
function parseScriptsRecursively(category: Category): ReadonlyArray<Script> {
return [
...category.scripts,
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),

View File

@@ -1,3 +1,3 @@
export interface IDocumentable {
export interface Documentable {
readonly docs: ReadonlyArray<string>;
}

View File

@@ -0,0 +1,6 @@
import type { IEntity } from '@/infrastructure/Entity/IEntity';
import type { Documentable } from './Documentable';
export interface Executable<TExecutableKey>
extends Documentable, IEntity<TExecutableKey> {
}

View File

@@ -1,6 +1,6 @@
import type { IScriptCode } from './IScriptCode';
import type { ScriptCode } from './ScriptCode';
export class ScriptCode implements IScriptCode {
export class DistinctReversibleScriptCode implements ScriptCode {
constructor(
public readonly execute: string,
public readonly revert: string | undefined,

View File

@@ -1,4 +1,4 @@
export interface IScriptCode {
export interface ScriptCode {
readonly execute: string;
readonly revert?: string;
}

View File

@@ -0,0 +1,12 @@
import { DistinctReversibleScriptCode } from './DistinctReversibleScriptCode';
import type { ScriptCode } from './ScriptCode';
export interface ScriptCodeFactory {
(
...args: ConstructorParameters<typeof DistinctReversibleScriptCode>
): ScriptCode;
}
export const createScriptCode: ScriptCodeFactory = (
...args
) => new DistinctReversibleScriptCode(...args);

View File

@@ -1,12 +1,12 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { RecommendationLevel } from './RecommendationLevel';
import type { IScript } from './IScript';
import type { IScriptCode } from './IScriptCode';
import type { Script } from './Script';
import type { ScriptCode } from './Code/ScriptCode';
export class Script extends BaseEntity<string> implements IScript {
export class CollectionScript extends BaseEntity<string> implements Script {
public readonly name: string;
public readonly code: IScriptCode;
public readonly code: ScriptCode;
public readonly docs: ReadonlyArray<string>;
@@ -28,7 +28,7 @@ export class Script extends BaseEntity<string> implements IScript {
export interface ScriptInitParameters {
readonly name: string;
readonly code: IScriptCode;
readonly code: ScriptCode;
readonly docs: ReadonlyArray<string>;
readonly level?: RecommendationLevel;
}

View File

@@ -0,0 +1,11 @@
import { RecommendationLevel } from './RecommendationLevel';
import type { Executable } from '../Executable';
import type { Documentable } from '../Documentable';
import type { ScriptCode } from './Code/ScriptCode';
export interface Script extends Executable<string>, Documentable {
readonly name: string;
readonly level?: RecommendationLevel;
readonly code: ScriptCode;
canRevert(): boolean;
}

View File

@@ -1,15 +0,0 @@
import type { IEntity } from '../infrastructure/Entity/IEntity';
import type { IScript } from './IScript';
import type { IDocumentable } from './IDocumentable';
export interface ICategory extends IEntity<number>, IDocumentable {
readonly id: number;
readonly name: string;
readonly subCategories: ReadonlyArray<ICategory>;
readonly scripts: ReadonlyArray<IScript>;
includes(script: IScript): boolean;
getAllScriptsRecursively(): ReadonlyArray<IScript>;
}
export type { IEntity } from '../infrastructure/Entity/IEntity';
export type { IScript } from './IScript';

View File

@@ -1,19 +1,19 @@
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import type { IScript } from '@/domain/IScript';
import type { ICategory } from '@/domain/ICategory';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import type { Script } from '@/domain/Executables/Script/Script';
import type { Category } from '@/domain/Executables/Category/Category';
export interface ICategoryCollection {
readonly scripting: IScriptingDefinition;
readonly os: OperatingSystem;
readonly totalScripts: number;
readonly totalCategories: number;
readonly actions: ReadonlyArray<ICategory>;
readonly actions: ReadonlyArray<Category>;
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
getCategory(categoryId: number): ICategory;
getScript(scriptId: string): IScript;
getAllScripts(): ReadonlyArray<IScript>;
getAllCategories(): ReadonlyArray<ICategory>;
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<Script>;
getCategory(categoryId: number): Category;
getScript(scriptId: string): Script;
getAllScripts(): ReadonlyArray<Script>;
getAllCategories(): ReadonlyArray<Category>;
}

View File

@@ -1,12 +0,0 @@
import { RecommendationLevel } from './RecommendationLevel';
import type { IEntity } from '../infrastructure/Entity/IEntity';
import type { IDocumentable } from './IDocumentable';
import type { IScriptCode } from './IScriptCode';
export interface IScript extends IEntity<string>, IDocumentable {
readonly name: string;
readonly level?: RecommendationLevel;
readonly docs: ReadonlyArray<string>;
readonly code: IScriptCode;
canRevert(): boolean;
}

View File

@@ -1,10 +0,0 @@
import { ScriptCode } from './ScriptCode';
import type { IScriptCode } from './IScriptCode';
export interface ScriptCodeFactory {
(
...args: ConstructorParameters<typeof ScriptCode>
): IScriptCode;
}
export const createScriptCode: ScriptCodeFactory = (...args) => new ScriptCode(...args);