2025-09-20 05:22:11 +08:00
|
|
|
import { and, count, desc, eq, exists, gt, isNotNull, isNull, ne, or, sql } from 'drizzle-orm';
|
|
|
|
import { z } from 'zod';
|
|
|
|
import { db } from '../../connection';
|
|
|
|
import {
|
|
|
|
assetPermissions,
|
|
|
|
chats,
|
|
|
|
messages,
|
|
|
|
userFavorites,
|
|
|
|
users,
|
|
|
|
usersToOrganizations,
|
|
|
|
} from '../../schema';
|
2025-09-26 01:44:35 +08:00
|
|
|
import type { ChatListItem, PaginatedResponse } from '../../schema-types';
|
|
|
|
import { PaginationInputSchema, createPaginatedResponse } from '../../schema-types';
|
2025-09-20 05:22:11 +08:00
|
|
|
|
|
|
|
export const ListChatsRequestSchema = z
|
|
|
|
.object({
|
|
|
|
userId: z.string().uuid(),
|
|
|
|
})
|
|
|
|
.merge(PaginationInputSchema);
|
|
|
|
|
|
|
|
export type ListChatsResponse = PaginatedResponse<ChatListItem>;
|
|
|
|
export type ListChatsRequest = z.infer<typeof ListChatsRequestSchema>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a subquery for chats the user owns
|
|
|
|
*/
|
|
|
|
function getOwnedChats(userId: string) {
|
|
|
|
return db
|
|
|
|
.select({ chatId: chats.id })
|
|
|
|
.from(chats)
|
|
|
|
.where(and(eq(chats.createdBy, userId), isNull(chats.deletedAt)));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a subquery for chats directly shared with the user via asset_permissions
|
|
|
|
*/
|
|
|
|
function getDirectlySharedChats(userId: string) {
|
|
|
|
return db
|
|
|
|
.select({ chatId: assetPermissions.assetId })
|
|
|
|
.from(assetPermissions)
|
|
|
|
.where(
|
|
|
|
and(
|
|
|
|
eq(assetPermissions.identityId, userId),
|
|
|
|
eq(assetPermissions.identityType, 'user'),
|
|
|
|
eq(assetPermissions.assetType, 'chat'),
|
|
|
|
isNull(assetPermissions.deletedAt)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a subquery for workspace-shared chats where user has contributed or favorited
|
|
|
|
*/
|
|
|
|
function getWorkspaceSharedChats(userId: string) {
|
|
|
|
return db
|
|
|
|
.selectDistinct({ chatId: chats.id })
|
|
|
|
.from(chats)
|
|
|
|
.innerJoin(usersToOrganizations, eq(chats.organizationId, usersToOrganizations.organizationId))
|
|
|
|
.where(
|
|
|
|
and(
|
|
|
|
eq(usersToOrganizations.userId, userId),
|
|
|
|
isNull(usersToOrganizations.deletedAt),
|
|
|
|
ne(chats.workspaceSharing, 'none'),
|
|
|
|
isNull(chats.deletedAt),
|
|
|
|
or(
|
|
|
|
// User has contributed (created messages in this chat)
|
|
|
|
exists(
|
|
|
|
db
|
|
|
|
.select()
|
|
|
|
.from(messages)
|
|
|
|
.where(
|
|
|
|
and(
|
|
|
|
eq(messages.chatId, chats.id),
|
|
|
|
eq(messages.createdBy, userId),
|
|
|
|
isNotNull(messages.requestMessage),
|
|
|
|
isNull(messages.deletedAt)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
),
|
|
|
|
// User has favorited this chat
|
|
|
|
exists(
|
|
|
|
db
|
|
|
|
.select()
|
|
|
|
.from(userFavorites)
|
|
|
|
.where(
|
|
|
|
and(
|
|
|
|
eq(userFavorites.userId, userId),
|
|
|
|
eq(userFavorites.assetId, chats.id),
|
|
|
|
eq(userFavorites.assetType, 'chat'),
|
|
|
|
isNull(userFavorites.deletedAt)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a combined subquery for all accessible chat IDs using UNION
|
|
|
|
*/
|
|
|
|
function getAccessibleChatIds(userId: string) {
|
|
|
|
const ownedChats = getOwnedChats(userId);
|
|
|
|
const directlySharedChats = getDirectlySharedChats(userId);
|
|
|
|
const workspaceSharedChats = getWorkspaceSharedChats(userId);
|
|
|
|
|
|
|
|
return ownedChats
|
|
|
|
.union(directlySharedChats)
|
|
|
|
.union(workspaceSharedChats)
|
|
|
|
.as('accessible_chat_ids');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List chats with pagination support
|
|
|
|
*
|
|
|
|
* This function efficiently retrieves a list of chats with their associated user information.
|
|
|
|
* It uses a CTE-style approach with UNION to gather all accessible chats, then applies
|
|
|
|
* content filtering and pagination. Only includes chats with meaningful content.
|
|
|
|
*
|
|
|
|
* Returns a list of chat items with user information and pagination details.
|
|
|
|
*/
|
|
|
|
export async function listChats(params: ListChatsRequest): Promise<ListChatsResponse> {
|
|
|
|
const { userId, page, page_size } = ListChatsRequestSchema.parse(params);
|
|
|
|
|
|
|
|
// Calculate offset based on page number
|
|
|
|
const offset = (page - 1) * page_size;
|
|
|
|
|
|
|
|
// Create the accessible chat IDs subquery (our CTE equivalent)
|
|
|
|
const accessibleChatIds = getAccessibleChatIds(userId);
|
|
|
|
|
|
|
|
// Where conditions for filtering chats
|
|
|
|
const contentFilterConditions = and(
|
|
|
|
isNull(chats.deletedAt),
|
|
|
|
ne(chats.title, ''),
|
|
|
|
or(
|
|
|
|
// Has at least one message with a request_message
|
|
|
|
exists(
|
|
|
|
db
|
|
|
|
.select()
|
|
|
|
.from(messages)
|
|
|
|
.where(
|
|
|
|
and(
|
|
|
|
eq(messages.chatId, chats.id),
|
|
|
|
isNotNull(messages.requestMessage),
|
|
|
|
ne(messages.requestMessage, ''),
|
|
|
|
isNull(messages.deletedAt)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
),
|
|
|
|
// Has more than 1 message
|
|
|
|
gt(
|
|
|
|
db
|
|
|
|
.select({ count: count() })
|
|
|
|
.from(messages)
|
|
|
|
.where(and(eq(messages.chatId, chats.id), isNull(messages.deletedAt))),
|
|
|
|
1
|
|
|
|
)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Main query: join chats with accessible IDs and apply content filtering
|
|
|
|
const results = await db
|
|
|
|
.select({
|
|
|
|
id: chats.id,
|
|
|
|
title: chats.title,
|
|
|
|
createdAt: chats.createdAt,
|
|
|
|
updatedAt: chats.updatedAt,
|
|
|
|
createdBy: chats.createdBy,
|
|
|
|
mostRecentFileId: chats.mostRecentFileId,
|
|
|
|
mostRecentFileType: chats.mostRecentFileType,
|
|
|
|
mostRecentVersionNumber: chats.mostRecentVersionNumber,
|
|
|
|
organizationId: chats.organizationId,
|
|
|
|
workspaceSharing: chats.workspaceSharing,
|
|
|
|
updatedBy: chats.updatedBy,
|
|
|
|
userName: users.name,
|
|
|
|
userEmail: users.email,
|
|
|
|
userAvatarUrl: users.avatarUrl,
|
|
|
|
})
|
|
|
|
.from(chats)
|
|
|
|
.innerJoin(accessibleChatIds, eq(chats.id, accessibleChatIds.chatId))
|
|
|
|
.innerJoin(users, eq(chats.createdBy, users.id))
|
|
|
|
.where(contentFilterConditions)
|
|
|
|
.orderBy(desc(chats.updatedAt))
|
|
|
|
.limit(page_size)
|
|
|
|
.offset(offset);
|
|
|
|
|
|
|
|
// Get total count for pagination using the same conditions
|
|
|
|
const [countResult] = await db
|
|
|
|
.select({ count: count() })
|
|
|
|
.from(chats)
|
|
|
|
.innerJoin(accessibleChatIds, eq(chats.id, accessibleChatIds.chatId))
|
|
|
|
.where(contentFilterConditions);
|
|
|
|
|
|
|
|
// Transform results to ChatListItem format
|
|
|
|
const chatItems: ChatListItem[] = [];
|
|
|
|
|
|
|
|
for (const chat of results) {
|
|
|
|
if (chat.title.trim()) {
|
|
|
|
chatItems.push({
|
|
|
|
id: chat.id,
|
|
|
|
name: chat.title,
|
|
|
|
created_at: chat.createdAt,
|
|
|
|
updated_at: chat.updatedAt,
|
|
|
|
created_by: chat.createdBy,
|
|
|
|
created_by_id: chat.createdBy,
|
|
|
|
created_by_name: chat.userName || chat.userEmail,
|
|
|
|
created_by_avatar: chat.userAvatarUrl,
|
|
|
|
last_edited: chat.updatedAt,
|
|
|
|
latest_file_id: chat.mostRecentFileId,
|
|
|
|
latest_file_type: chat.mostRecentFileType as
|
|
|
|
| 'metric_file'
|
|
|
|
| 'dashboard_file'
|
|
|
|
| 'report_file', // TODO: talk to nate about this, it is why we see the console errors
|
|
|
|
latest_version_number: chat.mostRecentVersionNumber ?? undefined,
|
|
|
|
is_shared: chat.createdBy !== userId,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return paginated response
|
|
|
|
return createPaginatedResponse({
|
|
|
|
data: chatItems,
|
|
|
|
page,
|
|
|
|
page_size,
|
|
|
|
total: countResult?.count ?? 0,
|
|
|
|
});
|
|
|
|
}
|