Commit 29832c62 authored by ThinhNC's avatar ThinhNC

Merge branch 'refactor/audit-fixes-and-dynamic-rbac-permissions' into 'develop'

fix: crawl job repository, server initialization, health service, and database seed script

See merge request !11
parents 18858339 c917a34b
This diff is collapsed.
...@@ -49,9 +49,31 @@ export class CrawlJobRepository { ...@@ -49,9 +49,31 @@ export class CrawlJobRepository {
where.mode = query.mode; where.mode = query.mode;
} }
if (query.search) { if (query.search) {
const trimmedSearch = query.search.trim();
let matchingIds: string[] = [];
try {
const searchPattern = `%${trimmedSearch}%`;
const matched = await prisma.$queryRaw<{ id: string }[]>`
SELECT id FROM "crawl_jobs"
WHERE id::text ILIKE ${searchPattern}
LIMIT 100
`;
matchingIds = matched.map((r) => r.id);
} catch {
const isFullUuid =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
trimmedSearch,
);
if (isFullUuid) {
matchingIds = [trimmedSearch];
}
}
where.OR = [ where.OR = [
{ startUrl: { contains: query.search, mode: "insensitive" } }, { startUrl: { contains: trimmedSearch, mode: "insensitive" } },
{ domain: { contains: query.search, mode: "insensitive" } }, { domain: { contains: trimmedSearch, mode: "insensitive" } },
...(matchingIds.length > 0 ? [{ id: { in: matchingIds } }] : []),
]; ];
} }
...@@ -113,13 +135,19 @@ export class CrawlJobRepository { ...@@ -113,13 +135,19 @@ export class CrawlJobRepository {
prisma.crawlJob.count({ where }), prisma.crawlJob.count({ where }),
]); ]);
const totalPages = Math.max(1, Math.ceil(total / limit));
return { return {
items, items,
total,
page,
limit,
pageSize: limit,
totalPages,
meta: { meta: {
total, total,
page, page,
limit, limit,
totalPages: Math.ceil(total / limit), totalPages,
}, },
}; };
} }
......
...@@ -51,12 +51,22 @@ export class HealthService { ...@@ -51,12 +51,22 @@ export class HealthService {
if (crawlQueue) { if (crawlQueue) {
const redisStart = Date.now(); const redisStart = Date.now();
try { try {
const client = await crawlQueue.client; const client = await Promise.race([
crawlQueue.client,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Redis connection timeout")), 1500)
),
]);
if ( if (
"ping" in client && "ping" in client &&
typeof (client as { ping: () => Promise<string> }).ping === "function" typeof (client as { ping: () => Promise<string> }).ping === "function"
) { ) {
await (client as { ping: () => Promise<string> }).ping(); await Promise.race([
(client as { ping: () => Promise<string> }).ping(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Redis ping timeout")), 1500)
),
]);
} }
checks.redis = { checks.redis = {
status: "up", status: "up",
...@@ -88,12 +98,19 @@ export class HealthService { ...@@ -88,12 +98,19 @@ export class HealthService {
if (crawlQueue) { if (crawlQueue) {
try { try {
const [waiting, active, completed, failed] = await Promise.all([ const metricsPromise = Promise.all([
crawlQueue.getWaitingCount(), crawlQueue.getWaitingCount(),
crawlQueue.getActiveCount(), crawlQueue.getActiveCount(),
crawlQueue.getCompletedCount(), crawlQueue.getCompletedCount(),
crawlQueue.getFailedCount(), crawlQueue.getFailedCount(),
]); ]);
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Queue metrics timeout")), 1500)
);
const [waiting, active, completed, failed] = await Promise.race([
metricsPromise,
timeoutPromise,
]);
crawlQueueMetrics = { waiting, active, completed, failed }; crawlQueueMetrics = { waiting, active, completed, failed };
} catch { } catch {
crawlQueueMetrics = "unavailable"; crawlQueueMetrics = "unavailable";
...@@ -102,12 +119,19 @@ export class HealthService { ...@@ -102,12 +119,19 @@ export class HealthService {
if (webhookQueue) { if (webhookQueue) {
try { try {
const [waiting, active, completed, failed] = await Promise.all([ const metricsPromise = Promise.all([
webhookQueue.getWaitingCount(), webhookQueue.getWaitingCount(),
webhookQueue.getActiveCount(), webhookQueue.getActiveCount(),
webhookQueue.getCompletedCount(), webhookQueue.getCompletedCount(),
webhookQueue.getFailedCount(), webhookQueue.getFailedCount(),
]); ]);
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Webhook queue metrics timeout")), 1500)
);
const [waiting, active, completed, failed] = await Promise.race([
metricsPromise,
timeoutPromise,
]);
webhookQueueMetrics = { waiting, active, completed, failed }; webhookQueueMetrics = { waiting, active, completed, failed };
} catch { } catch {
webhookQueueMetrics = "unavailable"; webhookQueueMetrics = "unavailable";
......
...@@ -3,48 +3,56 @@ import { envConfig } from "./config/env.config"; ...@@ -3,48 +3,56 @@ import { envConfig } from "./config/env.config";
import Redis from "ioredis"; import Redis from "ioredis";
async function bootstrap() { async function bootstrap() {
if (!envConfig.redis.enabled) { let isRedisAvailable = false;
console.error( if (envConfig.redis.enabled) {
"[Server] REDIS_ENABLED is not set to true. Redis is required.", const redis = new Redis({
); host: envConfig.redis.host,
console.error( port: envConfig.redis.port,
"[Server] Start Docker and set REDIS_ENABLED=true in .env, then try again.", maxRetriesPerRequest: 0,
); lazyConnect: true,
process.exit(1); connectTimeout: 1500,
} retryStrategy: () => null,
enableOfflineQueue: false,
});
const redis = new Redis({ redis.on("error", () => {});
host: envConfig.redis.host,
port: envConfig.redis.port,
maxRetriesPerRequest: 0,
lazyConnect: true,
});
redis.on("error", () => {}); try {
await Promise.race([
try { redis.connect(),
await redis.connect(); new Promise((_, reject) => setTimeout(() => reject(new Error("Redis connection timeout")), 1500)),
await redis.ping(); ]);
await redis.quit(); await Promise.race([
console.log("[Server] Redis connection confirmed."); redis.ping(),
} catch { new Promise((_, reject) => setTimeout(() => reject(new Error("Redis ping timeout")), 1500)),
console.error("[Server] Cannot connect to Redis. Is Docker running?"); ]);
console.error("[Server] Run: docker compose up -d"); await redis.quit();
process.exit(1); isRedisAvailable = true;
console.log("[Server] Redis connection confirmed.");
} catch {
try {
redis.disconnect();
} catch {}
console.warn("[Server] Redis is offline. Running in degraded mode without queue workers (Database & APIs active).");
}
} else {
console.warn("[Server] REDIS_ENABLED is false. Running in degraded mode without queue workers (Database & APIs active).");
} }
// Only import app AFTER Redis is confirmed — this delays crawlQueue instantiation // Import app so Database endpoints and Express routes are fully available
const { default: app } = await import("./app"); const { default: app } = await import("./app");
const { initLocalStorage } = await import("./common/helpers/file.helper"); const { initLocalStorage } = await import("./common/helpers/file.helper");
initLocalStorage(); initLocalStorage();
await import("./queues/webhook.worker"); if (isRedisAvailable) {
console.log("[Server] Webhook worker initialized in background."); await import("./queues/webhook.worker");
console.log("[Server] Webhook worker initialized in background.");
const { startScheduleWorker } = await import("./queues/schedule.worker"); const { startScheduleWorker } = await import("./queues/schedule.worker");
startScheduleWorker(); startScheduleWorker();
console.log("[Server] Schedule worker initialized in background."); console.log("[Server] Schedule worker initialized in background.");
}
app.listen(envConfig.port, () => { app.listen(envConfig.port, () => {
console.log( console.log(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment