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:
11
src/domain/Executables/Category/Category.ts
Normal file
11
src/domain/Executables/Category/Category.ts
Normal 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>;
|
||||
}
|
||||
59
src/domain/Executables/Category/CollectionCategory.ts
Normal file
59
src/domain/Executables/Category/CollectionCategory.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import type { Category } from './Category';
|
||||
import type { Script } from '../Script/Script';
|
||||
|
||||
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<Category>;
|
||||
|
||||
public readonly scripts: ReadonlyArray<Script>;
|
||||
|
||||
constructor(parameters: CategoryInitParameters) {
|
||||
super(parameters.id);
|
||||
validateParameters(parameters);
|
||||
this.name = parameters.name;
|
||||
this.docs = parameters.docs;
|
||||
this.subCategories = parameters.subcategories;
|
||||
this.scripts = parameters.scripts;
|
||||
}
|
||||
|
||||
public includes(script: Script): boolean {
|
||||
return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id);
|
||||
}
|
||||
|
||||
public getAllScriptsRecursively(): readonly Script[] {
|
||||
if (!this.allSubScripts) {
|
||||
this.allSubScripts = parseScriptsRecursively(this);
|
||||
}
|
||||
return this.allSubScripts;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CategoryInitParameters {
|
||||
readonly id: number;
|
||||
readonly name: string;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly subcategories: ReadonlyArray<Category>;
|
||||
readonly scripts: ReadonlyArray<Script>;
|
||||
}
|
||||
|
||||
function parseScriptsRecursively(category: Category): ReadonlyArray<Script> {
|
||||
return [
|
||||
...category.scripts,
|
||||
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
];
|
||||
}
|
||||
|
||||
function validateParameters(parameters: CategoryInitParameters) {
|
||||
if (!parameters.name) {
|
||||
throw new Error('missing name');
|
||||
}
|
||||
if (parameters.subcategories.length === 0 && parameters.scripts.length === 0) {
|
||||
throw new Error('A category must have at least one sub-category or script');
|
||||
}
|
||||
}
|
||||
3
src/domain/Executables/Documentable.ts
Normal file
3
src/domain/Executables/Documentable.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Documentable {
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
}
|
||||
6
src/domain/Executables/Executable.ts
Normal file
6
src/domain/Executables/Executable.ts
Normal 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> {
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ScriptCode } from './ScriptCode';
|
||||
|
||||
export class DistinctReversibleScriptCode implements ScriptCode {
|
||||
constructor(
|
||||
public readonly execute: string,
|
||||
public readonly revert: string | undefined,
|
||||
) {
|
||||
validateCode(execute);
|
||||
validateRevertCode(revert, execute);
|
||||
}
|
||||
}
|
||||
|
||||
function validateRevertCode(revertCode: string | undefined, execute: string) {
|
||||
if (!revertCode) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateCode(revertCode);
|
||||
if (execute === revertCode) {
|
||||
throw new Error('Code itself and its reverting code cannot be the same');
|
||||
}
|
||||
} catch (err) {
|
||||
throw Error(`(revert): ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCode(code: string): void {
|
||||
if (code.length === 0) {
|
||||
throw new Error('missing code');
|
||||
}
|
||||
}
|
||||
4
src/domain/Executables/Script/Code/ScriptCode.ts
Normal file
4
src/domain/Executables/Script/Code/ScriptCode.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface ScriptCode {
|
||||
readonly execute: string;
|
||||
readonly revert?: string;
|
||||
}
|
||||
12
src/domain/Executables/Script/Code/ScriptCodeFactory.ts
Normal file
12
src/domain/Executables/Script/Code/ScriptCodeFactory.ts
Normal 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);
|
||||
40
src/domain/Executables/Script/CollectionScript.ts
Normal file
40
src/domain/Executables/Script/CollectionScript.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { RecommendationLevel } from './RecommendationLevel';
|
||||
import type { Script } from './Script';
|
||||
import type { ScriptCode } from './Code/ScriptCode';
|
||||
|
||||
export class CollectionScript extends BaseEntity<string> implements Script {
|
||||
public readonly name: string;
|
||||
|
||||
public readonly code: ScriptCode;
|
||||
|
||||
public readonly docs: ReadonlyArray<string>;
|
||||
|
||||
public readonly level?: RecommendationLevel;
|
||||
|
||||
constructor(parameters: ScriptInitParameters) {
|
||||
super(parameters.name);
|
||||
this.name = parameters.name;
|
||||
this.code = parameters.code;
|
||||
this.docs = parameters.docs;
|
||||
this.level = parameters.level;
|
||||
validateLevel(parameters.level);
|
||||
}
|
||||
|
||||
public canRevert(): boolean {
|
||||
return Boolean(this.code.revert);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ScriptInitParameters {
|
||||
readonly name: string;
|
||||
readonly code: ScriptCode;
|
||||
readonly docs: ReadonlyArray<string>;
|
||||
readonly level?: RecommendationLevel;
|
||||
}
|
||||
|
||||
function validateLevel(level?: RecommendationLevel) {
|
||||
if (level !== undefined && !(level in RecommendationLevel)) {
|
||||
throw new Error(`invalid level: ${level}`);
|
||||
}
|
||||
}
|
||||
4
src/domain/Executables/Script/RecommendationLevel.ts
Normal file
4
src/domain/Executables/Script/RecommendationLevel.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum RecommendationLevel {
|
||||
Standard = 0,
|
||||
Strict = 1,
|
||||
}
|
||||
11
src/domain/Executables/Script/Script.ts
Normal file
11
src/domain/Executables/Script/Script.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user