Scaffold project and implement config, DB schema/queries

- Set up package.json (ESM, scripts), tsconfig.json, vitest.config.ts
- Install runtime and dev dependencies
- Add CLAUDE.md with architecture notes and code quality rules
- Config module with env var parsing and JWT_SECRET validation
- DB schema: users + files tables with FK cascade
- DB queries: createUser, getUserBy*, createFile, getFileById, getFilesByUserId, deleteFile
- Tests for config, db/users, db/files (15 tests passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 15:48:21 -08:00
parent 5902cc404a
commit b6aa6211a9
14 changed files with 3591 additions and 3 deletions

34
src/config.ts Normal file
View File

@@ -0,0 +1,34 @@
export interface Config {
port: number;
host: string;
jwtSecret: string;
jwtExpiry: string;
dbPath: string;
uploadDir: string;
logFile: string;
maxFileSize: number;
baseUrl: string;
cookieSecure: boolean;
trustProxy: boolean;
}
export function loadConfig(): Config {
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error('JWT_SECRET environment variable is required');
}
return {
port: parseInt(process.env.PORT ?? '3000', 10),
host: process.env.HOST ?? '0.0.0.0',
jwtSecret,
jwtExpiry: process.env.JWT_EXPIRY ?? '7d',
dbPath: process.env.DB_PATH ?? './data/nanodrop.db',
uploadDir: process.env.UPLOAD_DIR ?? './data/uploads',
logFile: process.env.LOG_FILE ?? './data/nanodrop.log',
maxFileSize: parseInt(process.env.MAX_FILE_SIZE ?? '104857600', 10),
baseUrl: process.env.BASE_URL ?? 'http://localhost:3000',
cookieSecure: process.env.COOKIE_SECURE === 'true',
trustProxy: process.env.TRUST_PROXY === 'true',
};
}

51
src/db/files.ts Normal file
View File

@@ -0,0 +1,51 @@
import type Database from 'better-sqlite3';
export interface FileRow {
id: string;
user_id: number;
original_name: string;
mime_type: string;
size: number;
stored_name: string;
created_at: string;
}
export interface CreateFileParams {
id: string;
userId: number;
originalName: string;
mimeType: string;
size: number;
storedName: string;
}
export function createFile(db: Database.Database, params: CreateFileParams): FileRow {
const stmt = db.prepare(
`INSERT INTO files (id, user_id, original_name, mime_type, size, stored_name)
VALUES (?, ?, ?, ?, ?, ?) RETURNING *`
);
return stmt.get(
params.id,
params.userId,
params.originalName,
params.mimeType,
params.size,
params.storedName,
) as FileRow;
}
export function getFileById(db: Database.Database, id: string): FileRow | undefined {
const stmt = db.prepare('SELECT * FROM files WHERE id = ?');
return stmt.get(id) as FileRow | undefined;
}
export function getFilesByUserId(db: Database.Database, userId: number): FileRow[] {
const stmt = db.prepare('SELECT * FROM files WHERE user_id = ? ORDER BY created_at DESC');
return stmt.all(userId) as FileRow[];
}
export function deleteFile(db: Database.Database, id: string, userId: number): boolean {
const stmt = db.prepare('DELETE FROM files WHERE id = ? AND user_id = ?');
const result = stmt.run(id, userId);
return result.changes > 0;
}

28
src/db/schema.ts Normal file
View File

@@ -0,0 +1,28 @@
import Database from 'better-sqlite3';
export function initDb(dbPath: string): Database.Database {
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
original_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
size INTEGER NOT NULL,
stored_name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
`);
return db;
}

30
src/db/users.ts Normal file
View File

@@ -0,0 +1,30 @@
import type Database from 'better-sqlite3';
export interface UserRow {
id: number;
username: string;
password_hash: string;
created_at: string;
}
export interface CreateUserParams {
username: string;
passwordHash: string;
}
export function createUser(db: Database.Database, params: CreateUserParams): UserRow {
const stmt = db.prepare(
'INSERT INTO users (username, password_hash) VALUES (?, ?) RETURNING *'
);
return stmt.get(params.username, params.passwordHash) as UserRow;
}
export function getUserByUsername(db: Database.Database, username: string): UserRow | undefined {
const stmt = db.prepare('SELECT * FROM users WHERE username = ?');
return stmt.get(username) as UserRow | undefined;
}
export function getUserById(db: Database.Database, id: number): UserRow | undefined {
const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
return stmt.get(id) as UserRow | undefined;
}