mirror of https://github.com/buster-so/buster.git
adding v2 list chats endpoint
This commit is contained in:
parent
0aaa53c685
commit
731f2d4e64
|
@ -0,0 +1,20 @@
|
|||
import { listChats } from '@buster/database/queries';
|
||||
import { type GetChatsListResponseV2, GetChatsRequestSchemaV2 } from '@buster/server-shared/chats';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { Hono } from 'hono';
|
||||
|
||||
const app = new Hono().get('/', zValidator('query', GetChatsRequestSchemaV2), async (c) => {
|
||||
const user = c.get('busterUser');
|
||||
const queryParams = c.req.valid('query');
|
||||
|
||||
const listChatsParams = {
|
||||
userId: user.id,
|
||||
...queryParams,
|
||||
};
|
||||
|
||||
const paginatedChats: GetChatsListResponseV2 = await listChats(listChatsParams);
|
||||
|
||||
return c.json(paginatedChats);
|
||||
});
|
||||
|
||||
export default app;
|
|
@ -11,12 +11,14 @@ import { requireAuth } from '../../../middleware/auth';
|
|||
import '../../../types/hono.types'; //I added this to fix intermitent type errors. Could probably be removed.
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { z } from 'zod';
|
||||
import GET from './GET';
|
||||
import { cancelChatHandler } from './cancel-chat';
|
||||
import { createChatHandler } from './handler';
|
||||
|
||||
const app = new Hono()
|
||||
// Apply authentication middleware
|
||||
.use('*', requireAuth)
|
||||
.route('/', GET)
|
||||
// POST /chats - Create a new chat
|
||||
.post('/', zValidator('json', ChatCreateRequestSchema), async (c) => {
|
||||
const request = c.req.valid('json');
|
||||
|
|
|
@ -194,7 +194,7 @@ export const processSyncJob: ReturnType<
|
|||
// Process batches with controlled concurrency
|
||||
for (let i = 0; i < embeddingBatches.length; i += EMBEDDING_CONCURRENCY) {
|
||||
const concurrentBatches = embeddingBatches.slice(i, i + EMBEDDING_CONCURRENCY);
|
||||
|
||||
|
||||
logger.info('Processing concurrent embedding batch group', {
|
||||
[identifierType]: identifier,
|
||||
groupStart: i,
|
||||
|
@ -254,7 +254,7 @@ export const processSyncJob: ReturnType<
|
|||
|
||||
// Wait for all concurrent batches to complete
|
||||
const results = await Promise.all(embeddingPromises);
|
||||
|
||||
|
||||
// Flatten and add to results
|
||||
for (const batchResult of results) {
|
||||
allValuesWithEmbeddings.push(...batchResult);
|
||||
|
@ -330,7 +330,7 @@ export const processSyncJob: ReturnType<
|
|||
errorsInBatch: batchResult.errors.length,
|
||||
errors: batchResult.errors,
|
||||
});
|
||||
|
||||
|
||||
upsertErrors.push(...batchResult.errors);
|
||||
}
|
||||
|
||||
|
@ -483,7 +483,7 @@ async function queryDistinctColumnValues({
|
|||
}): Promise<string[]> {
|
||||
// Get the data source type to determine proper identifier quoting
|
||||
const dataSourceType = adapter.getDataSourceType();
|
||||
|
||||
|
||||
// Determine the appropriate quote character based on data source type
|
||||
let quoteChar = '';
|
||||
switch (dataSourceType) {
|
||||
|
@ -525,11 +525,11 @@ async function queryDistinctColumnValues({
|
|||
FROM ${fullyQualifiedTable}
|
||||
WHERE ${columnRef} IS NOT NULL
|
||||
AND TRIM(${columnRef}) != ''${
|
||||
limit
|
||||
? `
|
||||
limit
|
||||
? `
|
||||
LIMIT ${limit}`
|
||||
: ''
|
||||
}
|
||||
: ''
|
||||
}
|
||||
`;
|
||||
|
||||
logger.info('Executing distinct values query', {
|
||||
|
|
|
@ -23,7 +23,6 @@ describe('Chat API Requests', () => {
|
|||
name: 'Test Chat 1',
|
||||
created_at: '2024-03-20T00:00:00Z',
|
||||
updated_at: '2024-03-20T00:00:00Z',
|
||||
is_favorited: false,
|
||||
created_by: 'test-user',
|
||||
created_by_id: 'test-user-id',
|
||||
created_by_name: 'Test User',
|
||||
|
@ -32,7 +31,6 @@ describe('Chat API Requests', () => {
|
|||
latest_file_id: 'file-1',
|
||||
latest_file_type: 'dashboard_file',
|
||||
latest_version_number: 1,
|
||||
latest_file_name: 'Test File',
|
||||
is_shared: false,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -18,3 +18,10 @@ export {
|
|||
GetChatTitleInputSchema,
|
||||
type GetChatTitleInput,
|
||||
} from './get-chat-title';
|
||||
|
||||
export {
|
||||
listChats,
|
||||
ListChatsRequestSchema,
|
||||
type ListChatsRequest,
|
||||
type ListChatsResponse,
|
||||
} from './list-chats';
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
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';
|
||||
import { type AssetType, ChatListItem } from '../../schema-types';
|
||||
import {
|
||||
type PaginatedResponse,
|
||||
PaginationInputSchema,
|
||||
createPaginatedResponse,
|
||||
} from '../shared-types';
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
import { AssetTypeSchema } from './asset';
|
||||
|
||||
export const ChatListItemSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
is_favorited: z.boolean(),
|
||||
updated_at: z.string(),
|
||||
created_at: z.string(),
|
||||
created_by: z.string(),
|
||||
|
@ -12,9 +12,8 @@ export const ChatListItemSchema = z.object({
|
|||
created_by_avatar: z.string().nullable(),
|
||||
last_edited: z.string(),
|
||||
latest_file_id: z.string().nullable(),
|
||||
latest_file_type: z.enum(['metric_file', 'dashboard_file', 'report_file']),
|
||||
latest_file_type: AssetTypeSchema.exclude(['chat', 'collection']),
|
||||
latest_version_number: z.number().optional(),
|
||||
latest_file_name: z.string().nullable(),
|
||||
is_shared: z.boolean(),
|
||||
});
|
||||
|
|
@ -35,3 +35,5 @@ export * from './slack';
|
|||
export * from './github';
|
||||
|
||||
export * from './search';
|
||||
|
||||
export * from './chat';
|
||||
|
|
|
@ -34,3 +34,5 @@ export {
|
|||
ResponseMessageSchema,
|
||||
ReasoningMessageSchema,
|
||||
} from './schema-types/message-schemas';
|
||||
|
||||
export type { ChatListItem } from './schema-types/chat';
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export * from './chat.types';
|
||||
export * from './chat-errors.types';
|
||||
export * from './chat-message.types';
|
||||
export * from './chat-list.types';
|
||||
export * from './requests';
|
||||
export * from './responses';
|
||||
|
||||
|
@ -26,4 +25,5 @@ export {
|
|||
type ReasoningFileType,
|
||||
type ResponseMessageFileType,
|
||||
type ReasoningMessage_ThoughtFileType,
|
||||
type ChatListItem,
|
||||
} from '@buster/database/schema-types';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { z } from 'zod';
|
||||
import { PaginatedRequestSchema } from '../type-utilities';
|
||||
|
||||
// Pagination parameters for chat list
|
||||
export const GetChatsListRequestSchema = z.object({
|
||||
|
@ -8,6 +9,9 @@ export const GetChatsListRequestSchema = z.object({
|
|||
|
||||
export type GetChatsListRequest = z.infer<typeof GetChatsListRequestSchema>;
|
||||
|
||||
export const GetChatsRequestSchemaV2 = PaginatedRequestSchema;
|
||||
export type GetChatsRequestV2 = z.infer<typeof GetChatsRequestSchemaV2>;
|
||||
|
||||
// Request for getting a single chat
|
||||
export const GetChatRequestSchema = z.object({
|
||||
id: z.string(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ChatListItemSchema } from '@buster/database/schema-types';
|
||||
import { z } from 'zod';
|
||||
import { ChatListItemSchema } from './chat-list.types';
|
||||
import { PaginatedResponseSchema } from '../type-utilities';
|
||||
import { ChatWithMessagesSchema } from './chat.types';
|
||||
|
||||
// Response for getting a single chat
|
||||
|
@ -10,6 +11,10 @@ export type GetChatResponse = z.infer<typeof GetChatResponseSchema>;
|
|||
export const GetChatsListResponseSchema = z.array(ChatListItemSchema);
|
||||
export type GetChatsListResponse = z.infer<typeof GetChatsListResponseSchema>;
|
||||
|
||||
// Response for getting a list of chats v2
|
||||
export const GetChatsListResponseSchemaV2 = PaginatedResponseSchema(ChatListItemSchema);
|
||||
export type GetChatsListResponseV2 = z.infer<typeof GetChatsListResponseSchemaV2>;
|
||||
|
||||
// Response for getting logs list (same as chats list)
|
||||
export const GetLogsListResponseSchema = GetChatsListResponseSchema;
|
||||
export type GetLogsListResponse = z.infer<typeof GetLogsListResponseSchema>;
|
||||
|
|
Loading…
Reference in New Issue