@accounter/server
Version:
Accounter GraphQL server
170 lines • 6.95 kB
JavaScript
import { createServer } from 'node:http';
import { createYoga, useReadinessCheck } from 'graphql-yoga';
import pg from 'pg';
import { shutdownTelemetry, startTelemetry } from './telemetry/index.js';
import 'reflect-metadata';
import { useSchema } from '@envelop/core';
import { useGraphQLModules } from '@envelop/graphql-modules';
import { useHive } from '@graphql-hive/yoga';
import { useDeferStream } from '@graphql-yoga/plugin-defer-stream';
import { env } from './environment.js';
import { cleanupExpiredInvitations } from './jobs/cleanup-expired-invitations.js';
import { scheduleDailyAtUtc } from './jobs/utils.js';
import { createGraphQLApp } from './modules-app.js';
import { DBProvider } from './modules/app-providers/db.provider.js';
import { Auth0ManagementProvider } from './modules/auth/providers/auth0-management.provider.js';
import { authPlugin } from './plugins/auth-plugin.js';
import { dbCleanupPlugin } from './plugins/db-cleanup-plugin.js';
const { Pool } = pg;
const DAY_IN_MS = 24 * 60 * 60 * 1000;
const CLEANUP_SCHEDULE_HOUR_UTC = 2;
const CLEANUP_SCHEDULE_MINUTE_UTC = 0;
async function main() {
await startTelemetry();
// Create a shared connection pool for the entire application
const pool = new Pool({
user: env.postgres.user,
password: env.postgres.password,
host: env.postgres.host,
port: Number(env.postgres.port),
database: env.postgres.db,
ssl: env.postgres.ssl ? { rejectUnauthorized: false } : false,
max: env.postgres.max, // maximum number of clients in the pool
});
if (env.auth0) {
console.log(`Auth0 configured: ${env.auth0.domain}`);
}
else {
console.log('Auth0 not configured');
}
const dbProvider = new DBProvider(pool);
const auth0Provider = new Auth0ManagementProvider(env);
const logger = {
info: (message, meta) => {
if (meta !== undefined) {
console.log(message, meta);
return;
}
console.log(message);
},
error: (message, meta) => {
if (meta !== undefined) {
console.error(message, meta);
return;
}
console.error(message);
},
};
const cleanupSchedule = env.auth0
? scheduleDailyAtUtc(CLEANUP_SCHEDULE_HOUR_UTC, CLEANUP_SCHEDULE_MINUTE_UTC, DAY_IN_MS, async () => {
try {
const result = await cleanupExpiredInvitations(dbProvider, auth0Provider, logger);
logger.info('Invitation cleanup complete', result);
}
catch (error) {
logger.error('Scheduled invitation cleanup run failed', { error });
}
})
: null;
if (cleanupSchedule) {
logger.info('Scheduled invitation cleanup job', {
cron: `${CLEANUP_SCHEDULE_MINUTE_UTC} ${CLEANUP_SCHEDULE_HOUR_UTC} * * *`,
timezone: 'UTC',
});
}
const { application, transformedSchema } = await createGraphQLApp(env, pool);
const yoga = createYoga({
plugins: [
dbCleanupPlugin(),
authPlugin(),
useGraphQLModules(application),
useSchema(transformedSchema),
useDeferStream(),
useHive({
enabled: !!env.hive,
token: env.hive?.hiveToken ?? '',
usage: !!env.hive,
}),
useReadinessCheck({
check: async () => {
const isHealthy = await dbProvider.healthCheck();
if (!isHealthy) {
throw new Error('Database health check failed');
}
return isHealthy;
},
}),
],
context: (yogaContext) => {
return {
...yogaContext,
env,
pool,
};
},
});
const server = createServer(yoga);
// Graceful shutdown handler
let isShuttingDown = false;
const gracefulShutdown = async (reason, err, exitCode = 1) => {
if (isShuttingDown)
return;
isShuttingDown = true;
if (err) {
process.stderr.write(`[shutdown] Reason: ${reason} ${String(err)}\n`);
}
else {
process.stderr.write(`[shutdown] Reason: ${reason}\n`);
}
// Stop accepting new connections
await new Promise(resolve => {
server.close(() => resolve());
});
cleanupSchedule?.cancel();
// Allow in-flight requests some time to complete, then close the pool
const FORCE_EXIT_TIMEOUT_MS = 10_000;
const forceExitTimer = setTimeout(() => {
process.stderr.write('[shutdown] Force exiting after timeout\n');
process.exit(exitCode);
}, FORCE_EXIT_TIMEOUT_MS);
// Do not keep the event loop alive just for the timer
forceExitTimer.unref();
try {
await pool.end();
}
catch (e) {
process.stderr.write(`[shutdown] Error while closing DB pool ${String(e)}\n`);
}
try {
await shutdownTelemetry();
}
catch (e) {
process.stderr.write(`[shutdown] Error while shutting down telemetry ${String(e)}\n`);
}
finally {
clearTimeout(forceExitTimer);
process.exit(exitCode);
}
};
// Pool errors: log but don't shutdown on transient errors
// The pool will attempt to recover from connection issues automatically
pool.on('error', err => {
process.stderr.write(`[pool] Unexpected error on idle client: ${String(err)}\n`);
// Only shutdown on critical errors that indicate the pool is permanently broken
// For now, log and let the pool attempt recovery
// If queries start failing consistently, they'll be caught by application error handlers
});
// OS signals
process.on('SIGINT', () => gracefulShutdown('SIGINT', undefined, 0).catch(e => process.stderr.write(`[shutdown] Fatal error during shutdown: ${String(e)}\n`)));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM', undefined, 0).catch(e => process.stderr.write(`[shutdown] Fatal error during shutdown: ${String(e)}\n`)));
// Unhandled errors
process.on('unhandledRejection', reason => gracefulShutdown('unhandledRejection', reason, 1).catch(e => process.stderr.write(`[shutdown] Fatal error during shutdown: ${String(e)}\n`)));
process.on('uncaughtException', err => gracefulShutdown('uncaughtException', err, 1).catch(e => process.stderr.write(`[shutdown] Fatal error during shutdown: ${String(e)}\n`)));
server.listen({
port: 4000,
}, () => {
process.stdout.write('GraphQL API located at http://localhost:4000/graphql\n');
});
}
main();
//# sourceMappingURL=index.js.map