import { logger } from "./logger.service"; import { type SpaceNode, type CreateSpaceParams, type UpdateSpaceParams, type SpaceAssignmentResult, } from "@core/types"; import { type Space } from "@prisma/client"; import { triggerSpaceAssignment } from "~/trigger/spaces/space-assignment"; import { assignStatementsToSpace, createSpace, deleteSpace, getSpace, getSpaceStatements, initializeStatementSpaceIds, removeStatementsFromSpace, updateSpace, } from "./graphModels/space"; import { prisma } from "~/trigger/utils/prisma"; export class SpaceService { /** * Create a new space for a user */ async createSpace(params: CreateSpaceParams): Promise { logger.info(`Creating space "${params.name}" for user ${params.userId}`); // Validate input if (!params.name || params.name.trim().length === 0) { throw new Error("Space name is required"); } if (params.name.length > 100) { throw new Error("Space name too long (max 100 characters)"); } if (params.description && params.description.length > 1000) { throw new Error("Space description too long (max 1000 characters)"); } // Check for duplicate names const existingSpaces = await prisma.space.findMany({ where: { name: params.name, workspaceId: params.workspaceId, }, }); if (existingSpaces.length > 0) { throw new Error("A space with this name already exists"); } const space = await prisma.space.create({ data: { name: params.name.trim(), description: params.description?.trim(), workspaceId: params.workspaceId, status: "pending", }, }); await createSpace( space.id, params.name.trim(), params.description?.trim(), params.userId, ); logger.info(`Created space ${space.id} successfully`); // Trigger automatic LLM assignment for the new space try { await triggerSpaceAssignment({ userId: params.userId, workspaceId: params.workspaceId, mode: "new_space", newSpaceId: space.id, batchSize: 25, // Analyze recent statements for the new space }); logger.info(`Triggered LLM space assignment for new space ${space.id}`); } catch (error) { // Don't fail space creation if LLM assignment fails logger.warn( `Failed to trigger LLM assignment for space ${space.id}:`, error as Record, ); } return space; } /** * Get all spaces for a user */ async getUserSpaces(userId: string): Promise { const user = await prisma.user.findFirst({ where: { id: userId, }, include: { Workspace: true, }, }); return await prisma.space.findMany({ where: { workspaceId: user?.Workspace?.id, }, }); } async getSpaceByName(name: string, userId: string) { const user = await prisma.user.findFirst({ where: { id: userId, }, include: { Workspace: true, }, }); const space = await prisma.space.findFirst({ where: { name: name, workspaceId: user?.Workspace?.id, }, }); return space; } /** * Get a specific space by ID */ async getSpace(spaceId: string, userId: string) { const space = await prisma.space.findUnique({ where: { id: spaceId, }, }); const nodeData = await getSpace(spaceId, userId); return { ...(nodeData as SpaceNode), ...space, }; } /** * Update a space */ async updateSpace( spaceId: string, updates: UpdateSpaceParams, userId: string, ): Promise { logger.info(`Updating space ${spaceId} for user ${userId}`); // Validate input if (updates.name !== undefined) { if (!updates.name || updates.name.trim().length === 0) { throw new Error("Space name cannot be empty"); } if (updates.name.length > 100) { throw new Error("Space name too long (max 100 characters)"); } // Check for duplicate names (excluding current space) const existingSpaces = await prisma.space.findMany({ where: { name: updates.name, workspaceId: userId, }, }); const duplicates = existingSpaces.filter((space) => space.id !== spaceId); if (duplicates.length > 0) { throw new Error("A space with this name already exists"); } } if ( updates.description !== undefined && updates.description.length > 1000 ) { throw new Error("Space description too long (max 1000 characters)"); } const space = await prisma.space.update({ where: { id: spaceId, }, data: { name: updates.name, description: updates.description, icon: updates.icon, status: updates.status, }, }); try { await updateSpace(spaceId, updates, userId); } catch (e) { logger.info(`Nothing to update to graph`); } logger.info(`Updated space ${spaceId} successfully`); return space; } /** * Delete a space and clean up all statement references */ async deleteSpace(spaceId: string, userId: string): Promise { logger.info(`Deleting space ${spaceId} for user ${userId}`); const space = await prisma.space.delete({ where: { id: spaceId, }, }); if (space.name === "Profile") { throw new Error("Bad request"); } await deleteSpace(spaceId, userId); logger.info(`Deleted space ${spaceId} successfully`); return space; } /** * Assign statements to a space */ async assignStatementsToSpace( statementIds: string[], spaceId: string, userId: string, ): Promise { logger.info( `Assigning ${statementIds.length} statements to space ${spaceId} for user ${userId}`, ); // Validate input if (statementIds.length === 0) { throw new Error("No statement IDs provided"); } if (statementIds.length > 1000) { throw new Error("Too many statements (max 1000 per operation)"); } const result = await assignStatementsToSpace(statementIds, spaceId, userId); if (result.success) { logger.info( `Successfully assigned ${result.statementsUpdated} statements to space ${spaceId}`, ); } else { logger.warn( `Failed to assign statements to space ${spaceId}: ${result.error}`, ); } return result; } /** * Remove statements from a space */ async removeStatementsFromSpace( statementIds: string[], spaceId: string, userId: string, ): Promise { logger.info( `Removing ${statementIds.length} statements from space ${spaceId} for user ${userId}`, ); // Validate input if (statementIds.length === 0) { throw new Error("No statement IDs provided"); } if (statementIds.length > 1000) { throw new Error("Too many statements (max 1000 per operation)"); } const result = await removeStatementsFromSpace( statementIds, spaceId, userId, ); if (result.success) { logger.info( `Successfully removed ${result.statementsUpdated} statements from space ${spaceId}`, ); } else { logger.warn( `Failed to remove statements from space ${spaceId}: ${result.error}`, ); } return result; } /** * Get all statements in a space */ async getSpaceStatements(spaceId: string, userId: string) { logger.info(`Fetching statements for space ${spaceId} for user ${userId}`); return await getSpaceStatements(spaceId, userId); } /** * Search spaces by name */ async searchSpacesByName( query: string, workspaceId: string, ): Promise { if (!query || query.trim().length === 0) { return []; } return await prisma.space.findMany({ where: { workspaceId, name: { contains: query, mode: "insensitive", }, }, }); } /** * Get spaces that contain specific statements */ async getSpacesForStatements( statementIds: string[], userId: string, ): Promise<{ statementId: string; spaces: Space[] }[]> { const userSpaces = await this.getUserSpaces(userId); const result: { statementId: string; spaces: Space[] }[] = []; for (const statementId of statementIds) { const spacesContainingStatement = []; for (const space of userSpaces) { const statements = await this.getSpaceStatements(space.id, userId); if (statements.some((stmt) => stmt.uuid === statementId)) { spacesContainingStatement.push(space); } } result.push({ statementId, spaces: spacesContainingStatement, }); } return result; } /** * Initialize spaceIds for existing statements (migration utility) */ async initializeSpaceIds(userId?: string): Promise { logger.info( `Initializing spaceIds for ${userId ? `user ${userId}` : "all users"}`, ); const updatedCount = await initializeStatementSpaceIds(userId); logger.info(`Initialized spaceIds for ${updatedCount} statements`); return updatedCount; } /** * Validate space access */ async validateSpaceAccess(spaceId: string, userId: string): Promise { const space = await this.getSpace(spaceId, userId); return space !== null && space.isActive; } /** * Bulk assign statements to multiple spaces */ async bulkAssignStatements( statementIds: string[], spaceIds: string[], userId: string, ): Promise<{ spaceId: string; result: SpaceAssignmentResult }[]> { logger.info( `Bulk assigning ${statementIds.length} statements to ${spaceIds.length} spaces for user ${userId}`, ); const results: { spaceId: string; result: SpaceAssignmentResult }[] = []; for (const spaceId of spaceIds) { try { const result = await this.assignStatementsToSpace( statementIds, spaceId, userId, ); results.push({ spaceId, result }); } catch (error) { results.push({ spaceId, result: { success: false, statementsUpdated: 0, error: error instanceof Error ? error.message : "Unknown error", }, }); } } return results; } }