core/apps/webapp/app/services/space.server.ts

429 lines
10 KiB
TypeScript

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<Space> {
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<string, unknown>,
);
}
return space;
}
/**
* Get all spaces for a user
*/
async getUserSpaces(userId: string): Promise<Space[]> {
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<Space> {
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<Space> {
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<SpaceAssignmentResult> {
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<SpaceAssignmentResult> {
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<Space[]> {
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<number> {
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<boolean> {
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;
}
}