From 629c94dddc3161b3bbe5f6434e4f0e72e19ded22 Mon Sep 17 00:00:00 2001 From: Hendrik Belitz Date: Wed, 9 Jul 2025 14:54:29 +0200 Subject: [PATCH] Tenant creation, update and config functions --- .../__tests__/tenant-admin-service.test.ts | 150 +++++++++++++++--- .../server/services/tenant-admin-service.ts | 83 ++++++++-- 2 files changed, 197 insertions(+), 36 deletions(-) diff --git a/src/lib/server/services/__tests__/tenant-admin-service.test.ts b/src/lib/server/services/__tests__/tenant-admin-service.test.ts index 09f242f..54520a5 100644 --- a/src/lib/server/services/__tests__/tenant-admin-service.test.ts +++ b/src/lib/server/services/__tests__/tenant-admin-service.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; // Mock the database module @@ -106,32 +107,6 @@ describe("TenantAdminService", () => { }); }); - describe("update", () => { - it("should update tenant data", async () => { - const tenantId = "tenant-123"; - const updateData = { - longName: "Updated Clinic Name", - description: "Updated description" - }; - - const mockConfig = { setConfig: vi.fn() }; - const mockUpdateBuilder = { - set: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis() - }; - - mockTenantConfig.create.mockResolvedValue(mockConfig); - mockCentralDb.update.mockReturnValue(mockUpdateBuilder); - - const service = await TenantAdminService.getTenantById(tenantId); - await service.update(updateData); - - expect(mockCentralDb.update).toHaveBeenCalled(); - expect(mockUpdateBuilder.set).toHaveBeenCalledWith(updateData); - expect(mockUpdateBuilder.where).toHaveBeenCalled(); - }); - }); - describe("getDb", () => { it("should return database connection", async () => { const tenantId = "tenant-123"; @@ -187,4 +162,127 @@ describe("TenantAdminService", () => { expect(config).toBe(mockConfig); }); }); + + describe("updateTenantData", () => { + it("should update tenant data successfully", async () => { + const tenantId = "tenant-123"; + const updateData = { + longName: "Updated Clinic Name", + description: "Updated description", + logo: Buffer.from("logo data") + }; + + const mockUpdatedTenant = { + id: tenantId, + shortName: "test-clinic", + ...updateData, + updatedAt: new Date() + }; + + const mockConfig = { setConfig: vi.fn() }; + const mockUpdateBuilder = { + set: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([mockUpdatedTenant]) + }; + + mockTenantConfig.create.mockResolvedValue(mockConfig); + mockCentralDb.update.mockReturnValue(mockUpdateBuilder); + + const service = await TenantAdminService.getTenantById(tenantId); + const result = await service.updateTenantData(updateData); + + expect(mockCentralDb.update).toHaveBeenCalled(); + expect(mockUpdateBuilder.set).toHaveBeenCalledWith({ + ...updateData, + updatedAt: expect.any(Date) + }); + expect(mockUpdateBuilder.where).toHaveBeenCalled(); + expect(result).toEqual(mockUpdatedTenant); + }); + + it("should throw NotFoundError when tenant not found", async () => { + const tenantId = "tenant-123"; + const updateData = { longName: "Updated Name" }; + + const mockConfig = { setConfig: vi.fn() }; + const mockUpdateBuilder = { + set: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + returning: vi.fn().mockResolvedValue([]) + }; + + mockTenantConfig.create.mockResolvedValue(mockConfig); + mockCentralDb.update.mockReturnValue(mockUpdateBuilder); + + const service = await TenantAdminService.getTenantById(tenantId); + + await expect(service.updateTenantData(updateData)).rejects.toThrow( + "Tenant with ID tenant-123 not found" + ); + }); + }); + + describe("updateTenantConfig", () => { + it("should update tenant configuration successfully", async () => { + const tenantId = "tenant-123"; + const configUpdates = { + brandColor: "#FF0000", + maxChannels: 10, + requireEmail: false + }; + + const mockConfig = { + setConfig: vi.fn().mockResolvedValue(undefined) + }; + + mockTenantConfig.create.mockResolvedValue(mockConfig); + + const service = await TenantAdminService.getTenantById(tenantId); + const result = await service.updateTenantConfig(configUpdates); + + expect(mockConfig.setConfig).toHaveBeenCalledTimes(3); + expect(mockConfig.setConfig).toHaveBeenCalledWith("brandColor", "#FF0000"); + expect(mockConfig.setConfig).toHaveBeenCalledWith("maxChannels", 10); + expect(mockConfig.setConfig).toHaveBeenCalledWith("requireEmail", false); + + expect(result).toEqual([ + { key: "brandColor", value: "#FF0000" }, + { key: "maxChannels", value: 10 }, + { key: "requireEmail", value: false } + ]); + }); + + it("should handle empty config updates", async () => { + const tenantId = "tenant-123"; + const configUpdates = {}; + + const mockConfig = { + setConfig: vi.fn().mockResolvedValue(undefined) + }; + + mockTenantConfig.create.mockResolvedValue(mockConfig); + + const service = await TenantAdminService.getTenantById(tenantId); + const result = await service.updateTenantConfig(configUpdates); + + expect(mockConfig.setConfig).not.toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + it("should propagate config errors", async () => { + const tenantId = "tenant-123"; + const configUpdates = { brandColor: "#FF0000" }; + + const mockConfig = { + setConfig: vi.fn().mockRejectedValue(new Error("Config error")) + }; + + mockTenantConfig.create.mockResolvedValue(mockConfig); + + const service = await TenantAdminService.getTenantById(tenantId); + + await expect(service.updateTenantConfig(configUpdates)).rejects.toThrow("Config error"); + }); + }); }); diff --git a/src/lib/server/services/tenant-admin-service.ts b/src/lib/server/services/tenant-admin-service.ts index c22a3b5..545a2c3 100644 --- a/src/lib/server/services/tenant-admin-service.ts +++ b/src/lib/server/services/tenant-admin-service.ts @@ -7,7 +7,7 @@ import { env } from "$env/dynamic/private"; import { eq } from "drizzle-orm"; import logger from "$lib/logger"; import z from "zod/v4"; -import { ValidationError } from "../utils/errors"; +import { ValidationError, NotFoundError } from "../utils/errors"; if (!env.DATABASE_URL) throw new Error("DATABASE_URL is not set"); @@ -89,6 +89,11 @@ export class TenantAdminService { } } + /** + * Create a tenant admin service by its ID + * @param id + * @returns new TenantAdminService + */ static async getTenantById(id: string) { const log = logger.setContext("TenantAdminService"); log.debug("Getting tenant by ID", { tenantId: id }); @@ -105,31 +110,89 @@ export class TenantAdminService { } } - async update(updateData: Partial>) { + /** + * Access the tenants configuration + */ + get configuration() { + return this.#config; + } + + /** + * Update tenant data (longName, shortName, description, logo) + */ + async updateTenantData( + updateData: Partial> + ) { const log = logger.setContext("TenantAdminService"); - log.debug("Updating tenant", { + log.debug("Updating tenant data", { tenantId: this.tenantId, updateFields: Object.keys(updateData) }); try { - await centralDb + const result = await centralDb .update(centralSchema.tenant) - .set(updateData) - .where(eq(centralSchema.tenant.id, this.tenantId)); + .set({ + ...updateData, + updatedAt: new Date() + }) + .where(eq(centralSchema.tenant.id, this.tenantId)) + .returning(); - log.debug("Tenant updated successfully", { + if (!result[0]) { + log.warn("Tenant update failed: Tenant not found", { tenantId: this.tenantId }); + throw new NotFoundError(`Tenant with ID ${this.tenantId} not found`); + } + + log.debug("Tenant data updated successfully", { tenantId: this.tenantId, updateFields: Object.keys(updateData) }); + + return result[0]; } catch (error) { - log.error("Failed to update tenant", { tenantId: this.tenantId, error: String(error) }); + if (error instanceof NotFoundError) throw error; + log.error("Failed to update tenant data", { + tenantId: this.tenantId, + error: String(error) + }); throw error; } } - get configuration() { - return this.#config; + /** + * Update tenant configuration entries using TenantConfig + */ + async updateTenantConfig(configUpdates: Record) { + const log = logger.setContext("TenantAdminService"); + log.debug("Updating tenant configuration", { + tenantId: this.tenantId, + configKeys: Object.keys(configUpdates) + }); + + try { + const results = []; + + for (const [key, value] of Object.entries(configUpdates)) { + await this.#config.setConfig(key, value); + results.push({ key, value }); + } + + log.debug("Tenant configuration updated successfully", { + tenantId: this.tenantId, + configKeys: Object.keys(configUpdates), + updatedCount: results.length + }); + + return results; + } catch (error) { + log.error("Failed to update tenant configuration", { + tenantId: this.tenantId, + configKeys: Object.keys(configUpdates), + error: String(error) + }); + throw error; + } } /**