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:
34
src/config.ts
Normal file
34
src/config.ts
Normal 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
51
src/db/files.ts
Normal 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
28
src/db/schema.ts
Normal 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
30
src/db/users.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user