Ves qué sala está libre y la tomas.
Dos pantallas: la grilla del día, donde un click toma una sala, y la lista de salas.
Corre en sube, que le pone la base de datos, el dominio, el TLS y el inicio de sesión. La app no trae código de autenticación.
Por una carrera que casi todas las apps de reserva pierden.
Lo natural es guardar startsAt y endsAt, y antes de insertar preguntar si
la sala está libre. Eso está mal, y no por descuido: entre la consulta y el
insert hay una ventana. Dos personas reservando la misma sala en el mismo
segundo consultan las dos, ven libre las dos, insertan las dos. La app acaba de
duplicar una reunión comportándose exactamente como está escrita. Ninguna
cantidad de cuidado en ese if cierra la ventana, porque la ventana está entre
las dos sentencias.
Acá el tiempo se corta en bloques de media hora y el conflicto se convierte en un valor. Y un valor se puede volver imposible:
uniqueIndex('bookings_slot').on(table.roomId, table.onDate, table.slot)Una reunión de 9:00 a 10:30 son tres filas con el mismo groupId. Postgres
rechaza la segunda reserva bajo cualquier concurrencia, y la app se entera
porque le avisan, no porque preguntó:
try {
await db.insert(bookings).values(wanted) // una sola sentencia: todo o nada
} catch (error) {
if (isConflict(error)) redirect(`/?dia=${date}&choque=1`)
throw error
}Dos detalles que importan:
- Un solo
insertpara toda la reunión. Si un bloque está tomado falla entera y no queda media reserva ocupando una sala para una reunión que nunca ocurrió. - Solo se atrapa el 23505. Ese es el código de Postgres para "un índice único dijo que no". Cualquier otro error se relanza: tragarse todo convertiría una base de datos caída en un amable "la sala está ocupada".
El precio es real y está en el esquema: las reservas caen en medias horas. Para
una sala de reuniones eso es como la gente reserva igual. Si necesitas precisión
arbitraria, la respuesta es un tstzrange con una exclusion constraint — la
misma idea, la base de datos rechaza, con más maquinaria.
Solo quien la tomó. La regla es el where, no un if:
.where(and(eq(bookings.groupId, groupId), eq(bookings.bookedBy, who.id)))El dueño sale de la sesión y de la fila, nunca del formulario, así que mandar a
mano el groupId de otra persona no calza con ninguna fila.
npm install
npm run buildConstruye sin base de datos. Para verlo con datos hay que desplegarlo:
npm i -g @sube.sh/cli # o usa npx
sube new "Reserva de salas"
sube deployapp/page.tsx la grilla del día
app/salas/page.tsx las salas
src/lib/slots.ts el tiempo como números de bloque
src/db/schema.ts dos tablas: rooms y bookings
middleware.ts y next.config.ts los pone la plataforma. AGENTS.md explica
las reglas que un asistente de IA debe respetar al modificar esta app.
MIT.