Skip to content

Commit 52a93c3

Browse files
authored
refactor(db): add upsert() helper to reduce SQL boilerplate (#139)
## Summary Add a custom `upsert()` utility function that generates `INSERT...ON CONFLICT DO UPDATE` statements from objects, reducing repetitive SQL boilerplate while maintaining zero external dependencies. ## Changes - Add `src/lib/db/utils.ts` with `upsert()` and `bulkUpsert()` helpers - Add `test/lib/db/utils.test.ts` with 13 tests covering edge cases - Refactor 8 UPSERT statements across 6 files to use the new helper ## Files Refactored | File | Functions | |------|-----------| | `auth.ts` | `setAuthToken()` | | `defaults.ts` | `setDefaults()` | | `user.ts` | `setUserInfo()` | | `regions.ts` | `setOrgRegion()`, `setOrgRegions()` | | `project-cache.ts` | `setCachedProject()`, `setCachedProjectByDsnKey()` | | `dsn-cache.ts` | `setCachedDsn()` | ## Before/After ```typescript // Before: 10+ lines of SQL db.query(` INSERT INTO table (col1, col2, col3) VALUES (?, ?, ?) ON CONFLICT(col1) DO UPDATE SET col2 = excluded.col2, col3 = excluded.col3 `).run(val1, val2, val3); // After: 4 lines const { sql, values } = upsert("table", { col1, col2, col3 }, ["col1"]); db.query(sql).run(...values); ```
1 parent db838d2 commit 52a93c3

8 files changed

Lines changed: 317 additions & 112 deletions

File tree

src/lib/db/auth.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { withDbSpan } from "../telemetry.js";
66
import { getDatabase } from "./index.js";
7+
import { runUpsert } from "./utils.js";
78

89
/** Refresh when less than 10% of token lifetime remains */
910
export const REFRESH_THRESHOLD = 0.1;
@@ -77,16 +78,19 @@ export function setAuthToken(
7778
const expiresAt = expiresIn ? now + expiresIn * 1000 : null;
7879
const issuedAt = expiresIn ? now : null;
7980

80-
db.query(`
81-
INSERT INTO auth (id, token, refresh_token, expires_at, issued_at, updated_at)
82-
VALUES (1, ?, ?, ?, ?, ?)
83-
ON CONFLICT(id) DO UPDATE SET
84-
token = excluded.token,
85-
refresh_token = excluded.refresh_token,
86-
expires_at = excluded.expires_at,
87-
issued_at = excluded.issued_at,
88-
updated_at = excluded.updated_at
89-
`).run(token, newRefreshToken ?? null, expiresAt, issuedAt, now);
81+
runUpsert(
82+
db,
83+
"auth",
84+
{
85+
id: 1,
86+
token,
87+
refresh_token: newRefreshToken ?? null,
88+
expires_at: expiresAt,
89+
issued_at: issuedAt,
90+
updated_at: now,
91+
},
92+
["id"]
93+
);
9094
});
9195
}
9296

src/lib/db/defaults.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { getDatabase } from "./index.js";
6+
import { runUpsert } from "./utils.js";
67

