Initial commit

This commit is contained in:
nholtware 2026-11-05 01:44:23 -06:00
commit 107d03869b
28 changed files with 6437 additions and 0 deletions

25
src/lib/config.ts Normal file
View file

@ -0,0 +1,25 @@
import { z } from "zod";
const schema = z.object({
PORT: z.coerce.number().int().positive().default(3000),
SERVICE_NAME: z.string().min(1, "SERVICE_NAME is required"),
LOG_LEVEL: z
.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"])
.default("info"),
GIT_SHA: z.string().default("dev"),
DATABASE_URL: z.string().url().optional(),
REDIS_URL: z.string().url().optional(),
});
export type Config = z.infer<typeof schema>;
export function parseConfig(env: NodeJS.ProcessEnv): Config {
const result = schema.safeParse(env);
if (!result.success) {
const problems = result.error.issues
.map((i) => ` ${i.path.join(".")}: ${i.message}`)
.join("\n");
throw new Error(`Invalid environment:\n${problems}`);
}
return result.data;
}

24
src/lib/db.ts Normal file
View file

@ -0,0 +1,24 @@
import pg from "pg";
export interface Db {
query: pg.Pool["query"];
ping(): Promise<void>;
close(): Promise<void>;
}
export function createDb(connectionString: string): Db {
const pool = new pg.Pool({
connectionString,
max: 5,
connectionTimeoutMillis: 2000,
});
// Idle-client errors must not crash the process.
pool.on("error", () => {});
return {
query: pool.query.bind(pool) as pg.Pool["query"],
async ping() {
await pool.query("SELECT 1");
},
close: () => pool.end(),
};
}

14
src/lib/logger.ts Normal file
View file

@ -0,0 +1,14 @@
import pino from "pino";
import type { Config } from "./config.js";
// Logging contract: one JSON object per line on stdout with level, time,
// msg, service, version. Level is emitted as a label, time as ISO-8601.
export function createLogger(config: Config): pino.Logger {
return pino({
level: config.LOG_LEVEL,
base: { service: config.SERVICE_NAME, version: config.GIT_SHA },
messageKey: "msg",
timestamp: pino.stdTimeFunctions.isoTime,
formatters: { level: (label) => ({ level: label }) },
});
}

26
src/lib/redis.ts Normal file
View file

@ -0,0 +1,26 @@
import { Redis } from "ioredis";
export interface Cache {
incr(key: string): Promise<number>;
ping(): Promise<void>;
close(): Promise<void>;
}
export function createCache(url: string): Cache {
const client = new Redis(url, {
maxRetriesPerRequest: 1,
connectTimeout: 2000,
enableOfflineQueue: false,
lazyConnect: false,
});
client.on("error", () => {});
return {
incr: (key) => client.incr(key),
async ping() {
await client.ping();
},
async close() {
client.disconnect();
},
};
}