Release: CI/CD Pipeline & Architecture Updates #177
@@ -28,21 +28,32 @@ steps:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *install_deps
|
||||
- pnpm lint || true # Non-blocking while fixing legacy code
|
||||
- pnpm lint || true # Non-blocking while fixing legacy code
|
||||
depends_on:
|
||||
- install
|
||||
when:
|
||||
- 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:
|
||||
image: *node_image
|
||||
environment:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *install_deps
|
||||
- pnpm --filter "@mosaic/api" prisma:generate
|
||||
- pnpm typecheck
|
||||
depends_on:
|
||||
- install
|
||||
- prisma-generate
|
||||
|
||||
test:
|
||||
image: *node_image
|
||||
@@ -50,9 +61,10 @@ steps:
|
||||
SKIP_ENV_VALIDATION: "true"
|
||||
commands:
|
||||
- *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:
|
||||
- install
|
||||
- prisma-generate
|
||||
|
||||
build:
|
||||
image: *node_image
|
||||
@@ -61,7 +73,9 @@ steps:
|
||||
NODE_ENV: "production"
|
||||
commands:
|
||||
- *install_deps
|
||||
- pnpm --filter "@mosaic/api" prisma:generate
|
||||
- pnpm build
|
||||
depends_on:
|
||||
- typecheck # Only block on critical checks
|
||||
- typecheck # Only block on critical checks
|
||||
- security-audit
|
||||
- prisma-generate
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@nestjs/websockets": "^11.1.12",
|
||||
"@prisma/client": "^6.19.2",
|
||||
"@types/marked": "^6.0.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^7.0.1",
|
||||
"better-auth": "^1.4.17",
|
||||
@@ -66,7 +67,6 @@
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/highlight.js": "^10.1.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
|
||||
@@ -100,8 +100,8 @@ export class ActivityLoggingInterceptor implements NestInterceptor {
|
||||
entityType,
|
||||
entityId,
|
||||
details,
|
||||
ipAddress: ip,
|
||||
userAgent,
|
||||
ipAddress: ip ?? undefined,
|
||||
userAgent: userAgent ?? undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
// Don't fail the request if activity logging fails
|
||||
|
||||
@@ -10,8 +10,8 @@ export interface CreateActivityLogInput {
|
||||
entityType: EntityType;
|
||||
entityId: string;
|
||||
details?: Prisma.JsonValue;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
ipAddress?: string | undefined;
|
||||
userAgent?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,8 +21,12 @@ export class AuthGuard implements CanActivate {
|
||||
throw new UnauthorizedException("Invalid or expired session");
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
request.user = sessionData.user;
|
||||
// Attach user to request (with type assertion for session data structure)
|
||||
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;
|
||||
|
||||
return true;
|
||||
|
||||
@@ -93,10 +93,10 @@ export class QueryBuilder {
|
||||
return {};
|
||||
}
|
||||
|
||||
const filter: Prisma.JsonObject = {};
|
||||
const filter: Record<string, unknown> = {};
|
||||
|
||||
if (from || to) {
|
||||
const dateFilter: Prisma.JsonObject = {};
|
||||
const dateFilter: Record<string, unknown> = {};
|
||||
if (from) {
|
||||
dateFilter.gte = from;
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export class QueryBuilder {
|
||||
filter[field] = dateFilter;
|
||||
}
|
||||
|
||||
return filter;
|
||||
return filter as Prisma.JsonObject;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,8 +21,10 @@ export class DomainsService {
|
||||
const domain = await this.prisma.domain.create({
|
||||
data: {
|
||||
name: createDomainDto.name,
|
||||
description: createDomainDto.description,
|
||||
color: createDomainDto.color,
|
||||
slug: createDomainDto.slug,
|
||||
description: createDomainDto.description ?? null,
|
||||
color: createDomainDto.color ?? null,
|
||||
icon: createDomainDto.icon ?? null,
|
||||
workspace: {
|
||||
connect: { id: workspaceId },
|
||||
},
|
||||
@@ -53,9 +55,11 @@ export class DomainsService {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.DomainWhereInput = {
|
||||
workspaceId: query.workspaceId,
|
||||
};
|
||||
const where: Prisma.DomainWhereInput = query.workspaceId
|
||||
? {
|
||||
workspaceId: query.workspaceId,
|
||||
}
|
||||
: {};
|
||||
|
||||
// Add search filter if provided
|
||||
if (query.search) {
|
||||
@@ -130,12 +134,24 @@ export class DomainsService {
|
||||
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({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
data: updateDomainDto,
|
||||
data: updateData,
|
||||
include: {
|
||||
_count: {
|
||||
select: { tasks: true, events: true, projects: true, ideas: true },
|
||||
|
||||
@@ -18,19 +18,23 @@ export class EventsService {
|
||||
* Create a new event
|
||||
*/
|
||||
async create(workspaceId: string, userId: string, createEventDto: CreateEventDto) {
|
||||
const projectConnection = createEventDto.projectId
|
||||
? { connect: { id: createEventDto.projectId } }
|
||||
: undefined;
|
||||
|
||||
const data: Prisma.EventCreateInput = {
|
||||
title: createEventDto.title,
|
||||
description: createEventDto.description,
|
||||
description: createEventDto.description ?? null,
|
||||
startTime: createEventDto.startTime,
|
||||
endTime: createEventDto.endTime,
|
||||
location: createEventDto.location,
|
||||
endTime: createEventDto.endTime ?? null,
|
||||
location: createEventDto.location ?? null,
|
||||
workspace: { connect: { id: workspaceId } },
|
||||
creator: { connect: { id: userId } },
|
||||
allDay: createEventDto.allDay ?? false,
|
||||
metadata: createEventDto.metadata
|
||||
? (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({
|
||||
@@ -62,9 +66,11 @@ export class EventsService {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.EventWhereInput = {
|
||||
workspaceId: query.workspaceId,
|
||||
};
|
||||
const where: Prisma.EventWhereInput = query.workspaceId
|
||||
? {
|
||||
workspaceId: query.workspaceId,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (query.projectId) {
|
||||
where.projectId = query.projectId;
|
||||
@@ -155,12 +161,32 @@ export class EventsService {
|
||||
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({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
data: updateEventDto,
|
||||
data: updateData,
|
||||
include: {
|
||||
creator: {
|
||||
select: { id: true, name: true, email: true },
|
||||
|
||||
@@ -19,10 +19,18 @@ export class IdeasService {
|
||||
* Create a new idea
|
||||
*/
|
||||
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 = {
|
||||
title: createIdeaDto.title,
|
||||
title: createIdeaDto.title ?? null,
|
||||
content: createIdeaDto.content,
|
||||
category: createIdeaDto.category,
|
||||
category: createIdeaDto.category ?? null,
|
||||
workspace: { connect: { id: workspaceId } },
|
||||
creator: { connect: { id: userId } },
|
||||
status: createIdeaDto.status ?? IdeaStatus.CAPTURED,
|
||||
@@ -31,8 +39,8 @@ export class IdeasService {
|
||||
metadata: createIdeaDto.metadata
|
||||
? (createIdeaDto.metadata as unknown as Prisma.InputJsonValue)
|
||||
: {},
|
||||
domain: createIdeaDto.domainId ? { connect: { id: createIdeaDto.domainId } } : undefined,
|
||||
project: createIdeaDto.projectId ? { connect: { id: createIdeaDto.projectId } } : undefined,
|
||||
...(domainConnection && { domain: domainConnection }),
|
||||
...(projectConnection && { project: projectConnection }),
|
||||
};
|
||||
|
||||
const idea = await this.prisma.idea.create({
|
||||
@@ -67,7 +75,7 @@ export class IdeasService {
|
||||
workspace: { connect: { id: workspaceId } },
|
||||
creator: { connect: { id: userId } },
|
||||
content: captureIdeaDto.content,
|
||||
title: captureIdeaDto.title,
|
||||
title: captureIdeaDto.title ?? null,
|
||||
status: IdeaStatus.CAPTURED,
|
||||
priority: "MEDIUM",
|
||||
tags: [],
|
||||
@@ -101,9 +109,11 @@ export class IdeasService {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.IdeaWhereInput = {
|
||||
workspaceId: query.workspaceId,
|
||||
};
|
||||
const where: Prisma.IdeaWhereInput = query.workspaceId
|
||||
? {
|
||||
workspaceId: query.workspaceId,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (query.status) {
|
||||
where.status = query.status;
|
||||
@@ -206,12 +216,31 @@ export class IdeasService {
|
||||
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({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
data: updateIdeaDto,
|
||||
data: updateData,
|
||||
include: {
|
||||
creator: {
|
||||
select: { id: true, name: true, email: true },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "../../prisma/prisma.service";
|
||||
import { LinkResolutionService } from "./link-resolution.service";
|
||||
import { parseWikiLinks } from "../utils/wiki-link-parser";
|
||||
@@ -81,29 +82,24 @@ export class LinkSyncService {
|
||||
});
|
||||
|
||||
// Resolve all parsed links
|
||||
const linkCreations: {
|
||||
sourceId: string;
|
||||
targetId: string | null;
|
||||
linkText: string;
|
||||
displayText: string;
|
||||
positionStart: number;
|
||||
positionEnd: number;
|
||||
resolved: boolean;
|
||||
}[] = [];
|
||||
const linkCreations: Prisma.KnowledgeLinkUncheckedCreateInput[] = [];
|
||||
|
||||
for (const link of parsedLinks) {
|
||||
const targetId = await this.linkResolver.resolveLink(workspaceId, link.target);
|
||||
|
||||
// Create link record (resolved or unresolved)
|
||||
linkCreations.push({
|
||||
sourceId: entryId,
|
||||
targetId: targetId ?? null,
|
||||
linkText: link.target,
|
||||
displayText: link.displayText,
|
||||
positionStart: link.start,
|
||||
positionEnd: link.end,
|
||||
resolved: targetId !== null,
|
||||
});
|
||||
// Only create link record if targetId was resolved
|
||||
// (Schema requires targetId to be non-null)
|
||||
if (targetId) {
|
||||
linkCreations.push({
|
||||
sourceId: entryId,
|
||||
targetId,
|
||||
linkText: link.target,
|
||||
displayText: link.displayText,
|
||||
positionStart: link.start,
|
||||
positionEnd: link.end,
|
||||
resolved: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which existing links to keep/delete
|
||||
|
||||
@@ -105,7 +105,7 @@ export class LayoutsService {
|
||||
workspaceId,
|
||||
userId,
|
||||
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({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
userId,
|
||||
},
|
||||
data: updateLayoutDto,
|
||||
data: updateData,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export class PromptFormatterService {
|
||||
|
||||
if (options?.includeDateTime === true) {
|
||||
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 tzStr: string = context?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
parts.push(`Current date: ${dateStr}, Time: ${timeStr} (${tzStr})`);
|
||||
|
||||
@@ -64,7 +64,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
|
||||
SELECT current_database(), version()
|
||||
`;
|
||||
|
||||
if (result.length > 0) {
|
||||
if (result.length > 0 && result[0]) {
|
||||
const dbVersion = result[0].version.split(" ")[0];
|
||||
return {
|
||||
connected: true,
|
||||
|
||||
@@ -21,10 +21,10 @@ export class ProjectsService {
|
||||
async create(workspaceId: string, userId: string, createProjectDto: CreateProjectDto) {
|
||||
const data: Prisma.ProjectCreateInput = {
|
||||
name: createProjectDto.name,
|
||||
description: createProjectDto.description,
|
||||
color: createProjectDto.color,
|
||||
startDate: createProjectDto.startDate,
|
||||
endDate: createProjectDto.endDate,
|
||||
description: createProjectDto.description ?? null,
|
||||
color: createProjectDto.color ?? null,
|
||||
startDate: createProjectDto.startDate ?? null,
|
||||
endDate: createProjectDto.endDate ?? null,
|
||||
workspace: { connect: { id: workspaceId } },
|
||||
creator: { connect: { id: userId } },
|
||||
status: createProjectDto.status ?? ProjectStatus.PLANNING,
|
||||
@@ -62,9 +62,11 @@ export class ProjectsService {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.ProjectWhereInput = {
|
||||
workspaceId: query.workspaceId,
|
||||
};
|
||||
const where: Prisma.ProjectWhereInput = query.workspaceId
|
||||
? {
|
||||
workspaceId: query.workspaceId,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (query.status) {
|
||||
where.status = query.status;
|
||||
@@ -175,12 +177,25 @@ export class ProjectsService {
|
||||
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({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
data: updateProjectDto,
|
||||
data: updateData,
|
||||
include: {
|
||||
creator: {
|
||||
select: { id: true, name: true, email: true },
|
||||
|
||||
@@ -19,7 +19,22 @@ export class TasksService {
|
||||
* Create a new task
|
||||
*/
|
||||
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 } },
|
||||
creator: { connect: { id: userId } },
|
||||
status: createTaskDto.status ?? TaskStatus.NOT_STARTED,
|
||||
@@ -28,12 +43,10 @@ export class TasksService {
|
||||
metadata: createTaskDto.metadata
|
||||
? (createTaskDto.metadata as unknown as Prisma.InputJsonValue)
|
||||
: {},
|
||||
assignee: createTaskDto.assigneeId
|
||||
? { connect: { id: createTaskDto.assigneeId } }
|
||||
: undefined,
|
||||
project: createTaskDto.projectId ? { connect: { id: createTaskDto.projectId } } : undefined,
|
||||
parent: createTaskDto.parentId ? { connect: { id: createTaskDto.parentId } } : undefined,
|
||||
});
|
||||
...(assigneeConnection && { assignee: assigneeConnection }),
|
||||
...(projectConnection && { project: projectConnection }),
|
||||
...(parentConnection && { parent: parentConnection }),
|
||||
};
|
||||
|
||||
// Set completedAt if status is COMPLETED
|
||||
if (data.status === TaskStatus.COMPLETED) {
|
||||
@@ -72,16 +85,18 @@ export class TasksService {
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build where clause
|
||||
const where: Prisma.TaskWhereInput = {
|
||||
workspaceId: query.workspaceId,
|
||||
};
|
||||
const where: Prisma.TaskWhereInput = query.workspaceId
|
||||
? {
|
||||
workspaceId: query.workspaceId,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (query.status) {
|
||||
where.status = query.status;
|
||||
where.status = Array.isArray(query.status) ? { in: query.status } : query.status;
|
||||
}
|
||||
|
||||
if (query.priority) {
|
||||
where.priority = query.priority;
|
||||
where.priority = Array.isArray(query.priority) ? { in: query.priority } : query.priority;
|
||||
}
|
||||
|
||||
if (query.assigneeId) {
|
||||
@@ -190,23 +205,39 @@ export class TasksService {
|
||||
throw new NotFoundException(`Task with ID ${id} not found`);
|
||||
}
|
||||
|
||||
// Build update data
|
||||
const data: Prisma.TaskUpdateInput = {
|
||||
title: updateTaskDto.title,
|
||||
description: updateTaskDto.description,
|
||||
status: updateTaskDto.status,
|
||||
priority: updateTaskDto.priority,
|
||||
dueDate: updateTaskDto.dueDate,
|
||||
sortOrder: updateTaskDto.sortOrder,
|
||||
metadata: updateTaskDto.metadata
|
||||
? (updateTaskDto.metadata as unknown as Prisma.InputJsonValue)
|
||||
: undefined,
|
||||
assignee: updateTaskDto.assigneeId
|
||||
? { connect: { id: updateTaskDto.assigneeId } }
|
||||
: undefined,
|
||||
project: updateTaskDto.projectId ? { connect: { id: updateTaskDto.projectId } } : undefined,
|
||||
parent: updateTaskDto.parentId ? { connect: { id: updateTaskDto.parentId } } : undefined,
|
||||
};
|
||||
// Build update data - only include defined fields
|
||||
const data: Prisma.TaskUpdateInput = {};
|
||||
|
||||
if (updateTaskDto.title !== undefined) {
|
||||
data.title = updateTaskDto.title;
|
||||
}
|
||||
if (updateTaskDto.description !== undefined) {
|
||||
data.description = updateTaskDto.description;
|
||||
}
|
||||
if (updateTaskDto.status !== undefined) {
|
||||
data.status = updateTaskDto.status;
|
||||
}
|
||||
if (updateTaskDto.priority !== undefined) {
|
||||
data.priority = updateTaskDto.priority;
|
||||
}
|
||||
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
|
||||
if (updateTaskDto.status) {
|
||||
|
||||
@@ -27,14 +27,14 @@ export class ValkeyService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
this.client = new Redis(valkeyUrl, {
|
||||
maxRetriesPerRequest: 3,
|
||||
retryStrategy: (times) => {
|
||||
retryStrategy: (times: number) => {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
this.logger.warn(
|
||||
`Valkey connection retry attempt ${times.toString()}, waiting ${delay.toString()}ms`
|
||||
);
|
||||
return delay;
|
||||
},
|
||||
reconnectOnError: (err) => {
|
||||
reconnectOnError: (err: Error) => {
|
||||
this.logger.error("Valkey connection error:", err.message);
|
||||
return true;
|
||||
},
|
||||
@@ -44,7 +44,7 @@ export class ValkeyService implements OnModuleInit, OnModuleDestroy {
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -52,19 +52,20 @@ export class WebSocketGateway implements OnGatewayConnection, OnGatewayDisconnec
|
||||
* @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.
|
||||
*/
|
||||
async handleConnection(client: AuthenticatedSocket): Promise<void> {
|
||||
const { userId, workspaceId } = client.data;
|
||||
async handleConnection(client: Socket): Promise<void> {
|
||||
const authenticatedClient = client as AuthenticatedSocket;
|
||||
const { userId, workspaceId } = authenticatedClient.data;
|
||||
|
||||
if (!userId || !workspaceId) {
|
||||
this.logger.warn(`Client ${client.id} connected without authentication`);
|
||||
client.disconnect();
|
||||
this.logger.warn(`Client ${authenticatedClient.id} connected without authentication`);
|
||||
authenticatedClient.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
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.
|
||||
* @returns void
|
||||
*/
|
||||
handleDisconnect(client: AuthenticatedSocket): void {
|
||||
const { workspaceId } = client.data;
|
||||
handleDisconnect(client: Socket): void {
|
||||
const authenticatedClient = client as AuthenticatedSocket;
|
||||
const { workspaceId } = authenticatedClient.data;
|
||||
|
||||
if (workspaceId) {
|
||||
const room = this.getWorkspaceRoom(workspaceId);
|
||||
void client.leave(room);
|
||||
this.logger.log(`Client ${client.id} left room ${room}`);
|
||||
void authenticatedClient.leave(room);
|
||||
this.logger.log(`Client ${authenticatedClient.id} left room ${room}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ export class WidgetDataService {
|
||||
|
||||
items.push(
|
||||
...tasks
|
||||
.filter((task) => task.dueDate !== null)
|
||||
.filter((task): task is typeof task & { dueDate: Date } => task.dueDate !== null)
|
||||
.map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -77,6 +77,9 @@ importers:
|
||||
'@types/marked':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
'@types/multer':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
adm-zip:
|
||||
specifier: ^0.5.16
|
||||
version: 0.5.16
|
||||
@@ -162,9 +165,6 @@ importers:
|
||||
'@types/highlight.js':
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0
|
||||
'@types/multer':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@types/node':
|
||||
specifier: ^22.13.4
|
||||
version: 22.19.7
|
||||
|
||||
Reference in New Issue
Block a user