refactor: migrate backend to pnpm monorepo with Hono module architecture
- Replace Bun with pnpm 9 + Turborepo + tsx; apps/api renamed to apps/backend - Split backend into http/, modules/, lib/ layers mirroring tai-specguard - Add use-case + Result pattern, Problem Details RFC 7807, basePath /api/v1 - Mount Better Auth at /api/v1/auth, keep session-auth whitelist - Split Prisma schema into prisma/models/*, generate into generated/ - Rework packages/shared into lib/ + schemas/ with pagination DTOs - Implement health, groups, students, payments, waitlist modules - Add vitest suite with prisma mocks (21 tests), biome lint - Point web client to /api/v1/auth and /api/v1/groups/from-organization
This commit is contained in:
89
apps/backend/src/http/problem-builders.ts
Normal file
89
apps/backend/src/http/problem-builders.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { ProblemDetails } from '@gruperly/shared';
|
||||
import { notFoundProblem } from './problem-details';
|
||||
import { PROBLEM_DOMAIN } from './problem-domain';
|
||||
|
||||
export function validationProblem(params: {
|
||||
detail?: string;
|
||||
instance?: string;
|
||||
errors?: Record<string, string[]>;
|
||||
}): ProblemDetails {
|
||||
return {
|
||||
type: `${PROBLEM_DOMAIN}/problems/validation`,
|
||||
title: 'Bad Request',
|
||||
status: 400,
|
||||
detail: params.detail ?? 'Invalid request data',
|
||||
instance: params.instance,
|
||||
errors: params.errors,
|
||||
};
|
||||
}
|
||||
|
||||
export function conflictProblem(params: {
|
||||
detail: string;
|
||||
code?: string;
|
||||
instance?: string;
|
||||
}): ProblemDetails {
|
||||
return {
|
||||
type: `${PROBLEM_DOMAIN}/problems/conflict`,
|
||||
title: 'Conflict',
|
||||
status: 409,
|
||||
detail: params.detail,
|
||||
instance: params.instance,
|
||||
code: params.code,
|
||||
};
|
||||
}
|
||||
|
||||
export function forbiddenProblem(params: {
|
||||
detail: string;
|
||||
code?: string;
|
||||
instance?: string;
|
||||
}): ProblemDetails {
|
||||
return {
|
||||
type: `${PROBLEM_DOMAIN}/problems/forbidden`,
|
||||
title: 'Forbidden',
|
||||
status: 403,
|
||||
detail: params.detail,
|
||||
instance: params.instance,
|
||||
code: params.code,
|
||||
};
|
||||
}
|
||||
|
||||
export function organizationNotFoundProblem(id: string): ProblemDetails {
|
||||
return {
|
||||
type: `${PROBLEM_DOMAIN}/problems/organization-not-found`,
|
||||
title: 'Not Found',
|
||||
status: 404,
|
||||
detail: `Organization ${id} was not found.`,
|
||||
code: 'organization_not_found',
|
||||
};
|
||||
}
|
||||
|
||||
export function groupOwnerRequiredProblem(): ProblemDetails {
|
||||
return forbiddenProblem({
|
||||
detail: 'Only the organization owner can create the group.',
|
||||
code: 'group_owner_required',
|
||||
});
|
||||
}
|
||||
|
||||
export function databaseUnavailableProblem(params?: {
|
||||
instance?: string;
|
||||
}): ProblemDetails {
|
||||
return {
|
||||
type: `${PROBLEM_DOMAIN}/problems/database-unavailable`,
|
||||
title: 'Service Unavailable',
|
||||
status: 503,
|
||||
detail: 'Database health check failed',
|
||||
instance: params?.instance,
|
||||
code: 'database_unavailable',
|
||||
};
|
||||
}
|
||||
|
||||
export function noGroupAccessProblem(): ProblemDetails {
|
||||
return forbiddenProblem({
|
||||
detail: 'You do not have access to this group.',
|
||||
code: 'group_access_denied',
|
||||
});
|
||||
}
|
||||
|
||||
export function notFoundResourceProblem(resourceName: string, id: string): ProblemDetails {
|
||||
return notFoundProblem(`${resourceName} ${id}`);
|
||||
}
|
||||
Reference in New Issue
Block a user