fix: Resolve CI typecheck failures and improve type safety
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Fixes CI pipeline failures caused by missing Prisma Client generation and TypeScript type safety issues. Added Prisma generation step to CI pipeline, installed missing type dependencies, and resolved 40+ exactOptionalPropertyTypes violations across service layer. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,15 +34,26 @@ steps:
|
|||||||
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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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,30 +82,25 @@ 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
|
||||||
|
// (Schema requires targetId to be non-null)
|
||||||
|
if (targetId) {
|
||||||
linkCreations.push({
|
linkCreations.push({
|
||||||
sourceId: entryId,
|
sourceId: entryId,
|
||||||
targetId: targetId ?? null,
|
targetId,
|
||||||
linkText: link.target,
|
linkText: link.target,
|
||||||
displayText: link.displayText,
|
displayText: link.displayText,
|
||||||
positionStart: link.start,
|
positionStart: link.start,
|
||||||
positionEnd: link.end,
|
positionEnd: link.end,
|
||||||
resolved: targetId !== null,
|
resolved: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine which existing links to keep/delete
|
// Determine which existing links to keep/delete
|
||||||
// We'll use a simple strategy: delete all existing and recreate
|
// We'll use a simple strategy: delete all existing and recreate
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})`);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
6
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user