feat(#37-41): Add domains, ideas, relationships, agents, widgets schema
Schema additions for issues #37-41: New models: - Domain (#37): Life domains (work, marriage, homelab, etc.) - Idea (#38): Brain dumps with pgvector embeddings - Relationship (#39): Generic entity linking (blocks, depends_on) - Agent (#40): ClawdBot agent tracking with metrics - AgentSession (#40): Conversation session tracking - WidgetDefinition (#41): HUD widget registry - UserLayout (#41): Per-user dashboard configuration Updated models: - Task, Event, Project: Added domainId foreign key - User, Workspace: Added new relations New enums: - IdeaStatus: CAPTURED, PROCESSING, ACTIONABLE, ARCHIVED, DISCARDED - RelationshipType: BLOCKS, BLOCKED_BY, DEPENDS_ON, etc. - AgentStatus: IDLE, WORKING, WAITING, ERROR, TERMINATED - EntityType: Added IDEA, DOMAIN Migration: 20260129182803_add_domains_ideas_agents_widgets
This commit is contained in:
348
apps/api/src/activity/dto/create-activity-log.dto.spec.ts
Normal file
348
apps/api/src/activity/dto/create-activity-log.dto.spec.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { validate } from "class-validator";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { CreateActivityLogDto } from "./create-activity-log.dto";
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
|
||||
describe("CreateActivityLogDto", () => {
|
||||
describe("required fields validation", () => {
|
||||
it("should pass with all required fields valid", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail when workspaceId is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const workspaceIdError = errors.find((e) => e.property === "workspaceId");
|
||||
expect(workspaceIdError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when userId is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const userIdError = errors.find((e) => e.property === "userId");
|
||||
expect(userIdError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when action is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const actionError = errors.find((e) => e.property === "action");
|
||||
expect(actionError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when entityType is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const entityTypeError = errors.find((e) => e.property === "entityType");
|
||||
expect(entityTypeError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when entityId is missing", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const entityIdError = errors.find((e) => e.property === "entityId");
|
||||
expect(entityIdError).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("UUID validation", () => {
|
||||
it("should fail with invalid workspaceId UUID", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "invalid-uuid",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("workspaceId");
|
||||
});
|
||||
|
||||
it("should fail with invalid userId UUID", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "not-a-uuid",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const userIdError = errors.find((e) => e.property === "userId");
|
||||
expect(userIdError).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail with invalid entityId UUID", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "bad-entity-id",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const entityIdError = errors.find((e) => e.property === "entityId");
|
||||
expect(entityIdError).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("enum validation", () => {
|
||||
it("should pass with all valid ActivityAction values", async () => {
|
||||
const actions = Object.values(ActivityAction);
|
||||
|
||||
for (const action of actions) {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail with invalid action value", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: "INVALID_ACTION",
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const actionError = errors.find((e) => e.property === "action");
|
||||
expect(actionError?.constraints?.isEnum).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass with all valid EntityType values", async () => {
|
||||
const entityTypes = Object.values(EntityType);
|
||||
|
||||
for (const entityType of entityTypes) {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fail with invalid entityType value", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: "INVALID_TYPE",
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const entityTypeError = errors.find((e) => e.property === "entityType");
|
||||
expect(entityTypeError?.constraints?.isEnum).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("optional fields validation", () => {
|
||||
it("should pass with valid details object", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
details: {
|
||||
field: "status",
|
||||
oldValue: "TODO",
|
||||
newValue: "IN_PROGRESS",
|
||||
},
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with non-object details", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
details: "not an object",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const detailsError = errors.find((e) => e.property === "details");
|
||||
expect(detailsError?.constraints?.isObject).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass with valid ipAddress", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
ipAddress: "192.168.1.1",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should pass with valid IPv6 address", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
ipAddress: "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail when ipAddress exceeds max length", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
ipAddress: "a".repeat(46),
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const ipError = errors.find((e) => e.property === "ipAddress");
|
||||
expect(ipError?.constraints?.maxLength).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass with valid userAgent", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail when userAgent exceeds max length", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
userAgent: "a".repeat(501),
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const userAgentError = errors.find((e) => e.property === "userAgent");
|
||||
expect(userAgentError?.constraints?.maxLength).toBeDefined();
|
||||
});
|
||||
|
||||
it("should pass when optional fields are not provided", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.CREATED,
|
||||
entityType: EntityType.TASK,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("complete validation", () => {
|
||||
it("should pass with all fields valid", async () => {
|
||||
const dto = plainToInstance(CreateActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.PROJECT,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
details: {
|
||||
changes: ["status", "priority"],
|
||||
metadata: { source: "web-app" },
|
||||
},
|
||||
ipAddress: "10.0.0.1",
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
43
apps/api/src/activity/dto/create-activity-log.dto.ts
Normal file
43
apps/api/src/activity/dto/create-activity-log.dto.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
import {
|
||||
IsUUID,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsObject,
|
||||
IsString,
|
||||
MaxLength,
|
||||
} from "class-validator";
|
||||
|
||||
/**
|
||||
* DTO for creating a new activity log entry
|
||||
*/
|
||||
export class CreateActivityLogDto {
|
||||
@IsUUID("4", { message: "workspaceId must be a valid UUID" })
|
||||
workspaceId!: string;
|
||||
|
||||
@IsUUID("4", { message: "userId must be a valid UUID" })
|
||||
userId!: string;
|
||||
|
||||
@IsEnum(ActivityAction, { message: "action must be a valid ActivityAction" })
|
||||
action!: ActivityAction;
|
||||
|
||||
@IsEnum(EntityType, { message: "entityType must be a valid EntityType" })
|
||||
entityType!: EntityType;
|
||||
|
||||
@IsUUID("4", { message: "entityId must be a valid UUID" })
|
||||
entityId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject({ message: "details must be an object" })
|
||||
details?: Record<string, unknown>;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "ipAddress must be a string" })
|
||||
@MaxLength(45, { message: "ipAddress must not exceed 45 characters" })
|
||||
ipAddress?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString({ message: "userAgent must be a string" })
|
||||
@MaxLength(500, { message: "userAgent must not exceed 500 characters" })
|
||||
userAgent?: string;
|
||||
}
|
||||
2
apps/api/src/activity/dto/index.ts
Normal file
2
apps/api/src/activity/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./create-activity-log.dto";
|
||||
export * from "./query-activity-log.dto";
|
||||
254
apps/api/src/activity/dto/query-activity-log.dto.spec.ts
Normal file
254
apps/api/src/activity/dto/query-activity-log.dto.spec.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { validate } from "class-validator";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { QueryActivityLogDto } from "./query-activity-log.dto";
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
|
||||
describe("QueryActivityLogDto", () => {
|
||||
describe("workspaceId validation", () => {
|
||||
it("should pass with valid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "invalid-uuid",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("workspaceId");
|
||||
expect(errors[0].constraints?.isUuid).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when workspaceId is missing", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const workspaceIdError = errors.find((e) => e.property === "workspaceId");
|
||||
expect(workspaceIdError).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("userId validation", () => {
|
||||
it("should pass with valid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "not-a-uuid",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("userId");
|
||||
});
|
||||
|
||||
it("should pass when userId is not provided (optional)", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("action validation", () => {
|
||||
it("should pass with valid ActivityAction", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
action: ActivityAction.CREATED,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid action value", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
action: "INVALID_ACTION",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("action");
|
||||
});
|
||||
|
||||
it("should pass when action is not provided (optional)", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("entityType validation", () => {
|
||||
it("should pass with valid EntityType", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
entityType: EntityType.TASK,
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid entityType value", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
entityType: "INVALID_TYPE",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("entityType");
|
||||
});
|
||||
});
|
||||
|
||||
describe("entityId validation", () => {
|
||||
it("should pass with valid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid UUID", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
entityId: "invalid-entity-id",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("entityId");
|
||||
});
|
||||
});
|
||||
|
||||
describe("date validation", () => {
|
||||
it("should pass with valid ISO date strings", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
startDate: "2024-01-01T00:00:00.000Z",
|
||||
endDate: "2024-01-31T23:59:59.999Z",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should fail with invalid date format", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
startDate: "not-a-date",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0].property).toBe("startDate");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pagination validation", () => {
|
||||
it("should pass with valid page and limit", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
page: "1",
|
||||
limit: "50",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.page).toBe(1);
|
||||
expect(dto.limit).toBe(50);
|
||||
});
|
||||
|
||||
it("should fail when page is less than 1", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
page: "0",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const pageError = errors.find((e) => e.property === "page");
|
||||
expect(pageError?.constraints?.min).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when limit exceeds 100", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
limit: "101",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const limitError = errors.find((e) => e.property === "limit");
|
||||
expect(limitError?.constraints?.max).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when page is not an integer", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
page: "1.5",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const pageError = errors.find((e) => e.property === "page");
|
||||
expect(pageError?.constraints?.isInt).toBeDefined();
|
||||
});
|
||||
|
||||
it("should fail when limit is not an integer", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
limit: "50.5",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
const limitError = errors.find((e) => e.property === "limit");
|
||||
expect(limitError?.constraints?.isInt).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("multiple filters", () => {
|
||||
it("should pass with all valid filters combined", async () => {
|
||||
const dto = plainToInstance(QueryActivityLogDto, {
|
||||
workspaceId: "550e8400-e29b-41d4-a716-446655440000",
|
||||
userId: "550e8400-e29b-41d4-a716-446655440001",
|
||||
action: ActivityAction.UPDATED,
|
||||
entityType: EntityType.PROJECT,
|
||||
entityId: "550e8400-e29b-41d4-a716-446655440002",
|
||||
startDate: "2024-01-01T00:00:00.000Z",
|
||||
endDate: "2024-01-31T23:59:59.999Z",
|
||||
page: "2",
|
||||
limit: "25",
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
56
apps/api/src/activity/dto/query-activity-log.dto.ts
Normal file
56
apps/api/src/activity/dto/query-activity-log.dto.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { ActivityAction, EntityType } from "@prisma/client";
|
||||
import {
|
||||
IsUUID,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsInt,
|
||||
Min,
|
||||
Max,
|
||||
IsDateString,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
/**
|
||||
* DTO for querying activity logs with filters and pagination
|
||||
*/
|
||||
export class QueryActivityLogDto {
|
||||
@IsUUID("4", { message: "workspaceId must be a valid UUID" })
|
||||
workspaceId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID("4", { message: "userId must be a valid UUID" })
|
||||
userId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(ActivityAction, { message: "action must be a valid ActivityAction" })
|
||||
action?: ActivityAction;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(EntityType, { message: "entityType must be a valid EntityType" })
|
||||
entityType?: EntityType;
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID("4", { message: "entityId must be a valid UUID" })
|
||||
entityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString({}, { message: "startDate must be a valid ISO 8601 date string" })
|
||||
startDate?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString({}, { message: "endDate must be a valid ISO 8601 date string" })
|
||||
endDate?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt({ message: "page must be an integer" })
|
||||
@Min(1, { message: "page must be at least 1" })
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt({ message: "limit must be an integer" })
|
||||
@Min(1, { message: "limit must be at least 1" })
|
||||
@Max(100, { message: "limit must not exceed 100" })
|
||||
limit?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user