diff --git a/src/dao/DaoFactory.ts b/src/dao/DaoFactory.ts index c0ebc0b..15565e3 100644 --- a/src/dao/DaoFactory.ts +++ b/src/dao/DaoFactory.ts @@ -6,6 +6,7 @@ import { UserConfigDao, UserConfigDaoImpl } from './UserConfigDao.js'; import { OAuthClientDao, OAuthClientDaoImpl } from './OAuthClientDao.js'; import { OAuthTokenDao, OAuthTokenDaoImpl } from './OAuthTokenDao.js'; import { BearerKeyDao, BearerKeyDaoImpl } from './BearerKeyDao.js'; +import { ToolCallActivityDao } from './ToolCallActivityDao.js'; /** * DAO Factory interface for creating DAO instances @@ -19,6 +20,7 @@ export interface DaoFactory { getOAuthClientDao(): OAuthClientDao; getOAuthTokenDao(): OAuthTokenDao; getBearerKeyDao(): BearerKeyDao; + getToolCallActivityDao(): ToolCallActivityDao | null; // Only available in DB mode } /** @@ -106,6 +108,11 @@ export class JsonFileDaoFactory implements DaoFactory { return this.bearerKeyDao; } + getToolCallActivityDao(): ToolCallActivityDao | null { + // Tool call activity is only available in DB mode + return null; + } + /** * Reset all cached DAO instances (useful for testing) */ @@ -194,3 +201,14 @@ export function getOAuthTokenDao(): OAuthTokenDao { export function getBearerKeyDao(): BearerKeyDao { return getDaoFactory().getBearerKeyDao(); } + +export function getToolCallActivityDao(): ToolCallActivityDao | null { + return getDaoFactory().getToolCallActivityDao(); +} + +/** + * Check if the application is using database mode + */ +export function isUsingDatabase(): boolean { + return getDaoFactory().getToolCallActivityDao() !== null; +} diff --git a/src/dao/DatabaseDaoFactory.ts b/src/dao/DatabaseDaoFactory.ts index 7ab6c5b..0c544ac 100644 --- a/src/dao/DatabaseDaoFactory.ts +++ b/src/dao/DatabaseDaoFactory.ts @@ -8,6 +8,7 @@ import { OAuthClientDao, OAuthTokenDao, BearerKeyDao, + ToolCallActivityDao, } from './index.js'; import { UserDaoDbImpl } from './UserDaoDbImpl.js'; import { ServerDaoDbImpl } from './ServerDaoDbImpl.js'; @@ -17,6 +18,7 @@ import { UserConfigDaoDbImpl } from './UserConfigDaoDbImpl.js'; import { OAuthClientDaoDbImpl } from './OAuthClientDaoDbImpl.js'; import { OAuthTokenDaoDbImpl } from './OAuthTokenDaoDbImpl.js'; import { BearerKeyDaoDbImpl } from './BearerKeyDaoDbImpl.js'; +import { ToolCallActivityDaoDbImpl } from './ToolCallActivityDao.js'; /** * Database-backed DAO factory implementation @@ -32,6 +34,7 @@ export class DatabaseDaoFactory implements DaoFactory { private oauthClientDao: OAuthClientDao | null = null; private oauthTokenDao: OAuthTokenDao | null = null; private bearerKeyDao: BearerKeyDao | null = null; + private toolCallActivityDao: ToolCallActivityDao | null = null; /** * Get singleton instance @@ -103,6 +106,13 @@ export class DatabaseDaoFactory implements DaoFactory { return this.bearerKeyDao!; } + getToolCallActivityDao(): ToolCallActivityDao | null { + if (!this.toolCallActivityDao) { + this.toolCallActivityDao = new ToolCallActivityDaoDbImpl(); + } + return this.toolCallActivityDao; + } + /** * Reset all cached DAO instances (useful for testing) */ @@ -115,5 +125,6 @@ export class DatabaseDaoFactory implements DaoFactory { this.oauthClientDao = null; this.oauthTokenDao = null; this.bearerKeyDao = null; + this.toolCallActivityDao = null; } } diff --git a/src/dao/ToolCallActivityDao.ts b/src/dao/ToolCallActivityDao.ts new file mode 100644 index 0000000..757bf09 --- /dev/null +++ b/src/dao/ToolCallActivityDao.ts @@ -0,0 +1,186 @@ +import { + IToolCallActivity, + IToolCallActivitySearchParams, + IToolCallActivityPage, + IToolCallActivityStats, +} from '../types/index.js'; +import { ToolCallActivityRepository } from '../db/repositories/ToolCallActivityRepository.js'; + +/** + * Tool Call Activity DAO interface (DB mode only) + */ +export interface ToolCallActivityDao { + /** + * Create a new tool call activity + */ + create(activity: Omit): Promise; + + /** + * Find activity by ID + */ + findById(id: string): Promise; + + /** + * Update an existing activity + */ + update(id: string, updates: Partial): Promise; + + /** + * Delete an activity + */ + delete(id: string): Promise; + + /** + * Find activities with pagination and filtering + */ + findWithPagination( + page: number, + pageSize: number, + params?: IToolCallActivitySearchParams, + ): Promise; + + /** + * Get recent activities + */ + findRecent(limit: number): Promise; + + /** + * Get activity statistics + */ + getStats(): Promise; + + /** + * Delete old activities (cleanup) + */ + deleteOlderThan(date: Date): Promise; + + /** + * Count total activities + */ + count(): Promise; +} + +/** + * Database-backed implementation of ToolCallActivityDao + */ +export class ToolCallActivityDaoDbImpl implements ToolCallActivityDao { + private repository: ToolCallActivityRepository; + + constructor() { + this.repository = new ToolCallActivityRepository(); + } + + async create(activity: Omit): Promise { + const created = await this.repository.create({ + serverName: activity.serverName, + toolName: activity.toolName, + keyId: activity.keyId, + keyName: activity.keyName, + status: activity.status, + request: activity.request, + response: activity.response, + errorMessage: activity.errorMessage, + durationMs: activity.durationMs, + clientIp: activity.clientIp, + sessionId: activity.sessionId, + groupName: activity.groupName, + }); + return this.mapToInterface(created); + } + + async findById(id: string): Promise { + const activity = await this.repository.findById(id); + return activity ? this.mapToInterface(activity) : null; + } + + async update( + id: string, + updates: Partial, + ): Promise { + const updated = await this.repository.update(id, { + serverName: updates.serverName, + toolName: updates.toolName, + keyId: updates.keyId, + keyName: updates.keyName, + status: updates.status, + request: updates.request, + response: updates.response, + errorMessage: updates.errorMessage, + durationMs: updates.durationMs, + clientIp: updates.clientIp, + sessionId: updates.sessionId, + groupName: updates.groupName, + }); + return updated ? this.mapToInterface(updated) : null; + } + + async delete(id: string): Promise { + return await this.repository.delete(id); + } + + async findWithPagination( + page: number = 1, + pageSize: number = 20, + params: IToolCallActivitySearchParams = {}, + ): Promise { + const result = await this.repository.findWithPagination(page, pageSize, params); + return { + items: result.items.map((item) => this.mapToInterface(item)), + total: result.total, + page: result.page, + pageSize: result.pageSize, + totalPages: result.totalPages, + }; + } + + async findRecent(limit: number = 10): Promise { + const activities = await this.repository.findRecent(limit); + return activities.map((activity) => this.mapToInterface(activity)); + } + + async getStats(): Promise { + return await this.repository.getStats(); + } + + async deleteOlderThan(date: Date): Promise { + return await this.repository.deleteOlderThan(date); + } + + async count(): Promise { + return await this.repository.count(); + } + + private mapToInterface(activity: { + id: string; + serverName: string; + toolName: string; + keyId?: string; + keyName?: string; + status: 'pending' | 'success' | 'error'; + request?: string; + response?: string; + errorMessage?: string; + durationMs?: number; + clientIp?: string; + sessionId?: string; + groupName?: string; + createdAt: Date; + }): IToolCallActivity { + return { + id: activity.id, + serverName: activity.serverName, + toolName: activity.toolName, + keyId: activity.keyId, + keyName: activity.keyName, + status: activity.status, + request: activity.request, + response: activity.response, + errorMessage: activity.errorMessage, + durationMs: activity.durationMs, + clientIp: activity.clientIp, + sessionId: activity.sessionId, + groupName: activity.groupName, + createdAt: activity.createdAt, + }; + } +} diff --git a/src/dao/index.ts b/src/dao/index.ts index 9f7584e..d5cebdc 100644 --- a/src/dao/index.ts +++ b/src/dao/index.ts @@ -9,6 +9,7 @@ export * from './UserConfigDao.js'; export * from './OAuthClientDao.js'; export * from './OAuthTokenDao.js'; export * from './BearerKeyDao.js'; +export * from './ToolCallActivityDao.js'; // Export database implementations export * from './UserDaoDbImpl.js'; diff --git a/src/db/entities/ToolCallActivity.ts b/src/db/entities/ToolCallActivity.ts new file mode 100644 index 0000000..a524450 --- /dev/null +++ b/src/db/entities/ToolCallActivity.ts @@ -0,0 +1,62 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * Tool call activity entity for logging tool invocations (DB mode only) + */ +@Entity({ name: 'tool_call_activities' }) +export class ToolCallActivity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ type: 'varchar', length: 255, name: 'server_name' }) + serverName: string; + + @Index() + @Column({ type: 'varchar', length: 255, name: 'tool_name' }) + toolName: string; + + @Index() + @Column({ type: 'varchar', length: 255, name: 'key_id', nullable: true }) + keyId?: string; + + @Column({ type: 'varchar', length: 255, name: 'key_name', nullable: true }) + keyName?: string; + + @Index() + @Column({ type: 'varchar', length: 50, default: 'pending' }) + status: 'pending' | 'success' | 'error'; + + @Column({ type: 'text', nullable: true }) + request?: string; + + @Column({ type: 'text', nullable: true }) + response?: string; + + @Column({ type: 'text', name: 'error_message', nullable: true }) + errorMessage?: string; + + @Column({ type: 'int', name: 'duration_ms', nullable: true }) + durationMs?: number; + + @Column({ type: 'varchar', length: 100, name: 'client_ip', nullable: true }) + clientIp?: string; + + @Column({ type: 'varchar', length: 255, name: 'session_id', nullable: true }) + sessionId?: string; + + @Column({ type: 'varchar', length: 255, name: 'group_name', nullable: true }) + groupName?: string; + + @Index() + @CreateDateColumn({ name: 'created_at', type: 'timestamp' }) + createdAt: Date; +} + +export default ToolCallActivity; diff --git a/src/db/entities/index.ts b/src/db/entities/index.ts index 34997b3..f2296fb 100644 --- a/src/db/entities/index.ts +++ b/src/db/entities/index.ts @@ -7,6 +7,7 @@ import UserConfig from './UserConfig.js'; import OAuthClient from './OAuthClient.js'; import OAuthToken from './OAuthToken.js'; import BearerKey from './BearerKey.js'; +import ToolCallActivity from './ToolCallActivity.js'; // Export all entities export default [ @@ -19,6 +20,7 @@ export default [ OAuthClient, OAuthToken, BearerKey, + ToolCallActivity, ]; // Export individual entities for direct use @@ -32,4 +34,5 @@ export { OAuthClient, OAuthToken, BearerKey, + ToolCallActivity, }; diff --git a/src/db/repositories/ToolCallActivityRepository.ts b/src/db/repositories/ToolCallActivityRepository.ts new file mode 100644 index 0000000..a0fa6ad --- /dev/null +++ b/src/db/repositories/ToolCallActivityRepository.ts @@ -0,0 +1,200 @@ +import { Repository, FindOptionsWhere, ILike, Between } from 'typeorm'; +import { ToolCallActivity } from '../entities/ToolCallActivity.js'; +import { getAppDataSource } from '../connection.js'; + +/** + * Search parameters for filtering tool call activities + */ +export interface ToolCallActivitySearchParams { + serverName?: string; + toolName?: string; + keyId?: string; + status?: 'pending' | 'success' | 'error'; + groupName?: string; + startDate?: Date; + endDate?: Date; + searchQuery?: string; +} + +/** + * Pagination result for tool call activities + */ +export interface ToolCallActivityPage { + items: ToolCallActivity[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +/** + * Repository for ToolCallActivity entity + */ +export class ToolCallActivityRepository { + private repository: Repository; + + constructor() { + this.repository = getAppDataSource().getRepository(ToolCallActivity); + } + + /** + * Create a new tool call activity + */ + async create( + activity: Omit, + ): Promise { + const newActivity = this.repository.create(activity); + return await this.repository.save(newActivity); + } + + /** + * Find activity by ID + */ + async findById(id: string): Promise { + return await this.repository.findOne({ where: { id } }); + } + + /** + * Update an existing activity + */ + async update( + id: string, + updates: Partial, + ): Promise { + const activity = await this.findById(id); + if (!activity) { + return null; + } + const updated = this.repository.merge(activity, updates); + return await this.repository.save(updated); + } + + /** + * Delete an activity + */ + async delete(id: string): Promise { + const result = await this.repository.delete({ id }); + return (result.affected ?? 0) > 0; + } + + /** + * Find activities with pagination and filtering + */ + async findWithPagination( + page: number = 1, + pageSize: number = 20, + params: ToolCallActivitySearchParams = {}, + ): Promise { + const where: FindOptionsWhere[] = []; + const baseWhere: FindOptionsWhere = {}; + + // Add filters + if (params.serverName) { + baseWhere.serverName = params.serverName; + } + if (params.toolName) { + baseWhere.toolName = params.toolName; + } + if (params.keyId) { + baseWhere.keyId = params.keyId; + } + if (params.status) { + baseWhere.status = params.status; + } + if (params.groupName) { + baseWhere.groupName = params.groupName; + } + if (params.startDate && params.endDate) { + baseWhere.createdAt = Between(params.startDate, params.endDate); + } + + // Handle search query - search across multiple fields + if (params.searchQuery) { + const searchPattern = `%${params.searchQuery}%`; + where.push( + { ...baseWhere, serverName: ILike(searchPattern) }, + { ...baseWhere, toolName: ILike(searchPattern) }, + { ...baseWhere, keyName: ILike(searchPattern) }, + { ...baseWhere, groupName: ILike(searchPattern) }, + ); + } else { + where.push(baseWhere); + } + + const [items, total] = await this.repository.findAndCount({ + where: where.length > 0 ? where : undefined, + order: { createdAt: 'DESC' }, + skip: (page - 1) * pageSize, + take: pageSize, + }); + + return { + items, + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize), + }; + } + + /** + * Get recent activities + */ + async findRecent(limit: number = 10): Promise { + return await this.repository.find({ + order: { createdAt: 'DESC' }, + take: limit, + }); + } + + /** + * Get activity statistics + */ + async getStats(): Promise<{ + total: number; + success: number; + error: number; + pending: number; + avgDurationMs: number; + }> { + const stats = await this.repository + .createQueryBuilder('activity') + .select([ + 'COUNT(*) as total', + 'SUM(CASE WHEN status = \'success\' THEN 1 ELSE 0 END) as success', + 'SUM(CASE WHEN status = \'error\' THEN 1 ELSE 0 END) as error', + 'SUM(CASE WHEN status = \'pending\' THEN 1 ELSE 0 END) as pending', + 'AVG(duration_ms) as avgDurationMs', + ]) + .getRawOne(); + + return { + total: parseInt(stats?.total || '0', 10), + success: parseInt(stats?.success || '0', 10), + error: parseInt(stats?.error || '0', 10), + pending: parseInt(stats?.pending || '0', 10), + avgDurationMs: parseFloat(stats?.avgDurationMs || '0'), + }; + } + + /** + * Delete old activities (cleanup) + */ + async deleteOlderThan(date: Date): Promise { + const result = await this.repository + .createQueryBuilder() + .delete() + .where('created_at < :date', { date }) + .execute(); + return result.affected ?? 0; + } + + /** + * Count total activities + */ + async count(): Promise { + return await this.repository.count(); + } +} + +export default ToolCallActivityRepository; diff --git a/src/db/repositories/index.ts b/src/db/repositories/index.ts index 3367431..3d83e1a 100644 --- a/src/db/repositories/index.ts +++ b/src/db/repositories/index.ts @@ -7,6 +7,7 @@ import { UserConfigRepository } from './UserConfigRepository.js'; import { OAuthClientRepository } from './OAuthClientRepository.js'; import { OAuthTokenRepository } from './OAuthTokenRepository.js'; import { BearerKeyRepository } from './BearerKeyRepository.js'; +import { ToolCallActivityRepository } from './ToolCallActivityRepository.js'; // Export all repositories export { @@ -19,4 +20,5 @@ export { OAuthClientRepository, OAuthTokenRepository, BearerKeyRepository, + ToolCallActivityRepository, }; diff --git a/src/types/index.ts b/src/types/index.ts index e5e8186..31a1c5f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -481,3 +481,51 @@ export interface BatchCreateGroupsResponse { failureCount: number; // Number of groups that failed results: BatchGroupResult[]; // Detailed results for each group } + +// Tool call activity interface for logging tool invocations (DB mode only) +export interface IToolCallActivity { + id?: string; + serverName: string; + toolName: string; + keyId?: string; + keyName?: string; + status: 'pending' | 'success' | 'error'; + request?: string; + response?: string; + errorMessage?: string; + durationMs?: number; + clientIp?: string; + sessionId?: string; + groupName?: string; + createdAt?: Date; +} + +// Tool call activity search parameters +export interface IToolCallActivitySearchParams { + serverName?: string; + toolName?: string; + keyId?: string; + status?: 'pending' | 'success' | 'error'; + groupName?: string; + startDate?: Date; + endDate?: Date; + searchQuery?: string; +} + +// Tool call activity pagination result +export interface IToolCallActivityPage { + items: IToolCallActivity[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +// Tool call activity statistics +export interface IToolCallActivityStats { + total: number; + success: number; + error: number; + pending: number; + avgDurationMs: number; +}