78
type DefaultsRow = {
89
organization: string | null;
@@ -51,12 +52,10 @@ export async function setDefaults(
5152
const newProject =
5253
project === undefined ? (current?.project ?? null) : project;
5354

54-
db.query(`
55-
INSERT INTO defaults (id, organization, project, updated_at)
56-
VALUES (1, ?, ?, ?)
57-
ON CONFLICT(id) DO UPDATE SET
58-
organization = excluded.organization,
59-
project = excluded.project,
60-
updated_at = excluded.updated_at
61-
`).run(newOrg, newProject, now);
55+
runUpsert(
56+
db,
57+
"defaults",
58+
{ id: 1, organization: newOrg, project: newProject, updated_at: now },
59+
["id"]
60+
);
6261
}

src/lib/db/dsn-cache.ts

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import type { CachedDsnEntry, ResolvedProjectInfo } from "../dsn/types.js";
66
import { getDatabase, maybeCleanupCaches } from "./index.js";
7+
import { runUpsert } from "./utils.js";
78

89
type DsnCacheRow = {
910
directory: string;
@@ -74,37 +75,24 @@ export async function setCachedDsn(
7475
const db = getDatabase();
7576
const now = Date.now();
7677

77-
db.query(`
78-
INSERT INTO dsn_cache
79-
(directory, dsn, project_id, org_id, source, source_path,
80-
resolved_org_slug, resolved_org_name, resolved_project_slug, resolved_project_name,
81-
cached_at, last_accessed)
82-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
83-
ON CONFLICT(directory) DO UPDATE SET
84-
dsn = excluded.dsn,
85-
project_id = excluded.project_id,
86-
org_id = excluded.org_id,
87-
source = excluded.source,
88-
source_path = excluded.source_path,
89-
resolved_org_slug = excluded.resolved_org_slug,
90-
resolved_org_name = excluded.resolved_org_name,
91-
resolved_project_slug = excluded.resolved_project_slug,
92-
resolved_project_name = excluded.resolved_project_name,
93-
cached_at = excluded.cached_at,
94-
last_accessed = excluded.last_accessed
95-
`).run(
96-
directory,
97-
entry.dsn,
98-
entry.projectId,
99-
entry.orgId ?? null,
100-
entry.source,
101-
entry.sourcePath ?? null,
102-
entry.resolved?.orgSlug ?? null,
103-
entry.resolved?.orgName ?? null,
104-
entry.resolved?.projectSlug ?? null,
105-
entry.resolved?.projectName ?? null,
106-
now,
107-
now
78+
runUpsert(
79+
db,
80+
"dsn_cache",
81+
{
82+
directory,
83+
dsn: entry.dsn,
84+
project_id: entry.projectId,
85+
org_id: entry.orgId ?? null,
86+
source: entry.source,
87+
source_path: entry.sourcePath ?? null,
88+
resolved_org_slug: entry.resolved?.orgSlug ?? null,
89+
resolved_org_name: entry.resolved?.orgName ?? null,
90+
resolved_project_slug: entry.resolved?.projectSlug ?? null,
91+
resolved_project_name: entry.resolved?.projectName ?? null,
92+
cached_at: now,
93+
last_accessed: now,
94+
},
95+
["directory"]
10896
);
10997

11098
maybeCleanupCaches();

src/lib/db/project-cache.ts

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import type { CachedProject } from "../../types/index.js";
66
import { getDatabase, maybeCleanupCaches } from "./index.js";
7+
import { runUpsert } from "./utils.js";
78

89
type ProjectCacheRow = {
910
cache_key: string;
@@ -68,25 +69,19 @@ export async function setCachedProject(
6869
const key = projectCacheKey(orgId, projectId);
6970
const now = Date.now();
7071

71-
db.query(`
72-
INSERT INTO project_cache
73-
(cache_key, org_slug, org_name, project_slug, project_name, cached_at, last_accessed)
74-
VALUES (?, ?, ?, ?, ?, ?, ?)
75-
ON CONFLICT(cache_key) DO UPDATE SET
76-
org_slug = excluded.org_slug,
77-
org_name = excluded.org_name,
78-
project_slug = excluded.project_slug,
79-
project_name = excluded.project_name,
80-
cached_at = excluded.cached_at,
81-
last_accessed = excluded.last_accessed
82-
`).run(
83-
key,
84-
info.orgSlug,
85-
info.orgName,
86-
info.projectSlug,
87-
info.projectName,
88-
now,
89-
now
72+
runUpsert(
73+
db,
74+
"project_cache",
75+
{
76+
cache_key: key,
77+
org_slug: info.orgSlug,
78+
org_name: info.orgName,
79+
project_slug: info.projectSlug,
80+
project_name: info.projectName,
81+
cached_at: now,
82+
last_accessed: now,
83+
},
84+
["cache_key"]
9085
);
9186

9287
maybeCleanupCaches();
@@ -120,25 +115,19 @@ export async function setCachedProjectByDsnKey(
120115
const key = dsnCacheKey(publicKey);
121116
const now = Date.now();
122117

123-
db.query(`
124-
INSERT INTO project_cache
125-
(cache_key, org_slug, org_name, project_slug, project_name, cached_at, last_accessed)
126-
VALUES (?, ?, ?, ?, ?, ?, ?)
127-
ON CONFLICT(cache_key) DO UPDATE SET
128-
org_slug = excluded.org_slug,
129-
org_name = excluded.org_name,
130-
project_slug = excluded.project_slug,
131-
project_name = excluded.project_name,
132-
cached_at = excluded.cached_at,
133-
last_accessed = excluded.last_accessed
134-
`).run(
135-
key,
136-
info.orgSlug,
137-
info.orgName,
138-
info.projectSlug,
139-
info.projectName,
140-
now,
141-
now
118+
runUpsert(
119+
db,
120+
"project_cache",
121+
{
122+
cache_key: key,
123+
org_slug: info.orgSlug,
124+
org_name: info.orgName,
125+
project_slug: info.projectSlug,
126+
project_name: info.projectName,
127+
cached_at: now,
128+
last_accessed: now,
129+
},
130+
["cache_key"]
142131
);
143132

144133
maybeCleanupCaches();

src/lib/db/regions.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import { getDatabase } from "./index.js";
10+
import { runUpsert } from "./utils.js";
1011

1112
const TABLE = "org_regions";
1213

@@ -46,13 +47,12 @@ export async function setOrgRegion(
4647
const db = getDatabase();
4748
const now = Date.now();
4849

49-
db.query(`
50-
INSERT INTO ${TABLE} (org_slug, region_url, updated_at)
51-
VALUES (?, ?, ?)
52-
ON CONFLICT(org_slug) DO UPDATE SET
53-
region_url = excluded.region_url,
54-
updated_at = excluded.updated_at
55-
`).run(orgSlug, regionUrl, now);
50+
runUpsert(
51+
db,
52+
TABLE,
53+
{ org_slug: orgSlug, region_url: regionUrl, updated_at: now },
54+
["org_slug"]
55+
);
5656
}
5757

5858
/**
@@ -71,17 +71,14 @@ export async function setOrgRegions(
7171
const db = getDatabase();
7272
const now = Date.now();
7373

74-
const stmt = db.query(`
75-
INSERT INTO ${TABLE} (org_slug, region_url, updated_at)
76-
VALUES (?, ?, ?)
77-
ON CONFLICT(org_slug) DO UPDATE SET
78-
region_url = excluded.region_url,
79-
updated_at = excluded.updated_at
80-
`);
81-
8274
db.transaction(() => {
8375
for (const [orgSlug, regionUrl] of entries) {
84-
stmt.run(orgSlug, regionUrl, now);
76+
runUpsert(
77+
db,
78+
TABLE,
79+
{ org_slug: orgSlug, region_url: regionUrl, updated_at: now },
80+
["org_slug"]
81+
);
8582
}
8683
})();
8784
}

src/lib/db/user.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { getDatabase } from "./index.js";
8+
import { runUpsert } from "./utils.js";
89

910
export type UserInfo = {
1011
userId: string;
@@ -47,13 +48,16 @@ export function setUserInfo(info: UserInfo): void {
4748
const db = getDatabase();
4849
const now = Date.now();
4950

50-
db.query(`
51-
INSERT INTO user_info (id, user_id, email, username, updated_at)
52-
VALUES (1, ?, ?, ?, ?)
53-
ON CONFLICT(id) DO UPDATE SET
54-
user_id = excluded.user_id,
55-
email = excluded.email,
56-
username = excluded.username,
57-
updated_at = excluded.updated_at
58-
`).run(info.userId, info.email ?? null, info.username ?? null, now);
51+
runUpsert(
52+
db,
53+
"user_info",
54+
{
55+
id: 1,
56+
user_id: info.userId,
57+
email: info.email ?? null,
58+
username: info.username ?? null,
59+
updated_at: now,
60+
},
61+
["id"]
62+
);
5963
}

0 commit comments

Comments
 (0)