Release: CI/CD Pipeline & Architecture Updates #177

Merged
jason.woltje merged 173 commits from develop into main 2026-02-01 19:18:48 +00:00
19 changed files with 256 additions and 115 deletions
Showing only changes of commit c221b63d14 - Show all commits

View File

@@ -28,21 +28,32 @@ steps:
SKIP_ENV_VALIDATION: "true" SKIP_ENV_VALIDATION: "true"
commands: commands:
- *install_deps - *install_deps
- pnpm lint || true # Non-blocking while fixing legacy code - pnpm lint || true # Non-blocking while fixing legacy code
depends_on: depends_on:
- install - install
when: when:
- evaluate: 'CI_PIPELINE_EVENT != "pull_request" || CI_COMMIT_BRANCH != "main"' - evaluate: 'CI_PIPELINE_EVENT != "pull_request" || CI_COMMIT_BRANCH != "main"'
prisma-generate:
image: *node_image
environment:
SKIP_ENV_VALIDATION: "true"
commands:
- *install_deps
- pnpm --filter "@mosaic/api" prisma:generate
depends_on:
- install
typecheck: typecheck:
image: *node_image image: *node_image
environment: environment:
SKIP_ENV_VALIDATION: "true" SKIP_ENV_VALIDATION: "true"
commands: commands:
- *install_deps - *install_deps
- pnpm --filter "@mosaic/api" prisma:generate
- pnpm typecheck - pnpm typecheck
depends_on: depends_on:
- install - prisma-generate
test: test:
image: *node_image image: *node_image
@@ -50,9 +61,10 @@ steps:
SKIP_ENV_VALIDATION: "true" SKIP_ENV_VALIDATION: "true"
commands: commands:
- *install_deps - *install_deps
- pnpm test -- --run || true # Non-blocking while fixing legacy tests - pnpm --filter "@mosaic/api" prisma:generate
- pnpm test || true # Non-blocking while fixing legacy tests
depends_on: depends_on:
- install - prisma-generate
build: build:
image: *node_image image: *node_image
@@ -61,7 +73,9 @@ steps:
NODE_ENV: "production" NODE_ENV: "production"
commands: commands:
- *install_deps - *install_deps
- pnpm --filter "@mosaic/api" prisma:generate
- pnpm build - pnpm build
depends_on: depends_on:
- typecheck # Only block on critical checks - typecheck # Only block on critical checks
- security-audit - security-audit
- prisma-generate

View File

@@ -36,6 +36,7 @@
"@nestjs/websockets": "^11.1.12", "@nestjs/websockets": "^11.1.12",
"@prisma/client": "^6.19.2", "@prisma/client": "^6.19.2",
"@types/marked": "^6.0.0", "@types/marked": "^6.0.0",
"@types/multer": "^2.0.0",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"better-auth": "^1.4.17", "better-auth": "^1.4.17",
@@ -66,7 +67,6 @@
"@types/archiver": "^7.0.0", "@types/archiver": "^7.0.0",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/highlight.js": "^10.1.0", "@types/highlight.js": "^10.1.0",
"@types/multer": "^2.0.0",
"@types/node": "^22.13.4", "@types/node": "^22.13.4",
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.16.0",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",

View File

