detached 47beb8f

refactor(db): add Convex HTTP client for server-side calls

commit
47beb8fc0b3a76ca33a6ae2050c1b784e0990b6e
author
bnema · · 2026-01-18 18:34 UTC
parent
d01ad39
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 };

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close