feat: project scaffolding and env config loader
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Added by code-review-graph
|
||||||
|
.code-review-graph/
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
storage/
|
||||||
|
dist/
|
||||||
Generated
+4419
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user