From e60c0bb0d7d407e22817affc4ddc541dcb2d2a65 Mon Sep 17 00:00:00 2001 From: Acbox Date: Sat, 10 Jan 2026 20:17:02 +0800 Subject: [PATCH] feat: basic api server --- packages/api/package.json | 18 +- packages/api/src/client.ts | 10 + packages/api/src/index.ts | 11 +- packages/api/src/middlewares/cors.ts | 9 + packages/api/src/middlewares/index.ts | 1 + packages/api/src/modules/agent/index.ts | 5 + packages/api/src/modules/index.ts | 3 + packages/api/src/modules/memory/index.ts | 40 +++ .../api/src/modules/memory/message/index.ts | 22 ++ .../api/src/modules/memory/message/model.ts | 17 ++ .../api/src/modules/memory/message/service.ts | 43 ++++ packages/api/src/modules/memory/model.ts | 20 ++ packages/api/src/modules/memory/service.ts | 35 +++ packages/api/src/modules/model/index.ts | 185 ++++++++++++++ packages/api/src/modules/model/model.ts | 55 ++++ packages/api/src/modules/model/service.ts | 64 +++++ packages/api/src/modules/settings/index.ts | 91 +++++++ packages/api/src/modules/settings/model.ts | 32 +++ packages/api/src/modules/settings/service.ts | 63 +++++ packages/api/test/README.md | 39 +++ packages/api/test/memory-message.test.ts | 80 ++++++ packages/api/test/memory.test.ts | 81 ++++++ packages/api/test/model.test.ts | 238 ++++++++++++++++++ packages/api/test/settings.test.ts | 155 ++++++++++++ packages/api/test/setup.ts | 7 + packages/api/tsconfig.json | 10 +- packages/db/package.json | 1 + packages/db/src/model.ts | 7 + packages/db/src/schema.ts | 4 +- packages/db/src/settings.ts | 9 + pnpm-lock.yaml | 73 ++++++ 31 files changed, 1421 insertions(+), 7 deletions(-) create mode 100644 packages/api/src/client.ts create mode 100644 packages/api/src/middlewares/cors.ts create mode 100644 packages/api/src/middlewares/index.ts create mode 100644 packages/api/src/modules/agent/index.ts create mode 100644 packages/api/src/modules/index.ts create mode 100644 packages/api/src/modules/memory/index.ts create mode 100644 packages/api/src/modules/memory/message/index.ts create mode 100644 packages/api/src/modules/memory/message/model.ts create mode 100644 packages/api/src/modules/memory/message/service.ts create mode 100644 packages/api/src/modules/memory/model.ts create mode 100644 packages/api/src/modules/memory/service.ts create mode 100644 packages/api/src/modules/model/index.ts create mode 100644 packages/api/src/modules/model/model.ts create mode 100644 packages/api/src/modules/model/service.ts create mode 100644 packages/api/src/modules/settings/index.ts create mode 100644 packages/api/src/modules/settings/model.ts create mode 100644 packages/api/src/modules/settings/service.ts create mode 100644 packages/api/test/README.md create mode 100644 packages/api/test/memory-message.test.ts create mode 100644 packages/api/test/memory.test.ts create mode 100644 packages/api/test/model.test.ts create mode 100644 packages/api/test/settings.test.ts create mode 100644 packages/api/test/setup.ts create mode 100644 packages/db/src/model.ts create mode 100644 packages/db/src/settings.ts diff --git a/packages/api/package.json b/packages/api/package.json index 0a0dd6ef..5b0e7c61 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -4,10 +4,24 @@ "scripts": { "dev": "bun run --env-file=../../.env --watch src/index.ts", "build": "bun build src/index.ts --outfile dist/index.js --target bun --minify", - "start": "bun run dist/index.js" + "start": "bun run dist/index.js", + "test": "vitest" + }, + "exports": { + "./client": "./src/client.ts" }, "dependencies": { - "elysia": "latest" + "@elysiajs/cors": "^1.4.1", + "@elysiajs/cron": "^1.4.1", + "@elysiajs/eden": "^1.4.6", + "@memohome/agent": "workspace:*", + "@memohome/db": "workspace:*", + "@memohome/memory": "workspace:*", + "@memohome/shared": "workspace:*", + "drizzle-orm": "^0.45.1", + "elysia": "latest", + "node-cron": "^4.2.1", + "zod": "^4.3.5" }, "devDependencies": { "bun-types": "latest" diff --git a/packages/api/src/client.ts b/packages/api/src/client.ts new file mode 100644 index 00000000..99ae219d --- /dev/null +++ b/packages/api/src/client.ts @@ -0,0 +1,10 @@ +import { app } from './index' +import { treaty } from '@elysiajs/eden' + +export type ApiClient = typeof app + +export const createClient = ( + baseUrl: string = process.env.API_BASE_URL ?? 'http://localhost:7002' +) => { + return treaty(baseUrl) +} \ No newline at end of file diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index b9414ea6..4e54860e 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,8 +1,17 @@ import { Elysia } from 'elysia' +import { corsMiddleware } from './middlewares' +import { agentModule, modelModule, settingsModule } from './modules' +import { memoryModule } from './modules/memory' const port = process.env.API_SERVER_PORT || 7002 -const app = new Elysia().get('/', () => 'Hello Elysia').listen(port) +export const app = new Elysia() + .use(corsMiddleware) + .use(agentModule) + .use(memoryModule) + .use(modelModule) + .use(settingsModule) + .listen(port) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` diff --git a/packages/api/src/middlewares/cors.ts b/packages/api/src/middlewares/cors.ts new file mode 100644 index 00000000..fdb7aa5a --- /dev/null +++ b/packages/api/src/middlewares/cors.ts @@ -0,0 +1,9 @@ +import cors from '@elysiajs/cors' + +export const corsMiddleware = cors({ + origin: '*', + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization'], + exposeHeaders: ['Content-Type', 'Authorization'], + credentials: true, +}) \ No newline at end of file diff --git a/packages/api/src/middlewares/index.ts b/packages/api/src/middlewares/index.ts new file mode 100644 index 00000000..e50f281e --- /dev/null +++ b/packages/api/src/middlewares/index.ts @@ -0,0 +1 @@ +export * from './cors' \ No newline at end of file diff --git a/packages/api/src/modules/agent/index.ts b/packages/api/src/modules/agent/index.ts new file mode 100644 index 00000000..1c7acaea --- /dev/null +++ b/packages/api/src/modules/agent/index.ts @@ -0,0 +1,5 @@ +import Elysia from 'elysia' + +export const agentModule = new Elysia({ + prefix: '/agent', +}) \ No newline at end of file diff --git a/packages/api/src/modules/index.ts b/packages/api/src/modules/index.ts new file mode 100644 index 00000000..f788d966 --- /dev/null +++ b/packages/api/src/modules/index.ts @@ -0,0 +1,3 @@ +export * from './agent' +export * from './model' +export * from './settings' \ No newline at end of file diff --git a/packages/api/src/modules/memory/index.ts b/packages/api/src/modules/memory/index.ts new file mode 100644 index 00000000..cf14d4a9 --- /dev/null +++ b/packages/api/src/modules/memory/index.ts @@ -0,0 +1,40 @@ +import Elysia from 'elysia' +import { messageModule } from './message' +import { AddMemoryModel, SearchMemoryModel } from './model' +import { addMemory, searchMemory } from './service' +import { MemoryUnit } from '@memohome/memory' + +export const memoryModule = new Elysia({ + prefix: '/memory', +}) + .use(messageModule) + // Add memory + .post('/', async ({ body }) => { + try { + const result = await addMemory(body as unknown as MemoryUnit) + return { + success: true, + data: result, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to add memory', + } + } + }, AddMemoryModel) + // Search memory + .get('/search', async ({ query }) => { + try { + const results = await searchMemory(query.query, query.userId) + return { + success: true, + data: results, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to search memory', + } + } + }, SearchMemoryModel) \ No newline at end of file diff --git a/packages/api/src/modules/memory/message/index.ts b/packages/api/src/modules/memory/message/index.ts new file mode 100644 index 00000000..b93b9612 --- /dev/null +++ b/packages/api/src/modules/memory/message/index.ts @@ -0,0 +1,22 @@ +import Elysia from 'elysia' +import { GetMemoryMessageFilterModel, GetMemoryMessageModel } from './model' +import { getMemoryMessages, getMemoryMessagesFilter } from './service' + +export const messageModule = new Elysia({ + prefix: '/message', +}) + .get('/', async ({ query }) => { + const units = await getMemoryMessages(query) + return { + success: true, + units, + } + }, GetMemoryMessageModel) + .get('/filter', async ({ query }) => { + const units = await getMemoryMessagesFilter(query) + + return { + units, + success: true, + } + }, GetMemoryMessageFilterModel) \ No newline at end of file diff --git a/packages/api/src/modules/memory/message/model.ts b/packages/api/src/modules/memory/message/model.ts new file mode 100644 index 00000000..b8ccfe01 --- /dev/null +++ b/packages/api/src/modules/memory/message/model.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' + +export const GetMemoryMessageModel = { + query: z.object({ + limit: z.string().transform(Number).default(10), + page: z.string().transform(Number).default(1), + userId: z.string(), + }), +} + +export const GetMemoryMessageFilterModel = { + query: z.object({ + from: z.date(), + to: z.date(), + userId: z.string(), + }), +} \ No newline at end of file diff --git a/packages/api/src/modules/memory/message/service.ts b/packages/api/src/modules/memory/message/service.ts new file mode 100644 index 00000000..94774959 --- /dev/null +++ b/packages/api/src/modules/memory/message/service.ts @@ -0,0 +1,43 @@ +import { db } from '@memohome/db' +import { history } from '@memohome/db/schema' +import { eq, desc, and, gte, lte, asc } from 'drizzle-orm' + +export const getMemoryMessages = async ( + query: { + limit: number + page: number + userId: string + } +) => { + const { limit, page, userId } = query + const results = await db + .select() + .from(history) + .where(eq(history.user, userId)) + .orderBy(desc(history.timestamp)) + .limit(limit) + .offset((page - 1) * limit) + + return results +} + +export const getMemoryMessagesFilter = async ( + query: { + from: Date + to: Date + userId: string + } +) => { + const { from, to, userId } = query + const results = await db + .select() + .from(history) + .where(and( + gte(history.timestamp, from), + lte(history.timestamp, to), + eq(history.user, userId), + )) + .orderBy(asc(history.timestamp)) + + return results +} \ No newline at end of file diff --git a/packages/api/src/modules/memory/model.ts b/packages/api/src/modules/memory/model.ts new file mode 100644 index 00000000..90c4d27c --- /dev/null +++ b/packages/api/src/modules/memory/model.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' + +// MemoryUnit schema +const MemoryUnitSchema = z.object({ + messages: z.array(z.object()), + timestamp: z.coerce.date(), + user: z.string(), +}) + +export const AddMemoryModel = { + body: MemoryUnitSchema, +} + +export const SearchMemoryModel = { + query: z.object({ + query: z.string().min(1, 'Search query is required'), + userId: z.string().min(1, 'User ID is required'), + }), +} + diff --git a/packages/api/src/modules/memory/service.ts b/packages/api/src/modules/memory/service.ts new file mode 100644 index 00000000..ab2feded --- /dev/null +++ b/packages/api/src/modules/memory/service.ts @@ -0,0 +1,35 @@ +import { createMemory, MemoryUnit } from '@memohome/memory' +import { getEmbeddingModel, getSummaryModel } from '@/modules/model/service' +import { ChatModel, EmbeddingModel } from '@memohome/shared' + +export const addMemory = async (memoryUnit: MemoryUnit) => { + const [embeddingModel, summaryModel] = await Promise.all([ + getEmbeddingModel(memoryUnit.user), + getSummaryModel(memoryUnit.user), + ]) + if (!embeddingModel || !summaryModel) { + throw new Error('Embedding or summary model not found') + } + const { addMemory } = createMemory({ + summaryModel: summaryModel.model as ChatModel, + embeddingModel: embeddingModel.model as EmbeddingModel, + }) + await addMemory(memoryUnit) + return memoryUnit +} + +export const searchMemory = async (query: string, userId: string) => { + const [embeddingModel, summaryModel] = await Promise.all([ + getEmbeddingModel(userId), + getSummaryModel(userId), + ]) + if (!embeddingModel || !summaryModel) { + throw new Error('Embedding or summary model not found') + } + const { searchMemory } = createMemory({ + summaryModel: summaryModel.model as ChatModel, + embeddingModel: embeddingModel.model as EmbeddingModel, + }) + const results = await searchMemory(query, userId) + return results +} \ No newline at end of file diff --git a/packages/api/src/modules/model/index.ts b/packages/api/src/modules/model/index.ts new file mode 100644 index 00000000..80df45b8 --- /dev/null +++ b/packages/api/src/modules/model/index.ts @@ -0,0 +1,185 @@ +import Elysia from 'elysia' +import { + CreateModelModel, + UpdateModelModel, + GetModelByIdModel, + DeleteModelModel, + GetDefaultModelModel, +} from './model' +import { + getModels, + getModelById, + createModel, + updateModel, + deleteModel, + getChatModel, + getSummaryModel, + getEmbeddingModel, +} from './service' +import { Model } from '@memohome/shared' + +export const modelModule = new Elysia({ + prefix: '/model', +}) + // Get all models + .get('/', async () => { + try { + const models = await getModels() + return { + success: true, + data: models, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch models', + } + } + }) + // Get model by ID + .get('/:id', async ({ params }) => { + try { + const { id } = params + const model = await getModelById(id) + if (!model) { + return { + success: false, + error: 'Model not found', + } + } + return { + success: true, + data: model, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch model', + } + } + }, GetModelByIdModel) + // Create new model + .post('/', async ({ body }) => { + try { + const newModel = await createModel(body as Model) + return { + success: true, + data: newModel, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create model', + } + } + }, CreateModelModel) + // Update model + .put('/:id', async ({ params, body }) => { + try { + const { id } = params + const updatedModel = await updateModel(id, body as Model) + if (!updatedModel) { + return { + success: false, + error: 'Model not found', + } + } + return { + success: true, + data: updatedModel, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update model', + } + } + }, UpdateModelModel) + // Delete model + .delete('/:id', async ({ params }) => { + try { + const { id } = params + const deletedModel = await deleteModel(id) + if (!deletedModel) { + return { + success: false, + error: 'Model not found', + } + } + return { + success: true, + data: deletedModel, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete model', + } + } + }, DeleteModelModel) + // Get default chat model + .get('/chat/default', async ({ query }) => { + try { + const { userId } = query + const chatModel = await getChatModel(userId) + if (!chatModel) { + return { + success: false, + error: 'Default chat model not found or not set', + } + } + return { + success: true, + data: chatModel, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch default chat model', + } + } + }, GetDefaultModelModel) + // Get default summary model + .get('/summary/default', async ({ query }) => { + try { + const { userId } = query + const summaryModel = await getSummaryModel(userId) + if (!summaryModel) { + return { + success: false, + error: 'Default summary model not found or not set', + } + } + return { + success: true, + data: summaryModel, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch default summary model', + } + } + }, GetDefaultModelModel) + // Get default embedding model + .get('/embedding/default', async ({ query }) => { + try { + const { userId } = query + const embeddingModel = await getEmbeddingModel(userId) + if (!embeddingModel) { + return { + success: false, + error: 'Default embedding model not found or not set', + } + } + return { + success: true, + data: embeddingModel, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch default embedding model', + } + } + }, GetDefaultModelModel) diff --git a/packages/api/src/modules/model/model.ts b/packages/api/src/modules/model/model.ts new file mode 100644 index 00000000..82222df2 --- /dev/null +++ b/packages/api/src/modules/model/model.ts @@ -0,0 +1,55 @@ +import { z } from 'zod' + +const BaseModelSchema = z.object({ + modelId: z.string().min(1, 'Model ID is required'), + baseUrl: z.string(), + apiKey: z.string().min(1, 'API key is required'), + clientType: z.string(), + name: z.string().optional(), +}) + +// Chat model schema (type is optional and defaults to 'chat') +const ChatModelSchema = BaseModelSchema.extend({ + type: z.enum(['chat']).optional().default('chat'), +}) + +// Embedding model schema (type must be 'embedding' and dimensions is required) +const EmbeddingModelSchema = BaseModelSchema.extend({ + type: z.literal('embedding'), + dimensions: z.number().int().positive('Dimensions must be a positive integer'), +}) + +// Union of both model types +const ModelSchema = z.union([ChatModelSchema, EmbeddingModelSchema]) + +// Export the inferred type from the schema +export type ModelInput = z.infer + +export const CreateModelModel = { + body: ModelSchema, +} + +export const UpdateModelModel = { + params: z.object({ + id: z.string(), + }), + body: ModelSchema, +} + +export const GetModelByIdModel = { + params: z.object({ + id: z.string(), + }), +} + +export const DeleteModelModel = { + params: z.object({ + id: z.string(), + }), +} + +export const GetDefaultModelModel = { + query: z.object({ + userId: z.string().min(1, 'User ID is required'), + }), +} diff --git a/packages/api/src/modules/model/service.ts b/packages/api/src/modules/model/service.ts new file mode 100644 index 00000000..64eb5166 --- /dev/null +++ b/packages/api/src/modules/model/service.ts @@ -0,0 +1,64 @@ +import { db } from '@memohome/db' +import { model } from '@memohome/db/schema' +import { Model } from '@memohome/shared' +import { eq } from 'drizzle-orm' +import { getSettings } from '@/modules/settings/service' + +export const getModels = async () => { + const models = await db.select().from(model) + return models +} + +export const getModelById = async (id: string) => { + const [result] = await db.select().from(model).where(eq(model.id, id)) + return result +} + +export const createModel = async (data: Model) => { + const [newModel] = await db + .insert(model) + .values({ model: data }) + .returning() + return newModel +} + +export const updateModel = async (id: string, data: Model) => { + const [updatedModel] = await db + .update(model) + .set({ model: data }) + .where(eq(model.id, id)) + .returning() + return updatedModel +} + +export const deleteModel = async (id: string) => { + const [deletedModel] = await db + .delete(model) + .where(eq(model.id, id)) + .returning() + return deletedModel +} + +export const getChatModel = async (userId: string) => { + const userSettings = await getSettings(userId) + if (!userSettings?.defaultChatModel) { + return null + } + return await getModelById(userSettings.defaultChatModel) +} + +export const getSummaryModel = async (userId: string) => { + const userSettings = await getSettings(userId) + if (!userSettings?.defaultSummaryModel) { + return null + } + return await getModelById(userSettings.defaultSummaryModel) +} + +export const getEmbeddingModel = async (userId: string) => { + const userSettings = await getSettings(userId) + if (!userSettings?.defaultEmbeddingModel) { + return null + } + return await getModelById(userSettings.defaultEmbeddingModel) +} \ No newline at end of file diff --git a/packages/api/src/modules/settings/index.ts b/packages/api/src/modules/settings/index.ts new file mode 100644 index 00000000..3f7309e4 --- /dev/null +++ b/packages/api/src/modules/settings/index.ts @@ -0,0 +1,91 @@ +import Elysia from 'elysia' +import { + GetSettingsModel, + CreateSettingsModel, + UpdateSettingsModel, +} from './model' +import { + getSettings, + createSettings, + updateSettings, + upsertSettings, +} from './service' + +export const settingsModule = new Elysia({ + prefix: '/settings', +}) + // Get user settings + .get('/:userId', async ({ params }) => { + try { + const { userId } = params + const userSettings = await getSettings(userId) + if (!userSettings) { + return { + success: false, + error: 'Settings not found', + } + } + return { + success: true, + data: userSettings, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to fetch settings', + } + } + }, GetSettingsModel) + // Create new settings + .post('/', async ({ body }) => { + try { + const newSettings = await createSettings(body) + return { + success: true, + data: newSettings, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create settings', + } + } + }, CreateSettingsModel) + // Update settings + .put('/:userId', async ({ params, body }) => { + try { + const { userId } = params + const updatedSettings = await updateSettings(userId, body) + if (!updatedSettings) { + return { + success: false, + error: 'Settings not found', + } + } + return { + success: true, + data: updatedSettings, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update settings', + } + } + }, UpdateSettingsModel) + // Upsert settings (create or update) + .patch('/', async ({ body }) => { + try { + const result = await upsertSettings(body) + return { + success: true, + data: result, + } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to upsert settings', + } + } + }, CreateSettingsModel) + diff --git a/packages/api/src/modules/settings/model.ts b/packages/api/src/modules/settings/model.ts new file mode 100644 index 00000000..52476d66 --- /dev/null +++ b/packages/api/src/modules/settings/model.ts @@ -0,0 +1,32 @@ +import { z } from 'zod' + +const SettingsSchema = z.object({ + userId: z.string().min(1, 'User ID is required'), + defaultChatModel: z.string().uuid().nullable().optional(), + defaultEmbeddingModel: z.string().uuid().nullable().optional(), + defaultSummaryModel: z.string().uuid().nullable().optional(), +}) + +export type SettingsInput = z.infer + +export const GetSettingsModel = { + params: z.object({ + userId: z.string(), + }), +} + +export const CreateSettingsModel = { + body: SettingsSchema, +} + +export const UpdateSettingsModel = { + params: z.object({ + userId: z.string(), + }), + body: z.object({ + defaultChatModel: z.string().uuid().nullable().optional(), + defaultEmbeddingModel: z.string().uuid().nullable().optional(), + defaultSummaryModel: z.string().uuid().nullable().optional(), + }), +} + diff --git a/packages/api/src/modules/settings/service.ts b/packages/api/src/modules/settings/service.ts new file mode 100644 index 00000000..d6adceb0 --- /dev/null +++ b/packages/api/src/modules/settings/service.ts @@ -0,0 +1,63 @@ +import { db } from '@memohome/db' +import { settings } from '@memohome/db/schema' +import { eq } from 'drizzle-orm' +import type { SettingsInput } from './model' + +export const getSettings = async (userId: string) => { + const [result] = await db + .select() + .from(settings) + .where(eq(settings.userId, userId)) + return result +} + +export const createSettings = async (data: SettingsInput) => { + const [newSettings] = await db + .insert(settings) + .values({ + userId: data.userId, + defaultChatModel: data.defaultChatModel || null, + defaultEmbeddingModel: data.defaultEmbeddingModel || null, + defaultSummaryModel: data.defaultSummaryModel || null, + }) + .returning() + return newSettings +} + +export const updateSettings = async ( + userId: string, + data: Partial> +) => { + const [updatedSettings] = await db + .update(settings) + .set({ + defaultChatModel: data.defaultChatModel, + defaultEmbeddingModel: data.defaultEmbeddingModel, + defaultSummaryModel: data.defaultSummaryModel, + }) + .where(eq(settings.userId, userId)) + .returning() + return updatedSettings +} + +export const upsertSettings = async (data: SettingsInput) => { + const [result] = await db + .insert(settings) + .values({ + userId: data.userId, + defaultChatModel: data.defaultChatModel || null, + defaultEmbeddingModel: data.defaultEmbeddingModel || null, + defaultSummaryModel: data.defaultSummaryModel || null, + }) + .onConflictDoUpdate({ + target: settings.userId, + set: { + defaultChatModel: data.defaultChatModel, + defaultEmbeddingModel: data.defaultEmbeddingModel, + defaultSummaryModel: data.defaultSummaryModel, + }, + }) + .returning() + return result +} + diff --git a/packages/api/test/README.md b/packages/api/test/README.md new file mode 100644 index 00000000..e1c86f0f --- /dev/null +++ b/packages/api/test/README.md @@ -0,0 +1,39 @@ +# API Tests + +这个目录包含了使用 Elysia Eden Client 编写的 API 测试。 + +## 测试文件 + +- `setup.ts` - 测试设置文件,配置测试服务器和 eden client +- `memory.test.ts` - Memory API 测试 +- `memory-message.test.ts` - Memory Message API 测试 +- `model.test.ts` - Model API 测试 +- `settings.test.ts` - Settings API 测试 + +## 运行测试 + +从项目根目录运行: + +```bash +pnpm test +``` + +或者只运行 API 包的测试: + +```bash +cd packages/api +pnpm test +``` + +## 测试说明 + +测试使用 vitest 作为测试框架,并使用 Elysia Eden Client (treaty) 来测试 API 端点。 + +测试服务器会在测试开始前启动(端口 7003),测试结束后自动关闭。 + +## 注意事项 + +- 确保数据库已配置并运行(某些测试可能需要数据库连接) +- 测试使用独立的测试端口(7003)以避免与开发服务器冲突 +- 某些测试可能需要先创建数据才能测试查询功能 + diff --git a/packages/api/test/memory-message.test.ts b/packages/api/test/memory-message.test.ts new file mode 100644 index 00000000..88c83113 --- /dev/null +++ b/packages/api/test/memory-message.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest' +import { getTestClient } from './setup' + +describe('Memory Message API', () => { + const client = getTestClient() + + describe('GET /memory/message', () => { + it('should get memory messages successfully', async () => { + const response = await client.memory.message.get({ + query: { + userId: 'test-user-123', + limit: 10, + page: 1, + }, + }) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(response.data?.units).toBeDefined() + }) + + it('should use default limit and page when not provided', async () => { + const response = await client.memory.message.get({ + query: { + userId: 'test-user-123', + limit: 10, + page: 1, + }, + }) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + }) + + it('should return error for missing userId', async () => { + const response = await client.memory.message.get({ + // @ts-expect-error - Testing invalid input + query: { + limit: 10, + page: 1, + // missing userId + }, + }) + + expect([400, 422]).toContain(response.status) + }) + }) + + describe('GET /memory/message/filter', () => { + it('should filter memory messages successfully', async () => { + const response = await client.memory.message.filter.get({ + query: { + userId: 'test-user-123', + from: new Date('2024-01-01') as unknown as string, + to: new Date('2024-12-31') as unknown as string, + }, + }) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(response.data?.units).toBeDefined() + }) + + it('should return error for missing required fields', async () => { + const response = await client.memory.message.filter.get({ + // @ts-expect-error - Testing invalid input + query: { + userId: 'test-user-123', + // missing from and to + }, + }) + + expect([400, 422]).toContain(response.status) + }) + }) +}) + diff --git a/packages/api/test/memory.test.ts b/packages/api/test/memory.test.ts new file mode 100644 index 00000000..a8f07f3a --- /dev/null +++ b/packages/api/test/memory.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect } from 'vitest' +import { getTestClient } from './setup' + +describe('Memory API', () => { + const client = getTestClient() + + describe('POST /memory', () => { + it('should add memory successfully', async () => { + const memoryData = { + messages: [ + { role: 'user', content: 'Hello, this is a test message' }, + { role: 'assistant', content: 'Hello! How can I help you?' }, + ], + timestamp: new Date(), + user: 'test-user-123', + } + + const response = await client.memory.post(memoryData) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + console.log(response.data?.error) + expect(response.data?.success).toBe(true) + expect(response.data?.data).toBeDefined() + }) + + it('should return error for invalid memory data', async () => { + const invalidData = { + messages: [], + // missing timestamp and user + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response = await client.memory.post(invalidData as any) + + // Elysia 会返回 400 或 422 对于验证错误 + expect([400, 422]).toContain(response.status) + }) + }) + + describe('GET /memory/search', () => { + it('should search memory successfully', async () => { + const response = await client.memory.search.get({ + query: { + query: 'test search', + userId: 'test-user-123', + }, + }) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(Array.isArray(response.data?.data)).toBe(true) + }) + + it('should return error for missing query', async () => { + const response = await client.memory.search.get({ + // @ts-expect-error - Testing invalid input + query: { + userId: 'test-user-123', + // missing query + }, + }) + + expect([400, 422]).toContain(response.status) + }) + + it('should return error for missing userId', async () => { + const response = await client.memory.search.get({ + // @ts-expect-error - Testing invalid input + query: { + query: 'test search', + // missing userId + }, + }) + + expect([400, 422]).toContain(response.status) + }) + }) +}) + diff --git a/packages/api/test/model.test.ts b/packages/api/test/model.test.ts new file mode 100644 index 00000000..38406622 --- /dev/null +++ b/packages/api/test/model.test.ts @@ -0,0 +1,238 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { getTestClient } from './setup' + +describe('Model API', () => { + const client = getTestClient() + let createdModelId: string | null = null + + describe('GET /model', () => { + it('should get all models successfully', async () => { + const response = await client.model.get() + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(Array.isArray(response.data?.data)).toBe(true) + }) + }) + + describe('POST /model', () => { + it('should create a chat model successfully', async () => { + const modelData = { + modelId: 'test-chat-model', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'test-api-key', + clientType: 'openai', + name: 'Test Chat Model', + type: 'chat' as const, + } + + const response = await client.model.post(modelData) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(response.data?.data).toBeDefined() + + if (response.data?.data?.id) { + createdModelId = response.data.data.id + } + }) + + it('should create an embedding model successfully', async () => { + const modelData = { + modelId: 'test-embedding-model', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'test-api-key', + clientType: 'openai', + name: 'Test Embedding Model', + type: 'embedding' as const, + dimensions: 1536, + } + + const response = await client.model.post(modelData) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(response.data?.data).toBeDefined() + }) + + it('should return error for invalid model data', async () => { + const invalidData = { + // missing required fields + name: 'Invalid Model', + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response = await client.model.post(invalidData as any) + + expect([400, 422]).toContain(response.status) + }) + + it('should return error for embedding model without dimensions', async () => { + const invalidData = { + modelId: 'test-embedding-model', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'test-api-key', + clientType: 'openai', + type: 'embedding' as const, + // missing dimensions + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response = await client.model.post(invalidData as any) + + expect([400, 422]).toContain(response.status) + }) + }) + + describe('GET /model/:id', () => { + it('should get model by id successfully', async () => { + if (!createdModelId) { + // 先创建一个模型 + const createResponse = await client.model.post({ + modelId: 'test-get-model', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'test-api-key', + clientType: 'openai', + type: 'chat' as const, + }) + + if (createResponse.data?.data?.id) { + createdModelId = createResponse.data.data.id + } + } + + if (createdModelId) { + const response = await client.model({ + id: createdModelId, + }).get() + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(response.data?.data).toBeDefined() + } + }) + + it('should return error for non-existent model', async () => { + const response = await client.model({ + id: 'non-existent-id', + }).get() + + expect(response.status).toBe(200) // API 返回 200 但 success: false + expect(response.data?.success).toBe(false) + expect(response.data?.error).toBeDefined() + }) + }) + + describe('PUT /model/:id', () => { + it('should update model successfully', async () => { + if (!createdModelId) { + const createResponse = await client.model.post({ + modelId: 'test-update-model', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'test-api-key', + clientType: 'openai', + type: 'chat' as const, + }) + + if (createResponse.data?.data?.id) { + createdModelId = createResponse.data.data.id + } + } + + if (createdModelId) { + const updateData = { + modelId: 'test-updated-model', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'updated-api-key', + clientType: 'openai', + name: 'Updated Model', + type: 'chat' as const, + } + + const response = await client.model[createdModelId].put(updateData) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(response.data?.data).toBeDefined() + } + }) + }) + + describe('DELETE /model/:id', () => { + it('should delete model successfully', async () => { + // 先创建一个模型用于删除 + const createResponse = await client.model.post({ + modelId: 'test-delete-model', + baseUrl: 'https://api.openai.com/v1', + apiKey: 'test-api-key', + clientType: 'openai', + type: 'chat' as const, + }) + + const modelId = createResponse.data?.data?.id + if (modelId) { + const response = await client.model[modelId].delete() + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + } + }) + }) + + describe('GET /model/chat/default', () => { + it('should get default chat model successfully', async () => { + const response = await client.model.chat.default.get({ + query: { + userId: 'test-user-123', + }, + }) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + // 可能返回 success: false 如果没有设置默认模型 + expect(response.data?.success !== undefined).toBe(true) + }) + + it('should return error for missing userId', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response = await client.model.chat.default.get({ query: {} } as any) + + expect([400, 422]).toContain(response.status) + }) + }) + + describe('GET /model/summary/default', () => { + it('should get default summary model successfully', async () => { + const response = await client.model.summary.default.get({ + query: { + userId: 'test-user-123', + }, + }) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success !== undefined).toBe(true) + }) + }) + + describe('GET /model/embedding/default', () => { + it('should get default embedding model successfully', async () => { + const response = await client.model.embedding.default.get({ + query: { + userId: 'test-user-123', + }, + }) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success !== undefined).toBe(true) + }) + }) +}) + diff --git a/packages/api/test/settings.test.ts b/packages/api/test/settings.test.ts new file mode 100644 index 00000000..f60aff20 --- /dev/null +++ b/packages/api/test/settings.test.ts @@ -0,0 +1,155 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { getTestClient } from './setup' + +describe('Settings API', () => { + const client = getTestClient() + const testUserId = 'test-user-settings-123' + + describe('GET /settings/:userId', () => { + it('should get settings successfully', async () => { + const response = await client.settings({ + userId: testUserId, + }).get() + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + // 可能返回 success: false 如果设置不存在 + expect(response.data?.success !== undefined).toBe(true) + }) + }) + + describe('POST /settings', () => { + it('should create settings successfully', async () => { + const settingsData = { + userId: testUserId, + defaultChatModel: null, + defaultEmbeddingModel: null, + defaultSummaryModel: null, + } + + const response = await client.settings.post(settingsData) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(response.data?.data).toBeDefined() + }) + + it('should create settings with model IDs', async () => { + const settingsData = { + userId: `${testUserId}-with-models`, + defaultChatModel: '00000000-0000-0000-0000-000000000001', + defaultEmbeddingModel: '00000000-0000-0000-0000-000000000002', + defaultSummaryModel: '00000000-0000-0000-0000-000000000003', + } + + const response = await client.settings.post(settingsData) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + }) + + it('should return error for invalid UUID format', async () => { + const invalidData = { + userId: testUserId, + defaultChatModel: 'invalid-uuid', + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response = await client.settings.post(invalidData as any) + + expect([400, 422]).toContain(response.status) + }) + + it('should return error for missing userId', async () => { + const invalidData = { + defaultChatModel: null, + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response = await client.settings.post(invalidData as any) + + expect([400, 422]).toContain(response.status) + }) + }) + + describe('PUT /settings/:userId', () => { + it('should update settings successfully', async () => { + // 先创建设置 + await client.settings.post({ + userId: `${testUserId}-update`, + defaultChatModel: null, + }) + + const updateData = { + defaultChatModel: '00000000-0000-0000-0000-000000000001', + defaultEmbeddingModel: '00000000-0000-0000-0000-000000000002', + defaultSummaryModel: null, + } + + const response = await client.settings({ + userId: `${testUserId}-update`, + }).put(updateData) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(response.data?.data).toBeDefined() + }) + + it('should return error for non-existent settings', async () => { + const updateData = { + defaultChatModel: '00000000-0000-0000-0000-000000000001', + } + + const response = await client.settings({ + userId: 'non-existent-user', + }).put(updateData) + + expect(response.status).toBe(200) // API 返回 200 但 success: false + expect(response.data?.success).toBe(false) + expect(response.data?.error).toBeDefined() + }) + }) + + describe('PATCH /settings', () => { + it('should upsert settings successfully', async () => { + const settingsData = { + userId: `${testUserId}-upsert`, + defaultChatModel: '00000000-0000-0000-0000-000000000001', + defaultEmbeddingModel: null, + defaultSummaryModel: null, + } + + const response = await client.settings.patch(settingsData) + + expect(response.status).toBe(200) + expect(response.data).toBeDefined() + expect(response.data?.success).toBe(true) + expect(response.data?.data).toBeDefined() + }) + + it('should update existing settings on upsert', async () => { + const userId = `${testUserId}-upsert-update` + + // 先创建 + await client.settings.post({ + userId, + defaultChatModel: null, + }) + + // 然后 upsert + const upsertData = { + userId, + defaultChatModel: '00000000-0000-0000-0000-000000000001', + } + + const response = await client.settings.patch(upsertData) + + expect(response.status).toBe(200) + expect(response.data?.success).toBe(true) + }) + }) +}) + diff --git a/packages/api/test/setup.ts b/packages/api/test/setup.ts new file mode 100644 index 00000000..567fac7f --- /dev/null +++ b/packages/api/test/setup.ts @@ -0,0 +1,7 @@ +import { createClient } from '../src/client' + +export const getTestClient = () => { + return createClient() +} + + diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 1ca2350a..12a1a66a 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -27,7 +27,7 @@ /* Modules */ "module": "ES2022", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "Bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ @@ -98,6 +98,10 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "paths": { + "@/*": ["./src/*"] + }, + }, + "include": ["src/**/*", "src/**/**/*"] } diff --git a/packages/db/package.json b/packages/db/package.json index c7f7cb3f..27a3d5ab 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -13,6 +13,7 @@ "studio": "drizzle-kit studio" }, "dependencies": { + "@memohome/shared": "workspace:*", "dotenv": "^17.2.3", "drizzle-orm": "^0.45.1", "pg": "^8.16.3" diff --git a/packages/db/src/model.ts b/packages/db/src/model.ts new file mode 100644 index 00000000..4822380a --- /dev/null +++ b/packages/db/src/model.ts @@ -0,0 +1,7 @@ +import { Model } from '@memohome/shared' +import { jsonb, pgTable, uuid } from 'drizzle-orm/pg-core' + +export const model = pgTable('model', { + id: uuid('id').primaryKey().defaultRandom(), + model: jsonb('model').notNull().$type(), +}) \ No newline at end of file diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index e157256e..6e8f6de0 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -1 +1,3 @@ -export * from './history' \ No newline at end of file +export * from './history' +export * from './model' +export * from './settings' \ No newline at end of file diff --git a/packages/db/src/settings.ts b/packages/db/src/settings.ts new file mode 100644 index 00000000..07a622e1 --- /dev/null +++ b/packages/db/src/settings.ts @@ -0,0 +1,9 @@ +import { pgTable, text, uuid } from 'drizzle-orm/pg-core' +import { model } from './model' + +export const settings = pgTable('settings', { + userId: text('user_id').primaryKey(), + defaultChatModel: uuid('default_chat_model').references(() => model.id), + defaultEmbeddingModel: uuid('default_embedding_model').references(() => model.id), + defaultSummaryModel: uuid('default_summary_model').references(() => model.id), +}) \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10265801..94b7456e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,9 +90,39 @@ importers: packages/api: dependencies: + '@elysiajs/cors': + specifier: ^1.4.1 + version: 1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) + '@elysiajs/cron': + specifier: ^1.4.1 + version: 1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) + '@elysiajs/eden': + specifier: ^1.4.6 + version: 1.4.6(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)) + '@memohome/agent': + specifier: workspace:* + version: link:../agent + '@memohome/db': + specifier: workspace:* + version: link:../db + '@memohome/memory': + specifier: workspace:* + version: link:../memory + '@memohome/shared': + specifier: workspace:* + version: link:../shared + drizzle-orm: + specifier: ^0.45.1 + version: 0.45.1(@cloudflare/workers-types@4.20260109.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.5)(pg@8.16.3)(sqlite3@5.1.7) elysia: specifier: latest version: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3) + node-cron: + specifier: ^4.2.1 + version: 4.2.1 + zod: + specifier: ^4.3.5 + version: 4.3.5 devDependencies: bun-types: specifier: latest @@ -100,6 +130,9 @@ importers: packages/db: dependencies: + '@memohome/shared': + specifier: workspace:* + version: link:../shared dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -524,6 +557,21 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@elysiajs/cors@1.4.1': + resolution: {integrity: sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ==} + peerDependencies: + elysia: '>= 1.4.0' + + '@elysiajs/cron@1.4.1': + resolution: {integrity: sha512-Y+jqXtMJ+m17QzNWlWc09ugd1Fn1Wh7lqE+y9qSW8eiQZEqsRADXWmbLuXRSRaSC5dkfOyRSiwNCp6n8L/yuqA==} + peerDependencies: + elysia: '>= 1.4.0' + + '@elysiajs/eden@1.4.6': + resolution: {integrity: sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q==} + peerDependencies: + elysia: '>=1.4.19' + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -2129,6 +2177,10 @@ packages: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + croner@6.0.7: + resolution: {integrity: sha512-k3Xx3Rcclfr60Yx4TmvsF3Yscuiql8LSvYLaphTsaq5Hk8La4Z/udmUANMOTKpgGGroI2F6/XOr9cU9OFkYluQ==} + engines: {node: '>=6.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -3253,6 +3305,10 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-cron@4.2.1: + resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==} + engines: {node: '>=6.0.0'} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -4592,6 +4648,19 @@ snapshots: '@drizzle-team/brocli@0.10.2': {} + '@elysiajs/cors@1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))': + dependencies: + elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3) + + '@elysiajs/cron@1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))': + dependencies: + croner: 6.0.7 + elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3) + + '@elysiajs/eden@1.4.6(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))': + dependencies: + elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3) + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -6139,6 +6208,8 @@ snapshots: dependencies: is-what: 5.5.0 + croner@6.0.7: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -7253,6 +7324,8 @@ snapshots: node-addon-api@7.1.1: {} + node-cron@4.2.1: {} + node-domexception@1.0.0: {} node-fetch@2.7.0(encoding@0.1.13):