mirror of
https://github.com/sakaljurgis/best-choice.git
synced 2026-07-08 21:47:40 +00:00
initial setup
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { Migration } from './types.js';
|
||||
import { migration as enablePgvector } from './0001-enable-pgvector.js';
|
||||
|
||||
export const migrations: Migration[] = [enablePgvector];
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { ClientBase } from 'pg';
|
||||
|
||||
export interface Migration {
|
||||
name: string;
|
||||
up: (client: ClientBase) => Promise<void>;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user