Release: CI/CD Pipeline & Architecture Updates #177
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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