feat: Phase 3 – Modul-System (Manifest, ZIP-Installation, Lifecycle, Prozess-Manager)

This commit is contained in:
MPM Dev
2026-10-07 15:37:09 +02:00
parent e87dc1f836
commit 4bbca21fd6
31 changed files with 2055 additions and 19 deletions

View File

@@ -0,0 +1,137 @@
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import type { ModuleManifest, ModuleRecord, ModuleStatus } from './manifest.types';
interface ModuleRow {
id: string;
module_id: string;
name: string;
slug: string;
version: string;
description: string;
author: string;
path: string;
status: ModuleStatus;
internal_port: number;
healthcheck_url: string;
enabled: boolean;
created_at: Date;
updated_at: Date;
}
const MODULE_COLUMNS = `id, module_id, name, slug, version, description, author, path,
status, internal_port, healthcheck_url, enabled, created_at, updated_at`;
/**
* Modul-Repository (Infrastructure): Datenbankzugriffe für die Modul-Registry.
* Keine Business-Logik – nur parametrisierten Datenzugriff.
*/
@Injectable()
export class ModuleRepository {
constructor(private readonly database: DatabaseService) {}
async list(): Promise<ModuleRecord[]> {
const result = await this.database.query<ModuleRow>(
`SELECT ${MODULE_COLUMNS} FROM modules ORDER BY created_at ASC`,
);
return result.rows.map((row) => this.mapRow(row));
}
async findByModuleId(moduleId: string): Promise<ModuleRecord | null> {
const result = await this.database.query<ModuleRow>(
`SELECT ${MODULE_COLUMNS} FROM modules WHERE module_id = $1`,
[moduleId],
);
return result.rows[0] ? this.mapRow(result.rows[0]) : null;
}
async findBySlug(slug: string): Promise<ModuleRecord | null> {
const result = await this.database.query<ModuleRow>(
`SELECT ${MODULE_COLUMNS} FROM modules WHERE slug = $1`,
[slug],
);
return result.rows[0] ? this.mapRow(result.rows[0]) : null;
}
async findByPort(port: number): Promise<ModuleRecord | null> {
const result = await this.database.query<ModuleRow>(
`SELECT ${MODULE_COLUMNS} FROM modules WHERE internal_port = $1`,
[port],
);
return result.rows[0] ? this.mapRow(result.rows[0]) : null;
}
async findById(id: string): Promise<ModuleRecord | null> {
const result = await this.database.query<ModuleRow>(
`SELECT ${MODULE_COLUMNS} FROM modules WHERE id = $1`,
[id],
);
return result.rows[0] ? this.mapRow(result.rows[0]) : null;
}
async findByStatus(statuses: readonly ModuleStatus[]): Promise<ModuleRecord[]> {
const result = await this.database.query<ModuleRow>(
`SELECT ${MODULE_COLUMNS} FROM modules WHERE status = ANY($1::text[])`,
[statuses],
);
return result.rows.map((row) => this.mapRow(row));
}
async create(manifest: ModuleManifest, directory: string): Promise<ModuleRecord> {
const result = await this.database.query<ModuleRow>(
`INSERT INTO modules
(module_id, name, slug, version, description, author, path, status, internal_port, healthcheck_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'INSTALLED', $8, $9)
RETURNING ${MODULE_COLUMNS}`,
[
manifest.id,
manifest.name,
manifest.slug,
manifest.version,
manifest.description,
manifest.author,
directory,
manifest.port,
manifest.healthcheck,
],
);
return this.mapRow(result.rows[0]);
}
async updateStatus(id: string, status: ModuleStatus): Promise<void> {
await this.database.query(
'UPDATE modules SET status = $2, updated_at = now() WHERE id = $1',
[id, status],
);
}
async updateEnabled(id: string, enabled: boolean): Promise<void> {
await this.database.query(
'UPDATE modules SET enabled = $2, updated_at = now() WHERE id = $1',
[id, enabled],
);
}
async delete(id: string): Promise<void> {
await this.database.query('DELETE FROM modules WHERE id = $1', [id]);
}
private mapRow(row: ModuleRow): ModuleRecord {
return {
id: row.id,
moduleId: row.module_id,
name: row.name,
slug: row.slug,
version: row.version,
description: row.description,
author: row.author,
path: row.path,
status: row.status,
internalPort: row.internal_port,
healthcheckUrl: row.healthcheck_url,
enabled: row.enabled,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
}