adding v2 list chats endpoint

This commit is contained in:
Wells Bunker 2025-09-19 15:22:11 -06:00
parent 0aaa53c685
commit 731f2d4e64
No known key found for this signature in database
GPG Key ID: DB16D6F2679B78FC
12 changed files with 285 additions and 15 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -18,3 +18,10 @@ export {
GetChatTitleInputSchema,
type GetChatTitleInput,
} from './get-chat-title';
export {
listChats,
ListChatsRequestSchema,
type ListChatsRequest,
type ListChatsResponse,
} from './list-chats';

View File

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

View File

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

View File

@ -35,3 +35,5 @@ export * from './slack';
export * from './github';
export * from './search';
export * from './chat';

View File

@ -34,3 +34,5 @@ export {
ResponseMessageSchema,
ReasoningMessageSchema,
} from './schema-types/message-schemas';
export type { ChatListItem } from './schema-types/chat';

View File

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

View File

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

View File

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