- 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.
56 lines
2.3 KiB
TypeScript
56 lines
2.3 KiB
TypeScript
import { PGlite } from '@electric-sql/pglite'
|
|
import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres'
|
|
import { migrate as migratePg } from 'drizzle-orm/node-postgres/migrator'
|
|
import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core'
|
|
import { drizzle as drizzlePglite } from 'drizzle-orm/pglite'
|
|
import { migrate as migratePglite } from 'drizzle-orm/pglite/migrator'
|
|
import { mkdirSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import pg from 'pg'
|
|
import * as schema from './schema.ts'
|
|
|
|
export type Db = PgDatabase<PgQueryResultHKT, typeof schema>
|
|
|
|
export interface Database {
|
|
db: Db
|
|
close(): Promise<void>
|
|
}
|
|
|
|
const migrationsFolder = join(import.meta.dirname, 'migrations')
|
|
|
|
/**
|
|
* Opens the database and applies pending migrations.
|
|
*
|
|
* - `postgres://…` connects to PostgreSQL (production).
|
|
* - `pglite://<dir>` uses embedded PostgreSQL stored in `<dir>` (local development, very small installs).
|
|
* - `memory://` uses a throwaway in-memory database (tests).
|
|
*/
|
|
export async function openDatabase(url: string): Promise<Database> {
|
|
if (/^postgres(ql)?:\/\//.test(url)) {
|
|
const pool = new pg.Pool({ connectionString: url })
|
|
const db = drizzlePg({ client: pool, schema, casing: 'snake_case' })
|
|
await migratePg(db, { migrationsFolder })
|
|
return { db: db as unknown as Db, close: () => pool.end() }
|
|
}
|
|
|
|
if (url !== 'memory://' && !url.startsWith('pglite://')) {
|
|
throw new Error(`Unsupported DATABASE_URL: expected postgres://, pglite:// or memory://`)
|
|
}
|
|
const dataDir = url === 'memory://' ? undefined : url.slice('pglite://'.length)
|
|
if (dataDir) mkdirSync(dataDir, { recursive: true })
|
|
const client = new PGlite(dataDir)
|
|
const db = drizzlePglite({ client, schema, casing: 'snake_case' })
|
|
await migratePglite(db, { migrationsFolder })
|
|
return { db: db as unknown as Db, close: () => client.close() }
|
|
}
|
|
|
|
/** True for a unique-constraint violation from either driver (Drizzle wraps driver errors in `cause`). */
|
|
export function isUniqueViolation(error: unknown): boolean {
|
|
let current: unknown = error
|
|
for (let depth = 0; current && typeof current === 'object' && depth < 5; depth++) {
|
|
if ((current as { code?: unknown }).code === '23505') return true
|
|
current = (current as { cause?: unknown }).cause
|
|
}
|
|
return false
|
|
}
|