Skip to content

Commit d879976

Browse files
committed
refactor(vps): extract shared dieOnDatabaseError helper
Review on #162 flagged dieOnDatabaseError as a likely duplicate -- correct: search, music, profile, and this PR's admin handlers each had an identical copy differing only in the log-tag prefix string. Extracted to apps/vps/src/http/handler-utils.ts as a tag -> wrapper factory, with each handler file partially applying its own tag once at module scope so call sites stay unchanged (dieOnDatabaseError(effect)).
1 parent a507c60 commit d879976

5 files changed

Lines changed: 56 additions & 67 deletions

File tree

apps/vps/src/http/admin.handlers.ts

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { postsTable } from '@/db/post.schema'
2121
import { releasesTable } from '@/db/release.schema'
2222
import { showSubscriptionsTable, showsTable } from '@/db/show.schema'
2323
import { DatabaseError, getErrorMessage } from '@/errors'
24+
import { dieOnDatabaseError } from '@/http/handler-utils'
25+
26+
const dieOnAdminDatabaseError = dieOnDatabaseError('admin')
2427

2528
type ContentTable =
2629
| typeof audioTable
@@ -511,17 +514,16 @@ export const AdminHandlersLive = HttpApiBuilder.group(Api, 'admin', (handlers) =
511514
.handle('getAdminOverview', () =>
512515
Effect.gen(function* () {
513516
yield* requireAdmin
514-
return yield* Effect.tryPromise({
515-
try: () => loadAdminOverview(),
516-
catch: (error) =>
517-
new DatabaseError({
518-
message: `Failed to fetch admin overview: ${getErrorMessage(error)}`,
519-
operation: 'select',
520-
table: 'admin-overview'
521-
})
522-
}).pipe(
523-
Effect.tapError((cause) => Effect.logError('[admin] overview query failed', cause)),
524-
Effect.catchTag('DatabaseError', (cause) => Effect.die(cause))
517+
return yield* dieOnAdminDatabaseError(
518+
Effect.tryPromise({
519+
try: () => loadAdminOverview(),
520+
catch: (error) =>
521+
new DatabaseError({
522+
message: `Failed to fetch admin overview: ${getErrorMessage(error)}`,
523+
operation: 'select',
524+
table: 'admin-overview'
525+
})
526+
})
525527
)
526528
})
527529
)
@@ -551,30 +553,27 @@ export const AdminHandlersLive = HttpApiBuilder.group(Api, 'admin', (handlers) =
551553
.handle('getNewsletterSubscribers', () =>
552554
Effect.gen(function* () {
553555
yield* requireAdmin
554-
const rows = yield* Effect.tryPromise({
555-
try: () =>
556-
db
557-
.select({
558-
id: newsletterSubscribersTable.id,
559-
email: newsletterSubscribersTable.email,
560-
name: newsletterSubscribersTable.name,
561-
source: newsletterSubscribersTable.source,
562-
unsubscribedAt: newsletterSubscribersTable.unsubscribedAt,
563-
createdAt: newsletterSubscribersTable.createdAt
556+
const rows = yield* dieOnAdminDatabaseError(
557+
Effect.tryPromise({
558+
try: () =>
559+
db
560+
.select({
561+
id: newsletterSubscribersTable.id,
562+
email: newsletterSubscribersTable.email,
563+
name: newsletterSubscribersTable.name,
564+
source: newsletterSubscribersTable.source,
565+
unsubscribedAt: newsletterSubscribersTable.unsubscribedAt,
566+
createdAt: newsletterSubscribersTable.createdAt
567+
})
568+
.from(newsletterSubscribersTable)
569+
.orderBy(desc(newsletterSubscribersTable.createdAt)),
570+
catch: (error) =>
571+
new DatabaseError({
572+
message: `Failed to fetch newsletter subscribers: ${getErrorMessage(error)}`,
573+
operation: 'select',
574+
table: 'newsletter_subscribers'
564575
})
565-
.from(newsletterSubscribersTable)
566-
.orderBy(desc(newsletterSubscribersTable.createdAt)),
567-
catch: (error) =>
568-
new DatabaseError({
569-
message: `Failed to fetch newsletter subscribers: ${getErrorMessage(error)}`,
570-
operation: 'select',
571-
table: 'newsletter_subscribers'
572-
})
573-
}).pipe(
574-
Effect.tapError((cause) =>
575-
Effect.logError('[admin] newsletter subscribers query failed', cause)
576-
),
577-
Effect.catchTag('DatabaseError', (cause) => Effect.die(cause))
576+
})
578577
)
579578

580579
return {

apps/vps/src/http/handler-utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Effect } from 'effect'
2+
3+
type DatabaseErrorTag = { readonly _tag: 'DatabaseError' }
4+
5+
// Undeclared DatabaseError becomes a logged defect (500), same as the old
6+
// runEffect's fallback for anything that wasn't a mapped HttpError. Shared
7+
// across handler groups instead of copy-pasted per group (each copy was
8+
// identical except the log-tag prefix) -- flagged in review on PR #162.
9+
export const dieOnDatabaseError =
10+
(logTag: string) =>
11+
<A, E, R>(effect: Effect.Effect<A, E | DatabaseErrorTag, R>) =>
12+
effect.pipe(
13+
Effect.tapErrorTag('DatabaseError', (cause) =>
14+
Effect.logError(`[${logTag}] database operation failed`, cause)
15+
),
16+
Effect.catchTag('DatabaseError', (cause) => Effect.die(cause))
17+
)

apps/vps/src/http/music.handlers.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ArtistResponse, CreateArtistInput, UpdateArtistInput } from '@gbfm
44
import { Effect } from 'effect'
55
import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi'
66
import type { SelectMusicArtist } from '@/db/music-entity.schema'
7+
import { dieOnDatabaseError as makeDieOnDatabaseError } from '@/http/handler-utils'
78
import { MusicEntityService } from '@/services/music-entity'
89

910
const toArtistResponse = (row: SelectMusicArtist): ArtistResponse => ({
@@ -22,17 +23,7 @@ const toServiceFields = <T extends CreateArtistInput | UpdateArtistInput>(
2223
publishedAt: input.publishedAt ? new Date(input.publishedAt) : undefined
2324
})
2425

25-
// Undeclared DatabaseError becomes a logged defect (500), same as the old
26-
// runEffect's fallback for anything that wasn't a mapped HttpError.
27-
const dieOnDatabaseError = <A, E, R>(effect: Effect.Effect<A, E | DatabaseErrorTag, R>) =>
28-
effect.pipe(
29-
Effect.tapErrorTag('DatabaseError', (cause) =>
30-
Effect.logError('[music] database operation failed', cause)
31-
),
32-
Effect.catchTag('DatabaseError', (cause) => Effect.die(cause))
33-
)
34-
35-
type DatabaseErrorTag = { readonly _tag: 'DatabaseError' }
26+
const dieOnDatabaseError = makeDieOnDatabaseError('music')
3627

3728
const requireAdmin = Effect.gen(function* () {
3829
const { user } = yield* AuthSession

apps/vps/src/http/profile.handlers.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
import { Api } from '@gbfm/api/api'
22
import { Effect } from 'effect'
33
import { HttpApiBuilder, HttpApiError } from 'effect/unstable/httpapi'
4+
import { dieOnDatabaseError as makeDieOnDatabaseError } from '@/http/handler-utils'
45
import { ProfileService } from '@/services/profile.service'
56

6-
// Undeclared DatabaseError becomes a logged defect (500), same as the old
7-
// runEffect's fallback for anything that wasn't a mapped HttpError.
8-
const dieOnDatabaseError = <A, E, R>(effect: Effect.Effect<A, E | DatabaseErrorTag, R>) =>
9-
effect.pipe(
10-
Effect.tapErrorTag('DatabaseError', (cause) =>
11-
Effect.logError('[profile] database operation failed', cause)
12-
),
13-
Effect.catchTag('DatabaseError', (cause) => Effect.die(cause))
14-
)
15-
16-
type DatabaseErrorTag = { readonly _tag: 'DatabaseError' }
7+
const dieOnDatabaseError = makeDieOnDatabaseError('profile')
178

189
export const ProfileHandlersLive = HttpApiBuilder.group(Api, 'profile', (handlers) =>
1910
handlers.handle('getPublicProfile', ({ params }) =>

apps/vps/src/http/search.handlers.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
import { Api } from '@gbfm/api/api'
22
import { Effect } from 'effect'
33
import { HttpApiBuilder } from 'effect/unstable/httpapi'
4+
import { dieOnDatabaseError as makeDieOnDatabaseError } from '@/http/handler-utils'
45
import { SearchService } from '@/services/search.service'
56

6-
// Undeclared DatabaseError becomes a logged defect (500), same fallback as
7-
// the old runEffect for anything that wasn't a mapped HttpError.
8-
const dieOnDatabaseError = <A, E, R>(effect: Effect.Effect<A, E | DatabaseErrorTag, R>) =>
9-
effect.pipe(
10-
Effect.tapErrorTag('DatabaseError', (cause) =>
11-
Effect.logError('[search] database operation failed', cause)
12-
),
13-
Effect.catchTag('DatabaseError', (cause) => Effect.die(cause))
14-
)
15-
16-
type DatabaseErrorTag = { readonly _tag: 'DatabaseError' }
7+
const dieOnDatabaseError = makeDieOnDatabaseError('search')
178

189
export const SearchHandlersLive = HttpApiBuilder.group(Api, 'search', (handlers) =>
1910
handlers.handle('searchContent', ({ query }) =>

0 commit comments

Comments
 (0)