- Add incident reporting routes and logic to handle creation, updating, and retrieval of incidents. - Implement visibility rules for users based on roles (reporters, supervisors). - Create tests for incident reporting functionality to ensure correctness and reliability. - Introduce site and area management routes, including validation for area hierarchy. - Establish user management routes with role-based access control for creating and updating users. - Add setup routes for initializing the application with an organization and admin user. - Implement shared enums and schemas for consistent validation and data handling across the application. - Create a test helper for setting up an in-memory database and API client for testing. - Add TypeScript configuration for server-side code with strict type checking and module resolution.
110 lines
5.3 KiB
TypeScript
110 lines
5.3 KiB
TypeScript
import { and, asc, eq, inArray, isNull } from 'drizzle-orm'
|
|
import { Hono } from 'hono'
|
|
import { z } from 'zod'
|
|
import { createAreaSchema, createSiteSchema, updateAreaSchema, updateSiteSchema } from '../../shared/schemas.ts'
|
|
import { requireAuth, requireRole } from '../auth/middleware.ts'
|
|
import { isUniqueViolation, type Db } from '../db/client.ts'
|
|
import { areas, sites } from '../db/schema.ts'
|
|
import { audit, diff } from '../lib/audit.ts'
|
|
import { currentUser, fail, validate, type AppEnv } from '../lib/http.ts'
|
|
|
|
const idParam = z.object({ id: z.uuid() })
|
|
const listQuery = z.object({ includeArchived: z.enum(['true', 'false']).optional() })
|
|
|
|
async function findSite(db: Db, organizationId: string, id: string) {
|
|
const [site] = await db.select().from(sites).where(and(eq(sites.id, id), eq(sites.organizationId, organizationId)))
|
|
if (!site) throw fail(404, 'Site not found')
|
|
return site
|
|
}
|
|
|
|
const archivedAt = (archived: boolean | undefined) => (archived === undefined ? undefined : archived ? new Date() : null)
|
|
|
|
export const siteRoutes = new Hono<AppEnv>()
|
|
.get('/', requireAuth, validate('query', listQuery), async (c) => {
|
|
const me = currentUser(c)
|
|
const includeArchived = c.req.valid('query').includeArchived === 'true'
|
|
const db = c.get('db')
|
|
const siteRows = await db
|
|
.select()
|
|
.from(sites)
|
|
.where(and(eq(sites.organizationId, me.organizationId), includeArchived ? undefined : isNull(sites.archivedAt)))
|
|
.orderBy(asc(sites.name))
|
|
const areaRows = siteRows.length
|
|
? await db
|
|
.select()
|
|
.from(areas)
|
|
.where(and(inArray(areas.siteId, siteRows.map((site) => site.id)), includeArchived ? undefined : isNull(areas.archivedAt)))
|
|
.orderBy(asc(areas.name))
|
|
: []
|
|
return c.json({ sites: siteRows.map((site) => ({ ...site, areas: areaRows.filter((area) => area.siteId === site.id) })) })
|
|
})
|
|
|
|
.post('/', requireRole('safety_manager'), validate('json', createSiteSchema), async (c) => {
|
|
const me = currentUser(c)
|
|
const db = c.get('db')
|
|
try {
|
|
const [site] = await db
|
|
.insert(sites)
|
|
.values({ ...c.req.valid('json'), organizationId: me.organizationId })
|
|
.returning()
|
|
await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'site', entityId: site.id, action: 'create', changes: { name: site.name } })
|
|
return c.json({ site: { ...site, areas: [] } }, 201)
|
|
} catch (error) {
|
|
if (isUniqueViolation(error)) throw fail(409, 'A site with that name already exists')
|
|
throw error
|
|
}
|
|
})
|
|
|
|
.patch('/:id', requireRole('safety_manager'), validate('param', idParam), validate('json', updateSiteSchema), async (c) => {
|
|
const me = currentUser(c)
|
|
const db = c.get('db')
|
|
const site = await findSite(db, me.organizationId, c.req.valid('param').id)
|
|
const { archived, ...fields } = c.req.valid('json')
|
|
const update = { ...fields, archivedAt: archivedAt(archived) }
|
|
const changes = diff(site, update)
|
|
if (Object.keys(changes).length === 0) return c.json({ site })
|
|
try {
|
|
const [updated] = await db.update(sites).set(update).where(eq(sites.id, site.id)).returning()
|
|
await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'site', entityId: site.id, action: 'update', changes })
|
|
return c.json({ site: updated })
|
|
} catch (error) {
|
|
if (isUniqueViolation(error)) throw fail(409, 'A site with that name already exists')
|
|
throw error
|
|
}
|
|
})
|
|
|
|
.post('/:id/areas', requireRole('safety_manager'), validate('param', idParam), validate('json', createAreaSchema), async (c) => {
|
|
const me = currentUser(c)
|
|
const db = c.get('db')
|
|
const site = await findSite(db, me.organizationId, c.req.valid('param').id)
|
|
const input = c.req.valid('json')
|
|
if (input.parentId) {
|
|
const [parent] = await db.select({ id: areas.id }).from(areas).where(and(eq(areas.id, input.parentId), eq(areas.siteId, site.id)))
|
|
if (!parent) throw fail(400, 'Parent area must belong to the same site')
|
|
}
|
|
const [area] = await db
|
|
.insert(areas)
|
|
.values({ ...input, siteId: site.id })
|
|
.returning()
|
|
await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'area', entityId: area.id, action: 'create', changes: { name: area.name, siteId: site.id } })
|
|
return c.json({ area }, 201)
|
|
})
|
|
|
|
export const areaRoutes = new Hono<AppEnv>().patch('/:id', requireRole('safety_manager'), validate('param', idParam), validate('json', updateAreaSchema), async (c) => {
|
|
const me = currentUser(c)
|
|
const db = c.get('db')
|
|
const [row] = await db
|
|
.select({ area: areas })
|
|
.from(areas)
|
|
.innerJoin(sites, eq(sites.id, areas.siteId))
|
|
.where(and(eq(areas.id, c.req.valid('param').id), eq(sites.organizationId, me.organizationId)))
|
|
if (!row) throw fail(404, 'Area not found')
|
|
const { archived, ...fields } = c.req.valid('json')
|
|
const update = { ...fields, archivedAt: archivedAt(archived) }
|
|
const changes = diff(row.area, update)
|
|
if (Object.keys(changes).length === 0) return c.json({ area: row.area })
|
|
const [area] = await db.update(areas).set(update).where(eq(areas.id, row.area.id)).returning()
|
|
await audit(db, { organizationId: me.organizationId, actorId: me.id, entityType: 'area', entityId: area.id, action: 'update', changes })
|
|
return c.json({ area })
|
|
})
|