perf: optimize MCP server (O(1) auth, compact JSON, trashedAt fix) + memento-note performance (lazy loading, server-side filtering, XSS fixes, dead code removal, security hardening)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m35s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m35s
MCP Server: - Fix validateApiKey: O(1) direct lookup by shortId instead of loading all keys - Add trashedAt:null filter to ALL note queries (trashed notes leaked in results) - Compact JSON output (~40% smaller responses) - Bounded session cache (Map with MAX_SESSIONS=500) to prevent memory leaks - PostgreSQL connection pooling (connection_limit=10) - Rewrite all 22 tool descriptions in clear English - Fix /sse fallback to proper 307 redirect memento-note Performance: - loading=lazy on all note images - Split notebooksRefreshKey from global refreshKey (note CRUD no longer re-fetches notebooks) - Remove searchKey from trash count deps (no re-fetch on every keystroke) - Server-side notebookId filter in getAllNotes() (biggest win) - Skip collaborator fetch for non-shared notes (eliminates N+1 API calls) - next/dynamic for MarkdownContent + 4 modals (code-split remark/rehype/KaTeX) - Memoize DOMPurify sanitize with useMemo Security: - XSS: DOMPurify sanitize in note-card and note-history-modal - Auth anti-enumeration: uniform errors in auth.ts - CRON_SECRET mandatory on cron endpoints - Rate limiting on login (5 attempts/min per email) - Centralized API auth helpers (requireAuth/requireAdmin) - randomize-labels changed GET→POST - Removed debug endpoints (/api/debug/config, /api/debug/test-chat) Cleanup: - Removed dead code: .backup-keep, settings-backup, fix-*.js, debug-theme, fix-labels route - Removed sensitive console.error in auth.ts - Ollama fetchWithTimeout (30s/60s AbortController) - i18n: full Arabic translation, Farsi missing keys - Masonry drag-and-drop fix (localOrderMap, cross-section block) - Sidebar notebook tooltip on truncation
This commit is contained in:
@@ -1,30 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Memento MCP Server - Streamable HTTP Transport (Optimized)
|
||||
* Memento MCP Server - Streamable HTTP Transport (Fast)
|
||||
*
|
||||
* Performance improvements:
|
||||
* - Prisma connection pooling
|
||||
* - Request timeout handling
|
||||
* - Response compression
|
||||
* - Connection keep-alive
|
||||
* - Request batching support
|
||||
* - Compact JSON output
|
||||
* - Bounded session cache
|
||||
* - Proper keep-alive & timeouts
|
||||
* - O(1) API key validation
|
||||
*
|
||||
* Environment variables:
|
||||
* PORT - Server port (default: 3001)
|
||||
* DATABASE_URL - Prisma database URL (default: ../../memento-note/prisma/dev.db)
|
||||
* USER_ID - Optional user ID to filter data
|
||||
* APP_BASE_URL - Optional Next.js app URL for AI features (default: http://localhost:3000)
|
||||
* MCP_REQUIRE_AUTH - Set to 'true' to require x-api-key or x-user-id header
|
||||
* MCP_API_KEY - Static API key for authentication (when MCP_REQUIRE_AUTH=true)
|
||||
* MCP_LOG_LEVEL - Log level: debug, info, warn, error (default: info)
|
||||
* MCP_REQUEST_TIMEOUT - Request timeout in ms (default: 30000)
|
||||
* Environment:
|
||||
* PORT Server port (default: 3001)
|
||||
* DATABASE_URL Prisma database URL
|
||||
* USER_ID Optional user ID filter
|
||||
* APP_BASE_URL Next.js app URL (default: http://localhost:3000)
|
||||
* MCP_REQUIRE_AUTH Set 'true' to require authentication
|
||||
* MCP_API_KEY Static fallback API key
|
||||
* MCP_LOG_LEVEL debug, info, warn, error (default: info)
|
||||
* MCP_REQUEST_TIMEOUT Timeout in ms (default: 30000)
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
@@ -32,13 +29,12 @@ import { registerTools } from './tools.js';
|
||||
import { validateApiKey, resolveUser } from './auth.js';
|
||||
import { requestContext } from './request-context.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Configuration
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const LOG_LEVEL = process.env.MCP_LOG_LEVEL || 'info';
|
||||
const REQUEST_TIMEOUT = parseInt(process.env.MCP_REQUEST_TIMEOUT, 10) || 30000;
|
||||
const MAX_SESSIONS = 500;
|
||||
const SESSION_TTL = 3600000;
|
||||
|
||||
const logLevels = { debug: 0, info: 1, warn: 2, error: 3 };
|
||||
const currentLogLevel = logLevels[LOG_LEVEL] ?? 1;
|
||||
|
||||
@@ -48,51 +44,57 @@ function log(level, ...args) {
|
||||
}
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
|
||||
// Database - requires DATABASE_URL environment variable
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
if (!databaseUrl) {
|
||||
console.error('ERROR: DATABASE_URL environment variable is required');
|
||||
console.error('ERROR: DATABASE_URL is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// OPTIMIZED: Prisma client with connection pooling
|
||||
const isPostgres = databaseUrl.startsWith('postgresql://') || databaseUrl.startsWith('postgres://');
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: { url: databaseUrl },
|
||||
},
|
||||
datasources: { db: { url: databaseUrl } },
|
||||
...(isPostgres ? { datasources: { db: { url: `${databaseUrl}${databaseUrl.includes('?') ? '&' : '?'}connection_limit=10&pool_timeout=10` } } } : {}),
|
||||
log: LOG_LEVEL === 'debug' ? ['query', 'info', 'warn', 'error'] : ['warn', 'error'],
|
||||
});
|
||||
|
||||
const appBaseUrl = process.env.APP_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
// ── Auth Middleware ──────────────────────────────────────────────────────────
|
||||
// ── Bounded Session Cache ───────────────────────────────────────────────────
|
||||
|
||||
const userSessions = {};
|
||||
const SESSION_TIMEOUT = 3600000; // 1 hour
|
||||
const sessions = new Map();
|
||||
|
||||
// Cleanup old sessions periodically
|
||||
setInterval(() => {
|
||||
function cleanupSessions() {
|
||||
const now = Date.now();
|
||||
let cleaned = 0;
|
||||
for (const [key, session] of Object.entries(userSessions)) {
|
||||
if (now - new Date(session.lastSeen).getTime() > SESSION_TIMEOUT) {
|
||||
delete userSessions[key];
|
||||
for (const [key, s] of sessions) {
|
||||
if (now - s._lastSeen > SESSION_TTL) {
|
||||
sessions.delete(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
if (cleaned > 0) {
|
||||
log('debug', `Cleaned up ${cleaned} expired sessions`);
|
||||
if (cleaned > 0) log('debug', `Cleaned ${cleaned} sessions`);
|
||||
}
|
||||
|
||||
function pruneIfFull() {
|
||||
if (sessions.size < MAX_SESSIONS) return;
|
||||
const entries = [...sessions.entries()].sort((a, b) => a[1]._lastSeen - b[1]._lastSeen);
|
||||
for (let i = 0; i < Math.floor(MAX_SESSIONS / 4); i++) {
|
||||
sessions.delete(entries[i][0]);
|
||||
}
|
||||
}, 600000); // Every 10 minutes
|
||||
}
|
||||
|
||||
setInterval(cleanupSessions, 600000);
|
||||
|
||||
// ── Express ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
|
||||
// ── Auth Middleware ──────────────────────────────────────────────────────────
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
// Dev mode: no auth required
|
||||
if (process.env.MCP_REQUIRE_AUTH !== 'true') {
|
||||
req.userSession = { id: 'dev-user', name: 'Development User', isAuth: false };
|
||||
return next();
|
||||
@@ -102,189 +104,128 @@ app.use(async (req, res, next) => {
|
||||
const headerUserId = req.headers['x-user-id'];
|
||||
|
||||
if (!apiKey && !headerUserId) {
|
||||
return res.status(401).json({
|
||||
error: 'Authentication required',
|
||||
message: 'Provide x-api-key header (recommended) or x-user-id header',
|
||||
});
|
||||
return res.status(401).json({ error: 'Authentication required', message: 'Provide x-api-key or x-user-id header' });
|
||||
}
|
||||
|
||||
// ── Method 1: API Key (recommended) ──────────────────────────────
|
||||
if (apiKey) {
|
||||
const keyUser = await validateApiKey(prisma, apiKey);
|
||||
if (keyUser) {
|
||||
const sessionKey = `key:${keyUser.apiKeyId}`;
|
||||
if (userSessions[sessionKey]) {
|
||||
req.userSession = userSessions[sessionKey];
|
||||
req.userSession.lastSeen = new Date().toISOString();
|
||||
} else {
|
||||
req.userSession = {
|
||||
id: randomUUID(),
|
||||
name: `${keyUser.userName} (${keyUser.apiKeyName})`,
|
||||
userId: keyUser.userId,
|
||||
userName: keyUser.userName,
|
||||
apiKeyId: keyUser.apiKeyId,
|
||||
connectedAt: new Date().toISOString(),
|
||||
lastSeen: new Date().toISOString(),
|
||||
requestCount: 0,
|
||||
isAuth: true,
|
||||
authMethod: 'api-key',
|
||||
};
|
||||
userSessions[sessionKey] = req.userSession;
|
||||
}
|
||||
req.userSession = getOrCreateSession(`key:${keyUser.apiKeyId}`, {
|
||||
name: `${keyUser.userName} (${keyUser.apiKeyName})`,
|
||||
userId: keyUser.userId,
|
||||
userName: keyUser.userName,
|
||||
apiKeyId: keyUser.apiKeyId,
|
||||
authMethod: 'api-key',
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// Fallback: static env var key
|
||||
if (process.env.MCP_API_KEY && apiKey === process.env.MCP_API_KEY) {
|
||||
const sessionKey = `static:${apiKey.substring(0, 8)}`;
|
||||
if (userSessions[sessionKey]) {
|
||||
req.userSession = userSessions[sessionKey];
|
||||
req.userSession.lastSeen = new Date().toISOString();
|
||||
} else {
|
||||
req.userSession = {
|
||||
id: randomUUID(),
|
||||
name: 'Static API Key User',
|
||||
userId: process.env.USER_ID || null,
|
||||
connectedAt: new Date().toISOString(),
|
||||
lastSeen: new Date().toISOString(),
|
||||
requestCount: 0,
|
||||
isAuth: true,
|
||||
authMethod: 'static-key',
|
||||
};
|
||||
userSessions[sessionKey] = req.userSession;
|
||||
}
|
||||
req.userSession = getOrCreateSession(`static:${apiKey.substring(0, 8)}`, {
|
||||
name: 'Static API Key User',
|
||||
userId: process.env.USER_ID || null,
|
||||
authMethod: 'static-key',
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(401).json({ error: 'Invalid API key' });
|
||||
}
|
||||
|
||||
// ── Method 2: User ID header (validate against DB) ──────────────
|
||||
if (headerUserId) {
|
||||
const user = await resolveUser(prisma, headerUserId);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'User not found', message: `No user matching: ${headerUserId}` });
|
||||
}
|
||||
|
||||
const sessionKey = `user:${user.id}`;
|
||||
if (userSessions[sessionKey]) {
|
||||
req.userSession = userSessions[sessionKey];
|
||||
req.userSession.lastSeen = new Date().toISOString();
|
||||
} else {
|
||||
req.userSession = {
|
||||
id: randomUUID(),
|
||||
name: user.name,
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
userEmail: user.email,
|
||||
userRole: user.role,
|
||||
connectedAt: new Date().toISOString(),
|
||||
lastSeen: new Date().toISOString(),
|
||||
requestCount: 0,
|
||||
isAuth: true,
|
||||
authMethod: 'user-id',
|
||||
};
|
||||
userSessions[sessionKey] = req.userSession;
|
||||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
req.userSession = getOrCreateSession(`user:${user.id}`, {
|
||||
name: user.name,
|
||||
userId: user.id,
|
||||
userName: user.name,
|
||||
userEmail: user.email,
|
||||
userRole: user.role,
|
||||
authMethod: 'user-id',
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(401).json({ error: 'Authentication failed' });
|
||||
});
|
||||
|
||||
// ── Request Logging ─────────────────────────────────────────────────────────
|
||||
function getOrCreateSession(key, base) {
|
||||
const existing = sessions.get(key);
|
||||
if (existing) {
|
||||
existing._lastSeen = Date.now();
|
||||
existing.requestCount = (existing.requestCount || 0) + 1;
|
||||
return existing;
|
||||
}
|
||||
pruneIfFull();
|
||||
const s = {
|
||||
id: randomUUID(),
|
||||
...base,
|
||||
connectedAt: new Date().toISOString(),
|
||||
requestCount: 1,
|
||||
isAuth: true,
|
||||
_lastSeen: Date.now(),
|
||||
};
|
||||
sessions.set(key, s);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Logging ─────────────────────────────────────────────────────────────────
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
|
||||
if (req.userSession) {
|
||||
req.userSession.requestCount = (req.userSession.requestCount || 0) + 1;
|
||||
}
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
const sessionId = req.userSession?.id?.substring(0, 8) || 'anon';
|
||||
log('debug', `[${sessionId}] ${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
|
||||
const ms = Date.now() - start;
|
||||
const sid = req.userSession?.id?.substring(0, 8) || 'anon';
|
||||
log('debug', `[${sid}] ${req.method} ${req.path} ${res.statusCode} ${ms}ms`);
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// ── Request Timeout Middleware ──────────────────────────────────────────────
|
||||
// ── Timeout ─────────────────────────────────────────────────────────────────
|
||||
|
||||
app.use((req, res, next) => {
|
||||
req.setTimeout(REQUEST_TIMEOUT);
|
||||
res.setTimeout(REQUEST_TIMEOUT, () => {
|
||||
log('warn', `Request timeout: ${req.method} ${req.path}`);
|
||||
res.status(504).json({ error: 'Gateway Timeout', message: 'Request took too long' });
|
||||
if (!res.headersSent) {
|
||||
res.status(504).json({ error: 'Gateway Timeout' });
|
||||
}
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// ── MCP Server Setup ────────────────────────────────────────────────────────
|
||||
// ── MCP Server ──────────────────────────────────────────────────────────────
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'memento-mcp-server',
|
||||
version: '3.1.0',
|
||||
},
|
||||
{
|
||||
capabilities: { tools: {} },
|
||||
},
|
||||
{ name: 'memento-mcp-server', version: '3.2.0' },
|
||||
{ capabilities: { tools: {} } },
|
||||
);
|
||||
|
||||
registerTools(server, prisma);
|
||||
|
||||
// ── HTTP Endpoints ──────────────────────────────────────────────────────────
|
||||
// ── Routes ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const transports = {};
|
||||
|
||||
// Health check
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
name: 'Memento MCP Server',
|
||||
version: '3.1.0',
|
||||
version: '3.2.0',
|
||||
status: 'running',
|
||||
endpoints: { mcp: '/mcp', health: '/', sessions: '/sessions' },
|
||||
auth: {
|
||||
enabled: process.env.MCP_REQUIRE_AUTH === 'true',
|
||||
method: 'x-api-key or x-user-id header',
|
||||
},
|
||||
tools: {
|
||||
notes: 11,
|
||||
notebooks: 6,
|
||||
labels: 4,
|
||||
reminders: 1,
|
||||
total: 22,
|
||||
},
|
||||
performance: {
|
||||
optimizations: [
|
||||
'Connection pooling',
|
||||
'Batch operations',
|
||||
'API key caching',
|
||||
'Request timeout handling',
|
||||
'Parallel query execution',
|
||||
],
|
||||
},
|
||||
auth: { enabled: process.env.MCP_REQUIRE_AUTH === 'true' },
|
||||
tools: 22,
|
||||
});
|
||||
});
|
||||
|
||||
// Session status
|
||||
app.get('/sessions', (req, res) => {
|
||||
const sessions = Object.values(userSessions).map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
connectedAt: s.connectedAt,
|
||||
lastSeen: s.lastSeen,
|
||||
requestCount: s.requestCount || 0,
|
||||
const list = [...sessions.values()].map(s => ({
|
||||
id: s.id, name: s.name, connectedAt: s.connectedAt,
|
||||
requestCount: s.requestCount || 0, authMethod: s.authMethod,
|
||||
}));
|
||||
res.json({
|
||||
activeUsers: sessions.length,
|
||||
sessions,
|
||||
uptime: process.uptime(),
|
||||
});
|
||||
res.json({ activeUsers: list.length, sessions: list, uptime: process.uptime() });
|
||||
});
|
||||
|
||||
// MCP endpoint - Streamable HTTP
|
||||
// MCP endpoint — Streamable HTTP
|
||||
app.all('/mcp', async (req, res) => {
|
||||
const sessionId = req.headers['mcp-session-id'];
|
||||
let transport;
|
||||
@@ -295,15 +236,15 @@ app.all('/mcp', async (req, res) => {
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (id) => {
|
||||
log('debug', `Session initialized: ${id}`);
|
||||
log('debug', `Session init: ${id}`);
|
||||
transports[id] = transport;
|
||||
},
|
||||
});
|
||||
|
||||
transport.onclose = () => {
|
||||
const sid = transport.sessionId;
|
||||
if (sid && transports[sid]) {
|
||||
log('debug', `Session closed: ${sid}`);
|
||||
if (sid) {
|
||||
log('debug', `Session close: ${sid}`);
|
||||
delete transports[sid];
|
||||
}
|
||||
};
|
||||
@@ -311,79 +252,45 @@ app.all('/mcp', async (req, res) => {
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
// Pass authenticated userId to tool handlers via AsyncLocalStorage
|
||||
const ctx = { userId: req.userSession?.userId || null };
|
||||
await requestContext.run(ctx, async () => {
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
});
|
||||
});
|
||||
|
||||
// Legacy /sse redirect for backward compat
|
||||
app.all('/sse', async (req, res) => {
|
||||
// Redirect to /mcp
|
||||
req.url = '/mcp';
|
||||
return app._router.handle(req, res, () => {
|
||||
res.status(404).json({ error: 'Not found' });
|
||||
});
|
||||
// Legacy /sse → /mcp redirect
|
||||
app.all('/sse', (req, res) => {
|
||||
res.redirect(307, '/mcp');
|
||||
});
|
||||
|
||||
// ── Start Server ────────────────────────────────────────────────────────────
|
||||
const transports = {};
|
||||
|
||||
// ── Start ────────────────────────────────────────────────────────────────────
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ Memento MCP Server v3.1.0 (Streamable HTTP) - Optimized ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
╔═══════════════════════════════════════════════════════╗
|
||||
║ Memento MCP Server v3.2.0 (Streamable HTTP) ║
|
||||
╚═══════════════════════════════════════════════════════╝
|
||||
|
||||
Server: http://localhost:${PORT}
|
||||
MCP: http://localhost:${PORT}/mcp
|
||||
Health: http://localhost:${PORT}/
|
||||
Sessions: http://localhost:${PORT}/sessions
|
||||
|
||||
Database: ${databaseUrl}
|
||||
App URL: ${appBaseUrl}
|
||||
User filter: per-request (from auth)
|
||||
Auth: ${process.env.MCP_REQUIRE_AUTH === 'true' ? 'ENABLED' : 'DISABLED (dev mode)'}
|
||||
Timeout: ${REQUEST_TIMEOUT}ms
|
||||
|
||||
Performance Optimizations:
|
||||
✅ Connection pooling
|
||||
✅ Batch operations
|
||||
✅ API key caching (60s TTL)
|
||||
✅ Parallel query execution
|
||||
✅ Request timeout handling
|
||||
✅ Session cleanup
|
||||
|
||||
Tools (22 total):
|
||||
Notes (11):
|
||||
create_note, get_notes, get_note, update_note, delete_note,
|
||||
search_notes, move_note, toggle_pin, toggle_archive,
|
||||
export_notes, import_notes
|
||||
|
||||
Notebooks (6):
|
||||
create_notebook, get_notebooks, get_notebook, update_notebook,
|
||||
delete_notebook, reorder_notebooks
|
||||
|
||||
Labels (4):
|
||||
create_label, get_labels, update_label, delete_label
|
||||
|
||||
Reminders (1):
|
||||
get_due_reminders
|
||||
|
||||
N8N config: SSE endpoint http://YOUR_IP:${PORT}/mcp
|
||||
Headers: x-api-key or x-user-id
|
||||
Server: http://localhost:${PORT}
|
||||
MCP: http://localhost:${PORT}/mcp
|
||||
Auth: ${process.env.MCP_REQUIRE_AUTH === 'true' ? 'ENABLED' : 'DISABLED (dev)'}
|
||||
Timeout: ${REQUEST_TIMEOUT}ms
|
||||
Database: ${isPostgres ? 'PostgreSQL' : 'SQLite'}
|
||||
Tools: 22
|
||||
`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
log('info', '\nShutting down MCP server...');
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
// ── Shutdown ─────────────────────────────────────────────────────────────────
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
log('info', '\nShutting down MCP server...');
|
||||
async function shutdown() {
|
||||
log('info', 'Shutting down...');
|
||||
await prisma.$disconnect();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('uncaughtException', (err) => log('error', 'Uncaught:', err.message));
|
||||
process.on('unhandledRejection', (reason) => log('error', 'Unhandled rejection:', reason));
|
||||
|
||||
Reference in New Issue
Block a user