ework-web 0.2.1 → 0.3.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/src/views/home.ts CHANGED
@@ -3,7 +3,7 @@ import { listProjectsWithCounts, createProject, canAdminProject, StoreError, typ
3
3
  import { relTime } from "../render/components";
4
4
  import { getDefaultUpstreamUrl, webUrlFromClone } from "./projectUpstreams";
5
5
 
6
- function projectCard(p: ProjectWithCounts, viewer: UserRow | null): string {
6
+ async function projectCard(p: ProjectWithCounts, viewer: UserRow | null): Promise<string> {
7
7
  const issuesHref = `/${encodeURIComponent(p.owner)}/${encodeURIComponent(p.name)}/issues`;
8
8
  const settingsHref = `/${encodeURIComponent(p.owner)}/${encodeURIComponent(p.name)}/settings/upstreams`;
9
9
  const desc = p.description ? `<div class="pcard-desc">${escapeHtml(p.description)}</div>` : "";
@@ -12,7 +12,7 @@ function projectCard(p: ProjectWithCounts, viewer: UserRow | null): string {
12
12
  const upstreamIcon = upstreamWebUrl
13
13
  ? `<a class="pcard-icon" href="${escapeAttr(upstreamWebUrl)}" target="_blank" rel="noopener noreferrer" title="查看上游:${escapeAttr(upstreamWebUrl)}" aria-label="查看上游">🔗</a>`
14
14
  : "";
15
- const adminIcon = canAdminProject(p.id, viewer)
15
+ const adminIcon = await canAdminProject(p.id, viewer)
16
16
  ? `<a class="pcard-icon" href="${escapeAttr(settingsHref)}" title="项目设置(上游 / Webhooks / 成员)" aria-label="项目设置">⚙️</a>`
17
17
  : "";
18
18
  const iconsHtml = (upstreamIcon || adminIcon)
@@ -29,13 +29,14 @@ function projectCard(p: ProjectWithCounts, viewer: UserRow | null): string {
29
29
  </div>`;
30
30
  }
31
31
 
32
- export function buildHome(
32
+ export async function buildHome(
33
33
  viewer: UserRow | null,
34
34
  flash: { kind: "ok" | "err"; msg: string } | null = null
35
- ): string {
36
- const projects = listProjectsWithCounts();
35
+ ): Promise<string> {
36
+ const projects = await listProjectsWithCounts();
37
+ const cards = await Promise.all(projects.map((p) => projectCard(p, viewer)));
37
38
  const list = projects.length
38
- ? projects.map((p) => projectCard(p, viewer)).join("")
39
+ ? cards.join("")
39
40
  : `<div class="empty">还没有项目。在下面创建一个:</div>`;
40
41
  const flashHtml = flash
41
42
  ? `<div class="flash ${flash.kind === "ok" ? "ok" : "err"}">${escapeHtml(flash.msg)}</div>`
@@ -90,15 +91,15 @@ ${tabNavHTML("projects")}
90
91
  </body></html>`;
91
92
  }
92
93
 
93
- export function handleCreateProject(
94
+ export async function handleCreateProject(
94
95
  form: Record<string, string | undefined>
95
- ): { location: string; error?: string; projectId?: number } {
96
+ ): Promise<{ location: string; error?: string; projectId?: number }> {
96
97
  const owner = (form.owner ?? "").trim();
97
98
  const name = (form.name ?? "").trim();
98
99
  const description = (form.description ?? "").trim();
99
100
  if (!owner || !name) return { location: "/projects", error: "owner 和 name 必填" };
100
101
  try {
101
- const p = createProject(owner, name, description);
102
+ const p = await createProject(owner, name, description);
102
103
  return { location: `/${encodeURIComponent(p.owner)}/${encodeURIComponent(p.name)}/issues`, projectId: p.id };
103
104
  } catch (e) {
104
105
  return {
@@ -13,18 +13,18 @@ function issueRow(it: IssueWithMeta, owner: string, repo: string, q: string): st
13
13
  </a>`;
14
14
  }
15
15
 
16
- export function buildIssueList(
16
+ export async function buildIssueList(
17
17
  owner: string,
18
18
  repo: string,
19
19
  state: "open" | "closed" | "all",
20
20
  writesEnabled: boolean,
21
21
  q: string
22
- ): string {
23
- const project = getProject(owner, repo);
22
+ ): Promise<string> {
23
+ const project = await getProject(owner, repo);
24
24
  if (!project) {
25
25
  return notFoundProject(owner, repo);
26
26
  }
27
- const issues = listIssues(project.id, { state, q, limit: LIST_PAGE_SIZE });
27
+ const issues = await listIssues(project.id, { state, q, limit: LIST_PAGE_SIZE });
28
28
  const matchesFirst = q
29
29
  ? [...issues].sort((a, b) => Number(containsCI(b.title, q)) - Number(containsCI(a.title, q)))
30
30
  : issues;
@@ -44,9 +44,9 @@ function toView(c: CommentRow): CommentView {
44
44
  };
45
45
  }
46
46
 
47
- export function viewsFromComments(rows: CommentRow[]): CommentView[] {
47
+ export async function viewsFromComments(rows: CommentRow[]): Promise<CommentView[]> {
48
48
  const views = rows.map((r) => toView(r));
49
- hydrateReactions(views);
49
+ await hydrateReactions(views);
50
50
  return views;
51
51
  }
52
52
 
@@ -82,23 +82,23 @@ function orderForDisplay(views: CommentView[], sort: "desc" | "asc"): CommentVie
82
82
  return sort === "asc" ? views : views.slice().reverse();
83
83
  }
84
84
 
85
- export function buildIssueThread(
85
+ export async function buildIssueThread(
86
86
  cfg: Config,
87
87
  owner: string,
88
88
  repo: string,
89
89
  number: number,
90
90
  viewerLogin?: string
91
- ): { html: string } {
92
- const project = getProject(owner, repo);
91
+ ): Promise<{ html: string }> {
92
+ const project = await getProject(owner, repo);
93
93
  if (!project) throw new StoreError(404, `项目 ${owner}/${repo} 不存在`);
94
- const issue = getIssueWithMeta(project.id, number);
94
+ const issue = await getIssueWithMeta(project.id, number);
95
95
  if (!issue) throw new StoreError(404, `#${number} 在 ${owner}/${repo} 不存在`);
96
96
 
97
- const total = countComments(issue.id);
97
+ const total = await countComments(issue.id);
98
98
  const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
99
99
  const currentPage = totalPages;
100
- const { rows } = listCommentsPage(issue.id, currentPage, PAGE_SIZE);
101
- const views = viewsFromComments(rows);
100
+ const { rows } = await listCommentsPage(issue.id, currentPage, PAGE_SIZE);
101
+ const views = await viewsFromComments(rows);
102
102
  const hasOlder = currentPage > 1;
103
103
  const displayViews = orderForDisplay(views, cfg.commentSort);
104
104
  const payload = payloadFromComments(issue, displayViews, currentPage, hasOlder, cfg.commentSort);
@@ -140,33 +140,33 @@ export interface IssuePageData {
140
140
  hasOlder: boolean;
141
141
  }
142
142
 
143
- export function fetchIssuePage(
143
+ export async function fetchIssuePage(
144
144
  owner: string,
145
145
  repo: string,
146
146
  number: number,
147
147
  page: number
148
- ): IssuePageData {
149
- const project = getProject(owner, repo);
148
+ ): Promise<IssuePageData> {
149
+ const project = await getProject(owner, repo);
150
150
  if (!project) throw new StoreError(404, `项目 ${owner}/${repo} 不存在`);
151
- const issue = getIssueWithMeta(project.id, number);
151
+ const issue = await getIssueWithMeta(project.id, number);
152
152
  if (!issue) throw new StoreError(404, `#${number} 不存在`);
153
- const { rows, page: clamped } = listCommentsPage(issue.id, page, PAGE_SIZE);
154
- const views = viewsFromComments(rows);
153
+ const { rows, page: clamped } = await listCommentsPage(issue.id, page, PAGE_SIZE);
154
+ const views = await viewsFromComments(rows);
155
155
  return { issue, views, currentPage: clamped, hasOlder: clamped > 1 };
156
156
  }
157
157
 
158
- export function fetchIssueSince(
158
+ export async function fetchIssueSince(
159
159
  owner: string,
160
160
  repo: string,
161
161
  number: number,
162
162
  sinceISO: string
163
- ): CommentView[] {
164
- const project = getProject(owner, repo);
163
+ ): Promise<CommentView[]> {
164
+ const project = await getProject(owner, repo);
165
165
  if (!project) throw new StoreError(404, `项目 ${owner}/${repo} 不存在`);
166
- const issue = getIssueWithMeta(project.id, number);
166
+ const issue = await getIssueWithMeta(project.id, number);
167
167
  if (!issue) throw new StoreError(404, `#${number} 不存在`);
168
- const rows = listCommentsSince(issue.id, sinceISO);
169
- return viewsFromComments(rows);
168
+ const rows = await listCommentsSince(issue.id, sinceISO);
169
+ return await viewsFromComments(rows);
170
170
  }
171
171
 
172
172
  export function safeJsonEmbed(v: unknown): string {
@@ -14,11 +14,11 @@ function issueRow(it: IssueWithMeta, q: string): string {
14
14
  </a>`;
15
15
  }
16
16
 
17
- export function buildIssuesFeed(
17
+ export async function buildIssuesFeed(
18
18
  state: "open" | "closed" | "all",
19
19
  q: string
20
- ): string {
21
- const issues = listAllIssues({ state, q, limit: FEED_PAGE_SIZE });
20
+ ): Promise<string> {
21
+ const issues = await listAllIssues({ state, q, limit: FEED_PAGE_SIZE });
22
22
  const matchesFirst = q
23
23
  ? [...issues].sort((a, b) => Number(containsCI(b.title, q)) - Number(containsCI(a.title, q)))
24
24
  : issues;
@@ -22,25 +22,24 @@ function roleBadge(role: ProjectRole): string {
22
22
  return `<span class="badge role-${cls}">${escapeHtml(role)}</span>`;
23
23
  }
24
24
 
25
- export function buildProjectMembersPage(
25
+ export async function buildProjectMembersPage(
26
26
  viewer: UserRow,
27
27
  project: ProjectRow,
28
28
  flash: Flash | null,
29
- ): string {
30
- const members = listProjectMembersWithUsers(project.id);
31
- const users = listUsers();
29
+ ): Promise<string> {
30
+ const members = await listProjectMembersWithUsers(project.id);
31
+ const users = await listUsers();
32
32
  const memberLogins = new Set(members.map((m) => m.user_login));
33
33
  const candidates = users.filter((u) => u.is_active === 1 && !memberLogins.has(u.login));
34
34
 
35
35
  const flashHtml = flash ? `<div class="flash ${flash.kind}">${escapeHtml(flash.msg)}</div>` : "";
36
36
 
37
+ const memberRows = await Promise.all(members.map((m) => memberRowHtml(m, viewer, project)));
37
38
  const rowsHtml = members.length
38
39
  ? `<table>
39
40
  <thead><tr><th>用户</th><th>角色</th><th>加入时间</th><th>操作</th></tr></thead>
40
41
  <tbody>
41
- ${members
42
- .map((m) => memberRowHtml(m, viewer, project))
43
- .join("")}
42
+ ${memberRows.join("")}
44
43
  </tbody>
45
44
  </table>`
46
45
  : `<div class="hint">该项目还没有成员记录(site-admin 已自动获得 admin 权限,可在下方添加)。</div>`;
@@ -117,9 +116,9 @@ ${addFormHtml}
117
116
  </main></body></html>`;
118
117
  }
119
118
 
120
- function memberRowHtml(m: ProjectMemberWithUser, viewer: UserRow, project: ProjectRow): string {
119
+ async function memberRowHtml(m: ProjectMemberWithUser, viewer: UserRow, project: ProjectRow): Promise<string> {
121
120
  const isSelf = m.user_login === viewer.login;
122
- const isAdminCountOne = m.role === "admin" && countProjectAdmins(project.id) <= 1;
121
+ const isAdminCountOne = m.role === "admin" && (await countProjectAdmins(project.id)) <= 1;
123
122
  const kindBadge = m.user_kind === "bot" ? `<span class="badge bot">bot</span>` : m.user_kind === "system" ? `<span class="badge">system</span>` : "";
124
123
  const inactiveBadge = m.user_is_active === 1 ? "" : `<span class="badge inactive">禁用</span>`;
125
124
  const selfNote = isSelf ? `<span class="meta">(你)</span>` : "";
@@ -129,13 +129,13 @@ export function parseUpstreamUrlsForm(text: string): string[] {
129
129
  .filter((line) => line.length > 0);
130
130
  }
131
131
 
132
- export function trySetUpstreamUrls(
132
+ export async function trySetUpstreamUrls(
133
133
  projectId: number,
134
134
  raw: string,
135
- ): { ok: true; urls: string[] } | { ok: false; msg: string } {
135
+ ): Promise<{ ok: true; urls: string[] } | { ok: false; msg: string }> {
136
136
  try {
137
137
  const urls = parseUpstreamUrlsForm(raw);
138
- const cleaned = setProjectUpstreamUrls(projectId, urls);
138
+ const cleaned = await setProjectUpstreamUrls(projectId, urls);
139
139
  return { ok: true, urls: cleaned };
140
140
  } catch (e) {
141
141
  const msg = e instanceof StoreError ? e.message : e instanceof Error ? e.message : "保存失败";
package/src/webhooks.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  // the full retry trail rather than an opaque final status.
17
17
 
18
18
  import { createHmac, randomUUID } from "node:crypto";
19
- import { rawDB } from "./db";
19
+ import { getDB } from "./db";
20
20
  import { log } from "./logger";
21
21
  import { loadConfig } from "./config";
22
22
  import {
@@ -121,16 +121,12 @@ function parseEvents(events: unknown): WebhookEventName[] {
121
121
 
122
122
  // ─── CRUD ────────────────────────────────────────────────────
123
123
 
124
- export function listWebhooks(projectId: number): WebhookRow[] {
125
- return rawDB()
126
- .query("SELECT * FROM webhooks WHERE project_id = ? ORDER BY id")
127
- .all(projectId) as WebhookRow[];
124
+ export async function listWebhooks(projectId: number): Promise<WebhookRow[]> {
125
+ return await getDB().all<WebhookRow>("SELECT * FROM {{webhooks}} WHERE project_id = ? ORDER BY id", [projectId]);
128
126
  }
129
127
 
130
- export function getWebhook(id: number): WebhookRow | null {
131
- const row = rawDB().query("SELECT * FROM webhooks WHERE id = ?").get(id) as
132
- | WebhookRow
133
- | undefined;
128
+ export async function getWebhook(id: number): Promise<WebhookRow | null> {
129
+ const row = await getDB().get<WebhookRow>("SELECT * FROM {{webhooks}} WHERE id = ?", [id]);
134
130
  return row ?? null;
135
131
  }
136
132
 
@@ -142,7 +138,7 @@ export interface CreateWebhookInput {
142
138
  active?: boolean;
143
139
  }
144
140
 
145
- export function createWebhook(input: CreateWebhookInput): WebhookRow {
141
+ export async function createWebhook(input: CreateWebhookInput): Promise<WebhookRow> {
146
142
  const url = input.url.trim();
147
143
  if (!url) throw new StoreError(400, "URL 不能为空");
148
144
  if (!/^https?:\/\//i.test(url)) throw new StoreError(400, "URL 必须是 http(s)://");
@@ -150,13 +146,10 @@ export function createWebhook(input: CreateWebhookInput): WebhookRow {
150
146
  const secret = (input.secret ?? "").slice(0, 256);
151
147
  const events = input.events && input.events.length > 0 ? input.events : DEFAULT_EVENTS;
152
148
  const ts = now();
153
- const db = rawDB();
154
- const info = db
155
- .query(
156
- `INSERT INTO webhooks (project_id, url, secret, content_type, events, active, created_at, updated_at)
157
- VALUES (?, ?, ?, 'application/json', ?, ?, ?, ?)`
158
- )
159
- .run(
149
+ const info = await getDB().run(
150
+ `INSERT INTO {{webhooks}} (project_id, url, secret, content_type, events, active, created_at, updated_at)
151
+ VALUES (?, ?, ?, 'application/json', ?, ?, ?, ?)`,
152
+ [
160
153
  input.project_id,
161
154
  url,
162
155
  secret,
@@ -164,26 +157,24 @@ export function createWebhook(input: CreateWebhookInput): WebhookRow {
164
157
  input.active === false ? 0 : 1,
165
158
  ts,
166
159
  ts
167
- );
168
- return getWebhook(Number(info.lastInsertRowid))!;
160
+ ]
161
+ );
162
+ return (await getWebhook(info.insertId))!;
169
163
  }
170
164
 
171
- export function deleteWebhook(id: number): void {
172
- rawDB().query("DELETE FROM webhooks WHERE id = ?").run(id);
165
+ export async function deleteWebhook(id: number): Promise<void> {
166
+ await getDB().run("DELETE FROM {{webhooks}} WHERE id = ?", [id]);
173
167
  }
174
168
 
175
- export function setWebhookActive(id: number, active: boolean): void {
176
- rawDB()
177
- .query("UPDATE webhooks SET active = ?, updated_at = ? WHERE id = ?")
178
- .run(active ? 1 : 0, now(), id);
169
+ export async function setWebhookActive(id: number, active: boolean): Promise<void> {
170
+ await getDB().run("UPDATE {{webhooks}} SET active = ?, updated_at = ? WHERE id = ?", [active ? 1 : 0, now(), id]);
179
171
  }
180
172
 
181
- export function listDeliveries(webhookId: number, limit = 50): WebhookDeliveryRow[] {
182
- return rawDB()
183
- .query(
184
- "SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY id DESC LIMIT ?"
185
- )
186
- .all(webhookId, limit) as WebhookDeliveryRow[];
173
+ export async function listDeliveries(webhookId: number, limit = 50): Promise<WebhookDeliveryRow[]> {
174
+ return await getDB().all<WebhookDeliveryRow>(
175
+ "SELECT * FROM {{webhook_deliveries}} WHERE webhook_id = ? ORDER BY id DESC LIMIT ?",
176
+ [webhookId, limit]
177
+ );
187
178
  }
188
179
 
189
180
  // ─── Payload builders (Gitea-compatible shape) ───────────────
@@ -504,21 +495,19 @@ async function postWithTimeout(
504
495
  }
505
496
  }
506
497
 
507
- function recordDelivery(
498
+ async function recordDelivery(
508
499
  webhookId: number,
509
500
  event: WebhookEventName,
510
501
  deliveryUuid: string,
511
502
  rawBody: string,
512
503
  result: DeliveryAttemptResult
513
- ): void {
504
+ ): Promise<void> {
514
505
  try {
515
- rawDB()
516
- .query(
517
- `INSERT INTO webhook_deliveries
518
- (webhook_id, event, delivery_uuid, payload, response_status, response_body, duration_ms, error, created_at)
519
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
520
- )
521
- .run(
506
+ await getDB().run(
507
+ `INSERT INTO {{webhook_deliveries}}
508
+ (webhook_id, event, delivery_uuid, payload, response_status, response_body, duration_ms, error, created_at)
509
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
510
+ [
522
511
  webhookId,
523
512
  event,
524
513
  deliveryUuid,
@@ -528,7 +517,8 @@ function recordDelivery(
528
517
  result.durationMs,
529
518
  result.error,
530
519
  now()
531
- );
520
+ ]
521
+ );
532
522
  } catch (e) {
533
523
  log.error("webhook: failed to record delivery", { err: e as Error });
534
524
  }
@@ -547,7 +537,7 @@ async function deliver(
547
537
  const headers = buildHeaders(event, deliveryUuid, rawBody, webhook.secret);
548
538
  const result = await postWithTimeout(webhook.url, rawBody, headers);
549
539
  const success = result.status !== null && result.status >= 200 && result.status < 300;
550
- recordDelivery(webhook.id, event, deliveryUuid, rawBody, result);
540
+ await recordDelivery(webhook.id, event, deliveryUuid, rawBody, result);
551
541
  if (success) return;
552
542
  // 410 Gone: receiver explicitly says "stop sending" — honor it, no retry.
553
543
  if (result.status === 410) return;
@@ -563,14 +553,15 @@ async function deliver(
563
553
  }
564
554
  }
565
555
 
566
- function fanOut(
556
+ async function fanOut(
567
557
  projectId: number,
568
558
  event: WebhookEventName,
569
559
  rawBody: string
570
- ): void {
571
- const webhooks = rawDB()
572
- .query("SELECT * FROM webhooks WHERE project_id = ? AND active = 1")
573
- .all(projectId) as WebhookRow[];
560
+ ): Promise<void> {
561
+ const webhooks = await getDB().all<WebhookRow>(
562
+ "SELECT * FROM {{webhooks}} WHERE project_id = ? AND active = 1",
563
+ [projectId]
564
+ );
574
565
  for (const wh of webhooks) {
575
566
  if (!parseEvents(wh.events).includes(event)) continue;
576
567
  void deliver(wh, event, rawBody);
@@ -579,26 +570,26 @@ function fanOut(
579
570
 
580
571
  // ─── Public emit API ─────────────────────────────────────────
581
572
 
582
- export function emitIssueEvent(
573
+ export async function emitIssueEvent(
583
574
  projectId: number,
584
575
  issueId: number,
585
576
  action: IssueAction,
586
577
  origin: string
587
- ): void {
578
+ ): Promise<void> {
588
579
  try {
589
- const project = getProjectById(projectId);
580
+ const project = await getProjectById(projectId);
590
581
  if (!project) return;
591
- const issue = getIssueById(issueId);
582
+ const issue = await getIssueById(issueId);
592
583
  if (!issue) return;
593
- const commentCount = countCommentsSafe(issueId);
584
+ const commentCount = await countCommentsSafe(issueId);
594
585
  // Resolve model: project override > global default. Empty string = no
595
586
  // override (daemon omits --model, lets opencode pick). globalDefault
596
587
  // comes from the config table via loadConfig() — cheap DB read.
597
- const globalDefault = loadConfig().defaultModel;
588
+ const globalDefault = (await loadConfig()).defaultModel;
598
589
  const model = resolveModel(project.model, globalDefault);
599
590
  const payload = buildIssuePayload(issue, project, commentCount, action, origin, model);
600
591
  const rawBody = JSON.stringify(payload);
601
- fanOut(projectId, "issues", rawBody);
592
+ await fanOut(projectId, "issues", rawBody);
602
593
  } catch (e) {
603
594
  log.error("webhook: emitIssueEvent failed", {
604
595
  err: e as Error,
@@ -609,25 +600,25 @@ export function emitIssueEvent(
609
600
  }
610
601
  }
611
602
 
612
- export function emitCommentEvent(
603
+ export async function emitCommentEvent(
613
604
  projectId: number,
614
605
  issueId: number,
615
606
  commentId: number,
616
607
  origin: string
617
- ): void {
608
+ ): Promise<void> {
618
609
  try {
619
- const project = getProjectById(projectId);
610
+ const project = await getProjectById(projectId);
620
611
  if (!project) return;
621
- const issue = getIssueById(issueId);
612
+ const issue = await getIssueById(issueId);
622
613
  if (!issue) return;
623
- const comment = getCommentByIdSafe(commentId);
614
+ const comment = await getCommentByIdSafe(commentId);
624
615
  if (!comment) return;
625
- const commentCount = countCommentsSafe(issueId);
626
- const globalDefault = loadConfig().defaultModel;
616
+ const commentCount = await countCommentsSafe(issueId);
617
+ const globalDefault = (await loadConfig()).defaultModel;
627
618
  const model = resolveModel(project.model, globalDefault);
628
619
  const payload = buildCommentPayload(issue, comment, project, commentCount, origin, model);
629
620
  const rawBody = JSON.stringify(payload);
630
- fanOut(projectId, "issue_comment", rawBody);
621
+ await fanOut(projectId, "issue_comment", rawBody);
631
622
  } catch (e) {
632
623
  log.error("webhook: emitCommentEvent failed", {
633
624
  err: e as Error,
@@ -638,11 +629,11 @@ export function emitCommentEvent(
638
629
  }
639
630
  }
640
631
 
641
- export function emitPingEvent(projectId: number, origin: string): void {
632
+ export async function emitPingEvent(projectId: number, origin: string): Promise<void> {
642
633
  // Synthetic ping: send a small `ping` event (no issue context). Useful for
643
634
  // verifying webhook configuration without triggering a real write.
644
635
  try {
645
- const project = getProjectById(projectId);
636
+ const project = await getProjectById(projectId);
646
637
  if (!project) return;
647
638
  const payload = {
648
639
  zen: "smoke",
@@ -652,7 +643,7 @@ export function emitPingEvent(projectId: number, origin: string): void {
652
643
  sender: buildUser(project.owner, origin),
653
644
  };
654
645
  const rawBody = JSON.stringify(payload);
655
- fanOut(projectId, "issues", rawBody);
646
+ await fanOut(projectId, "issues", rawBody);
656
647
  } catch (e) {
657
648
  log.error("webhook: emitPingEvent failed", { err: e as Error, projectId });
658
649
  }
@@ -660,22 +651,21 @@ export function emitPingEvent(projectId: number, origin: string): void {
660
651
 
661
652
  // ─── Helpers (kept here to avoid widening store.ts surface) ──
662
653
 
663
- function countCommentsSafe(issueId: number): number {
654
+ async function countCommentsSafe(issueId: number): Promise<number> {
664
655
  try {
665
- const row = rawDB()
666
- .query("SELECT COUNT(*) AS n FROM comments WHERE issue_id = ?")
667
- .get(issueId) as { n: number } | undefined;
656
+ const row = await getDB().get<{ n: number }>(
657
+ "SELECT COUNT(*) AS n FROM {{comments}} WHERE issue_id = ?",
658
+ [issueId]
659
+ );
668
660
  return row?.n ?? 0;
669
661
  } catch {
670
662
  return 0;
671
663
  }
672
664
  }
673
665
 
674
- function getCommentByIdSafe(id: number): CommentRow | null {
666
+ async function getCommentByIdSafe(id: number): Promise<CommentRow | null> {
675
667
  try {
676
- const row = rawDB().query("SELECT * FROM comments WHERE id = ?").get(id) as
677
- | CommentRow
678
- | undefined;
668
+ const row = await getDB().get<CommentRow>("SELECT * FROM {{comments}} WHERE id = ?", [id]);
679
669
  return row ?? null;
680
670
  } catch {
681
671
  return null;