@@ -100,8 +100,8 @@ export class ActivityLoggingInterceptor implements NestInterceptor {
entityType, entityType,
entityId, entityId,
details, details,
ipAddress: ip, ipAddress: ip ?? undefined,
userAgent, userAgent: userAgent ?? undefined,
}); });
} catch (error) { } catch (error) {
// Don't fail the request if activity logging fails // Don't fail the request if activity logging fails

View File

@@ -10,8 +10,8 @@ export interface CreateActivityLogInput {
entityType: EntityType; entityType: EntityType;
entityId: string; entityId: string;
details?: Prisma.JsonValue; details?: Prisma.JsonValue;
ipAddress?: string; ipAddress?: string | undefined;
userAgent?: string; userAgent?: string | undefined;
} }
/** /**

View File

@@ -21,8 +21,12 @@ export class AuthGuard implements CanActivate {
throw new UnauthorizedException("Invalid or expired session"); throw new UnauthorizedException("Invalid or expired session");
} }
// Attach user to request // Attach user to request (with type assertion for session data structure)
request.user = sessionData.user; const user = sessionData.user as unknown as AuthenticatedRequest["user"];
if (!user) {
throw new UnauthorizedException("Invalid user data in session");
}
request.user = user;
request.session = sessionData.session; request.session = sessionData.session;
return true; return true;

View File

@@ -93,10 +93,10 @@ export class QueryBuilder {
return {}; return {};
} }
const filter: Prisma.JsonObject = {}; const filter: Record<string, unknown> = {};
if (from || to) { if (from || to) {
const dateFilter: Prisma.JsonObject = {}; const dateFilter: Record<string, unknown> = {};
if (from) { if (from) {
dateFilter.gte = from; dateFilter.gte = from;
} }
@@ -106,7 +106,7 @@ export class QueryBuilder {
filter[field] = dateFilter; filter[field] = dateFilter;
} }
return filter; return filter as Prisma.JsonObject;
} }
/** /**

View File

@@ -21,8 +21,10 @@ export class DomainsService {
const domain = await this.prisma.domain.create({ const domain = await this.prisma.domain.create({
data: { data: {
name: createDomainDto.name, name: createDomainDto.name,
description: createDomainDto.description, slug: createDomainDto.slug,
color: createDomainDto.color, description: createDomainDto.description ?? null,
color: createDomainDto.color ?? null,
icon: createDomainDto.icon ?? null,
workspace: { workspace: {
connect: { id: workspaceId }, connect: { id: workspaceId },
}, },
@@ -53,9 +55,11 @@ export class DomainsService {
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Build where clause // Build where clause
const where: Prisma.DomainWhereInput = { const where: Prisma.DomainWhereInput = query.workspaceId
workspaceId: query.workspaceId, ? {
}; workspaceId: query.workspaceId,
}
: {};
// Add search filter if provided // Add search filter if provided
if (query.search) { if (query.search) {
@@ -130,12 +134,24 @@ export class DomainsService {
throw new NotFoundException(`Domain with ID ${id} not found`); throw new NotFoundException(`Domain with ID ${id} not found`);
} }
// Build update data, only including defined fields
const updateData: Prisma.DomainUpdateInput = {};
if (updateDomainDto.name !== undefined) updateData.name = updateDomainDto.name;
if (updateDomainDto.slug !== undefined) updateData.slug = updateDomainDto.slug;
if (updateDomainDto.description !== undefined)
updateData.description = updateDomainDto.description;
if (updateDomainDto.color !== undefined) updateData.color = updateDomainDto.color;
if (updateDomainDto.icon !== undefined) updateData.icon = updateDomainDto.icon;
if (updateDomainDto.metadata !== undefined) {
updateData.metadata = updateDomainDto.metadata as unknown as Prisma.InputJsonValue;
}
const domain = await this.prisma.domain.update({ const domain = await this.prisma.domain.update({
where: { where: {
id, id,
workspaceId, workspaceId,
}, },
data: updateDomainDto, data: updateData,
include: { include: {
_count: { _count: {
select: { tasks: true, events: true, projects: true, ideas: true }, select: { tasks: true, events: true, projects: true, ideas: true },

View File

@@ -18,19 +18,23 @@ export class EventsService {
* Create a new event * Create a new event
*/ */
async create(workspaceId: string, userId: string, createEventDto: CreateEventDto) { async create(workspaceId: string, userId: string, createEventDto: CreateEventDto) {
const projectConnection = createEventDto.projectId
? { connect: { id: createEventDto.projectId } }
: undefined;
const data: Prisma.EventCreateInput = { const data: Prisma.EventCreateInput = {
title: createEventDto.title, title: createEventDto.title,
description: createEventDto.description, description: createEventDto.description ?? null,
startTime: createEventDto.startTime, startTime: createEventDto.startTime,
endTime: createEventDto.endTime, endTime: createEventDto.endTime ?? null,
location: createEventDto.location, location: createEventDto.location ?? null,
workspace: { connect: { id: workspaceId } }, workspace: { connect: { id: workspaceId } },
creator: { connect: { id: userId } }, creator: { connect: { id: userId } },
allDay: createEventDto.allDay ?? false, allDay: createEventDto.allDay ?? false,
metadata: createEventDto.metadata metadata: createEventDto.metadata
? (createEventDto.metadata as unknown as Prisma.InputJsonValue) ? (createEventDto.metadata as unknown as Prisma.InputJsonValue)
: {}, : {},
project: createEventDto.projectId ? { connect: { id: createEventDto.projectId } } : undefined, ...(projectConnection && { project: projectConnection }),
}; };
const event = await this.prisma.event.create({ const event = await this.prisma.event.create({
@@ -62,9 +66,11 @@ export class EventsService {
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Build where clause // Build where clause
const where: Prisma.EventWhereInput = { const where: Prisma.EventWhereInput = query.workspaceId
workspaceId: query.workspaceId, ? {
}; workspaceId: query.workspaceId,
}
: {};
if (query.projectId) { if (query.projectId) {
where.projectId = query.projectId; where.projectId = query.projectId;
@@ -155,12 +161,32 @@ export class EventsService {
throw new NotFoundException(`Event with ID ${id} not found`); throw new NotFoundException(`Event with ID ${id} not found`);
} }
// Build update data, only including defined fields (excluding projectId)
const updateData: Prisma.EventUpdateInput = {};
if (updateEventDto.title !== undefined) updateData.title = updateEventDto.title;
if (updateEventDto.description !== undefined)
updateData.description = updateEventDto.description;
if (updateEventDto.startTime !== undefined) updateData.startTime = updateEventDto.startTime;
if (updateEventDto.endTime !== undefined) updateData.endTime = updateEventDto.endTime;
if (updateEventDto.allDay !== undefined) updateData.allDay = updateEventDto.allDay;
if (updateEventDto.location !== undefined) updateData.location = updateEventDto.location;
if (updateEventDto.recurrence !== undefined) {
updateData.recurrence = updateEventDto.recurrence as unknown as Prisma.InputJsonValue;
}
if (updateEventDto.metadata !== undefined) {
updateData.metadata = updateEventDto.metadata as unknown as Prisma.InputJsonValue;
}
// Handle project relation separately
if (updateEventDto.projectId !== undefined) {
updateData.project = { connect: { id: updateEventDto.projectId } };
}
const event = await this.prisma.event.update({ const event = await this.prisma.event.update({
where: { where: {
id, id,
workspaceId, workspaceId,
}, },
data: updateEventDto, data: updateData,
include: { include: {
creator: { creator: {
select: { id: true, name: true, email: true }, select: { id: true, name: true, email: true },

View File

@@ -19,10 +19,18 @@ export class IdeasService {
* Create a new idea * Create a new idea
*/ */
async create(workspaceId: string, userId: string, createIdeaDto: CreateIdeaDto) { async create(workspaceId: string, userId: string, createIdeaDto: CreateIdeaDto) {
const domainConnection = createIdeaDto.domainId
? { connect: { id: createIdeaDto.domainId } }
: undefined;
const projectConnection = createIdeaDto.projectId
? { connect: { id: createIdeaDto.projectId } }
: undefined;
const data: Prisma.IdeaCreateInput = { const data: Prisma.IdeaCreateInput = {
title: createIdeaDto.title, title: createIdeaDto.title ?? null,
content: createIdeaDto.content, content: createIdeaDto.content,
category: createIdeaDto.category, category: createIdeaDto.category ?? null,
workspace: { connect: { id: workspaceId } }, workspace: { connect: { id: workspaceId } },
creator: { connect: { id: userId } }, creator: { connect: { id: userId } },
status: createIdeaDto.status ?? IdeaStatus.CAPTURED, status: createIdeaDto.status ?? IdeaStatus.CAPTURED,
@@ -31,8 +39,8 @@ export class IdeasService {
metadata: createIdeaDto.metadata metadata: createIdeaDto.metadata
? (createIdeaDto.metadata as unknown as Prisma.InputJsonValue) ? (createIdeaDto.metadata as unknown as Prisma.InputJsonValue)
: {}, : {},
domain: createIdeaDto.domainId ? { connect: { id: createIdeaDto.domainId } } : undefined, ...(domainConnection && { domain: domainConnection }),
project: createIdeaDto.projectId ? { connect: { id: createIdeaDto.projectId } } : undefined, ...(projectConnection && { project: projectConnection }),
}; };
const idea = await this.prisma.idea.create({ const idea = await this.prisma.idea.create({
@@ -67,7 +75,7 @@ export class IdeasService {
workspace: { connect: { id: workspaceId } }, workspace: { connect: { id: workspaceId } },
creator: { connect: { id: userId } }, creator: { connect: { id: userId } },
content: captureIdeaDto.content, content: captureIdeaDto.content,
title: captureIdeaDto.title, title: captureIdeaDto.title ?? null,
status: IdeaStatus.CAPTURED, status: IdeaStatus.CAPTURED,
priority: "MEDIUM", priority: "MEDIUM",
tags: [], tags: [],
@@ -101,9 +109,11 @@ export class IdeasService {
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Build where clause // Build where clause
const where: Prisma.IdeaWhereInput = { const where: Prisma.IdeaWhereInput = query.workspaceId
workspaceId: query.workspaceId, ? {
}; workspaceId: query.workspaceId,
}
: {};
if (query.status) { if (query.status) {
where.status = query.status; where.status = query.status;
@@ -206,12 +216,31 @@ export class IdeasService {
throw new NotFoundException(`Idea with ID ${id} not found`); throw new NotFoundException(`Idea with ID ${id} not found`);
} }
// Build update data, only including defined fields (excluding domainId and projectId)
const updateData: Prisma.IdeaUpdateInput = {};
if (updateIdeaDto.title !== undefined) updateData.title = updateIdeaDto.title;
if (updateIdeaDto.content !== undefined) updateData.content = updateIdeaDto.content;
if (updateIdeaDto.status !== undefined) updateData.status = updateIdeaDto.status;
if (updateIdeaDto.priority !== undefined) updateData.priority = updateIdeaDto.priority;
if (updateIdeaDto.category !== undefined) updateData.category = updateIdeaDto.category;
if (updateIdeaDto.tags !== undefined) updateData.tags = updateIdeaDto.tags;
if (updateIdeaDto.metadata !== undefined) {
updateData.metadata = updateIdeaDto.metadata as unknown as Prisma.InputJsonValue;
}
// Handle domain and project relations separately
if (updateIdeaDto.domainId !== undefined) {
updateData.domain = { connect: { id: updateIdeaDto.domainId } };
}
if (updateIdeaDto.projectId !== undefined) {
updateData.project = { connect: { id: updateIdeaDto.projectId } };
}
const idea = await this.prisma.idea.update({ const idea = await this.prisma.idea.update({
where: { where: {
id, id,
workspaceId, workspaceId,
}, },
data: updateIdeaDto, data: updateData,
include: { include: {
creator: { creator: {
select: { id: true, name: true, email: true }, select: { id: true, name: true, email: true },

View File

@@ -1,4 +1,5 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { PrismaService } from "../../prisma/prisma.service"; import { PrismaService } from "../../prisma/prisma.service";
import { LinkResolutionService } from "./link-resolution.service"; import { LinkResolutionService } from "./link-resolution.service";
import { parseWikiLinks } from "../utils/wiki-link-parser"; import { parseWikiLinks } from "../utils/wiki-link-parser";
@@ -81,29 +82,24 @@ export class LinkSyncService {
}); });
// Resolve all parsed links // Resolve all parsed links
const linkCreations: { const linkCreations: Prisma.KnowledgeLinkUncheckedCreateInput[] = [];
sourceId: string;
targetId: string | null;
linkText: string;
displayText: string;
positionStart: number;
positionEnd: number;
resolved: boolean;
}[] = [];
for (const link of parsedLinks) { for (const link of parsedLinks) {
const targetId = await this.linkResolver.resolveLink(workspaceId, link.target); const targetId = await this.linkResolver.resolveLink(workspaceId, link.target);
// Create link record (resolved or unresolved) // Only create link record if targetId was resolved
linkCreations.push({ // (Schema requires targetId to be non-null)
sourceId: entryId, if (targetId) {
targetId: targetId ?? null, linkCreations.push({
linkText: link.target, sourceId: entryId,
displayText: link.displayText, targetId,
positionStart: link.start, linkText: link.target,
positionEnd: link.end, displayText: link.displayText,
resolved: targetId !== null, positionStart: link.start,
}); positionEnd: link.end,
resolved: true,
});
}
} }
// Determine which existing links to keep/delete // Determine which existing links to keep/delete

View File

@@ -105,7 +105,7 @@ export class LayoutsService {
workspaceId, workspaceId,
userId, userId,
isDefault: createLayoutDto.isDefault ?? false, isDefault: createLayoutDto.isDefault ?? false,
layout: createLayoutDto.layout as unknown as Prisma.JsonValue, layout: createLayoutDto.layout as unknown as Prisma.InputJsonValue,
}, },
}); });
}); });
@@ -141,13 +141,21 @@ export class LayoutsService {
}); });
} }
// Build update data, handling layout field separately
const updateData: Prisma.UserLayoutUpdateInput = {};
if (updateLayoutDto.name !== undefined) updateData.name = updateLayoutDto.name;
if (updateLayoutDto.isDefault !== undefined) updateData.isDefault = updateLayoutDto.isDefault;
if (updateLayoutDto.layout !== undefined) {
updateData.layout = updateLayoutDto.layout as unknown as Prisma.InputJsonValue;
}
return tx.userLayout.update({ return tx.userLayout.update({
where: { where: {
id, id,
workspaceId, workspaceId,
userId, userId,
}, },
data: updateLayoutDto, data: updateData,
}); });
}); });
} }

View File

@@ -72,7 +72,7 @@ export class PromptFormatterService {
if (options?.includeDateTime === true) { if (options?.includeDateTime === true) {
const now = new Date(); const now = new Date();
const dateStr: string = context?.currentDate ?? now.toISOString().split("T")[0]; const dateStr: string = context?.currentDate ?? now.toISOString().split("T")[0] ?? "";
const timeStr: string = context?.currentTime ?? now.toTimeString().slice(0, 5); const timeStr: string = context?.currentTime ?? now.toTimeString().slice(0, 5);
const tzStr: string = context?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone; const tzStr: string = context?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
parts.push(`Current date: ${dateStr}, Time: ${timeStr} (${tzStr})`); parts.push(`Current date: ${dateStr}, Time: ${timeStr} (${tzStr})`);

View File

@@ -64,7 +64,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
SELECT current_database(), version() SELECT current_database(), version()
`; `;
if (result.length > 0) { if (result.length > 0 && result[0]) {
const dbVersion = result[0].version.split(" ")[0]; const dbVersion = result[0].version.split(" ")[0];
return { return {
connected: true, connected: true,

View File

@@ -21,10 +21,10 @@ export class ProjectsService {
async create(workspaceId: string, userId: string, createProjectDto: CreateProjectDto) { async create(workspaceId: string, userId: string, createProjectDto: CreateProjectDto) {
const data: Prisma.ProjectCreateInput = { const data: Prisma.ProjectCreateInput = {
name: createProjectDto.name, name: createProjectDto.name,
description: createProjectDto.description, description: createProjectDto.description ?? null,
color: createProjectDto.color, color: createProjectDto.color ?? null,
startDate: createProjectDto.startDate, startDate: createProjectDto.startDate ?? null,
endDate: createProjectDto.endDate, endDate: createProjectDto.endDate ?? null,
workspace: { connect: { id: workspaceId } }, workspace: { connect: { id: workspaceId } },
creator: { connect: { id: userId } }, creator: { connect: { id: userId } },
status: createProjectDto.status ?? ProjectStatus.PLANNING, status: createProjectDto.status ?? ProjectStatus.PLANNING,
@@ -62,9 +62,11 @@ export class ProjectsService {
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Build where clause // Build where clause
const where: Prisma.ProjectWhereInput = { const where: Prisma.ProjectWhereInput = query.workspaceId
workspaceId: query.workspaceId, ? {
}; workspaceId: query.workspaceId,
}
: {};
if (query.status) { if (query.status) {
where.status = query.status; where.status = query.status;
@@ -175,12 +177,25 @@ export class ProjectsService {
throw new NotFoundException(`Project with ID ${id} not found`); throw new NotFoundException(`Project with ID ${id} not found`);
} }
// Build update data, only including defined fields
const updateData: Prisma.ProjectUpdateInput = {};
if (updateProjectDto.name !== undefined) updateData.name = updateProjectDto.name;
if (updateProjectDto.description !== undefined)
updateData.description = updateProjectDto.description;
if (updateProjectDto.status !== undefined) updateData.status = updateProjectDto.status;
if (updateProjectDto.startDate !== undefined) updateData.startDate = updateProjectDto.startDate;
if (updateProjectDto.endDate !== undefined) updateData.endDate = updateProjectDto.endDate;
if (updateProjectDto.color !== undefined) updateData.color = updateProjectDto.color;
if (updateProjectDto.metadata !== undefined) {
updateData.metadata = updateProjectDto.metadata as unknown as Prisma.InputJsonValue;
}
const project = await this.prisma.project.update({ const project = await this.prisma.project.update({
where: { where: {
id, id,
workspaceId, workspaceId,
}, },
data: updateProjectDto, data: updateData,
include: { include: {
creator: { creator: {
select: { id: true, name: true, email: true }, select: { id: true, name: true, email: true },

View File

@@ -19,7 +19,22 @@ export class TasksService {
* Create a new task * Create a new task
*/ */
async create(workspaceId: string, userId: string, createTaskDto: CreateTaskDto) { async create(workspaceId: string, userId: string, createTaskDto: CreateTaskDto) {
const data: Prisma.TaskCreateInput = Object.assign({}, createTaskDto, { const assigneeConnection = createTaskDto.assigneeId
? { connect: { id: createTaskDto.assigneeId } }
: undefined;
const projectConnection = createTaskDto.projectId
? { connect: { id: createTaskDto.projectId } }
: undefined;
const parentConnection = createTaskDto.parentId
? { connect: { id: createTaskDto.parentId } }
: undefined;
const data: Prisma.TaskCreateInput = {
title: createTaskDto.title,
description: createTaskDto.description ?? null,
dueDate: createTaskDto.dueDate ?? null,
workspace: { connect: { id: workspaceId } }, workspace: { connect: { id: workspaceId } },
creator: { connect: { id: userId } }, creator: { connect: { id: userId } },
status: createTaskDto.status ?? TaskStatus.NOT_STARTED, status: createTaskDto.status ?? TaskStatus.NOT_STARTED,
@@ -28,12 +43,10 @@ export class TasksService {
metadata: createTaskDto.metadata metadata: createTaskDto.metadata
? (createTaskDto.metadata as unknown as Prisma.InputJsonValue) ? (createTaskDto.metadata as unknown as Prisma.InputJsonValue)
: {}, : {},
assignee: createTaskDto.assigneeId ...(assigneeConnection && { assignee: assigneeConnection }),
? { connect: { id: createTaskDto.assigneeId } } ...(projectConnection && { project: projectConnection }),
: undefined, ...(parentConnection && { parent: parentConnection }),
project: createTaskDto.projectId ? { connect: { id: createTaskDto.projectId } } : undefined, };
parent: createTaskDto.parentId ? { connect: { id: createTaskDto.parentId } } : undefined,
});
// Set completedAt if status is COMPLETED // Set completedAt if status is COMPLETED
if (data.status === TaskStatus.COMPLETED) { if (data.status === TaskStatus.COMPLETED) {
@@ -72,16 +85,18 @@ export class TasksService {
const skip = (page - 1) * limit; const skip = (page - 1) * limit;
// Build where clause // Build where clause
const where: Prisma.TaskWhereInput = { const where: Prisma.TaskWhereInput = query.workspaceId
workspaceId: query.workspaceId, ? {
}; workspaceId: query.workspaceId,
}
: {};
if (query.status) { if (query.status) {
where.status = query.status; where.status = Array.isArray(query.status) ? { in: query.status } : query.status;
} }
if (query.priority) { if (query.priority) {
where.priority = query.priority; where.priority = Array.isArray(query.priority) ? { in: query.priority } : query.priority;
} }
if (query.assigneeId) { if (query.assigneeId) {
@@ -190,23 +205,39 @@ export class TasksService {
throw new NotFoundException(`Task with ID ${id} not found`); throw new NotFoundException(`Task with ID ${id} not found`);
} }
// Build update data // Build update data - only include defined fields
const data: Prisma.TaskUpdateInput = { const data: Prisma.TaskUpdateInput = {};
title: updateTaskDto.title,
description: updateTaskDto.description, if (updateTaskDto.title !== undefined) {
status: updateTaskDto.status, data.title = updateTaskDto.title;
priority: updateTaskDto.priority, }
dueDate: updateTaskDto.dueDate, if (updateTaskDto.description !== undefined) {
sortOrder: updateTaskDto.sortOrder, data.description = updateTaskDto.description;
metadata: updateTaskDto.metadata }
? (updateTaskDto.metadata as unknown as Prisma.InputJsonValue) if (updateTaskDto.status !== undefined) {
: undefined, data.status = updateTaskDto.status;
assignee: updateTaskDto.assigneeId }
? { connect: { id: updateTaskDto.assigneeId } } if (updateTaskDto.priority !== undefined) {
: undefined, data.priority = updateTaskDto.priority;
project: updateTaskDto.projectId ? { connect: { id: updateTaskDto.projectId } } : undefined, }
parent: updateTaskDto.parentId ? { connect: { id: updateTaskDto.parentId } } : undefined, if (updateTaskDto.dueDate !== undefined) {
}; data.dueDate = updateTaskDto.dueDate;
}
if (updateTaskDto.sortOrder !== undefined) {
data.sortOrder = updateTaskDto.sortOrder;
}
if (updateTaskDto.metadata !== undefined) {
data.metadata = updateTaskDto.metadata as unknown as Prisma.InputJsonValue;
}
if (updateTaskDto.assigneeId !== undefined && updateTaskDto.assigneeId !== null) {
data.assignee = { connect: { id: updateTaskDto.assigneeId } };
}
if (updateTaskDto.projectId !== undefined && updateTaskDto.projectId !== null) {
data.project = { connect: { id: updateTaskDto.projectId } };
}
if (updateTaskDto.parentId !== undefined && updateTaskDto.parentId !== null) {
data.parent = { connect: { id: updateTaskDto.parentId } };
}
// Handle completedAt based on status changes // Handle completedAt based on status changes
if (updateTaskDto.status) { if (updateTaskDto.status) {

View File

@@ -27,14 +27,14 @@ export class ValkeyService implements OnModuleInit, OnModuleDestroy {
this.client = new Redis(valkeyUrl, { this.client = new Redis(valkeyUrl, {
maxRetriesPerRequest: 3, maxRetriesPerRequest: 3,
retryStrategy: (times) => { retryStrategy: (times: number) => {
const delay = Math.min(times * 50, 2000); const delay = Math.min(times * 50, 2000);
this.logger.warn( this.logger.warn(
`Valkey connection retry attempt ${times.toString()}, waiting ${delay.toString()}ms` `Valkey connection retry attempt ${times.toString()}, waiting ${delay.toString()}ms`
); );
return delay; return delay;
}, },
reconnectOnError: (err) => { reconnectOnError: (err: Error) => {
this.logger.error("Valkey connection error:", err.message); this.logger.error("Valkey connection error:", err.message);
return true; return true;
}, },
@@ -44,7 +44,7 @@ export class ValkeyService implements OnModuleInit, OnModuleDestroy {
this.logger.log("Valkey connected successfully"); this.logger.log("Valkey connected successfully");
}); });
this.client.on("error", (err) => { this.client.on("error", (err: Error) => {
this.logger.error("Valkey client error:", err.message); this.logger.error("Valkey client error:", err.message);
}); });

View File

@@ -52,19 +52,20 @@ export class WebSocketGateway implements OnGatewayConnection, OnGatewayDisconnec
* @param client - The authenticated socket client containing userId and workspaceId in data. * @param client - The authenticated socket client containing userId and workspaceId in data.
* @returns Promise that resolves when the client is joined to the workspace room or disconnected. * @returns Promise that resolves when the client is joined to the workspace room or disconnected.
*/ */
async handleConnection(client: AuthenticatedSocket): Promise<void> { async handleConnection(client: Socket): Promise<void> {
const { userId, workspaceId } = client.data; const authenticatedClient = client as AuthenticatedSocket;
const { userId, workspaceId } = authenticatedClient.data;
if (!userId || !workspaceId) { if (!userId || !workspaceId) {
this.logger.warn(`Client ${client.id} connected without authentication`); this.logger.warn(`Client ${authenticatedClient.id} connected without authentication`);
client.disconnect(); authenticatedClient.disconnect();
return; return;
} }
const room = this.getWorkspaceRoom(workspaceId); const room = this.getWorkspaceRoom(workspaceId);
await client.join(room); await authenticatedClient.join(room);
this.logger.log(`Client ${client.id} joined room ${room}`); this.logger.log(`Client ${authenticatedClient.id} joined room ${room}`);
} }
/** /**
@@ -72,13 +73,14 @@ export class WebSocketGateway implements OnGatewayConnection, OnGatewayDisconnec
* @param client - The socket client containing workspaceId in data. * @param client - The socket client containing workspaceId in data.
* @returns void * @returns void
*/ */
handleDisconnect(client: AuthenticatedSocket): void { handleDisconnect(client: Socket): void {
const { workspaceId } = client.data; const authenticatedClient = client as AuthenticatedSocket;
const { workspaceId } = authenticatedClient.data;
if (workspaceId) { if (workspaceId) {
const room = this.getWorkspaceRoom(workspaceId); const room = this.getWorkspaceRoom(workspaceId);
void client.leave(room); void authenticatedClient.leave(room);
this.logger.log(`Client ${client.id} left room ${room}`); this.logger.log(`Client ${authenticatedClient.id} left room ${room}`);
} }
} }

View File

@@ -178,7 +178,7 @@ export class WidgetDataService {
items.push( items.push(
...tasks ...tasks
.filter((task) => task.dueDate !== null) .filter((task): task is typeof task & { dueDate: Date } => task.dueDate !== null)
.map((task) => ({ .map((task) => ({
id: task.id, id: task.id,
title: task.title, title: task.title,

6
pnpm-lock.yaml generated
View File

@@ -77,6 +77,9 @@ importers:
'@types/marked': '@types/marked':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
'@types/multer':
specifier: ^2.0.0
version: 2.0.0
adm-zip: adm-zip:
specifier: ^0.5.16 specifier: ^0.5.16
version: 0.5.16 version: 0.5.16
@@ -162,9 +165,6 @@ importers:
'@types/highlight.js': '@types/highlight.js':
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0 version: 10.1.0
'@types/multer':
specifier: ^2.0.0
version: 2.0.0
'@types/node': '@types/node':
specifier: ^22.13.4 specifier: ^22.13.4
version: 22.19.7 version: 22.19.7