From 8c24c1e267be2906fb18840cfae4b9684d9c22d9 Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 19 Jun 2026 12:33:55 +0200 Subject: [PATCH] feat(03-01): add Module SDK package and Prisma module registry schema - Create @tessera/module-sdk with TesseraModule, ModuleRoute, ModuleManifest, ModuleCategory types - Add Module and TenantModuleActivation Prisma models with tenant-scoped unique constraint - Apply migration add-module-registry to PostgreSQL - Framework-agnostic ComponentType for lazy-loaded module UIs --- .../migration.sql | 38 +++++++++ apps/api/prisma/schema.prisma | 26 +++++++ packages/module-sdk/package.json | 13 ++++ packages/module-sdk/src/index.ts | 14 ++++ packages/module-sdk/src/types.ts | 78 +++++++++++++++++++ packages/module-sdk/tsconfig.json | 8 ++ pnpm-lock.yaml | 6 ++ 7 files changed, 183 insertions(+) create mode 100644 apps/api/prisma/migrations/20260619103242_add_module_registry/migration.sql create mode 100644 packages/module-sdk/package.json create mode 100644 packages/module-sdk/src/index.ts create mode 100644 packages/module-sdk/src/types.ts create mode 100644 packages/module-sdk/tsconfig.json diff --git a/apps/api/prisma/migrations/20260619103242_add_module_registry/migration.sql b/apps/api/prisma/migrations/20260619103242_add_module_registry/migration.sql new file mode 100644 index 0000000..97c5037 --- /dev/null +++ b/apps/api/prisma/migrations/20260619103242_add_module_registry/migration.sql @@ -0,0 +1,38 @@ +-- CreateTable +CREATE TABLE "Module" ( + "id" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "name" TEXT NOT NULL, + "version" TEXT NOT NULL, + "category" TEXT NOT NULL, + "description" JSONB NOT NULL, + "icon" TEXT, + "isSystem" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Module_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TenantModuleActivation" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL, + "moduleId" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "activatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "TenantModuleActivation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Module_slug_key" ON "Module"("slug"); + +-- CreateIndex +CREATE INDEX "TenantModuleActivation_tenantId_idx" ON "TenantModuleActivation"("tenantId"); + +-- CreateIndex +CREATE UNIQUE INDEX "TenantModuleActivation_tenantId_moduleId_key" ON "TenantModuleActivation"("tenantId", "moduleId"); + +-- AddForeignKey +ALTER TABLE "TenantModuleActivation" ADD CONSTRAINT "TenantModuleActivation_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "Module"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 1c4e3c9..1e8e461 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -84,3 +84,29 @@ model LdapFieldMapping { @@unique([ldapConfigId, ldapField]) } + +model Module { + id String @id @default(uuid()) + slug String @unique + name String + version String + category String + description Json + icon String? + isSystem Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + activations TenantModuleActivation[] +} + +model TenantModuleActivation { + id String @id @default(uuid()) + tenantId String + moduleId String + isActive Boolean @default(true) + activatedAt DateTime @default(now()) + module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade) + + @@unique([tenantId, moduleId]) + @@index([tenantId]) +} diff --git a/packages/module-sdk/package.json b/packages/module-sdk/package.json new file mode 100644 index 0000000..b70ac9f --- /dev/null +++ b/packages/module-sdk/package.json @@ -0,0 +1,13 @@ +{ + "name": "@tessera/module-sdk", + "version": "0.1.0", + "private": true, + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "type-check": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.5.0" + } +} diff --git a/packages/module-sdk/src/index.ts b/packages/module-sdk/src/index.ts new file mode 100644 index 0000000..8e0daef --- /dev/null +++ b/packages/module-sdk/src/index.ts @@ -0,0 +1,14 @@ +/** + * @tessera/module-sdk + * + * Public API for the Tessera module system. + * All module implementations should import types from this package. + */ +export type { + ComponentType, + ModuleCategory, + HttpMethod, + ModuleRoute, + TesseraModule, + ModuleManifest, +} from './types'; diff --git a/packages/module-sdk/src/types.ts b/packages/module-sdk/src/types.ts new file mode 100644 index 0000000..5d09be7 --- /dev/null +++ b/packages/module-sdk/src/types.ts @@ -0,0 +1,78 @@ +/** + * @tessera/module-sdk + * + * Defines the contract for Tessera modules. + * All modules must implement the TesseraModule interface. + */ + +/** + * Generic component type for lazy-loaded module UIs. + * Framework-agnostic — maps to React.ComponentType when consumed by the frontend. + */ +// biome-ignore lint: generic component type for framework independence +export type ComponentType = (props: any) => any; + +/** + * Category classification for modules. + * Modules are grouped by category in the marketplace and sidebar. + */ +export type ModuleCategory = + | 'infrastructure' + | 'monitoring' + | 'development' + | 'security' + | 'communication' + | 'workflow' + | 'analytics' + | 'integration' + | string; + +/** + * HTTP method for module-defined API routes. + */ +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + +/** + * Defines a single API route that a module exposes. + */ +export interface ModuleRoute { + /** HTTP method */ + method: HttpMethod; + /** Route path relative to the module's base path (e.g., '/check') */ + path: string; + /** Handler identifier (maps to a controller method) */ + handler: string; +} + +/** + * Full module definition including frontend components. + * Used by the frontend to render module UIs. + */ +export interface TesseraModule { + /** Unique module identifier (UUID) */ + id: string; + /** Human-readable module name */ + name: string; + /** Semantic version string */ + version: string; + /** URL-safe unique slug (e.g., 'domaincheck') */ + slug: string; + /** Module category for grouping */ + category: ModuleCategory; + /** i18n descriptions keyed by locale (e.g., { de: '...', en: '...' }) */ + description: Record; + /** Optional icon identifier (e.g., 'Globe', 'Shield') */ + icon?: string; + /** Optional API routes the module exposes */ + routes?: ModuleRoute[]; + /** Lazy-loaded main component for the module page */ + component: () => Promise<{ default: ComponentType }>; + /** Optional lazy-loaded card component for the dashboard */ + cardComponent?: () => Promise<{ default: ComponentType }>; +} + +/** + * Backend-only module metadata (no frontend component references). + * Used for module registration and API-side operations. + */ +export type ModuleManifest = Omit; diff --git a/packages/module-sdk/tsconfig.json b/packages/module-sdk/tsconfig.json new file mode 100644 index 0000000..792172f --- /dev/null +++ b/packages/module-sdk/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0d0912..2661953 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,6 +161,12 @@ importers: specifier: ^5.5.0 version: 5.9.3 + packages/module-sdk: + devDependencies: + typescript: + specifier: ^5.5.0 + version: 5.9.3 + packages/shared: devDependencies: typescript: