feat: project scaffolding and env config loader

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 14:06:56 +02:00
co-authored by Claude Sonnet 5
parent fe93139c6e
commit 378ca762a2
6 changed files with 4553 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
PORT=3000
STORAGE_DIR=./storage
DB_HOST=127.0.0.1
DB_USER=convert_user
DB_PASSWORD=change_me
DB_NAME=file_converter
MAX_FILE_SIZE_MB=100
RETENTION_HOURS=1
WORKER_POLL_INTERVAL_MS=1500
WORKER_CONCURRENCY=3
RATE_LIMIT_MAX_JOBS=20
RATE_LIMIT_WINDOW_MINUTES=10
+7
View File
@@ -0,0 +1,7 @@
# Added by code-review-graph
.code-review-graph/
node_modules/
.env
storage/
dist/
+4419
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "file-converter",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "node src/app.js",
"worker": "node src/worker.js",
"cleanup": "node src/cleanup.js",
"test": "vitest run"
},
"dependencies": {
"docx": "^9.7.1",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-rate-limit": "^8.6.1",
"file-type": "^22.0.1",
"mammoth": "^1.12.0",
"mariadb": "^3.5.3",
"multer": "^2.2.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^6.2.108",
"puppeteer": "^25.4.0",
"sharp": "^0.35.3",
"uuid": "^14.0.1"
},
"devDependencies": {
"supertest": "^7.2.2",
"vitest": "^4.1.10"
}
}
+27
View File
@@ -0,0 +1,27 @@
import 'dotenv/config';
const REQUIRED_VARS = ['STORAGE_DIR', 'DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
export function loadConfig() {
const missing = REQUIRED_VARS.filter((key) => !process.env[key]);
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
return {
port: Number(process.env.PORT ?? 3000),
storageDir: process.env.STORAGE_DIR,
db: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
},
maxFileSizeMb: Number(process.env.MAX_FILE_SIZE_MB ?? 100),
retentionHours: Number(process.env.RETENTION_HOURS ?? 1),
workerPollIntervalMs: Number(process.env.WORKER_POLL_INTERVAL_MS ?? 1500),
workerConcurrency: Number(process.env.WORKER_CONCURRENCY ?? 3),
rateLimitMaxJobs: Number(process.env.RATE_LIMIT_MAX_JOBS ?? 20),
rateLimitWindowMinutes: Number(process.env.RATE_LIMIT_WINDOW_MINUTES ?? 10),
};
}
+57
View File
@@ -0,0 +1,57 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { loadConfig } from '../src/config.js';
const REQUIRED_VARS = ['STORAGE_DIR', 'DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
let savedEnv;
beforeEach(() => {
savedEnv = { ...process.env };
});
afterEach(() => {
process.env = savedEnv;
});
describe('loadConfig', () => {
it('returns parsed config when all required vars are set', () => {
process.env.STORAGE_DIR = './storage';
process.env.DB_HOST = 'localhost';
process.env.DB_USER = 'user';
process.env.DB_PASSWORD = 'pass';
process.env.DB_NAME = 'db';
process.env.PORT = '4000';
process.env.MAX_FILE_SIZE_MB = '50';
const config = loadConfig();
expect(config.port).toBe(4000);
expect(config.storageDir).toBe('./storage');
expect(config.db).toEqual({ host: 'localhost', user: 'user', password: 'pass', database: 'db' });
expect(config.maxFileSizeMb).toBe(50);
});
it('applies defaults for optional vars', () => {
process.env.STORAGE_DIR = './storage';
process.env.DB_HOST = 'localhost';
process.env.DB_USER = 'user';
process.env.DB_PASSWORD = 'pass';
process.env.DB_NAME = 'db';
delete process.env.PORT;
delete process.env.MAX_FILE_SIZE_MB;
delete process.env.RETENTION_HOURS;
delete process.env.WORKER_CONCURRENCY;
const config = loadConfig();
expect(config.port).toBe(3000);
expect(config.maxFileSizeMb).toBe(100);
expect(config.retentionHours).toBe(1);
expect(config.workerConcurrency).toBe(3);
});
it('throws listing every missing required variable', () => {
for (const key of REQUIRED_VARS) delete process.env[key];
expect(() => loadConfig()).toThrowError(/STORAGE_DIR.*DB_HOST.*DB_USER.*DB_PASSWORD.*DB_NAME/s);
});
});