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 {
where.mode = query.mode;
}
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 = [
{ startUrl: { contains: query.search, mode: "insensitive" } },
{ domain: { contains: query.search, mode: "insensitive" } },
{ startUrl: { contains: trimmedSearch, mode: "insensitive" } },
{ domain: { contains: trimmedSearch, mode: "insensitive" } },
...(matchingIds.length > 0 ? [{ id: { in: matchingIds } }] : []),
];
}
......@@ -113,13 +135,19 @@ export class CrawlJobRepository {
prisma.crawlJob.count({ where }),
]);
const totalPages = Math.max(1, Math.ceil(total / limit));
return {
items,
total,
page,
limit,
pageSize: limit,
totalPages,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
totalPages,
},
};
}
......
......@@ -51,12 +51,22 @@ export class HealthService {
if (crawlQueue) {
const redisStart = Date.now();
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 (
"ping" in client &&
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 = {
status: "up",
......@@ -88,12 +98,19 @@ export class HealthService {
if (crawlQueue) {
try {
const [waiting, active, completed, failed] = await Promise.all([
const metricsPromise = Promise.all([
crawlQueue.getWaitingCount(),
crawlQueue.getActiveCount(),
crawlQueue.getCompletedCount(),
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 };
} catch {
crawlQueueMetrics = "unavailable";
......@@ -102,12 +119,19 @@ export class HealthService {
if (webhookQueue) {
try {
const [waiting, active, completed, failed] = await Promise.all([
const metricsPromise = Promise.all([
webhookQueue.getWaitingCount(),
webhookQueue.getActiveCount(),
webhookQueue.getCompletedCount(),
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 };
} catch {
webhookQueueMetrics = "unavailable";
......
......@@ -3,48 +3,56 @@ import { envConfig } from "./config/env.config";
import Redis from "ioredis";
async function bootstrap() {
if (!envConfig.redis.enabled) {
console.error(
"[Server] REDIS_ENABLED is not set to true. Redis is required.",
);
console.error(
"[Server] Start Docker and set REDIS_ENABLED=true in .env, then try again.",
);
process.exit(1);
}
let isRedisAvailable = false;
if (envConfig.redis.enabled) {
const redis = new Redis({
host: envConfig.redis.host,
port: envConfig.redis.port,
maxRetriesPerRequest: 0,
lazyConnect: true,
connectTimeout: 1500,
retryStrategy: () => null,
enableOfflineQueue: false,
});
redis.on("error", () => {});
try {
await redis.connect();
await redis.ping();
await Promise.race([
redis.connect(),
new Promise((_, reject) => setTimeout(() => reject(new Error("Redis connection timeout")), 1500)),
]);
await Promise.race([
redis.ping(),
new Promise((_, reject) => setTimeout(() => reject(new Error("Redis ping timeout")), 1500)),
]);
await redis.quit();
isRedisAvailable = true;
console.log("[Server] Redis connection confirmed.");
} catch {
console.error("[Server] Cannot connect to Redis. Is Docker running?");
console.error("[Server] Run: docker compose up -d");
process.exit(1);
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 { initLocalStorage } = await import("./common/helpers/file.helper");
initLocalStorage();
if (isRedisAvailable) {
await import("./queues/webhook.worker");
console.log("[Server] Webhook worker initialized in background.");
const { startScheduleWorker } = await import("./queues/schedule.worker");
startScheduleWorker();
console.log("[Server] Schedule worker initialized in background.");
}
app.listen(envConfig.port, () => {
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