feat(#82): implement Personality Module
- Add Personality model to Prisma schema with FormalityLevel enum - Create migration and seed with 6 default personalities - Implement CRUD API with TDD approach (97.67% coverage) * PersonalitiesService: findAll, findOne, findDefault, create, update, remove * PersonalitiesController: REST endpoints with auth guards * Comprehensive test coverage (21 passing tests) - Add Personality types to shared package - Create frontend components: * PersonalitySelector: dropdown for choosing personality * PersonalityPreview: preview personality style and system prompt * PersonalityForm: create/edit personalities with validation * Settings page: manage personalities with CRUD operations - Integrate with Ollama API: * Support personalityId in chat endpoint * Auto-inject system prompt from personality * Fall back to default personality if not specified - API client for frontend personality management All tests passing with 97.67% backend coverage (exceeds 85% requirement)
This commit is contained in:
43
apps/api/src/personalities/dto/create-personality.dto.ts
Normal file
43
apps/api/src/personalities/dto/create-personality.dto.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { IsString, IsIn, IsOptional, IsBoolean, MinLength, MaxLength } from "class-validator";
|
||||
|
||||
export const FORMALITY_LEVELS = [
|
||||
"VERY_CASUAL",
|
||||
"CASUAL",
|
||||
"NEUTRAL",
|
||||
"FORMAL",
|
||||
"VERY_FORMAL",
|
||||
] as const;
|
||||
|
||||
export type FormalityLevel = (typeof FORMALITY_LEVELS)[number];
|
||||
|
||||
export class CreatePersonalityDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(100)
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(50)
|
||||
tone!: string;
|
||||
|
||||
@IsIn(FORMALITY_LEVELS)
|
||||
formalityLevel!: FormalityLevel;
|
||||
|
||||
@IsString()
|
||||
@MinLength(10)
|
||||
systemPromptTemplate!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDefault?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
2
apps/api/src/personalities/dto/index.ts
Normal file
2
apps/api/src/personalities/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./create-personality.dto";
|
||||
export * from "./update-personality.dto";
|
||||
4
apps/api/src/personalities/dto/update-personality.dto.ts
Normal file
4
apps/api/src/personalities/dto/update-personality.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from "@nestjs/mapped-types";
|
||||
import { CreatePersonalityDto } from "./create-personality.dto";
|
||||
|
||||
export class UpdatePersonalityDto extends PartialType(CreatePersonalityDto) {}
|
||||
15
apps/api/src/personalities/entities/personality.entity.ts
Normal file
15
apps/api/src/personalities/entities/personality.entity.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Personality as PrismaPersonality, FormalityLevel } from "@prisma/client";
|
||||
|
||||
export class Personality implements PrismaPersonality {
|
||||
id!: string;
|
||||
workspaceId!: string;
|
||||
name!: string;
|
||||
description!: string | null;
|
||||
tone!: string;
|
||||
formalityLevel!: FormalityLevel;
|
||||
systemPromptTemplate!: string;
|
||||
isDefault!: boolean;
|
||||
isActive!: boolean;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
}
|
||||
157
apps/api/src/personalities/personalities.controller.spec.ts
Normal file
157
apps/api/src/personalities/personalities.controller.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PersonalitiesController } from "./personalities.controller";
|
||||
import { PersonalitiesService } from "./personalities.service";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
||||
|
||||
describe("PersonalitiesController", () => {
|
||||
let controller: PersonalitiesController;
|
||||
let service: PersonalitiesService;
|
||||
|
||||
const mockWorkspaceId = "workspace-123";
|
||||
const mockUserId = "user-123";
|
||||
const mockPersonalityId = "personality-123";
|
||||
|
||||
const mockRequest = {
|
||||
user: { id: mockUserId },
|
||||
workspaceId: mockWorkspaceId,
|
||||
};
|
||||
|
||||
const mockPersonality = {
|
||||
id: mockPersonalityId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
name: "Professional",
|
||||
description: "Professional communication style",
|
||||
tone: "professional",
|
||||
formalityLevel: "FORMAL" as const,
|
||||
systemPromptTemplate: "You are a professional assistant.",
|
||||
isDefault: true,
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockPersonalitiesService = {
|
||||
findAll: vi.fn(),
|
||||
findOne: vi.fn(),
|
||||
findDefault: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
};
|
||||
|
||||
const mockAuthGuard = {
|
||||
canActivate: vi.fn().mockReturnValue(true),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PersonalitiesController],
|
||||
providers: [
|
||||
{
|
||||
provide: PersonalitiesService,
|
||||
useValue: mockPersonalitiesService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(AuthGuard)
|
||||
.useValue(mockAuthGuard)
|
||||
.compile();
|
||||
|
||||
controller = module.get<PersonalitiesController>(PersonalitiesController);
|
||||
service = module.get<PersonalitiesService>(PersonalitiesService);
|
||||
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should return all personalities", async () => {
|
||||
const mockPersonalities = [mockPersonality];
|
||||
mockPersonalitiesService.findAll.mockResolvedValue(mockPersonalities);
|
||||
|
||||
const result = await controller.findAll(mockRequest as any);
|
||||
|
||||
expect(result).toEqual(mockPersonalities);
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId, true);
|
||||
});
|
||||
|
||||
it("should filter by active status", async () => {
|
||||
mockPersonalitiesService.findAll.mockResolvedValue([mockPersonality]);
|
||||
|
||||
await controller.findAll(mockRequest as any, false);
|
||||
|
||||
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findOne", () => {
|
||||
it("should return a personality by id", async () => {
|
||||
mockPersonalitiesService.findOne.mockResolvedValue(mockPersonality);
|
||||
|
||||
const result = await controller.findOne(mockRequest as any, mockPersonalityId);
|
||||
|
||||
expect(result).toEqual(mockPersonality);
|
||||
expect(service.findOne).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findDefault", () => {
|
||||
it("should return the default personality", async () => {
|
||||
mockPersonalitiesService.findDefault.mockResolvedValue(mockPersonality);
|
||||
|
||||
const result = await controller.findDefault(mockRequest as any);
|
||||
|
||||
expect(result).toEqual(mockPersonality);
|
||||
expect(service.findDefault).toHaveBeenCalledWith(mockWorkspaceId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
const createDto: CreatePersonalityDto = {
|
||||
name: "Casual",
|
||||
description: "Casual communication style",
|
||||
tone: "casual",
|
||||
formalityLevel: "CASUAL",
|
||||
systemPromptTemplate: "You are a casual assistant.",
|
||||
};
|
||||
|
||||
it("should create a new personality", async () => {
|
||||
const newPersonality = { ...mockPersonality, ...createDto, id: "new-id" };
|
||||
mockPersonalitiesService.create.mockResolvedValue(newPersonality);
|
||||
|
||||
const result = await controller.create(mockRequest as any, createDto);
|
||||
|
||||
expect(result).toEqual(newPersonality);
|
||||
expect(service.create).toHaveBeenCalledWith(mockWorkspaceId, createDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
const updateDto: UpdatePersonalityDto = {
|
||||
description: "Updated description",
|
||||
};
|
||||
|
||||
it("should update a personality", async () => {
|
||||
const updatedPersonality = { ...mockPersonality, ...updateDto };
|
||||
mockPersonalitiesService.update.mockResolvedValue(updatedPersonality);
|
||||
|
||||
const result = await controller.update(mockRequest as any, mockPersonalityId, updateDto);
|
||||
|
||||
expect(result).toEqual(updatedPersonality);
|
||||
expect(service.update).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId, updateDto);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("should delete a personality", async () => {
|
||||
mockPersonalitiesService.remove.mockResolvedValue(mockPersonality);
|
||||
|
||||
const result = await controller.remove(mockRequest as any, mockPersonalityId);
|
||||
|
||||
expect(result).toEqual(mockPersonality);
|
||||
expect(service.remove).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
|
||||
});
|
||||
});
|
||||
});
|
||||
77
apps/api/src/personalities/personalities.controller.ts
Normal file
77
apps/api/src/personalities/personalities.controller.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Req,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { PersonalitiesService } from "./personalities.service";
|
||||
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
||||
import { Personality } from "./entities/personality.entity";
|
||||
|
||||
@Controller("personalities")
|
||||
@UseGuards(AuthGuard)
|
||||
export class PersonalitiesController {
|
||||
constructor(private readonly personalitiesService: PersonalitiesService) {}
|
||||
|
||||
/**
|
||||
* Get all personalities for the current workspace
|
||||
*/
|
||||
@Get()
|
||||
async findAll(
|
||||
@Req() req: any,
|
||||
@Query("isActive") isActive: boolean = true,
|
||||
): Promise<Personality[]> {
|
||||
return this.personalitiesService.findAll(req.workspaceId, isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default personality for the current workspace
|
||||
*/
|
||||
@Get("default")
|
||||
async findDefault(@Req() req: any): Promise<Personality> {
|
||||
return this.personalitiesService.findDefault(req.workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific personality by ID
|
||||
*/
|
||||
@Get(":id")
|
||||
async findOne(@Req() req: any, @Param("id") id: string): Promise<Personality> {
|
||||
return this.personalitiesService.findOne(req.workspaceId, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new personality
|
||||
*/
|
||||
@Post()
|
||||
async create(@Req() req: any, @Body() dto: CreatePersonalityDto): Promise<Personality> {
|
||||
return this.personalitiesService.create(req.workspaceId, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing personality
|
||||
*/
|
||||
@Put(":id")
|
||||
async update(
|
||||
@Req() req: any,
|
||||
@Param("id") id: string,
|
||||
@Body() dto: UpdatePersonalityDto,
|
||||
): Promise<Personality> {
|
||||
return this.personalitiesService.update(req.workspaceId, id, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a personality
|
||||
*/
|
||||
@Delete(":id")
|
||||
async remove(@Req() req: any, @Param("id") id: string): Promise<Personality> {
|
||||
return this.personalitiesService.remove(req.workspaceId, id);
|
||||
}
|
||||
}
|
||||
13
apps/api/src/personalities/personalities.module.ts
Normal file
13
apps/api/src/personalities/personalities.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { AuthModule } from "../auth/auth.module";
|
||||
import { PersonalitiesService } from "./personalities.service";
|
||||
import { PersonalitiesController } from "./personalities.controller";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AuthModule],
|
||||
controllers: [PersonalitiesController],
|
||||
providers: [PersonalitiesService],
|
||||
exports: [PersonalitiesService],
|
||||
})
|
||||
export class PersonalitiesModule {}
|
||||
255
apps/api/src/personalities/personalities.service.spec.ts
Normal file
255
apps/api/src/personalities/personalities.service.spec.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PersonalitiesService } from "./personalities.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
||||
import { NotFoundException, ConflictException } from "@nestjs/common";
|
||||
|
||||
describe("PersonalitiesService", () => {
|
||||
let service: PersonalitiesService;
|
||||
let prisma: PrismaService;
|
||||
|
||||
const mockWorkspaceId = "workspace-123";
|
||||
const mockUserId = "user-123";
|
||||
const mockPersonalityId = "personality-123";
|
||||
|
||||
const mockPersonality = {
|
||||
id: mockPersonalityId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
name: "Professional",
|
||||
description: "Professional communication style",
|
||||
tone: "professional",
|
||||
formalityLevel: "FORMAL" as const,
|
||||
systemPromptTemplate: "You are a professional assistant.",
|
||||
isDefault: true,
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockPrismaService = {
|
||||
personality: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn((callback) => callback(mockPrismaService)),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PersonalitiesService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PersonalitiesService>(PersonalitiesService);
|
||||
prisma = module.get<PrismaService>(PrismaService);
|
||||
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should return all personalities for a workspace", async () => {
|
||||
const mockPersonalities = [mockPersonality];
|
||||
mockPrismaService.personality.findMany.mockResolvedValue(mockPersonalities);
|
||||
|
||||
const result = await service.findAll(mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockPersonalities);
|
||||
expect(prisma.personality.findMany).toHaveBeenCalledWith({
|
||||
where: { workspaceId: mockWorkspaceId, isActive: true },
|
||||
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by active status", async () => {
|
||||
mockPrismaService.personality.findMany.mockResolvedValue([mockPersonality]);
|
||||
|
||||
await service.findAll(mockWorkspaceId, false);
|
||||
|
||||
expect(prisma.personality.findMany).toHaveBeenCalledWith({
|
||||
where: { workspaceId: mockWorkspaceId, isActive: false },
|
||||
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("findOne", () => {
|
||||
it("should return a personality by id", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||
|
||||
const result = await service.findOne(mockWorkspaceId, mockPersonalityId);
|
||||
|
||||
expect(result).toEqual(mockPersonality);
|
||||
expect(prisma.personality.findUnique).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: mockPersonalityId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when personality not found", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findDefault", () => {
|
||||
it("should return the default personality", async () => {
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);
|
||||
|
||||
const result = await service.findDefault(mockWorkspaceId);
|
||||
|
||||
expect(result).toEqual(mockPersonality);
|
||||
expect(prisma.personality.findFirst).toHaveBeenCalledWith({
|
||||
where: { workspaceId: mockWorkspaceId, isDefault: true, isActive: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when no default personality exists", async () => {
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findDefault(mockWorkspaceId)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
const createDto: CreatePersonalityDto = {
|
||||
name: "Casual",
|
||||
description: "Casual communication style",
|
||||
tone: "casual",
|
||||
formalityLevel: "CASUAL",
|
||||
systemPromptTemplate: "You are a casual assistant.",
|
||||
};
|
||||
|
||||
it("should create a new personality", async () => {
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.personality.create.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
...createDto,
|
||||
id: "new-personality-id",
|
||||
});
|
||||
|
||||
const result = await service.create(mockWorkspaceId, createDto);
|
||||
|
||||
expect(result).toMatchObject(createDto);
|
||||
expect(prisma.personality.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
workspaceId: mockWorkspaceId,
|
||||
...createDto,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw ConflictException when name already exists", async () => {
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);
|
||||
|
||||
await expect(service.create(mockWorkspaceId, createDto)).rejects.toThrow(ConflictException);
|
||||
});
|
||||
|
||||
it("should unset other defaults when creating a new default personality", async () => {
|
||||
const createDefaultDto = { ...createDto, isDefault: true };
|
||||
// First call to findFirst checks for name conflict (should be null)
|
||||
// Second call to findFirst finds the existing default personality
|
||||
mockPrismaService.personality.findFirst
|
||||
.mockResolvedValueOnce(null) // No name conflict
|
||||
.mockResolvedValueOnce(mockPersonality); // Existing default
|
||||
mockPrismaService.personality.update.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
isDefault: false,
|
||||
});
|
||||
mockPrismaService.personality.create.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
...createDefaultDto,
|
||||
});
|
||||
|
||||
await service.create(mockWorkspaceId, createDefaultDto);
|
||||
|
||||
expect(prisma.personality.update).toHaveBeenCalledWith({
|
||||
where: { id: mockPersonalityId },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
const updateDto: UpdatePersonalityDto = {
|
||||
description: "Updated description",
|
||||
tone: "updated",
|
||||
};
|
||||
|
||||
it("should update a personality", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue(null);
|
||||
mockPrismaService.personality.update.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
...updateDto,
|
||||
});
|
||||
|
||||
const result = await service.update(mockWorkspaceId, mockPersonalityId, updateDto);
|
||||
|
||||
expect(result).toMatchObject(updateDto);
|
||||
expect(prisma.personality.update).toHaveBeenCalledWith({
|
||||
where: { id: mockPersonalityId },
|
||||
data: updateDto,
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when personality not found", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.update(mockWorkspaceId, mockPersonalityId, updateDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it("should throw ConflictException when updating to existing name", async () => {
|
||||
const updateNameDto = { name: "Existing Name" };
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||
mockPrismaService.personality.findFirst.mockResolvedValue({
|
||||
...mockPersonality,
|
||||
id: "different-id",
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.update(mockWorkspaceId, mockPersonalityId, updateNameDto),
|
||||
).rejects.toThrow(ConflictException);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("should delete a personality", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||
mockPrismaService.personality.delete.mockResolvedValue(mockPersonality);
|
||||
|
||||
const result = await service.remove(mockWorkspaceId, mockPersonalityId);
|
||||
|
||||
expect(result).toEqual(mockPersonality);
|
||||
expect(prisma.personality.delete).toHaveBeenCalledWith({
|
||||
where: { id: mockPersonalityId },
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when personality not found", async () => {
|
||||
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(service.remove(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
156
apps/api/src/personalities/personalities.service.ts
Normal file
156
apps/api/src/personalities/personalities.service.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
||||
import { Personality } from "./entities/personality.entity";
|
||||
|
||||
@Injectable()
|
||||
export class PersonalitiesService {
|
||||
private readonly logger = new Logger(PersonalitiesService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Find all personalities for a workspace
|
||||
*/
|
||||
async findAll(workspaceId: string, isActive: boolean = true): Promise<Personality[]> {
|
||||
return this.prisma.personality.findMany({
|
||||
where: { workspaceId, isActive },
|
||||
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a specific personality by ID
|
||||
*/
|
||||
async findOne(workspaceId: string, id: string): Promise<Personality> {
|
||||
const personality = await this.prisma.personality.findUnique({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
if (!personality) {
|
||||
throw new NotFoundException(`Personality with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return personality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the default personality for a workspace
|
||||
*/
|
||||
async findDefault(workspaceId: string): Promise<Personality> {
|
||||
const personality = await this.prisma.personality.findFirst({
|
||||
where: { workspaceId, isDefault: true, isActive: true },
|
||||
});
|
||||
|
||||
if (!personality) {
|
||||
throw new NotFoundException(`No default personality found for workspace ${workspaceId}`);
|
||||
}
|
||||
|
||||
return personality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new personality
|
||||
*/
|
||||
async create(workspaceId: string, dto: CreatePersonalityDto): Promise<Personality> {
|
||||
// Check for duplicate name
|
||||
const existing = await this.prisma.personality.findFirst({
|
||||
where: { workspaceId, name: dto.name },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException(`Personality with name "${dto.name}" already exists`);
|
||||
}
|
||||
|
||||
// If creating a default personality, unset other defaults
|
||||
if (dto.isDefault) {
|
||||
await this.unsetOtherDefaults(workspaceId);
|
||||
}
|
||||
|
||||
const personality = await this.prisma.personality.create({
|
||||
data: {
|
||||
workspaceId,
|
||||
...dto,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`Created personality ${personality.id} for workspace ${workspaceId}`);
|
||||
return personality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing personality
|
||||
*/
|
||||
async update(
|
||||
workspaceId: string,
|
||||
id: string,
|
||||
dto: UpdatePersonalityDto,
|
||||
): Promise<Personality> {
|
||||
// Check existence
|
||||
await this.findOne(workspaceId, id);
|
||||
|
||||
// Check for duplicate name if updating name
|
||||
if (dto.name) {
|
||||
const existing = await this.prisma.personality.findFirst({
|
||||
where: { workspaceId, name: dto.name, id: { not: id } },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ConflictException(`Personality with name "${dto.name}" already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
// If setting as default, unset other defaults
|
||||
if (dto.isDefault === true) {
|
||||
await this.unsetOtherDefaults(workspaceId, id);
|
||||
}
|
||||
|
||||
const personality = await this.prisma.personality.update({
|
||||
where: { id },
|
||||
data: dto,
|
||||
});
|
||||
|
||||
this.logger.log(`Updated personality ${id} for workspace ${workspaceId}`);
|
||||
return personality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a personality
|
||||
*/
|
||||
async remove(workspaceId: string, id: string): Promise<Personality> {
|
||||
// Check existence
|
||||
await this.findOne(workspaceId, id);
|
||||
|
||||
const personality = await this.prisma.personality.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
this.logger.log(`Deleted personality ${id} from workspace ${workspaceId}`);
|
||||
return personality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the default flag on all other personalities in the workspace
|
||||
*/
|
||||
private async unsetOtherDefaults(workspaceId: string, excludeId?: string): Promise<void> {
|
||||
const currentDefault = await this.prisma.personality.findFirst({
|
||||
where: {
|
||||
workspaceId,
|
||||
isDefault: true,
|
||||
...(excludeId && { id: { not: excludeId } }),
|
||||
},
|
||||
});
|
||||
|
||||
if (currentDefault) {
|
||||
await this.prisma.personality.update({
|
||||
where: { id: currentDefault.id },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user