Release: CI/CD Pipeline & Architecture Updates #177

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

View File

@@ -28,21 +28,32 @@ steps:
SKIP_ENV_VALIDATION: "true"
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

View File

@@ -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",

View File

@@ -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

View File

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

View File

@@ -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;

View File

@@ -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;
}
/**

View File

@@ -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 },

View File

@@ -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 },

View File

@@ -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 },

View File

@@ -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

View File

@@ -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,
});
});
}

View File

@@ -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})`);

View File

@@ -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,

View File

@@ -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 },

View File

@@ -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) {

View File

@@ -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);
});

View File

@@ -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}`);
}
}

View File

@@ -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
View File

@@ -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