initial setup

This commit is contained in:
Jurgis Sakalauskas
2025-10-11 17:21:39 +03:00
commit ad2cf25bfd
37 changed files with 7084 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
{
"name": "@best-choice/server",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc --project tsconfig.json",
"start": "node dist/index.js",
"lint": "eslint \"src/**/*.ts\"",
"test": "echo \"No tests yet\""
},
"keywords": [],
"author": "",
"license": "MIT",
"description": "Express API for the best-choice project",
"dependencies": {
"dotenv": "^16.4.5",
"express": "^5.1.0",
"pg": "^8.12.0"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^24.7.1",
"@types/pg": "^8.11.6",
"tsx": "^4.20.6",
"typescript": "^5.9.3"
}
}
+10
View File
@@ -0,0 +1,10 @@
import express from 'express';
import apiRouter from './routes/api-router.js';
import { env } from './config/env.js';
const app = express();
app.use(express.json());
app.use(env.apiBasePath, apiRouter);
export default app;
+18
View File
@@ -0,0 +1,18 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { config } from 'dotenv';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '../../..');
config({ path: path.resolve(rootDir, '.env') });
const port = Number(process.env.SERVER_PORT ?? process.env.PORT ?? 3000);
export const env = {
port,
apiBasePath: process.env.API_BASE_PATH ?? '/api',
databaseUrl: process.env.DATABASE_URL ?? '',
appName: process.env.APP_NAME ?? 'BestChoice'
};
@@ -0,0 +1,5 @@
import { Request, Response } from 'express';
export function getHealth(_req: Request, res: Response) {
res.json({ status: 'ok', service: 'best-choice-api' });
}
@@ -0,0 +1,8 @@
import type { Migration } from './types.js';
export const migration: Migration = {
name: '0001-enable-pgvector',
up: async (client) => {
await client.query('CREATE EXTENSION IF NOT EXISTS vector;');
}
};
+4
View File
@@ -0,0 +1,4 @@
import type { Migration } from './types.js';
import { migration as enablePgvector } from './0001-enable-pgvector.js';
export const migrations: Migration[] = [enablePgvector];
+6
View File
@@ -0,0 +1,6 @@
import type { ClientBase } from 'pg';
export interface Migration {
name: string;
up: (client: ClientBase) => Promise<void>;
}
+18
View File
@@ -0,0 +1,18 @@
import { Pool } from 'pg';
import { env } from '../config/env.js';
if (!env.databaseUrl) {
throw new Error('DATABASE_URL is not set.');
}
export const pool = new Pool({
connectionString: env.databaseUrl
});
pool.on('error', (error) => {
console.error('Unexpected PostgreSQL client error', error);
});
export const getClient = () => pool.connect();
export const query = (text: string, params?: unknown[]) => pool.query(text, params);
+53
View File
@@ -0,0 +1,53 @@
import type { PoolClient } from 'pg';
import { pool } from './pool.js';
import { migrations } from './migrations/index.js';
const MIGRATIONS_TABLE = 'migrations';
const ensureMigrationsTable = async (client: PoolClient) => {
await client.query(`
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
run_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
};
export const runMigrations = async () => {
const client = await pool.connect();
try {
await ensureMigrationsTable(client);
const executedMigrations = await client.query<{ name: string }>(
`SELECT name FROM ${MIGRATIONS_TABLE} ORDER BY run_at ASC`
);
const executedCodeNames = new Set(
executedMigrations.rows.map((row) => row.name)
);
for (const migration of migrations) {
if (executedCodeNames.has(migration.name)) {
continue;
}
console.log(`Running migration ${migration.name}`);
await client.query('BEGIN');
try {
await migration.up(client);
await client.query(
`INSERT INTO ${MIGRATIONS_TABLE} (name) VALUES ($1)`,
[migration.name]
);
await client.query('COMMIT');
console.log(`Migration ${migration.name} completed`);
} catch (error) {
await client.query('ROLLBACK');
throw error;
}
}
} finally {
client.release();
}
};
+19
View File
@@ -0,0 +1,19 @@
import app from './app.js';
import { env } from './config/env.js';
import { runMigrations } from './db/run-migrations.js';
const startServer = async () => {
try {
await runMigrations();
const port = env.port;
app.listen(port, () => {
console.log(`API server ready on http://localhost:${port}`);
});
} catch (error) {
console.error('Failed to start server', error);
process.exit(1);
}
};
void startServer();
+8
View File
@@ -0,0 +1,8 @@
import { Router } from 'express';
import { getHealth } from '../controllers/health-controller.js';
const apiRouter = Router();
apiRouter.get('/health', getHealth);
export default apiRouter;
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": [
"src"
]
}