Close dialog on status change
This commit is contained in:
@@ -9,8 +9,8 @@ import { requestIdMiddleware } from './lib/http/request-id';
|
||||
export function createApp() {
|
||||
const app = new Hono<AppEnv>();
|
||||
|
||||
app.use('*', requestIdMiddleware)
|
||||
app.use('*', errorHandler)
|
||||
app.use('*', requestIdMiddleware);
|
||||
app.use('*', errorHandler);
|
||||
|
||||
const allowedOrigins = (Bun.env.CORS_ORIGIN ?? 'http://localhost:5173,http://127.0.0.1:5173')
|
||||
.split(',')
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
// lib/errors.ts
|
||||
export type ValidationIssue = {
|
||||
path: string
|
||||
message: string
|
||||
}
|
||||
path: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type AppError =
|
||||
| {
|
||||
type: 'validation'
|
||||
message: string
|
||||
issues?: ValidationIssue[]
|
||||
type: 'validation';
|
||||
message: string;
|
||||
issues?: ValidationIssue[];
|
||||
}
|
||||
| {
|
||||
type: 'not_found'
|
||||
message: string
|
||||
type: 'not_found';
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: 'conflict'
|
||||
message: string
|
||||
type: 'conflict';
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: 'unauthorized'
|
||||
message: string
|
||||
type: 'unauthorized';
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: 'forbidden'
|
||||
message: string
|
||||
type: 'forbidden';
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
type: 'unexpected'
|
||||
message: string
|
||||
}
|
||||
type: 'unexpected';
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const Errors = {
|
||||
validation(message: string, issues?: ValidationIssue[]): AppError {
|
||||
return { type: 'validation', message, issues }
|
||||
return { type: 'validation', message, issues };
|
||||
},
|
||||
|
||||
notFound(message = 'Resource not found'): AppError {
|
||||
return { type: 'not_found', message }
|
||||
return { type: 'not_found', message };
|
||||
},
|
||||
|
||||
conflict(message: string): AppError {
|
||||
return { type: 'conflict', message }
|
||||
return { type: 'conflict', message };
|
||||
},
|
||||
|
||||
unauthorized(message = 'Unauthorized'): AppError {
|
||||
return { type: 'unauthorized', message }
|
||||
return { type: 'unauthorized', message };
|
||||
},
|
||||
|
||||
forbidden(message = 'Forbidden'): AppError {
|
||||
return { type: 'forbidden', message }
|
||||
return { type: 'forbidden', message };
|
||||
},
|
||||
|
||||
unexpected(message = 'Unexpected error'): AppError {
|
||||
return { type: 'unexpected', message }
|
||||
}
|
||||
}
|
||||
return { type: 'unexpected', message };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// http/error-handler.ts
|
||||
import type { MiddlewareHandler } from 'hono'
|
||||
import { unexpectedProblem } from './problem-builders'
|
||||
import { logger } from '../logger'
|
||||
import type { MiddlewareHandler } from 'hono';
|
||||
import { logger } from '../logger';
|
||||
import { unexpectedProblem } from './problem-builders';
|
||||
|
||||
export const errorHandler: MiddlewareHandler = async (c, next) => {
|
||||
try {
|
||||
await next()
|
||||
await next();
|
||||
} catch (error) {
|
||||
const requestId = c.get('requestId') as string | undefined
|
||||
const instance = requestId ? `/requests/${requestId}` : undefined
|
||||
const requestId = c.get('requestId') as string | undefined;
|
||||
const instance = requestId ? `/requests/${requestId}` : undefined;
|
||||
|
||||
logger.error({
|
||||
requestId,
|
||||
error
|
||||
})
|
||||
error,
|
||||
});
|
||||
|
||||
return c.json(unexpectedProblem({ instance }), 500)
|
||||
return c.json(unexpectedProblem({ instance }), 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Context } from 'hono'
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status'
|
||||
import type { Result } from '@/lib/result'
|
||||
import { mapAppErrorToProblem } from './problem-mapper'
|
||||
import type { Result } from '@/lib/result';
|
||||
import type { Context } from 'hono';
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
||||
import { mapAppErrorToProblem } from './problem-mapper';
|
||||
|
||||
export function handleResult<T>(
|
||||
c: Context,
|
||||
@@ -9,12 +9,12 @@ export function handleResult<T>(
|
||||
successStatus: ContentfulStatusCode = 200
|
||||
) {
|
||||
if (!result.ok) {
|
||||
const requestId = c.get('requestId') as string | undefined
|
||||
const instance = requestId ? `/requests/${requestId}` : undefined
|
||||
const problem = mapAppErrorToProblem(result.error, instance)
|
||||
const requestId = c.get('requestId') as string | undefined;
|
||||
const instance = requestId ? `/requests/${requestId}` : undefined;
|
||||
const problem = mapAppErrorToProblem(result.error, instance);
|
||||
|
||||
return c.json(problem, problem.status)
|
||||
return c.json(problem, problem.status);
|
||||
}
|
||||
|
||||
return c.json(result.value, successStatus)
|
||||
}
|
||||
return c.json(result.value, successStatus);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// http/problem-builders.ts
|
||||
import type { ProblemDetails } from './problem-details'
|
||||
import type { ProblemDetails } from './problem-details';
|
||||
|
||||
export function validationProblem(params: {
|
||||
detail?: string
|
||||
instance?: string
|
||||
errors?: Record<string, string[]>
|
||||
detail?: string;
|
||||
instance?: string;
|
||||
errors?: Record<string, string[]>;
|
||||
}): ProblemDetails {
|
||||
return {
|
||||
type: 'https://api.myapp.dev/problems/validation',
|
||||
@@ -12,18 +12,18 @@ export function validationProblem(params: {
|
||||
status: 400,
|
||||
detail: params.detail ?? 'Invalid request data',
|
||||
instance: params.instance,
|
||||
errors: params.errors
|
||||
}
|
||||
errors: params.errors,
|
||||
};
|
||||
}
|
||||
|
||||
export function unexpectedProblem(params?: {
|
||||
instance?: string
|
||||
instance?: string;
|
||||
}): ProblemDetails {
|
||||
return {
|
||||
type: 'https://api.myapp.dev/problems/unexpected',
|
||||
title: 'Internal Server Error',
|
||||
status: 500,
|
||||
detail: 'Internal server error',
|
||||
instance: params?.instance
|
||||
}
|
||||
}
|
||||
instance: params?.instance,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status'
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
||||
|
||||
export type ProblemDetails = {
|
||||
type: string
|
||||
title: string
|
||||
status: ContentfulStatusCode
|
||||
detail?: string
|
||||
instance?: string
|
||||
errors?: Record<string, string[]>
|
||||
}
|
||||
type: string;
|
||||
title: string;
|
||||
status: ContentfulStatusCode;
|
||||
detail?: string;
|
||||
instance?: string;
|
||||
errors?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
@@ -1,53 +1,50 @@
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status'
|
||||
import type { AppError } from '@/lib/errors'
|
||||
import type { ProblemDetails } from './problem-details'
|
||||
import type { AppError } from '@/lib/errors';
|
||||
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
||||
import type { ProblemDetails } from './problem-details';
|
||||
|
||||
function statusFromError(error: AppError): ContentfulStatusCode {
|
||||
switch (error.type) {
|
||||
case 'validation':
|
||||
return 400
|
||||
return 400;
|
||||
case 'unauthorized':
|
||||
return 401
|
||||
return 401;
|
||||
case 'forbidden':
|
||||
return 403
|
||||
return 403;
|
||||
case 'not_found':
|
||||
return 404
|
||||
return 404;
|
||||
case 'conflict':
|
||||
return 409
|
||||
return 409;
|
||||
case 'unexpected':
|
||||
default:
|
||||
return 500
|
||||
return 500;
|
||||
}
|
||||
}
|
||||
|
||||
function titleFromStatus(status: ContentfulStatusCode): string {
|
||||
switch (status) {
|
||||
case 400:
|
||||
return 'Bad Request'
|
||||
return 'Bad Request';
|
||||
case 401:
|
||||
return 'Unauthorized'
|
||||
return 'Unauthorized';
|
||||
case 403:
|
||||
return 'Forbidden'
|
||||
return 'Forbidden';
|
||||
case 404:
|
||||
return 'Not Found'
|
||||
return 'Not Found';
|
||||
case 409:
|
||||
return 'Conflict'
|
||||
return 'Conflict';
|
||||
default:
|
||||
return 'Internal Server Error'
|
||||
return 'Internal Server Error';
|
||||
}
|
||||
}
|
||||
|
||||
export function mapAppErrorToProblem(
|
||||
error: AppError,
|
||||
instance?: string
|
||||
): ProblemDetails {
|
||||
const status = statusFromError(error)
|
||||
export function mapAppErrorToProblem(error: AppError, instance?: string): ProblemDetails {
|
||||
const status = statusFromError(error);
|
||||
|
||||
return {
|
||||
type: `https://api.myapp.dev/problems/${error.type}`,
|
||||
title: titleFromStatus(status),
|
||||
status,
|
||||
detail: status === 500 ? 'Internal server error' : error.message,
|
||||
instance
|
||||
}
|
||||
}
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { MiddlewareHandler } from 'hono'
|
||||
import type { MiddlewareHandler } from 'hono';
|
||||
|
||||
type Env = {
|
||||
Variables: {
|
||||
requestId: string
|
||||
}
|
||||
}
|
||||
requestId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const requestIdMiddleware: MiddlewareHandler<Env> = async (c, next) => {
|
||||
const requestId = crypto.randomUUID()
|
||||
const requestId = crypto.randomUUID();
|
||||
|
||||
c.set('requestId', requestId)
|
||||
c.header('X-Request-Id', requestId)
|
||||
c.set('requestId', requestId);
|
||||
c.header('X-Request-Id', requestId);
|
||||
|
||||
await next()
|
||||
}
|
||||
await next();
|
||||
};
|
||||
|
||||
@@ -1,52 +1,48 @@
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import type { ZodSchema } from 'zod';
|
||||
import { validationProblem } from './problem-builders';
|
||||
import { zodIssuesToRecord } from './zod-issues';
|
||||
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import type { ZodSchema } from 'zod'
|
||||
import { validationProblem } from './problem-builders'
|
||||
import { zodIssuesToRecord } from './zod-issues'
|
||||
type ValidationTarget = 'json' | 'query' | 'param' | 'header' | 'form';
|
||||
|
||||
type ValidationTarget = 'json' | 'query' | 'param' | 'header' | 'form'
|
||||
|
||||
function makeValidator<TSchema extends ZodSchema>(
|
||||
target: ValidationTarget,
|
||||
schema: TSchema
|
||||
) {
|
||||
function makeValidator<TSchema extends ZodSchema>(target: ValidationTarget, schema: TSchema) {
|
||||
return zValidator(target, schema, (result, c) => {
|
||||
if (result.success) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = c.get('requestId') as string | undefined
|
||||
const instance = requestId ? `/requests/${requestId}` : undefined
|
||||
const requestId = c.get('requestId') as string | undefined;
|
||||
const instance = requestId ? `/requests/${requestId}` : undefined;
|
||||
|
||||
return c.json(
|
||||
validationProblem({
|
||||
detail: `Invalid ${target} data`,
|
||||
instance,
|
||||
errors: zodIssuesToRecord(result.error.issues)
|
||||
errors: zodIssuesToRecord(result.error.issues),
|
||||
}),
|
||||
400
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const validate = {
|
||||
json<TSchema extends ZodSchema>(schema: TSchema) {
|
||||
return makeValidator('json', schema)
|
||||
return makeValidator('json', schema);
|
||||
},
|
||||
|
||||
query<TSchema extends ZodSchema>(schema: TSchema) {
|
||||
return makeValidator('query', schema)
|
||||
return makeValidator('query', schema);
|
||||
},
|
||||
|
||||
param<TSchema extends ZodSchema>(schema: TSchema) {
|
||||
return makeValidator('param', schema)
|
||||
return makeValidator('param', schema);
|
||||
},
|
||||
|
||||
header<TSchema extends ZodSchema>(schema: TSchema) {
|
||||
return makeValidator('header', schema)
|
||||
return makeValidator('header', schema);
|
||||
},
|
||||
|
||||
form<TSchema extends ZodSchema>(schema: TSchema) {
|
||||
return makeValidator('form', schema)
|
||||
}
|
||||
}
|
||||
return makeValidator('form', schema);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
type IssueLike = {
|
||||
path: PropertyKey[]
|
||||
message: string
|
||||
}
|
||||
path: PropertyKey[];
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function zodIssuesToRecord(
|
||||
issues: IssueLike[]
|
||||
): Record<string, string[]> {
|
||||
const out: Record<string, string[]> = {}
|
||||
export function zodIssuesToRecord(issues: IssueLike[]): Record<string, string[]> {
|
||||
const out: Record<string, string[]> = {};
|
||||
|
||||
for (const issue of issues) {
|
||||
const key = issue.path.length ? issue.path.join('.') : 'root'
|
||||
out[key] ??= []
|
||||
out[key].push(issue.message)
|
||||
const key = issue.path.length ? issue.path.join('.') : 'root';
|
||||
out[key] ??= [];
|
||||
out[key].push(issue.message);
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// lib/result.ts
|
||||
import type { AppError } from '@/lib/errors';
|
||||
|
||||
export type Result<T, E = AppError> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: E }
|
||||
export type Result<T, E = AppError> = { ok: true; value: T } | { ok: false; error: E };
|
||||
|
||||
export function ok<T>(value: T): Result<T> {
|
||||
return { ok: true, value }
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
export function err<E>(error: E): Result<never, E> {
|
||||
return { ok: false, error }
|
||||
}
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
@@ -494,7 +494,7 @@ export async function updateAdminBookingStatus(
|
||||
);
|
||||
}
|
||||
|
||||
if(input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
|
||||
if (input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
|
||||
throw new AdminBookingServiceError(
|
||||
'Solo se pueden marcar como no show las reservas confirmadas.',
|
||||
409
|
||||
@@ -517,7 +517,7 @@ export async function updateAdminBookingStatus(
|
||||
},
|
||||
});
|
||||
|
||||
if(input.status === 'COMPLETED' || input.status ==='NOSHOW') {
|
||||
if (input.status === 'COMPLETED' || input.status === 'NOSHOW') {
|
||||
await db.courtBooking.update({
|
||||
where: { id: booking.id },
|
||||
data: {
|
||||
@@ -528,8 +528,8 @@ export async function updateAdminBookingStatus(
|
||||
// if the booking is cancelled we delete it to free up the slot, but we keep a log of it with the cancelled status
|
||||
await db.courtBooking.delete({
|
||||
where: { id: booking.id },
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return mapBookingResponse({...booking, status: input.status });
|
||||
return mapBookingResponse({ ...booking, status: input.status });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Prisma, Sport } from '@/generated/prisma/client';
|
||||
import { Errors } from '@/lib/errors';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { err, ok, Result } from '@/lib/result';
|
||||
import { Result, err, ok } from '@/lib/result';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
|
||||
export type CreateSportInput = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { validate } from '@/lib/http/validate';
|
||||
import { requireAuth } from '@/middlewares/require-auth.middleware';
|
||||
import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware';
|
||||
import { createSportHandler } from '@/modules/sport/handlers/create-sport.handler';
|
||||
@@ -8,7 +9,6 @@ import { zValidator } from '@hono/zod-validator';
|
||||
import { createSportSchema, updateSportSchema } from '@repo/api-contract';
|
||||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
import { validate } from '@/lib/http/validate'
|
||||
export const sportRoutes = new Hono<AppEnv>();
|
||||
const sportIdParamsSchema = z.object({ id: z.uuid() });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user