diff --git a/package.json b/package.json index 1a6b603d9..b9f750981 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@uidotdev/usehooks": "2.4.1", "base-vaul": "1.2.1", "better-auth": "1.4.17", + "better-result": "^2.7.0", "boring-avatars": "2.0.4", "class-variance-authority": "0.7.1", "clsx": "2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25102b972..3f106076a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: better-auth: specifier: 1.4.17 version: 1.4.17(@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3))(@tanstack/react-start@1.157.16(crossws@0.4.4(srvx@0.8.16))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)))(prisma@6.19.2(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)(vitest@4.0.18) + better-result: + specifier: ^2.7.0 + version: 2.7.0 boring-avatars: specifier: 2.0.4 version: 2.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -919,6 +922,12 @@ packages: '@better-upload/server@3.0.12': resolution: {integrity: sha512-okXKhRTD43AwT6sXk5ZG7np2zdnJAppxgW5r46yek6HiHHwCGWcqsviLJ26sR0VisWlPk6ZC0mK85M7xDnn55w==} + '@clack/core@0.5.0': + resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} + + '@clack/prompts@0.11.0': + resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@code-inspector/core@1.4.1': resolution: {integrity: sha512-k5iLYvrBBPBPODcwuzgEcAZnXU4XTnEO1jOmNQBHCehN6nrMO1m5Efjz35KPkSX+8T4IWvXvLoXR5XPfhDlxug==} @@ -2056,79 +2065,66 @@ packages: resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.57.0': resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.57.0': resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.57.0': resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.57.0': resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.57.0': resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.57.0': resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.57.0': resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.57.0': resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.57.0': resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.57.0': resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.0': resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.57.0': resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.57.0': resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==} @@ -2424,28 +2420,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -3234,6 +3226,10 @@ packages: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} + better-result@2.7.0: + resolution: {integrity: sha512-7zrmXjAK8u8Z6SOe4R65XObOR5X+Y2I/VVku3t5cPOGQ8/WsBcfFmfnIPiEl5EBMDOzPHRwbiPbMtQBKYdw7RA==} + hasBin: true + big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} @@ -5059,56 +5055,48 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-gnu@1.31.1: resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -8109,6 +8097,17 @@ snapshots: fast-xml-parser: 5.3.3 zod: 4.3.6 + '@clack/core@0.5.0': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.11.0': + dependencies: + '@clack/core': 0.5.0 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@code-inspector/core@1.4.1': dependencies: '@vue/compiler-dom': 3.5.27 @@ -10838,6 +10837,10 @@ snapshots: dependencies: open: 8.4.2 + better-result@2.7.0: + dependencies: + '@clack/prompts': 0.11.0 + big-integer@1.6.52: {} binary-extensions@2.3.0: {} diff --git a/src/server/db/index.ts b/src/server/db/index.ts index b812498c9..69c78a15c 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -1,7 +1,44 @@ +import { Result, TaggedError } from 'better-result'; + import { envServer } from '@/env/server'; import { timingStore } from '@/server/timing-store'; -import { PrismaClient } from './generated/client'; +import { Prisma, PrismaClient } from './generated/client'; + +export class PrismaUniqueConstraintError extends TaggedError( + 'PrismaUniqueConstraintError' +)<{ + message: string; + target: unknown; +}>() {} + +export class PrismaNotFoundError extends TaggedError('PrismaNotFoundError')<{ + message: string; +}>() {} + +export class PrismaForeignKeyError extends TaggedError( + 'PrismaForeignKeyError' +)<{ + message: string; +}>() {} + +export class PrismaValidationError extends TaggedError( + 'PrismaValidationError' +)<{ + message: string; +}>() {} + +export class PrismaUnknownError extends TaggedError('PrismaUnknownError')<{ + message: string; + cause: unknown; +}>() {} + +export type PrismaError = + | PrismaUniqueConstraintError + | PrismaNotFoundError + | PrismaForeignKeyError + | PrismaValidationError + | PrismaUnknownError; const levels = { trace: ['query', 'error', 'warn', 'info'], @@ -50,3 +87,41 @@ const globalForPrisma = globalThis as unknown as { export const db = globalForPrisma.prisma ?? createPrisma(); if (import.meta.env.DEV) globalForPrisma.prisma = db; + +export const tryQuery = (promise: Promise) => + Result.tryPromise({ + try: () => promise, + catch: (e): PrismaError => { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + switch (e.code) { + case 'P2002': + return new PrismaUniqueConstraintError({ + message: 'Unique constraint violation', + target: e.meta?.target, + }); + case 'P2025': + return new PrismaNotFoundError({ + message: 'Record not found', + }); + case 'P2003': + return new PrismaForeignKeyError({ + message: 'Foreign key constraint violation', + }); + default: + return new PrismaUnknownError({ + message: `Database error: ${e.code}`, + cause: e, + }); + } + } + if (e instanceof Prisma.PrismaClientValidationError) { + return new PrismaValidationError({ + message: 'Database validation error', + }); + } + return new PrismaUnknownError({ + message: e instanceof Error ? e.message : 'Unknown database error', + cause: e, + }); + }, + }); diff --git a/src/server/orpc.ts b/src/server/orpc.ts index c589c0132..a5358ee2a 100644 --- a/src/server/orpc.ts +++ b/src/server/orpc.ts @@ -1,18 +1,35 @@ import { ORPCError, os } from '@orpc/server'; import { type ResponseHeadersPluginContext } from '@orpc/server/plugins'; import { getRequestHeaders } from '@tanstack/react-start/server'; +import { matchError } from 'better-result'; import { randomUUID } from 'node:crypto'; import { performance } from 'node:perf_hooks'; -import { match } from 'ts-pattern'; import { envClient } from '@/env/client'; import { Permission } from '@/features/auth/permissions'; import { auth } from '@/server/auth'; -import { db } from '@/server/db'; -import { Prisma } from '@/server/db/generated/client'; +import { db, type PrismaError } from '@/server/db'; import { logger } from '@/server/logger'; import { timingStore } from '@/server/timing-store'; +export function throwPrismaError(error: PrismaError): never { + throw matchError(error, { + PrismaUniqueConstraintError: (e) => + new ORPCError('CONFLICT', { + message: e.message, + data: { target: e.target }, + }), + PrismaNotFoundError: (e) => + new ORPCError('NOT_FOUND', { message: e.message }), + PrismaForeignKeyError: (e) => + new ORPCError('BAD_REQUEST', { message: e.message }), + PrismaValidationError: (e) => + new ORPCError('BAD_REQUEST', { message: e.message }), + PrismaUnknownError: (e) => + new ORPCError('INTERNAL_SERVER_ERROR', { message: e.message }), + }); +} + const base = os .$context() // Auth @@ -108,70 +125,6 @@ const base = os }); } return await next(); - }) - // Prisma Error Handler - .use(async ({ next, context }) => { - try { - return await next(); - } catch (error) { - if (error instanceof ORPCError) { - throw error; - } - - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw match(error.code) - .with('P2002', () => { - context.logger.warn( - error.meta, - `Prisma Error: ${error.code} ${error.message}` - ); - return new ORPCError('CONFLICT', { - message: 'Unique constraint violation', - data: { target: error.meta?.target }, - }); - }) - .with('P2025', () => { - context.logger.warn( - error.meta, - `Prisma Error ${error.code}: ${error.message}` - ); - return new ORPCError('NOT_FOUND', { - message: 'Record not found', - }); - }) - .with('P2003', () => { - context.logger.error( - error.meta, - `Prisma Error ${error.code}: ${error.message}` - ); - return new ORPCError('BAD_REQUEST', { - message: 'Foreign key constraint violation', - }); - }) - .otherwise(() => { - context.logger.error( - error.meta, - `Prisma Error ${error.code}: ${error.message}` - ); - return new ORPCError('INTERNAL_SERVER_ERROR', { - message: 'Database error', - }); - }); - } - - if (error instanceof Prisma.PrismaClientValidationError) { - context.logger.error( - `Prisma Client Validation Error: ${error.message}` - ); - throw new ORPCError('BAD_REQUEST', { - message: 'Database validation error', - }); - } - - throw new ORPCError('INTERNAL_SERVER_ERROR', { - message: 'Unhandled error', - }); - } }); export const publicProcedure = () => base; diff --git a/src/server/routers/account.ts b/src/server/routers/account.ts index df5a76e83..89197867b 100644 --- a/src/server/routers/account.ts +++ b/src/server/routers/account.ts @@ -2,7 +2,8 @@ import { z } from 'zod'; import { zFormFieldsOnboarding } from '@/features/auth/schema'; import { zUser } from '@/features/user/schema'; -import { protectedProcedure } from '@/server/orpc'; +import { tryQuery } from '@/server/db'; +import { protectedProcedure, throwPrismaError } from '@/server/orpc'; const tags = ['account']; @@ -19,13 +20,16 @@ export default { .output(z.void()) .handler(async ({ context, input }) => { context.logger.info('Update user'); - await context.db.user.update({ - where: { id: context.user.id }, - data: { - ...input, - onboardedAt: new Date(), - }, - }); + const result = await tryQuery( + context.db.user.update({ + where: { id: context.user.id }, + data: { + ...input, + onboardedAt: new Date(), + }, + }) + ); + if (result.isErr()) throwPrismaError(result.error); }), updateInfo: protectedProcedure({ @@ -44,11 +48,14 @@ export default { .output(z.void()) .handler(async ({ context, input }) => { context.logger.info('Update user'); - await context.db.user.update({ - where: { id: context.user.id }, - data: { - name: input.name ?? '', - }, - }); + const result = await tryQuery( + context.db.user.update({ + where: { id: context.user.id }, + data: { + name: input.name ?? '', + }, + }) + ); + if (result.isErr()) throwPrismaError(result.error); }), }; diff --git a/src/server/routers/book.ts b/src/server/routers/book.ts index 886a6f717..e9a625522 100644 --- a/src/server/routers/book.ts +++ b/src/server/routers/book.ts @@ -2,8 +2,9 @@ import { ORPCError } from '@orpc/client'; import { z } from 'zod'; import { zBook, zFormFieldsBook } from '@/features/book/schema'; +import { tryQuery } from '@/server/db'; import { Prisma } from '@/server/db/generated/client'; -import { protectedProcedure } from '@/server/orpc'; +import { protectedProcedure, throwPrismaError } from '@/server/orpc'; const tags = ['books']; @@ -54,22 +55,32 @@ export default { ], } satisfies Prisma.BookWhereInput; - const [total, items] = await Promise.all([ - context.db.book.count({ - where, - }), - context.db.book.findMany({ - // Get an extra item at the end which we'll use as next cursor - take: input.limit + 1, - cursor: input.cursor ? { id: input.cursor } : undefined, - orderBy: { - title: 'asc', - }, - where, - include: { genre: true }, - }), + const [totalResult, itemsResult] = await Promise.all([ + tryQuery( + context.db.book.count({ + where, + }) + ), + tryQuery( + context.db.book.findMany({ + // Get an extra item at the end which we'll use as next cursor + take: input.limit + 1, + cursor: input.cursor ? { id: input.cursor } : undefined, + orderBy: { + title: 'asc', + }, + where, + include: { genre: true }, + }) + ), ]); + if (totalResult.isErr()) throwPrismaError(totalResult.error); + if (itemsResult.isErr()) throwPrismaError(itemsResult.error); + + const total = totalResult.value; + const items = itemsResult.value; + let nextCursor: typeof input.cursor | undefined = undefined; if (items.length > input.limit) { const nextItem = items.pop(); @@ -101,17 +112,20 @@ export default { .output(zBook()) .handler(async ({ context, input }) => { context.logger.info('Getting book'); - const book = await context.db.book.findUnique({ - where: { id: input.id }, - include: { genre: true }, - }); + const result = await tryQuery( + context.db.book.findUnique({ + where: { id: input.id }, + include: { genre: true }, + }) + ); + if (result.isErr()) throwPrismaError(result.error); - if (!book) { + if (!result.value) { context.logger.warn('Unable to find book with the provided input'); throw new ORPCError('NOT_FOUND'); } - return book; + return result.value; }), create: protectedProcedure({ @@ -128,15 +142,19 @@ export default { .output(zBook()) .handler(async ({ context, input }) => { context.logger.info('Create book'); - return await context.db.book.create({ - data: { - title: input.title, - author: input.author, - genreId: input.genreId ?? undefined, - publisher: input.publisher, - coverId: input.coverId, - }, - }); + const result = await tryQuery( + context.db.book.create({ + data: { + title: input.title, + author: input.author, + genreId: input.genreId ?? undefined, + publisher: input.publisher, + coverId: input.coverId, + }, + }) + ); + if (result.isErr()) throwPrismaError(result.error); + return result.value; }), updateById: protectedProcedure({ @@ -153,16 +171,20 @@ export default { .output(zBook()) .handler(async ({ context, input }) => { context.logger.info('Update book'); - return await context.db.book.update({ - where: { id: input.id }, - data: { - title: input.title, - author: input.author, - genreId: input.genreId, - publisher: input.publisher ?? null, - coverId: input.coverId ?? null, - }, - }); + const result = await tryQuery( + context.db.book.update({ + where: { id: input.id }, + data: { + title: input.title, + author: input.author, + genreId: input.genreId, + publisher: input.publisher ?? null, + coverId: input.coverId ?? null, + }, + }) + ); + if (result.isErr()) throwPrismaError(result.error); + return result.value; }), deleteById: protectedProcedure({ @@ -183,8 +205,11 @@ export default { .output(z.void()) .handler(async ({ context, input }) => { context.logger.info('Delete book'); - await context.db.book.delete({ - where: { id: input.id }, - }); + const result = await tryQuery( + context.db.book.delete({ + where: { id: input.id }, + }) + ); + if (result.isErr()) throwPrismaError(result.error); }), }; diff --git a/src/server/routers/genre.ts b/src/server/routers/genre.ts index 2ada151dc..fd2976a5d 100644 --- a/src/server/routers/genre.ts +++ b/src/server/routers/genre.ts @@ -1,8 +1,9 @@ import { z } from 'zod'; import { zGenre } from '@/features/genre/schema'; +import { tryQuery } from '@/server/db'; import { Prisma } from '@/server/db/generated/client'; -import { protectedProcedure } from '@/server/orpc'; +import { protectedProcedure, throwPrismaError } from '@/server/orpc'; const tags = ['genres']; @@ -43,20 +44,29 @@ export default { }, } satisfies Prisma.GenreWhereInput; - const [total, items] = await Promise.all([ - context.db.genre.count({ - where, - }), - context.db.genre.findMany({ - // Get an extra item at the end which we'll use as next cursor - take: input.limit + 1, - cursor: input.cursor ? { id: input.cursor } : undefined, - orderBy: { - name: 'asc', - }, - where, - }), + const [totalResult, itemsResult] = await Promise.all([ + tryQuery( + context.db.genre.count({ + where, + }) + ), + tryQuery( + context.db.genre.findMany({ + // Get an extra item at the end which we'll use as next cursor + take: input.limit + 1, + cursor: input.cursor ? { id: input.cursor } : undefined, + orderBy: { + name: 'asc', + }, + where, + }) + ), ]); + if (totalResult.isErr()) throwPrismaError(totalResult.error); + if (itemsResult.isErr()) throwPrismaError(itemsResult.error); + + const total = totalResult.value; + const items = itemsResult.value; let nextCursor: typeof input.cursor | undefined = undefined; if (items.length > input.limit) { diff --git a/src/server/routers/user.ts b/src/server/routers/user.ts index c16ff712e..88b6d16d0 100644 --- a/src/server/routers/user.ts +++ b/src/server/routers/user.ts @@ -4,8 +4,9 @@ import { z } from 'zod'; import { zSession, zUser } from '@/features/user/schema'; import { auth } from '@/server/auth'; +import { tryQuery } from '@/server/db'; import { Prisma } from '@/server/db/generated/client'; -import { protectedProcedure } from '@/server/orpc'; +import { protectedProcedure, throwPrismaError } from '@/server/orpc'; const tags = ['users']; @@ -55,20 +56,29 @@ export default { } satisfies Prisma.UserWhereInput; context.logger.info('Getting users from database'); - const [total, items] = await Promise.all([ - context.db.user.count({ - where, - }), - context.db.user.findMany({ - // Get an extra item at the end which we'll use as next cursor - take: input.limit + 1, - cursor: input.cursor ? { id: input.cursor } : undefined, - orderBy: { - name: 'asc', - }, - where, - }), + const [totalResult, itemsResult] = await Promise.all([ + tryQuery( + context.db.user.count({ + where, + }) + ), + tryQuery( + context.db.user.findMany({ + // Get an extra item at the end which we'll use as next cursor + take: input.limit + 1, + cursor: input.cursor ? { id: input.cursor } : undefined, + orderBy: { + name: 'asc', + }, + where, + }) + ), ]); + if (totalResult.isErr()) throwPrismaError(totalResult.error); + if (itemsResult.isErr()) throwPrismaError(itemsResult.error); + + const total = totalResult.value; + const items = itemsResult.value; let nextCursor: typeof input.cursor | undefined = undefined; if (items.length > input.limit) { @@ -101,16 +111,19 @@ export default { .output(zUser()) .handler(async ({ context, input }) => { context.logger.info('Getting user'); - const user = await context.db.user.findUnique({ - where: { id: input.id }, - }); + const result = await tryQuery( + context.db.user.findUnique({ + where: { id: input.id }, + }) + ); + if (result.isErr()) throwPrismaError(result.error); - if (!user) { + if (!result.value) { context.logger.warn('Unable to find user with the provided input'); throw new ORPCError('NOT_FOUND'); } - return user; + return result.value; }), updateById: protectedProcedure({ @@ -134,28 +147,36 @@ export default { .output(zUser()) .handler(async ({ context, input }) => { context.logger.info('Getting current user email'); - const currentUser = await context.db.user.findUnique({ - where: { id: input.id }, - select: { email: true }, - }); + const currentUserResult = await tryQuery( + context.db.user.findUnique({ + where: { id: input.id }, + select: { email: true }, + }) + ); + if (currentUserResult.isErr()) throwPrismaError(currentUserResult.error); - if (!currentUser) { + if (!currentUserResult.value) { context.logger.warn('Unable to find user with the provided input'); throw new ORPCError('NOT_FOUND'); } context.logger.info('Update user'); - return await context.db.user.update({ - where: { id: input.id }, - data: { - name: input.name ?? '', - // Prevent to change role of the connected user - role: context.user.id === input.id ? undefined : input.role, - email: input.email, - // Set email as verified if admin changed the email - emailVerified: currentUser.email !== input.email ? true : undefined, - }, - }); + const result = await tryQuery( + context.db.user.update({ + where: { id: input.id }, + data: { + name: input.name ?? '', + // Prevent to change role of the connected user + role: context.user.id === input.id ? undefined : input.role, + email: input.email, + // Set email as verified if admin changed the email + emailVerified: + currentUserResult.value.email !== input.email ? true : undefined, + }, + }) + ); + if (result.isErr()) throwPrismaError(result.error); + return result.value; }), create: protectedProcedure({ @@ -178,14 +199,18 @@ export default { .output(zUser()) .handler(async ({ context, input }) => { context.logger.info('Create user'); - return await context.db.user.create({ - data: { - email: input.email, - emailVerified: true, - name: input.name ?? '', - role: input.role ?? 'user', - }, - }); + const result = await tryQuery( + context.db.user.create({ + data: { + email: input.email, + emailVerified: true, + name: input.name ?? '', + role: input.role ?? 'user', + }, + }) + ); + if (result.isErr()) throwPrismaError(result.error); + return result.value; }), deleteById: protectedProcedure({ @@ -256,20 +281,29 @@ export default { } satisfies Prisma.SessionWhereInput; context.logger.info('Getting user sessions from database'); - const [total, items] = await Promise.all([ - context.db.session.count({ - where, - }), - context.db.session.findMany({ - // Get an extra item at the end which we'll use as next cursor - take: input.limit + 1, - cursor: input.cursor ? { id: input.cursor } : undefined, - orderBy: { - createdAt: 'desc', - }, - where, - }), + const [totalResult, itemsResult] = await Promise.all([ + tryQuery( + context.db.session.count({ + where, + }) + ), + tryQuery( + context.db.session.findMany({ + // Get an extra item at the end which we'll use as next cursor + take: input.limit + 1, + cursor: input.cursor ? { id: input.cursor } : undefined, + orderBy: { + createdAt: 'desc', + }, + where, + }) + ), ]); + if (totalResult.isErr()) throwPrismaError(totalResult.error); + if (itemsResult.isErr()) throwPrismaError(itemsResult.error); + + const total = totalResult.value; + const items = itemsResult.value; let nextCursor: typeof input.cursor | undefined = undefined; if (items.length > input.limit) {