refactor(db): add Convex HTTP client for server-side calls
- commit
- 47beb8fc0b3a76ca33a6ae2050c1b784e0990b6e
- author
- bnema · · 2026-01-18 18:34 UTC
- parent
- d01ad39
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +147 -0
A src/lib/db/convex.server.ts +147 -0
| @@ -0,0 +1,147 @@ | |||
| 1 | + | import { CONVEX_API_SECRET } from '$env/static/private'; | |
| 2 | + | import { PUBLIC_CONVEX_URL } from '$env/static/public'; | |
| 3 | + | ||
| 4 | + | // Convert .cloud URL to .site URL for HTTP actions | |
| 5 | + | function getHttpUrl(): string { | |
| 6 | + | return PUBLIC_CONVEX_URL.replace('.convex.cloud', '.convex.site'); | |
| 7 | + | } | |
| 8 | + | ||
| 9 | + | // Generic fetch helper for Convex HTTP endpoints (server-side only) | |
| 10 | + | async function convexFetch<T>(path: string, body: Record<string, unknown> = {}): Promise<T> { | |
| 11 | + | const url = `${getHttpUrl()}${path}`; | |
| 12 | + | ||
| 13 | + | const response = await fetch(url, { | |
| 14 | + | method: 'POST', | |
| 15 | + | headers: { | |
| 16 | + | 'Content-Type': 'application/json', | |
| 17 | + | 'X-Convex-Secret': CONVEX_API_SECRET | |
| 18 | + | }, | |
| 19 | + | body: JSON.stringify(body) | |
| 20 | + | }); | |
| 21 | + | ||
| 22 | + | if (!response.ok) { | |
| 23 | + | const error = await response.text(); | |
| 24 | + | throw new Error(`Convex HTTP error: ${response.status} - ${error}`); | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | return response.json(); | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // ============ USERS ============ | |
| 31 | + | ||
| 32 | + | export const users = { | |
| 33 | + | getByNickname: (nickname: string) => | |
| 34 | + | convexFetch<ConvexUser | null>('/api/users/getByNickname', { nickname }), | |
| 35 | + | ||
| 36 | + | getById: (id: string) => convexFetch<ConvexUser | null>('/api/users/getById', { id }), | |
| 37 | + | ||
| 38 | + | create: (nickname: string, password: string) => | |
| 39 | + | convexFetch<{ userId: string }>('/api/users/create', { nickname, password }), | |
| 40 | + | ||
| 41 | + | updateStatus: (userId: string, status: string) => | |
| 42 | + | convexFetch<ConvexUser>('/api/users/updateStatus', { userId, status }), | |
| 43 | + | ||
| 44 | + | list: () => convexFetch<ConvexUser[]>('/api/users/list', {}), | |
| 45 | + | ||
| 46 | + | setAllOffline: () => convexFetch<{ updated: number }>('/api/users/setAllOffline', {}) | |
| 47 | + | }; | |
| 48 | + | ||
| 49 | + | // ============ SESSIONS ============ | |
| 50 | + | ||
| 51 | + | export const sessions = { | |
| 52 | + | create: (tokenHash: string, userId: string, expiresAt: number) => | |
| 53 | + | convexFetch<{ id: string; tokenHash: string }>('/api/sessions/create', { | |
| 54 | + | tokenHash, | |
| 55 | + | userId, | |
| 56 | + | expiresAt | |
| 57 | + | }), | |
| 58 | + | ||
| 59 | + | validate: (tokenHash: string) => | |
| 60 | + | convexFetch<{ session: ConvexSession | null; user: ConvexUser | null }>( | |
| 61 | + | '/api/sessions/validate', | |
| 62 | + | { tokenHash } | |
| 63 | + | ), | |
| 64 | + | ||
| 65 | + | remove: (tokenHash: string) => | |
| 66 | + | convexFetch<{ success: boolean }>('/api/sessions/remove', { tokenHash }) | |
| 67 | + | }; | |
| 68 | + | ||
| 69 | + | // ============ CHAT ROOMS ============ | |
| 70 | + | ||
| 71 | + | export const chatRooms = { | |
| 72 | + | getOrCreateDefault: () => convexFetch<ConvexChatRoom>('/api/chatRooms/getOrCreateDefault', {}), | |
| 73 | + | ||
| 74 | + | getById: (id: string) => convexFetch<ConvexChatRoom | null>('/api/chatRooms/getById', { id }) | |
| 75 | + | }; | |
| 76 | + | ||
| 77 | + | // ============ MESSAGES ============ | |
| 78 | + | ||
| 79 | + | export const messages = { | |
| 80 | + | getByRoom: (roomId: string, limit?: number, beforeTimestamp?: number) => | |
| 81 | + | convexFetch<{ messages: ConvexMessage[]; hasMore: boolean }>('/api/messages/getByRoom', { | |
| 82 | + | roomId, | |
| 83 | + | limit, | |
| 84 | + | beforeTimestamp | |
| 85 | + | }), | |
| 86 | + | ||
| 87 | + | send: ( | |
| 88 | + | chatRoomId: string, | |
| 89 | + | senderId: string, | |
| 90 | + | content: string, | |
| 91 | + | type?: string, | |
| 92 | + | styleData?: string, | |
| 93 | + | hasFormatting?: boolean | |
| 94 | + | ) => | |
| 95 | + | convexFetch<ConvexMessage>('/api/messages/send', { | |
| 96 | + | chatRoomId, | |
| 97 | + | senderId, | |
| 98 | + | content, | |
| 99 | + | type, | |
| 100 | + | styleData, | |
| 101 | + | hasFormatting | |
| 102 | + | }) | |
| 103 | + | }; | |
| 104 | + | ||
| 105 | + | // ============ TYPES ============ | |
| 106 | + | ||
| 107 | + | export interface ConvexUser { | |
| 108 | + | _id: string; | |
| 109 | + | _creationTime: number; | |
| 110 | + | password: string; | |
| 111 | + | nickname: string; | |
| 112 | + | status: string; | |
| 113 | + | avatarUrl?: string; | |
| 114 | + | createdAt: number; | |
| 115 | + | lastSeen?: number; | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | export interface ConvexSession { | |
| 119 | + | _id: string; | |
| 120 | + | _creationTime: number; | |
| 121 | + | tokenHash: string; | |
| 122 | + | userId: string; | |
| 123 | + | expiresAt: number; | |
| 124 | + | createdAt: number; | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | export interface ConvexMessage { | |
| 128 | + | _id: string; | |
| 129 | + | _creationTime: number; | |
| 130 | + | chatRoomId: string; | |
| 131 | + | senderId: string; | |
| 132 | + | content: string; | |
| 133 | + | type: string; | |
| 134 | + | timestamp: number; | |
| 135 | + | styleData?: string; | |
| 136 | + | hasFormatting?: boolean; | |
| 137 | + | } | |
| 138 | + | ||
| 139 | + | export interface ConvexChatRoom { | |
| 140 | + | _id: string; | |
| 141 | + | _creationTime: number; | |
| 142 | + | name?: string; | |
| 143 | + | type: string; | |
| 144 | + | createdAt: number; | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | export default { users, sessions, messages, chatRooms }; | |
| @@ -0,0 +1,147 @@ | |||||
| 1 | + | import { CONVEX_API_SECRET } from '$env/static/private'; | |||
| 2 | + | import { PUBLIC_CONVEX_URL } from '$env/static/public'; | |||
| 3 | + | ||||
| 4 | + | // Convert .cloud URL to .site URL for HTTP actions | |||
| 5 | + | function getHttpUrl(): string { | |||
| 6 | + | return PUBLIC_CONVEX_URL.replace('.convex.cloud', '.convex.site'); | |||
| 7 | + | } | |||
| 8 | + | ||||
| 9 | + | // Generic fetch helper for Convex HTTP endpoints (server-side only) | |||
| 10 | + | async function convexFetch<T>(path: string, body: Record<string, unknown> = {}): Promise<T> { | |||
| 11 | + | const url = `${getHttpUrl()}${path}`; | |||
| 12 | + | ||||
| 13 | + | const response = await fetch(url, { | |||
| 14 | + | method: 'POST', | |||
| 15 | + | headers: { | |||
| 16 | + | 'Content-Type': 'application/json', | |||
| 17 | + | 'X-Convex-Secret': CONVEX_API_SECRET | |||
| 18 | + | }, | |||
| 19 | + | body: JSON.stringify(body) | |||
| 20 | + | }); | |||
| 21 | + | ||||
| 22 | + | if (!response.ok) { | |||
| 23 | + | const error = await response.text(); | |||
| 24 | + | throw new Error(`Convex HTTP error: ${response.status} - ${error}`); | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | return response.json(); | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // ============ USERS ============ | |||
| 31 | + | ||||
| 32 | + | export const users = { | |||
| 33 | + | getByNickname: (nickname: string) => | |||
| 34 | + | convexFetch<ConvexUser | null>('/api/users/getByNickname', { nickname }), | |||
| 35 | + | ||||
| 36 | + | getById: (id: string) => convexFetch<ConvexUser | null>('/api/users/getById', { id }), | |||
| 37 | + | ||||
| 38 | + | create: (nickname: string, password: string) => | |||
| 39 | + | convexFetch<{ userId: string }>('/api/users/create', { nickname, password }), | |||
| 40 | + | ||||
| 41 | + | updateStatus: (userId: string, status: string) => | |||
| 42 | + | convexFetch<ConvexUser>('/api/users/updateStatus', { userId, status }), | |||
| 43 | + | ||||
| 44 | + | list: () => convexFetch<ConvexUser[]>('/api/users/list', {}), | |||
| 45 | + | ||||
| 46 | + | setAllOffline: () => convexFetch<{ updated: number }>('/api/users/setAllOffline', {}) | |||
| 47 | + | }; | |||
| 48 | + | ||||
| 49 | + | // ============ SESSIONS ============ | |||
| 50 | + | ||||
| 51 | + | export const sessions = { | |||
| 52 | + | create: (tokenHash: string, userId: string, expiresAt: number) => | |||
| 53 | + | convexFetch<{ id: string; tokenHash: string }>('/api/sessions/create', { | |||
| 54 | + | tokenHash, | |||
| 55 | + | userId, | |||
| 56 | + | expiresAt | |||
| 57 | + | }), | |||
| 58 | + | ||||
| 59 | + | validate: (tokenHash: string) => | |||
| 60 | + | convexFetch<{ session: ConvexSession | null; user: ConvexUser | null }>( | |||
| 61 | + | '/api/sessions/validate', | |||
| 62 | + | { tokenHash } | |||
| 63 | + | ), | |||
| 64 | + | ||||
| 65 | + | remove: (tokenHash: string) => | |||
| 66 | + | convexFetch<{ success: boolean }>('/api/sessions/remove', { tokenHash }) | |||
| 67 | + | }; | |||
| 68 | + | ||||
| 69 | + | // ============ CHAT ROOMS ============ | |||
| 70 | + | ||||
| 71 | + | export const chatRooms = { | |||
| 72 | + | getOrCreateDefault: () => convexFetch<ConvexChatRoom>('/api/chatRooms/getOrCreateDefault', {}), | |||
| 73 | + | ||||
| 74 | + | getById: (id: string) => convexFetch<ConvexChatRoom | null>('/api/chatRooms/getById', { id }) | |||
| 75 | + | }; | |||
| 76 | + | ||||
| 77 | + | // ============ MESSAGES ============ | |||
| 78 | + | ||||
| 79 | + | export const messages = { | |||
| 80 | + | getByRoom: (roomId: string, limit?: number, beforeTimestamp?: number) => | |||
| 81 | + | convexFetch<{ messages: ConvexMessage[]; hasMore: boolean }>('/api/messages/getByRoom', { | |||
| 82 | + | roomId, | |||
| 83 | + | limit, | |||
| 84 | + | beforeTimestamp | |||
| 85 | + | }), | |||
| 86 | + | ||||
| 87 | + | send: ( | |||
| 88 | + | chatRoomId: string, | |||
| 89 | + | senderId: string, | |||
| 90 | + | content: string, | |||
| 91 | + | type?: string, | |||
| 92 | + | styleData?: string, | |||
| 93 | + | hasFormatting?: boolean | |||
| 94 | + | ) => | |||
| 95 | + | convexFetch<ConvexMessage>('/api/messages/send', { | |||
| 96 | + | chatRoomId, | |||
| 97 | + | senderId, | |||
| 98 | + | content, | |||
| 99 | + | type, | |||
| 100 | + | styleData, | |||
| 101 | + | hasFormatting | |||
| 102 | + | }) | |||
| 103 | + | }; | |||
| 104 | + | ||||
| 105 | + | // ============ TYPES ============ | |||
| 106 | + | ||||
| 107 | + | export interface ConvexUser { | |||
| 108 | + | _id: string; | |||
| 109 | + | _creationTime: number; | |||
| 110 | + | password: string; | |||
| 111 | + | nickname: string; | |||
| 112 | + | status: string; | |||
| 113 | + | avatarUrl?: string; | |||
| 114 | + | createdAt: number; | |||
| 115 | + | lastSeen?: number; | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | export interface ConvexSession { | |||
| 119 | + | _id: string; | |||
| 120 | + | _creationTime: number; | |||
| 121 | + | tokenHash: string; | |||
| 122 | + | userId: string; | |||
| 123 | + | expiresAt: number; | |||
| 124 | + | createdAt: number; | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | export interface ConvexMessage { | |||
| 128 | + | _id: string; | |||
| 129 | + | _creationTime: number; | |||
| 130 | + | chatRoomId: string; | |||
| 131 | + | senderId: string; | |||
| 132 | + | content: string; | |||
| 133 | + | type: string; | |||
| 134 | + | timestamp: number; | |||
| 135 | + | styleData?: string; | |||
| 136 | + | hasFormatting?: boolean; | |||
| 137 | + | } | |||
| 138 | + | ||||
| 139 | + | export interface ConvexChatRoom { | |||
| 140 | + | _id: string; | |||
| 141 | + | _creationTime: number; | |||
| 142 | + | name?: string; | |||
| 143 | + | type: string; | |||
| 144 | + | createdAt: number; | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | export default { users, sessions, messages, chatRooms }; | |||