cognova 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +58 -0
- package/Claude/CLAUDE.md +92 -0
- package/Claude/hooks/lib/__init__.py +1 -0
- package/Claude/hooks/lib/hook_client.py +207 -0
- package/Claude/hooks/log-event.py +97 -0
- package/Claude/hooks/pre-compact.py +46 -0
- package/Claude/hooks/session-end.py +26 -0
- package/Claude/hooks/session-start.py +35 -0
- package/Claude/hooks/stop-extract.py +40 -0
- package/Claude/rules/frontmatter.md +54 -0
- package/Claude/rules/markdown.md +43 -0
- package/Claude/rules/note-organization.md +33 -0
- package/Claude/settings.json +54 -0
- package/Claude/skills/README.md +136 -0
- package/Claude/skills/_lib/__init__.py +1 -0
- package/Claude/skills/_lib/api.py +164 -0
- package/Claude/skills/_lib/output.py +95 -0
- package/Claude/skills/environment/SKILL.md +73 -0
- package/Claude/skills/environment/environment.py +239 -0
- package/Claude/skills/memory/SKILL.md +153 -0
- package/Claude/skills/memory/memory.py +270 -0
- package/Claude/skills/project/SKILL.md +105 -0
- package/Claude/skills/project/project.py +203 -0
- package/Claude/skills/skill-creator/SKILL.md +261 -0
- package/Claude/skills/task/SKILL.md +135 -0
- package/Claude/skills/task/task.py +310 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +39 -0
- package/app/assets/css/main.css +10 -0
- package/app/components/AppLogo.vue +40 -0
- package/app/components/AssistantPanel.client.vue +518 -0
- package/app/components/ConfirmModal.vue +84 -0
- package/app/components/TemplateMenu.vue +49 -0
- package/app/components/agents/AgentActivityChart.client.vue +105 -0
- package/app/components/agents/AgentActivityChart.server.vue +25 -0
- package/app/components/agents/AgentForm.vue +304 -0
- package/app/components/agents/AgentRunModal.vue +154 -0
- package/app/components/agents/AgentStatsCards.vue +98 -0
- package/app/components/chat/ChatInput.vue +85 -0
- package/app/components/chat/ConversationList.vue +78 -0
- package/app/components/chat/MessageBubble.vue +81 -0
- package/app/components/chat/StreamingMessage.vue +36 -0
- package/app/components/chat/ToolCallBlock.vue +77 -0
- package/app/components/editor/CodeEditor.client.vue +212 -0
- package/app/components/editor/CodeEditorFallback.vue +12 -0
- package/app/components/editor/DocumentEditor.vue +326 -0
- package/app/components/editor/DocumentMetadata.vue +140 -0
- package/app/components/editor/MarkdownEditor.vue +146 -0
- package/app/components/files/FileTree.vue +436 -0
- package/app/components/hooks/HookActivityChart.client.vue +117 -0
- package/app/components/hooks/HookActivityChart.server.vue +25 -0
- package/app/components/hooks/HookStatsCards.vue +63 -0
- package/app/components/hooks/RecentEventsTable.vue +123 -0
- package/app/components/hooks/ToolBreakdownTable.vue +72 -0
- package/app/components/search/DashboardSearch.vue +122 -0
- package/app/components/tasks/ProjectSelect.vue +35 -0
- package/app/components/tasks/TaskCard.vue +182 -0
- package/app/components/tasks/TaskDetail.vue +160 -0
- package/app/components/tasks/TaskForm.vue +280 -0
- package/app/components/tasks/TaskList.vue +69 -0
- package/app/components/view/ViewToc.vue +85 -0
- package/app/composables/useAgents.ts +153 -0
- package/app/composables/useAuth.ts +73 -0
- package/app/composables/useChat.ts +298 -0
- package/app/composables/useDocument.ts +141 -0
- package/app/composables/useEditor.ts +100 -0
- package/app/composables/useFileTree.ts +220 -0
- package/app/composables/useHookEvents.ts +68 -0
- package/app/composables/useMemories.ts +83 -0
- package/app/composables/useNotificationBus.ts +154 -0
- package/app/composables/usePreferences.ts +131 -0
- package/app/composables/useProjects.ts +97 -0
- package/app/composables/useSearch.ts +52 -0
- package/app/composables/useTasks.ts +201 -0
- package/app/composables/useTerminal.ts +135 -0
- package/app/layouts/auth.vue +20 -0
- package/app/layouts/dashboard.vue +186 -0
- package/app/layouts/view.vue +60 -0
- package/app/middleware/auth.ts +9 -0
- package/app/pages/agents/[id].vue +602 -0
- package/app/pages/agents/index.vue +412 -0
- package/app/pages/chat.vue +146 -0
- package/app/pages/dashboard.vue +80 -0
- package/app/pages/docs.vue +131 -0
- package/app/pages/hooks.vue +163 -0
- package/app/pages/index.vue +249 -0
- package/app/pages/login.vue +60 -0
- package/app/pages/memories.vue +282 -0
- package/app/pages/settings.vue +625 -0
- package/app/pages/tasks.vue +312 -0
- package/app/pages/view/[uuid].vue +376 -0
- package/dist/cli/index.js +2711 -0
- package/drizzle.config.ts +10 -0
- package/nuxt.config.ts +98 -0
- package/package.json +107 -0
- package/server/api/agents/[id]/cancel.post.ts +27 -0
- package/server/api/agents/[id]/run.post.ts +34 -0
- package/server/api/agents/[id]/runs.get.ts +45 -0
- package/server/api/agents/[id]/stats.get.ts +94 -0
- package/server/api/agents/[id].delete.ts +29 -0
- package/server/api/agents/[id].get.ts +25 -0
- package/server/api/agents/[id].patch.ts +55 -0
- package/server/api/agents/index.get.ts +15 -0
- package/server/api/agents/index.post.ts +48 -0
- package/server/api/agents/stats.get.ts +86 -0
- package/server/api/auth/[...all].ts +5 -0
- package/server/api/conversations/[id].delete.ts +16 -0
- package/server/api/conversations/[id].get.ts +34 -0
- package/server/api/conversations/index.get.ts +17 -0
- package/server/api/documents/[id]/index.delete.ts +47 -0
- package/server/api/documents/[id]/index.put.ts +102 -0
- package/server/api/documents/[id]/public.get.ts +60 -0
- package/server/api/documents/[id]/restore.post.ts +65 -0
- package/server/api/documents/by-path.post.ts +168 -0
- package/server/api/documents/index.get.ts +48 -0
- package/server/api/fs/delete.post.ts +41 -0
- package/server/api/fs/list.get.ts +99 -0
- package/server/api/fs/mkdir.post.ts +44 -0
- package/server/api/fs/move.post.ts +68 -0
- package/server/api/fs/read.post.ts +48 -0
- package/server/api/fs/rename.post.ts +55 -0
- package/server/api/fs/write.post.ts +51 -0
- package/server/api/health.get.ts +40 -0
- package/server/api/home.get.ts +26 -0
- package/server/api/hooks/events/index.get.ts +56 -0
- package/server/api/hooks/events/index.post.ts +36 -0
- package/server/api/hooks/stats.get.ts +99 -0
- package/server/api/memory/[id].delete.ts +26 -0
- package/server/api/memory/context.get.ts +83 -0
- package/server/api/memory/extract.post.ts +42 -0
- package/server/api/memory/search.get.ts +70 -0
- package/server/api/memory/store.post.ts +31 -0
- package/server/api/projects/[id]/index.delete.ts +40 -0
- package/server/api/projects/[id]/index.get.ts +25 -0
- package/server/api/projects/[id]/index.put.ts +50 -0
- package/server/api/projects/index.get.ts +20 -0
- package/server/api/projects/index.post.ts +34 -0
- package/server/api/secrets/[key].delete.ts +31 -0
- package/server/api/secrets/[key].get.ts +30 -0
- package/server/api/secrets/[key].put.ts +52 -0
- package/server/api/secrets/index.get.ts +20 -0
- package/server/api/secrets/index.post.ts +58 -0
- package/server/api/tasks/[id]/index.delete.ts +46 -0
- package/server/api/tasks/[id]/index.get.ts +24 -0
- package/server/api/tasks/[id]/index.put.ts +70 -0
- package/server/api/tasks/[id]/restore.post.ts +49 -0
- package/server/api/tasks/index.get.ts +53 -0
- package/server/api/tasks/index.post.ts +47 -0
- package/server/api/tasks/tags.get.ts +21 -0
- package/server/api/user/email.patch.ts +56 -0
- package/server/db/index.ts +76 -0
- package/server/db/migrate.ts +41 -0
- package/server/db/schema.ts +345 -0
- package/server/db/seed.ts +46 -0
- package/server/db/types.ts +28 -0
- package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
- package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
- package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
- package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
- package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
- package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
- package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
- package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
- package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
- package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
- package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
- package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
- package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
- package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
- package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
- package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
- package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
- package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
- package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
- package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
- package/server/drizzle/migrations/meta/_journal.json +76 -0
- package/server/middleware/auth.ts +79 -0
- package/server/plugins/00.env-validate.ts +38 -0
- package/server/plugins/01.api-token.ts +31 -0
- package/server/plugins/02.database.ts +54 -0
- package/server/plugins/03.file-watcher.ts +65 -0
- package/server/plugins/04.cron-agents.ts +26 -0
- package/server/routes/_ws/chat.ts +252 -0
- package/server/routes/notifications.ts +47 -0
- package/server/routes/terminal.ts +98 -0
- package/server/services/agent-executor.ts +218 -0
- package/server/services/cron-scheduler.ts +78 -0
- package/server/services/memory-extractor.ts +120 -0
- package/server/utils/agent-cleanup.ts +91 -0
- package/server/utils/agent-registry.ts +95 -0
- package/server/utils/auth.ts +33 -0
- package/server/utils/chat-session-manager.ts +59 -0
- package/server/utils/crypto.ts +40 -0
- package/server/utils/db-guard.ts +12 -0
- package/server/utils/db-state.ts +63 -0
- package/server/utils/document-sync.ts +207 -0
- package/server/utils/frontmatter.ts +84 -0
- package/server/utils/notification-bus.ts +60 -0
- package/server/utils/path-validator.ts +55 -0
- package/server/utils/pty-manager.ts +130 -0
- package/shared/types/index.ts +604 -0
- package/shared/utils/language-detection.ts +87 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import { getDb, schema } from '~~/server/db'
|
|
3
|
+
import { requireDb } from '~~/server/utils/db-guard'
|
|
4
|
+
import type { UpdateTaskInput } from '~~/shared/types'
|
|
5
|
+
|
|
6
|
+
export default defineEventHandler(async (event) => {
|
|
7
|
+
requireDb(event)
|
|
8
|
+
|
|
9
|
+
const id = getRouterParam(event, 'id')
|
|
10
|
+
|
|
11
|
+
if (!id)
|
|
12
|
+
throw createError({ statusCode: 400, message: 'Task ID is required' })
|
|
13
|
+
|
|
14
|
+
const body = await readBody<UpdateTaskInput>(event)
|
|
15
|
+
|
|
16
|
+
// Validate priority if provided
|
|
17
|
+
if (body.priority !== undefined && (body.priority < 1 || body.priority > 3))
|
|
18
|
+
throw createError({ statusCode: 400, message: 'Priority must be 1 (Low), 2 (Medium), or 3 (High)' })
|
|
19
|
+
|
|
20
|
+
const db = getDb()
|
|
21
|
+
|
|
22
|
+
// Check task exists
|
|
23
|
+
const [existing] = await db
|
|
24
|
+
.select({ id: schema.tasks.id, status: schema.tasks.status })
|
|
25
|
+
.from(schema.tasks)
|
|
26
|
+
.where(eq(schema.tasks.id, id))
|
|
27
|
+
.limit(1)
|
|
28
|
+
|
|
29
|
+
if (!existing)
|
|
30
|
+
throw createError({ statusCode: 404, message: 'Task not found' })
|
|
31
|
+
|
|
32
|
+
const userId = event.context.user?.id
|
|
33
|
+
|
|
34
|
+
const updates: Record<string, unknown> = {
|
|
35
|
+
modifiedAt: new Date(),
|
|
36
|
+
modifiedBy: userId
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (body.title !== undefined) updates.title = body.title.trim()
|
|
40
|
+
if (body.description !== undefined) updates.description = body.description?.trim() || null
|
|
41
|
+
if (body.priority !== undefined) updates.priority = body.priority
|
|
42
|
+
if (body.projectId !== undefined) updates.projectId = body.projectId || null
|
|
43
|
+
if (body.tags !== undefined) updates.tags = body.tags
|
|
44
|
+
if (body.dueDate !== undefined) updates.dueDate = body.dueDate ? new Date(body.dueDate) : null
|
|
45
|
+
|
|
46
|
+
// Handle status change
|
|
47
|
+
if (body.status !== undefined) {
|
|
48
|
+
updates.status = body.status
|
|
49
|
+
|
|
50
|
+
// Auto-set completedAt when marking as done
|
|
51
|
+
if (body.status === 'done' && existing.status !== 'done')
|
|
52
|
+
updates.completedAt = new Date()
|
|
53
|
+
else if (body.status !== 'done' && existing.status === 'done')
|
|
54
|
+
updates.completedAt = null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await db
|
|
58
|
+
.update(schema.tasks)
|
|
59
|
+
.set(updates)
|
|
60
|
+
.where(eq(schema.tasks.id, id))
|
|
61
|
+
|
|
62
|
+
// Fetch updated task with project relation
|
|
63
|
+
const [task] = await db.query.tasks.findMany({
|
|
64
|
+
where: (tasks, { eq }) => eq(tasks.id, id),
|
|
65
|
+
with: { project: true },
|
|
66
|
+
limit: 1
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
return { data: task }
|
|
70
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import { getDb, schema } from '~~/server/db'
|
|
3
|
+
import { requireDb } from '~~/server/utils/db-guard'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
requireDb(event)
|
|
7
|
+
|
|
8
|
+
const id = getRouterParam(event, 'id')
|
|
9
|
+
|
|
10
|
+
if (!id)
|
|
11
|
+
throw createError({ statusCode: 400, message: 'Task ID is required' })
|
|
12
|
+
|
|
13
|
+
const db = getDb()
|
|
14
|
+
|
|
15
|
+
// Check task exists
|
|
16
|
+
const [existing] = await db
|
|
17
|
+
.select({ id: schema.tasks.id, deletedAt: schema.tasks.deletedAt })
|
|
18
|
+
.from(schema.tasks)
|
|
19
|
+
.where(eq(schema.tasks.id, id))
|
|
20
|
+
.limit(1)
|
|
21
|
+
|
|
22
|
+
if (!existing)
|
|
23
|
+
throw createError({ statusCode: 404, message: 'Task not found' })
|
|
24
|
+
|
|
25
|
+
if (!existing.deletedAt)
|
|
26
|
+
throw createError({ statusCode: 400, message: 'Task is not deleted' })
|
|
27
|
+
|
|
28
|
+
const userId = event.context.user?.id
|
|
29
|
+
|
|
30
|
+
// Restore
|
|
31
|
+
await db
|
|
32
|
+
.update(schema.tasks)
|
|
33
|
+
.set({
|
|
34
|
+
deletedAt: null,
|
|
35
|
+
deletedBy: null,
|
|
36
|
+
modifiedAt: new Date(),
|
|
37
|
+
modifiedBy: userId
|
|
38
|
+
})
|
|
39
|
+
.where(eq(schema.tasks.id, id))
|
|
40
|
+
|
|
41
|
+
// Fetch updated task with project relation
|
|
42
|
+
const [task] = await db.query.tasks.findMany({
|
|
43
|
+
where: (tasks, { eq }) => eq(tasks.id, id),
|
|
44
|
+
with: { project: true },
|
|
45
|
+
limit: 1
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return { data: task }
|
|
49
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { eq, isNull, ilike, inArray, and, or } from 'drizzle-orm'
|
|
2
|
+
import { getDb, schema } from '~~/server/db'
|
|
3
|
+
import { requireDb } from '~~/server/utils/db-guard'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
requireDb(event)
|
|
7
|
+
|
|
8
|
+
const query = getQuery(event)
|
|
9
|
+
const includeDeleted = query.includeDeleted === 'true'
|
|
10
|
+
const status = query.status as string | string[] | undefined
|
|
11
|
+
const projectId = query.projectId as string | undefined
|
|
12
|
+
const search = query.search as string | undefined
|
|
13
|
+
|
|
14
|
+
const db = getDb()
|
|
15
|
+
|
|
16
|
+
const conditions = []
|
|
17
|
+
|
|
18
|
+
// Soft delete filter
|
|
19
|
+
if (!includeDeleted)
|
|
20
|
+
conditions.push(isNull(schema.tasks.deletedAt))
|
|
21
|
+
|
|
22
|
+
// Status filter (can be single or array)
|
|
23
|
+
if (status) {
|
|
24
|
+
const statuses = (Array.isArray(status) ? status : [status]) as ('todo' | 'in_progress' | 'done' | 'blocked')[]
|
|
25
|
+
conditions.push(inArray(schema.tasks.status, statuses))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Project filter
|
|
29
|
+
if (projectId)
|
|
30
|
+
conditions.push(eq(schema.tasks.projectId, projectId))
|
|
31
|
+
|
|
32
|
+
// Search filter (title and description)
|
|
33
|
+
if (search) {
|
|
34
|
+
const searchPattern = `%${search}%`
|
|
35
|
+
conditions.push(
|
|
36
|
+
or(
|
|
37
|
+
ilike(schema.tasks.title, searchPattern),
|
|
38
|
+
ilike(schema.tasks.description, searchPattern)
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tasks = await db.query.tasks.findMany({
|
|
44
|
+
where: conditions.length > 0 ? and(...conditions) : undefined,
|
|
45
|
+
with: {
|
|
46
|
+
project: true,
|
|
47
|
+
creator: true
|
|
48
|
+
},
|
|
49
|
+
orderBy: (tasks, { desc }) => [desc(tasks.createdAt)]
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return { data: tasks }
|
|
53
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getDb, schema } from '~~/server/db'
|
|
2
|
+
import { requireDb } from '~~/server/utils/db-guard'
|
|
3
|
+
import type { CreateTaskInput } from '~~/shared/types'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
requireDb(event)
|
|
7
|
+
|
|
8
|
+
const body = await readBody<CreateTaskInput>(event)
|
|
9
|
+
|
|
10
|
+
if (!body.title?.trim())
|
|
11
|
+
throw createError({ statusCode: 400, message: 'Task title is required' })
|
|
12
|
+
|
|
13
|
+
// Validate priority if provided
|
|
14
|
+
if (body.priority !== undefined && (body.priority < 1 || body.priority > 3))
|
|
15
|
+
throw createError({ statusCode: 400, message: 'Priority must be 1 (Low), 2 (Medium), or 3 (High)' })
|
|
16
|
+
|
|
17
|
+
const db = getDb()
|
|
18
|
+
|
|
19
|
+
const userId = event.context.user?.id
|
|
20
|
+
|
|
21
|
+
const result = await db
|
|
22
|
+
.insert(schema.tasks)
|
|
23
|
+
.values({
|
|
24
|
+
title: body.title.trim(),
|
|
25
|
+
description: body.description?.trim() || null,
|
|
26
|
+
status: body.status || 'todo',
|
|
27
|
+
priority: body.priority ?? 2,
|
|
28
|
+
projectId: body.projectId || null,
|
|
29
|
+
dueDate: body.dueDate ? new Date(body.dueDate) : null,
|
|
30
|
+
tags: body.tags || [],
|
|
31
|
+
createdBy: userId
|
|
32
|
+
})
|
|
33
|
+
.returning()
|
|
34
|
+
|
|
35
|
+
const task = result[0]
|
|
36
|
+
if (!task)
|
|
37
|
+
throw createError({ statusCode: 500, message: 'Failed to create task' })
|
|
38
|
+
|
|
39
|
+
// Fetch with project relation
|
|
40
|
+
const [taskWithProject] = await db.query.tasks.findMany({
|
|
41
|
+
where: (tasks, { eq }) => eq(tasks.id, task.id),
|
|
42
|
+
with: { project: true },
|
|
43
|
+
limit: 1
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return { data: taskWithProject }
|
|
47
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isNull } from 'drizzle-orm'
|
|
2
|
+
import { getDb, schema } from '~~/server/db'
|
|
3
|
+
import { requireDb } from '~~/server/utils/db-guard'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
requireDb(event)
|
|
7
|
+
|
|
8
|
+
const db = getDb()
|
|
9
|
+
|
|
10
|
+
// Get all unique tags from non-deleted tasks
|
|
11
|
+
const result = await db
|
|
12
|
+
.select({ tags: schema.tasks.tags })
|
|
13
|
+
.from(schema.tasks)
|
|
14
|
+
.where(isNull(schema.tasks.deletedAt))
|
|
15
|
+
|
|
16
|
+
// Flatten and deduplicate tags
|
|
17
|
+
const allTags = result.flatMap(r => r.tags || [])
|
|
18
|
+
const uniqueTags = [...new Set(allTags)].sort()
|
|
19
|
+
|
|
20
|
+
return { data: uniqueTags }
|
|
21
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import { getDb, schema } from '../../db'
|
|
3
|
+
import { auth } from '../../utils/auth'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const session = await auth.api.getSession({ headers: event.headers })
|
|
7
|
+
|
|
8
|
+
if (!session?.user) {
|
|
9
|
+
throw createError({
|
|
10
|
+
statusCode: 401,
|
|
11
|
+
message: 'Unauthorized'
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const body = await readBody<{ email: string }>(event)
|
|
16
|
+
|
|
17
|
+
if (!body.email) {
|
|
18
|
+
throw createError({
|
|
19
|
+
statusCode: 400,
|
|
20
|
+
message: 'Email is required'
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Basic email validation
|
|
25
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
26
|
+
if (!emailRegex.test(body.email)) {
|
|
27
|
+
throw createError({
|
|
28
|
+
statusCode: 400,
|
|
29
|
+
message: 'Invalid email format'
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const db = getDb()
|
|
34
|
+
|
|
35
|
+
// Check if email is already taken by another user
|
|
36
|
+
const existingUser = await db
|
|
37
|
+
.select()
|
|
38
|
+
.from(schema.user)
|
|
39
|
+
.where(eq(schema.user.email, body.email))
|
|
40
|
+
.limit(1)
|
|
41
|
+
|
|
42
|
+
if (existingUser.length > 0 && existingUser[0]!.id !== session.user.id) {
|
|
43
|
+
throw createError({
|
|
44
|
+
statusCode: 409,
|
|
45
|
+
message: 'Email is already in use'
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Update the user's email directly
|
|
50
|
+
await db
|
|
51
|
+
.update(schema.user)
|
|
52
|
+
.set({ email: body.email })
|
|
53
|
+
.where(eq(schema.user.id, session.user.id))
|
|
54
|
+
|
|
55
|
+
return { success: true, email: body.email }
|
|
56
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
2
|
+
import postgres from 'postgres'
|
|
3
|
+
import { sql } from 'drizzle-orm'
|
|
4
|
+
import * as schema from './schema'
|
|
5
|
+
import { setDbState } from '~~/server/utils/db-state'
|
|
6
|
+
|
|
7
|
+
interface DbConfig {
|
|
8
|
+
ssl: boolean | 'require' | 'prefer'
|
|
9
|
+
max: number
|
|
10
|
+
idleTimeout: number
|
|
11
|
+
connectTimeout: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getDbConfig(): DbConfig {
|
|
15
|
+
const url = process.env.DATABASE_URL || ''
|
|
16
|
+
const isNeon = url.includes('neon.tech')
|
|
17
|
+
const isLocal = url.includes('localhost') || url.includes('db:5432') || url.includes('127.0.0.1')
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
ssl: isNeon ? 'require' : false,
|
|
21
|
+
max: isLocal ? 10 : 5,
|
|
22
|
+
idleTimeout: isNeon ? 20 : 0,
|
|
23
|
+
connectTimeout: isNeon ? 10 : 5
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let queryClient: ReturnType<typeof postgres> | null = null
|
|
28
|
+
let db: ReturnType<typeof drizzle<typeof schema>> | null = null
|
|
29
|
+
|
|
30
|
+
export function getDb() {
|
|
31
|
+
if (!db) {
|
|
32
|
+
const connectionString = process.env.DATABASE_URL
|
|
33
|
+
if (!connectionString)
|
|
34
|
+
throw new Error('DATABASE_URL not configured')
|
|
35
|
+
|
|
36
|
+
const config = getDbConfig()
|
|
37
|
+
queryClient = postgres(connectionString, {
|
|
38
|
+
ssl: config.ssl,
|
|
39
|
+
max: config.max,
|
|
40
|
+
idle_timeout: config.idleTimeout,
|
|
41
|
+
connect_timeout: config.connectTimeout
|
|
42
|
+
})
|
|
43
|
+
db = drizzle(queryClient, { schema })
|
|
44
|
+
}
|
|
45
|
+
return db
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getQueryClient() {
|
|
49
|
+
if (!queryClient)
|
|
50
|
+
getDb()
|
|
51
|
+
return queryClient!
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function warmupDb(): Promise<boolean> {
|
|
55
|
+
try {
|
|
56
|
+
const database = getDb()
|
|
57
|
+
await database.execute(sql`SELECT 1`)
|
|
58
|
+
setDbState(true)
|
|
59
|
+
console.log('[db] Connection verified')
|
|
60
|
+
return true
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('[db] Warmup failed:', error)
|
|
63
|
+
setDbState(false, error instanceof Error ? error.message : 'Connection failed')
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function closeDb(): Promise<void> {
|
|
69
|
+
if (queryClient) {
|
|
70
|
+
await queryClient.end()
|
|
71
|
+
queryClient = null
|
|
72
|
+
db = null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { schema }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { migrate } from 'drizzle-orm/postgres-js/migrator'
|
|
2
|
+
import { sql } from 'drizzle-orm'
|
|
3
|
+
import { getDb } from './index'
|
|
4
|
+
|
|
5
|
+
const MIGRATION_LOCK_ID = 728492 // Arbitrary but consistent ID
|
|
6
|
+
|
|
7
|
+
export async function runMigrations(): Promise<boolean> {
|
|
8
|
+
const db = getDb()
|
|
9
|
+
|
|
10
|
+
console.log('[db] Attempting to acquire migration lock...')
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Try to acquire advisory lock (non-blocking)
|
|
14
|
+
const lockResult = await db.execute<{ pg_try_advisory_lock: boolean }>(
|
|
15
|
+
sql`SELECT pg_try_advisory_lock(${MIGRATION_LOCK_ID})`
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const acquired = lockResult[0]?.pg_try_advisory_lock === true
|
|
19
|
+
|
|
20
|
+
if (!acquired) {
|
|
21
|
+
console.log('[db] Migration lock held by another instance, skipping')
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log('[db] Lock acquired, running migrations...')
|
|
26
|
+
await migrate(db, { migrationsFolder: './server/drizzle/migrations' })
|
|
27
|
+
console.log('[db] Migrations complete')
|
|
28
|
+
|
|
29
|
+
return true
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('[db] Migration failed:', error)
|
|
32
|
+
throw error
|
|
33
|
+
} finally {
|
|
34
|
+
// Always release the lock
|
|
35
|
+
try {
|
|
36
|
+
await db.execute(sql`SELECT pg_advisory_unlock(${MIGRATION_LOCK_ID})`)
|
|
37
|
+
} catch {
|
|
38
|
+
// Ignore unlock errors
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|