Tenant creation, update and config functions

This commit is contained in:
Hendrik Belitz
2025-07-09 14:54:29 +02:00
parent 8ee744e3be
commit 629c94dddc
2 changed files with 197 additions and 36 deletions
@@ -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");
});
});
});
+73 -10
View File
@@ -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<Omit<centralSchema.InsertTenant, "id">>) {
/**
* Access the tenants configuration
*/
get configuration() {
return this.#config;
}
/**
* Update tenant data (longName, shortName, description, logo)
*/
async updateTenantData(
updateData: Partial<Pick<InsertTenant, "longName" | "shortName" | "description" | "logo">>
) {
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<string, boolean | number | string>) {
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;
}
}
/**