feat(db): configurable path, user field extensions, slug generation
- BIFROST_DB_PATH env var overrides the default bifrost.db path; lets vitest open ':memory:' per suite without touching prod data. - Extend User/UserPublic with title, cab_joined_date, slug. - Update SELECT lists for getUserPublicById and getAllUsersPublic. - Add getUserBySlug for /members/:slug routes. - Add slugifyName + generateUniqueSlug; createUser now auto-slugs from name and stamps cab_joined_date for cab-role users. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
53cb9a7e49
commit
56992ed4ca
1 changed files with 42 additions and 6 deletions
|
|
@ -18,6 +18,9 @@ export interface User {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
last_seen_at: string | null;
|
last_seen_at: string | null;
|
||||||
active: number;
|
active: number;
|
||||||
|
title: string | null; // job title, e.g. 'VP Engineering'
|
||||||
|
cab_joined_date: string | null; // ISO date; only set for role='cab'. Falls back to created_at when null.
|
||||||
|
slug: string | null; // URL handle, kebab-case, unique. Auto-generated on createUser.
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserPublic = Omit<User, 'password_hash'>;
|
export type UserPublic = Omit<User, 'password_hash'>;
|
||||||
|
|
@ -69,10 +72,13 @@ export interface AttendanceSummary {
|
||||||
|
|
||||||
// ── Connection ───────────────────────────────────────────────────
|
// ── Connection ───────────────────────────────────────────────────
|
||||||
|
|
||||||
// Persist across Vite HMR reloads in dev
|
// Persist across Vite HMR reloads in dev. Override BIFROST_DB_PATH for tests
|
||||||
|
// (use ':memory:'); defaults to bifrost.db in the project root.
|
||||||
|
const DB_PATH = process.env.BIFROST_DB_PATH ?? join(process.cwd(), 'bifrost.db');
|
||||||
|
|
||||||
const g = globalThis as typeof globalThis & { __bifrost_db?: Database.Database };
|
const g = globalThis as typeof globalThis & { __bifrost_db?: Database.Database };
|
||||||
if (!g.__bifrost_db) {
|
if (!g.__bifrost_db) {
|
||||||
g.__bifrost_db = new Database(join(process.cwd(), 'bifrost.db'));
|
g.__bifrost_db = new Database(DB_PATH);
|
||||||
g.__bifrost_db.pragma('journal_mode = WAL');
|
g.__bifrost_db.pragma('journal_mode = WAL');
|
||||||
g.__bifrost_db.pragma('foreign_keys = ON');
|
g.__bifrost_db.pragma('foreign_keys = ON');
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +100,7 @@ export function getUserById(id: number): User | null {
|
||||||
|
|
||||||
export function getUserPublicById(id: number): UserPublic | null {
|
export function getUserPublicById(id: number): UserPublic | null {
|
||||||
return db.prepare(
|
return db.prepare(
|
||||||
'SELECT id,email,name,organisation,role,bio,created_at,last_seen_at,active FROM users WHERE id = ? AND active = 1'
|
'SELECT id,email,name,organisation,role,bio,created_at,last_seen_at,active,title,cab_joined_date,slug FROM users WHERE id = ? AND active = 1'
|
||||||
).get(id) as UserPublic | null;
|
).get(id) as UserPublic | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,9 +111,11 @@ export function createUser(data: {
|
||||||
organisation: string;
|
organisation: string;
|
||||||
role: Role;
|
role: Role;
|
||||||
}): number {
|
}): number {
|
||||||
|
const slug = generateUniqueSlug(data.name);
|
||||||
|
const cabJoined = data.role === 'cab' ? new Date().toISOString().slice(0, 10) : null;
|
||||||
const r = db.prepare(
|
const r = db.prepare(
|
||||||
'INSERT INTO users (email,password_hash,name,organisation,role) VALUES (?,?,?,?,?)'
|
'INSERT INTO users (email,password_hash,name,organisation,role,slug,cab_joined_date) VALUES (?,?,?,?,?,?,?)'
|
||||||
).run(data.email, data.password_hash, data.name, data.organisation, data.role);
|
).run(data.email, data.password_hash, data.name, data.organisation, data.role, slug, cabJoined);
|
||||||
return Number(r.lastInsertRowid);
|
return Number(r.lastInsertRowid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,10 +137,38 @@ export function deactivateUser(id: number): void {
|
||||||
|
|
||||||
export function getAllUsersPublic(): UserPublic[] {
|
export function getAllUsersPublic(): UserPublic[] {
|
||||||
return db.prepare(
|
return db.prepare(
|
||||||
'SELECT id,email,name,organisation,role,bio,created_at,last_seen_at,active FROM users ORDER BY organisation,name'
|
'SELECT id,email,name,organisation,role,bio,created_at,last_seen_at,active,title,cab_joined_date,slug FROM users ORDER BY organisation,name'
|
||||||
).all() as UserPublic[];
|
).all() as UserPublic[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUserBySlug(slug: string): UserPublic | null {
|
||||||
|
return db.prepare(
|
||||||
|
'SELECT id,email,name,organisation,role,bio,created_at,last_seen_at,active,title,cab_joined_date,slug FROM users WHERE slug = ? AND active = 1'
|
||||||
|
).get(slug) as UserPublic | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Slugs ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function slugifyName(name: string): string {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFKD').replace(/[̀-ͯ]/g, '') // strip diacritics
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a slug guaranteed not to collide with any existing user. */
|
||||||
|
export function generateUniqueSlug(name: string): string {
|
||||||
|
const base = slugifyName(name) || 'member';
|
||||||
|
let candidate = base;
|
||||||
|
let n = 1;
|
||||||
|
while (db.prepare('SELECT 1 FROM users WHERE slug = ?').get(candidate)) {
|
||||||
|
n += 1;
|
||||||
|
candidate = `${base}-${n}`;
|
||||||
|
}
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Sessions ─────────────────────────────────────────────────────
|
// ── Sessions ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function createDbSession(id: string, userId: number, expiresAt: string): void {
|
export function createDbSession(id: string, userId: number, expiresAt: string): void {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue