feat(03-01): add ModuleRegistry NestJS module with CRUD and activation endpoints
- ModuleRegistryService with findAll, findBySlug, findActiveForTenant, activate/deactivate, seedModule - ModuleRegistryController with GET /modules, GET /modules/active, POST activate/deactivate - ModuleGuard + @UseModule() decorator for tenant-scoped module access control - ActivateModuleDto with UUID validation - Registered ModuleRegistryModule in AppModule imports
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
/**
|
||||
* Service managing the module registry and per-tenant activations.
|
||||
*
|
||||
* Modules are registered centrally; tenants activate/deactivate them
|
||||
* independently via TenantModuleActivation records (per D-07, D-08).
|
||||
*/
|
||||
@Injectable()
|
||||
export class ModuleRegistryService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Returns all registered modules.
|
||||
*/
|
||||
async findAll() {
|
||||
return this.prisma.module.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module by its unique slug.
|
||||
*/
|
||||
async findBySlug(slug: string) {
|
||||
return this.prisma.module.findUnique({
|
||||
where: { slug },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all active modules for a given tenant.
|
||||
*/
|
||||
async findActiveForTenant(tenantId: string) {
|
||||
const activations = await this.prisma.tenantModuleActivation.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
isActive: true,
|
||||
},
|
||||
include: {
|
||||
module: true,
|
||||
},
|
||||
});
|
||||
|
||||
return activations.map((a) => a.module);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates a module for a tenant (upsert: creates or re-activates).
|
||||
* Per D-08: dynamic activation without restart.
|
||||
*/
|
||||
async activateForTenant(tenantId: string, moduleId: string) {
|
||||
// Verify module exists
|
||||
const moduleExists = await this.prisma.module.findUnique({
|
||||
where: { id: moduleId },
|
||||
});
|
||||
if (!moduleExists) {
|
||||
throw new NotFoundException(`Module with id '${moduleId}' not found`);
|
||||
}
|
||||
|
||||
return this.prisma.tenantModuleActivation.upsert({
|
||||
where: {
|
||||
tenantId_moduleId: {
|
||||
tenantId,
|
||||
moduleId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
isActive: true,
|
||||
activatedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
tenantId,
|
||||
moduleId,
|
||||
isActive: true,
|
||||
},
|
||||
include: {
|
||||
module: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivates a module for a tenant (soft-delete: sets isActive=false).
|
||||
* Does not remove the activation record, preserving audit trail.
|
||||
*/
|
||||
async deactivateForTenant(tenantId: string, moduleId: string) {
|
||||
// Verify module exists
|
||||
const moduleExists = await this.prisma.module.findUnique({
|
||||
where: { id: moduleId },
|
||||
});
|
||||
if (!moduleExists) {
|
||||
throw new NotFoundException(`Module with id '${moduleId}' not found`);
|
||||
}
|
||||
|
||||
// Check if activation record exists
|
||||
const activation = await this.prisma.tenantModuleActivation.findUnique({
|
||||
where: {
|
||||
tenantId_moduleId: {
|
||||
tenantId,
|
||||
moduleId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!activation) {
|
||||
throw new NotFoundException(
|
||||
`Module '${moduleId}' is not activated for this tenant`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.prisma.tenantModuleActivation.update({
|
||||
where: {
|
||||
tenantId_moduleId: {
|
||||
tenantId,
|
||||
moduleId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
isActive: false,
|
||||
},
|
||||
include: {
|
||||
module: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a module (by slug) is active for a given tenant.
|
||||
* Used by ModuleGuard to gate access to module-specific endpoints.
|
||||
*/
|
||||
async isModuleActive(tenantId: string, moduleSlug: string): Promise<boolean> {
|
||||
const module = await this.prisma.module.findUnique({
|
||||
where: { slug: moduleSlug },
|
||||
});
|
||||
|
||||
if (!module) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activation = await this.prisma.tenantModuleActivation.findUnique({
|
||||
where: {
|
||||
tenantId_moduleId: {
|
||||
tenantId,
|
||||
moduleId: module.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return activation?.isActive === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers or updates a module in the registry by slug (upsert).
|
||||
* Used during application startup to seed built-in modules.
|
||||
*/
|
||||
async seedModule(manifest: {
|
||||
slug: string;
|
||||
name: string;
|
||||
version: string;
|
||||
category: string;
|
||||
description: Record<string, string>;
|
||||
icon?: string;
|
||||
isSystem?: boolean;
|
||||
}) {
|
||||
return this.prisma.module.upsert({
|
||||
where: { slug: manifest.slug },
|
||||
update: {
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
category: manifest.category,
|
||||
description: manifest.description,
|
||||
icon: manifest.icon ?? null,
|
||||
isSystem: manifest.isSystem ?? false,
|
||||
},
|
||||
create: {
|
||||
slug: manifest.slug,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
category: manifest.category,
|
||||
description: manifest.description,
|
||||
icon: manifest.icon ?? null,
|
||||
isSystem: manifest.isSystem ?? false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user