Commit 069b960b authored by ThinhNC's avatar ThinhNC

feat(backend): upgrade data crawler backend with schedule crawl, change...

feat(backend): upgrade data crawler backend with schedule crawl, change detection, and Supabase support
parents
This diff is collapsed.
This diff is collapsed.
NODE_ENV=development
PORT=3000
TRUST_PROXY=false
# Database Configuration (PostgreSQL / Supabase)
# Cách 1: Cấu hình từng biến (khuyến nghị cho Supabase Direct Connection hoặc Session Pooler)
DB_HOST=db.xxxxxxxxxxxxxxxxxxxx.supabase.co
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD="your_supabase_db_password"
DB_NAME=postgres
DB_SSL=true
# Cách 2 (Tùy chọn): Sử dụng trực tiếp connection string từ Supabase Dashboard
# DATABASE_URL="postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres?sslmode=require"
JWT_ACCESS_SECRET=change_me_access_secret
JWT_REFRESH_SECRET=change_me_refresh_secret
JWT_ACCESS_EXPIRES_IN=1d
JWT_REFRESH_EXPIRES_IN=7d
FIRECRAWL_API_KEY=your_firecrawl_api_key
FIRECRAWL_BASE_URL=https://api.firecrawl.dev
FIRECRAWL_REQUEST_TIMEOUT_MS=60000
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_ENABLED=true
# Global API rate limit per IP
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=1000
STORAGE_DRIVER=s3
STORAGE_EXPORT_DIR=storage/exports
S3_ENDPOINT=http://127.0.0.1:9000
S3_REGION=us-east-1
S3_BUCKET=data-crawler-exports
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_FORCE_PATH_STYLE=true
MAX_CRAWL_PAGES=100
MAX_CRAWL_DEPTH=3
WORKER_CONCURRENCY=3
WORKER_JOB_TIMEOUT_MS=300000
WORKER_MAX_STALLED_COUNT=1
USER_MAX_PAGES=100
USER_MAX_JOBS_PER_DAY=10
USER_MAX_CONCURRENT_JOBS=3
# SMTP Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
# Gmail requires the full email address and a 16-character App Password
# (with 2-Step Verification enabled), not the normal account password.
SMTP_USER=your_smtp_user
SMTP_PASS=your_16_character_app_password
SMTP_FROM="Data Crawler <no-reply@datacrawler.com>"
FRONTEND_URL=http://localhost:5173
# Webhook Configuration
WEBHOOK_ENCRYPTION_KEY=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
WEBHOOK_QUEUE_NAME=webhook-delivery
node_modules/
dist/
.env
*.log
storage/exports/*
!storage/exports/.gitkeep
.DS_Store
nopush/
coverage/
frontend-data-crawler-be/
\ No newline at end of file
shamefully-hoist=true
# Backend guidance
## Architecture
- Keep the request path `Route -> Controller -> Service -> Repository -> Prisma`.
- Only `*.repository.ts` files may import or call the Prisma client.
- Keep module code under `src/modules/<feature>/`; use the existing route, controller, service, repository, DTO, validation, and test conventions in the nearest module.
- Put shared errors, helpers, constants, storage code, and types under `src/common/` only when they are genuinely reused.
- Mount module routes through `src/routes/index.ts`.
## Data and integrations
- Treat `prisma/schema.prisma` as the database schema source of truth.
- Make schema changes through Prisma migrations and commit the schema and generated migration together.
- Never run `pnpm db:migrate:reset` unless the user explicitly requests destructive local reset.
- Keep queue processors idempotent where retries are possible, and preserve valid crawl-job state transitions.
- Parse and validate external input at boundaries. Reuse Zod validation and the existing application error shape.
- Keep configuration access in `src/config/`; document new variables in `.env.example` without real values.
- Preserve the clean/raw output contract described in `docs/DATA_CONTRACT_V1.md`.
## Generated and runtime files
- Do not hand-edit `dist/`, `coverage/`, `storage/exports/`, or generated Swagger JSON.
- When API annotations or routes change, regenerate Swagger with `pnpm swagger`.
## Validation
Run commands from `data-crawler-be/`.
- Focused test: `pnpm test -- <path-to-test> --runInBand`
- Test suite: `pnpm test -- --runInBand`
- Lint: `pnpm lint`
- Production compile: `pnpm build`
Add colocated Jest tests under `__tests__/` for service, worker, repository-boundary, export, or contract behavior that changes.
This diff is collapsed.
This diff is collapsed.
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: crawl_data_postgres
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-crawl_data_db}
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
container_name: crawl_data_redis
ports:
- "${REDIS_PORT:-6379}:6379"
minio:
image: minio/minio:latest
container_name: crawl_data_minio
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000"
- "9001:9001"
command: server /data --console-address ":9001"
volumes:
- minio_data:/data
volumes:
postgres_data:
minio_data:
This diff is collapsed.
# Storage export: Local, AWS S3 và MinIO
## 1. Cách hệ thống đang hoạt động
Storage được chọn bằng `STORAGE_DRIVER`:
| Cấu hình | Nơi lưu file |
| --- | --- |
| `STORAGE_DRIVER=local` | Lưu trực tiếp trong `STORAGE_EXPORT_DIR` |
| `STORAGE_DRIVER=s3` | Dùng AWS S3, MinIO hoặc dịch vụ tương thích S3 |
Với JSON, CSV, XLSX và Markdown, hệ thống tạo file staging rồi upload lên storage. ZIP được stream trực tiếp lên storage, không tạo file ZIP local.
Database lưu storage path, ví dụ:
```text
<jobId>/data/pages.json
<jobId>/<jobId>.zip
```
Khi download, backend đọc file từ storage provider đang được cấu hình.
## 2. Chạy local với MinIO
Khởi động các service:
```bash
docker compose up -d redis minio
```
Lệnh trên dùng database đã deploy theo cấu hình `DB_HOST` trong `.env`. Chỉ khởi động thêm `postgres` nếu muốn chạy database local:
```bash
docker compose up -d postgres redis minio
```
Nếu đã có Redis online và `REDIS_HOST` trỏ tới Redis đó, chỉ cần khởi động MinIO:
```bash
docker compose up -d minio
```
Mở MinIO Console tại `http://127.0.0.1:9001`:
```text
Username: minioadmin
Password: minioadmin
```
Tạo bucket `data-crawler-exports` một lần.
Cấu hình `.env`:
```env
STORAGE_DRIVER=s3
STORAGE_EXPORT_DIR=storage/exports
S3_ENDPOINT=http://127.0.0.1:9000
S3_REGION=us-east-1
S3_BUCKET=data-crawler-exports
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_FORCE_PATH_STYLE=true
```
Chạy API và worker ở hai terminal:
```bash
pnpm dev
```
```bash
pnpm worker
```
Swagger: `http://127.0.0.1:3000/api-docs`
Nếu backend chạy trong Docker cùng MinIO, dùng:
```env
S3_ENDPOINT=http://minio:9000
```
## 3. Triển khai online với AWS S3
Tạo private S3 bucket và cấp IAM Role hoặc credentials cho backend. API và worker phải dùng cùng cấu hình:
```env
STORAGE_DRIVER=s3
STORAGE_EXPORT_DIR=/tmp/data-crawler-exports
S3_ENDPOINT=
S3_REGION=ap-southeast-1
S3_BUCKET=your-production-bucket
S3_FORCE_PATH_STYLE=false
```
Nếu chạy trên AWS, nên dùng IAM Role và không đặt access key trực tiếp. Nếu deploy ngoài AWS, lưu credentials trong secret manager.
## 4. Triển khai online với MinIO
MinIO phải chạy như một service riêng có persistent volume, HTTPS, backup và restart policy.
```env
STORAGE_DRIVER=s3
STORAGE_EXPORT_DIR=/tmp/data-crawler-exports
S3_ENDPOINT=https://minio.example.com
S3_REGION=us-east-1
S3_BUCKET=data-crawler-exports
S3_ACCESS_KEY_ID=backend-app-key
S3_SECRET_ACCESS_KEY=backend-app-secret
S3_FORCE_PATH_STYLE=true
```
Không dùng `minioadmin/minioadmin` hoặc image tag `latest` trên production. Với dữ liệu quan trọng, dùng MinIO distributed hoặc managed storage như AWS S3.
## 5. Build và chạy production
```bash
pnpm install --frozen-lockfile
pnpm prisma:generate
pnpm db:migrate:deploy
pnpm build
pnpm start
```
Chạy worker từ code đã build:
```bash
node dist/queues/crawl.worker.js
```
## 6. Checklist
- API và worker dùng cùng storage config và bucket.
- Bucket đã tồn tại và không public.
- Credentials nằm trong secret manager.
- `STORAGE_EXPORT_DIR` writable và có cleanup định kỳ.
- MinIO production có persistent volume, HTTPS và backup.
- Database migrations đã được apply.
- API và worker có restart policy.
Lưu ý: AWS S3 và MinIO không được chọn theo kích thước file. Provider chỉ phụ thuộc cấu hình môi trường khi process khởi động.
# Hướng dẫn Kiểm thử API (Postman & Newman)
Tài liệu này hướng dẫn cách vận hành, cấu trúc và danh sách các kịch bản kiểm thử (test cases) cho hệ thống **Data Crawler Backend API**, bao gồm các luồng xác thực, quản lý crawl job, xem trước dữ liệu Clean/Raw và tải xuống các định dạng file export.
---
## 🚀 1. Cách chạy kiểm thử
### Cách 1: Chạy trực tiếp bằng Postman (GUI)
1. **Import vào Postman**:
- File Collection: [data-crawler.postman_collection.json](./data-crawler.postman_collection.json)
- File Environment: [data-crawler.postman_environment.json](./data-crawler.postman_environment.json)
2. **Chọn Environment**: Chọn `Data Crawler Local Environment` ở góc trên bên phải UI Postman.
3. **Chạy Collection Runner**:
- Click chuột phải vào Collection `Data Crawler BE API` -> Chọn **Run collection**.
- Chọn các thư mục kịch bản cần kiểm thử (`Auth`, `Crawl Jobs`, `Export & Download`, `Permission & Edge Case Tests`).
- Nhấn **Run Data Crawler BE API**.
### Cách 2: Chạy tự động bằng Newman (CLI)
Yêu cầu đã khởi chạy Server Backend tại `http://localhost:3000`. Chạy lệnh sau tại thư mục gốc của dự án:
```bash
npx newman run "docs/postman/data-crawler.postman_collection.json" -e "docs/postman/data-crawler.postman_environment.json" --reporters cli
```
Chạy từng folder kịch bản cụ thể:
```bash
npx newman run "docs/postman/data-crawler.postman_collection.json" -e "docs/postman/data-crawler.postman_environment.json" --folder "Crawl Jobs" --reporters cli
```
---
## 📋 2. Chi tiết các Nhóm Kiểm thử (Test Suites)
### 🔐 A. Nhóm Auth (Xác thực & Ủy quyền)
Kiểm tra luồng đăng nhập, lấy thông tin cá nhân, cập nhật tài khoản và cơ chế refresh token.
1. **Login**:
- *Endpoint*: `POST /auth/login`
- *Test Assertions*: Trả về HTTP 200, `success: true`, sinh ra `accessToken``refreshToken`, tự động lưu vào môi trường Postman (`token`, `refreshToken`).
2. **Get Me**:
- *Endpoint*: `GET /auth/me`
- *Test Assertions*: Đính kèm Bearer token. Trả về chính xác thông tin User (`id`, `email`, `role`, `isActive`).
3. **Refresh Token**:
- *Endpoint*: `POST /auth/refresh`
- *Test Assertions*: Nhận `refreshToken`, cấp lại `accessToken` mới, cập nhật lại biến môi trường.
4. **Logout**:
- *Endpoint*: `POST /auth/logout`
- *Test Assertions*: Thu hồi token trong database, xóa khỏi biến môi trường Postman.
5. **Login with Invalid Credentials (Edge Case)**:
- *Test Assertions*: Trả về `401 Unauthorized`.
6. **Get Me without Token (Edge Case)**:
- *Test Assertions*: Trả về `401 Unauthorized`.
---
### ⚙️ B. Nhóm Crawl Jobs & Clean/Raw Preview
Kiểm tra các hoạt động tạo tác vụ crawl, lấy danh sách trang, xem trước dữ liệu sạch/thô và lọc chất lượng.
1. **Create Crawl Job**:
- *Endpoint*: `POST /crawl-jobs`
- *Body Payload*: `{ "startUrl": "https://example.com", "mode": "CRAWL", "maxPages": 50, "maxDepth": 2 }`
- *Test Assertions*: HTTP 201 Created, trả về job ID mới (`job_id`).
2. **Get Crawl Jobs (Paginated & Filtered)**:
- *Endpoint*: `GET /crawl-jobs?status=PENDING&page=1&limit=10`
- *Test Assertions*: Trả về danh sách phân trang `{ items: Array, meta: { total, page, limit, totalPages } }`.
3. **Get Crawl Job by ID**:
- *Endpoint*: `GET /crawl-jobs/:id`
- *Test Assertions*: Trả về chi tiết các thông số của Job (`startUrl`, `mode`, `status`, `totalPages`, `successPages`, `failedPages`).
4. **Get Crawled Pages (Metadata List)**:
- *Endpoint*: `GET /crawl-jobs/:id/pages`
- *Query Params*: Supports `status`, `statusCode`, `search`, `dataQualityScore`, `hasTables`, `hasImages`, `hasLinks`, `wordCount`, `sortBy`, `order`.
- *Test Assertions*: Trả về mảng danh sách trang kèm theo `normalizedUrl`, `dataQualityScore`, `wordCount`, `warnings`, `hasSensitiveData` (không chứa payload markdown dài để tối ưu băng thông).
5. **Get Crawled Pages Preview (Clean vs. Raw Output)**:
- *Endpoint*: `GET /crawl-jobs/:id/pages/preview?minQualityScore=50` (hoặc `GET /crawl-jobs/:id/pages?preview=true`)
- *Test Assertions*:
- Phải chứa đủ 3 trường nội dung đại diện cho hai lớp Output:
- `rawMarkdown`: Markdown thô nguyên bản thu thập được.
- `mainContent`: Thân bài chính đã qua lọc bỏ nhiễu nav/footer/sidebar **(Khuyến nghị cho AI Agents / LLM)**.
- `cleanText`: Văn bản thuần túy đã xóa sạch ký tự định dạng Markdown.
- Hỗ trợ kiểm tra các chỉ số chất lượng: `dataQualityScore` (0-100), `wordCount`, `contentHash` (SHA-256), `warnings` (`NAV_NOISE`, `TOO_SHORT`, `DUPLICATE_CONTENT`, ...).
6. **Get Job Assets**:
- *Endpoint*: `GET /crawl-jobs/:id/assets?assetType=IMAGE`
- *Query Params*: `assetType` (enum: `IMAGE`, `LINK`, `PDF`, `FILE`, `VIDEO`, `OTHER`).
- *Test Assertions*: Trả về danh sách tài nguyên hình ảnh/liên kết thu thập được từ các trang.
---
### 💾 C. Nhóm Export & Download
Kiểm tra luồng khởi tạo và tải về các tập tin xuất bản cho Crawl Job đã hoàn thành (`COMPLETED`).
1. **Get Crawl Job Exports**:
- *Endpoint*: `GET /crawl-jobs/:id/exports`
- *Test Assertions*: Trả về danh sách các tệp tin xuất bản đã tạo của Job.
2. **Create Export for Job**:
- *Endpoint*: `POST /crawl-jobs/:id/exports`
- *Body Payload*: `{ "exportType": "ZIP" }` (Các định dạng hỗ trợ: `JSON`, `CSV`, `XLSX`, `MARKDOWN`, `ZIP`).
- *Test Assertions*: HTTP 201 Created, khởi tạo bản export thành công và lưu `export_id`.
3. **Download Export File**:
- *Endpoint*: `GET /exports/:exportId/download` (hoặc `GET /crawl-jobs/:id/download`)
- *Test Assertions*: Trả về stream binary tệp tin kèm theo đúng Header `Content-Disposition`.
- **Đặc quyền cấu trúc ZIP Output**:
- Thư mục `/data/raw/pages.raw.json` & `/data/clean/pages.clean.json`.
- Thư mục `/markdown/raw/` & `/markdown/clean/`.
- Các tệp `tables.xlsx`, `links.csv`, `images.csv`, `metadata.json`, `errors.json`.
---
### 🛡️ D. Nhóm Permission & Edge Case Tests (Phân quyền & Lỗi nghiệp vụ)
1. **Get Non-Existent Job**:
- Gửi ID UUID không tồn tại -> Kiểm tra phản hồi `404 Not Found``code: "CRAWL_JOB_NOT_FOUND"`.
2. **Cancel Completed Job**:
- Cố gắng hủy một job đã ở trạng thái `COMPLETED` -> Phản hồi `400 Bad Request`.
3. **Owner vs Admin Authorization**:
- Đảm bảo User thường không thể truy cập hoặc thao tác trên Crawl Job của người dùng khác (Hệ thống trả về `404 Not Found` để ẩn sự tồn tại của resource).
- Đảm bảo tài khoản ADMIN xem và quản lý được toàn bộ Job trong hệ thống.
This diff is collapsed.
{
"id": "a90ffc07-e85d-4f10-b98a-59df18c3539e",
"name": "Data Crawler Local Environment",
"values": [
{
"key": "base_url",
"value": "http://localhost:3000/api/v1",
"type": "default",
"enabled": true
},
{
"key": "token",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "refreshToken",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "reset_token",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "verify_token",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "registered_user_id",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "completed_job_id",
"value": "b37827f0-9b0b-4778-8f54-6b7da888bab5",
"type": "default",
"enabled": true
},
{
"key": "job_id",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "export_id",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "export_id_zip",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "export_id_json",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "export_id_csv",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "export_id_xlsx",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "export_id_md",
"value": "",
"type": "default",
"enabled": true
},
{
"key": "created_user_id",
"value": "",
"type": "default",
"enabled": true
}
],
"_postman_variable_scope": "environment"
}
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
export default [
{
ignores: ['dist/**', 'node_modules/**', 'eslint.config.js'],
},
{
files: ['src/**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
},
plugins: {
'@typescript-eslint': tsPlugin,
},
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
},
},
];
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
moduleNameMapper: {
'node-html-parser': '<rootDir>/src/__mocks__/node-html-parser.ts',
},
modulePathIgnorePatterns: ['<rootDir>/dist/'],
};
\ No newline at end of file
{
"name": "data-crawler-be",
"version": "1.0.0",
"description": "Backend API for data crawling service",
"main": "dist/server.js",
"packageManager": "pnpm@9.15.0",
"scripts": {
"dev": "pnpm swagger && ts-node-dev --respawn --transpile-only --ignore-watch src/docs/swagger.json src/server.ts",
"swagger": "ts-node src/docs/swagger.ts",
"worker": "ts-node-dev --respawn --transpile-only src/queues/crawl.worker.ts",
"build": "pnpm swagger && tsc",
"start": "node dist/server.js",
"prisma:generate": "node scripts/prisma-run.js generate",
"prisma:studio": "node scripts/prisma-run.js studio",
"db:migrate": "node scripts/prisma-run.js migrate dev",
"db:migrate:init": "node scripts/prisma-run.js migrate dev --name init",
"db:migrate:deploy": "node scripts/prisma-run.js migrate deploy",
"db:migrate:reset": "node scripts/prisma-run.js migrate reset",
"db:migrate:status": "node scripts/prisma-run.js migrate status",
"db:seed": "node scripts/prisma-run.js db seed -- --tsx prisma/seed.ts",
"lint": "eslint .",
"format": "prettier --write .",
"test": "jest"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1096.0",
"@aws-sdk/lib-storage": "^3.1096.0",
"@mendable/firecrawl-js": "^1.19.0",
"@prisma/client": "^5.22.0",
"archiver": "^7.0.1",
"axios": "^1.18.1",
"bcryptjs": "^2.4.3",
"bullmq": "^5.34.0",
"cheerio": "^1.2.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"exceljs": "^4.4.0",
"express": "^4.21.2",
"express-rate-limit": "^8.5.2",
"helmet": "^8.0.0",
"ioredis": "^5.4.2",
"json2csv": "^6.0.0-alpha.2",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"node-html-parser": "^8.0.4",
"nodemailer": "^9.0.3",
"swagger-ui-express": "^5.0.1",
"turndown": "^7.2.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/archiver": "^6.0.3",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/express-rate-limit": "^6.0.2",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.7",
"@types/morgan": "^1.9.9",
"@types/node": "^22.10.2",
"@types/nodemailer": "^8.0.1",
"@types/swagger-ui-express": "^4.1.8",
"@types/turndown": "^5.0.5",
"@typescript-eslint/eslint-plugin": "^8.63.0",
"@typescript-eslint/parser": "^8.63.0",
"eslint": "^9.17.0",
"jest": "^30.4.2",
"prettier": "^3.4.2",
"prisma": "^5.22.0",
"swagger-autogen": "^2.23.7",
"ts-jest": "^29.4.11",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}
This diff is collapsed.
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'CRAWLER_USER', 'VIEWER');
-- CreateEnum
CREATE TYPE "CrawlJobStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'PARTIAL_COMPLETED', 'FAILED', 'CANCELED', 'BLOCKED');
-- CreateEnum
CREATE TYPE "CrawlMode" AS ENUM ('SCRAPE', 'CRAWL', 'SITEMAP', 'URL_LIST');
-- CreateEnum
CREATE TYPE "CrawlPageStatus" AS ENUM ('PENDING', 'SUCCESS', 'FAILED', 'BLOCKED', 'REQUIRES_LOGIN', 'CAPTCHA_DETECTED', 'PAYWALL_DETECTED', 'TIMEOUT', 'SKIPPED');
-- CreateEnum
CREATE TYPE "ExportType" AS ENUM ('JSON', 'CSV', 'XLSX', 'MARKDOWN', 'MARKDOWN_ZIP', 'FULL_ZIP');
-- CreateEnum
CREATE TYPE "ExportStatus" AS ENUM ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED');
-- CreateEnum
CREATE TYPE "AssetType" AS ENUM ('IMAGE', 'LINK', 'PDF', 'FILE', 'VIDEO', 'OTHER');
-- CreateTable
CREATE TABLE "users" (
"id" UUID NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"full_name" TEXT,
"role" "UserRole" NOT NULL DEFAULT 'CRAWLER_USER',
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "crawl_jobs" (
"id" UUID NOT NULL,
"user_id" UUID NOT NULL,
"start_url" TEXT NOT NULL,
"domain" TEXT,
"mode" "CrawlMode" NOT NULL DEFAULT 'SCRAPE',
"status" "CrawlJobStatus" NOT NULL DEFAULT 'PENDING',
"max_pages" INTEGER NOT NULL DEFAULT 20,
"max_depth" INTEGER NOT NULL DEFAULT 1,
"total_pages" INTEGER NOT NULL DEFAULT 0,
"success_pages" INTEGER NOT NULL DEFAULT 0,
"failed_pages" INTEGER NOT NULL DEFAULT 0,
"error_message" TEXT,
"started_at" TIMESTAMP(3),
"finished_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "crawl_jobs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "crawl_pages" (
"id" UUID NOT NULL,
"job_id" UUID NOT NULL,
"url" TEXT NOT NULL,
"title" TEXT,
"description" TEXT,
"markdown_content" TEXT,
"html_content_path" TEXT,
"status" "CrawlPageStatus" NOT NULL DEFAULT 'PENDING',
"status_code" INTEGER,
"error_message" TEXT,
"crawled_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "crawl_pages_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "crawl_assets" (
"id" UUID NOT NULL,
"job_id" UUID NOT NULL,
"page_id" UUID,
"asset_type" "AssetType" NOT NULL,
"url" TEXT NOT NULL,
"source_url" TEXT,
"alt_text" TEXT,
"mime_type" TEXT,
"order_index" INTEGER,
"css_selector" TEXT,
"dom_path" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "crawl_assets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "crawl_exports" (
"id" UUID NOT NULL,
"job_id" UUID NOT NULL,
"export_type" "ExportType" NOT NULL,
"status" "ExportStatus" NOT NULL DEFAULT 'PENDING',
"file_name" TEXT NOT NULL,
"file_path" TEXT NOT NULL,
"file_size" INTEGER,
"mime_type" TEXT,
"error_message" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "crawl_exports_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "crawl_jobs_user_id_idx" ON "crawl_jobs"("user_id");
-- CreateIndex
CREATE INDEX "crawl_jobs_status_idx" ON "crawl_jobs"("status");
-- CreateIndex
CREATE INDEX "crawl_jobs_created_at_idx" ON "crawl_jobs"("created_at");
-- CreateIndex
CREATE INDEX "crawl_pages_job_id_idx" ON "crawl_pages"("job_id");
-- CreateIndex
CREATE INDEX "crawl_pages_status_idx" ON "crawl_pages"("status");
-- CreateIndex
CREATE INDEX "crawl_assets_job_id_idx" ON "crawl_assets"("job_id");
-- CreateIndex
CREATE INDEX "crawl_assets_page_id_idx" ON "crawl_assets"("page_id");
-- CreateIndex
CREATE INDEX "crawl_assets_asset_type_idx" ON "crawl_assets"("asset_type");
-- CreateIndex
CREATE INDEX "crawl_exports_job_id_idx" ON "crawl_exports"("job_id");
-- CreateIndex
CREATE INDEX "crawl_exports_export_type_idx" ON "crawl_exports"("export_type");
-- AddForeignKey
ALTER TABLE "crawl_jobs" ADD CONSTRAINT "crawl_jobs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "crawl_pages" ADD CONSTRAINT "crawl_pages_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "crawl_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "crawl_assets" ADD CONSTRAINT "crawl_assets_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "crawl_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "crawl_assets" ADD CONSTRAINT "crawl_assets_page_id_fkey" FOREIGN KEY ("page_id") REFERENCES "crawl_pages"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "crawl_exports" ADD CONSTRAINT "crawl_exports_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "crawl_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
/*
Warnings:
- The values [PARTIAL_COMPLETED,BLOCKED] on the enum `CrawlJobStatus` will be removed. If these variants are still used in the database, this will fail.
- The values [MARKDOWN_ZIP,FULL_ZIP] on the enum `ExportType` will be removed. If these variants are still used in the database, this will fail.
- You are about to drop the column `job_id` on the `crawl_assets` table. All the data in the column will be lost.
*/
-- CreateEnum
CREATE TYPE "LogLevel" AS ENUM ('INFO', 'WARNING', 'ERROR');
-- AlterEnum
BEGIN;
CREATE TYPE "CrawlJobStatus_new" AS ENUM ('PENDING', 'QUEUED', 'RUNNING', 'PROCESSING_EXPORT', 'COMPLETED', 'FAILED', 'CANCELED', 'EXPIRED');
ALTER TABLE "crawl_jobs" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "crawl_jobs" ALTER COLUMN "status" TYPE "CrawlJobStatus_new" USING ("status"::text::"CrawlJobStatus_new");
ALTER TYPE "CrawlJobStatus" RENAME TO "CrawlJobStatus_old";
ALTER TYPE "CrawlJobStatus_new" RENAME TO "CrawlJobStatus";
DROP TYPE "CrawlJobStatus_old";
ALTER TABLE "crawl_jobs" ALTER COLUMN "status" SET DEFAULT 'PENDING';
COMMIT;
-- AlterEnum
BEGIN;
CREATE TYPE "ExportType_new" AS ENUM ('JSON', 'CSV', 'XLSX', 'MARKDOWN', 'ZIP');
ALTER TABLE "crawl_exports" ALTER COLUMN "export_type" TYPE "ExportType_new" USING ("export_type"::text::"ExportType_new");
ALTER TYPE "ExportType" RENAME TO "ExportType_old";
ALTER TYPE "ExportType_new" RENAME TO "ExportType";
DROP TYPE "ExportType_old";
COMMIT;
-- DropForeignKey
ALTER TABLE "crawl_assets" DROP CONSTRAINT "crawl_assets_job_id_fkey";
-- DropIndex
DROP INDEX "crawl_assets_job_id_idx";
-- DropIndex
DROP INDEX "crawl_jobs_user_id_idx";
-- AlterTable
ALTER TABLE "crawl_assets" DROP COLUMN "job_id",
ADD COLUMN "crawl_job_id" UUID;
-- AlterTable
ALTER TABLE "crawl_exports" ADD COLUMN "checksum" TEXT,
ADD COLUMN "expired_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "crawl_jobs" ADD COLUMN "delay_ms" INTEGER NOT NULL DEFAULT 1000,
ADD COLUMN "respect_robots_txt" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "retry_count" INTEGER NOT NULL DEFAULT 3,
ADD COLUMN "timeout_ms" INTEGER NOT NULL DEFAULT 30000,
ADD COLUMN "user_agent" TEXT;
-- AlterTable
ALTER TABLE "crawl_pages" ADD COLUMN "content" TEXT;
-- CreateTable
CREATE TABLE "CrawlJobLog" (
"id" UUID NOT NULL,
"job_id" UUID NOT NULL,
"level" "LogLevel" NOT NULL,
"step" TEXT NOT NULL,
"message" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CrawlJobLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "crawl_jobs_domain_idx" ON "crawl_jobs"("domain");
-- CreateIndex
CREATE INDEX "crawl_jobs_user_id_status_idx" ON "crawl_jobs"("user_id", "status");
-- AddForeignKey
ALTER TABLE "crawl_assets" ADD CONSTRAINT "crawl_assets_crawl_job_id_fkey" FOREIGN KEY ("crawl_job_id") REFERENCES "crawl_jobs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CrawlJobLog" ADD CONSTRAINT "CrawlJobLog_job_id_fkey" FOREIGN KEY ("job_id") REFERENCES "crawl_jobs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- CreateTable
CREATE TABLE "refresh_tokens" (
"id" UUID NOT NULL,
"token" TEXT NOT NULL,
"user_id" UUID NOT NULL,
"user_agent" TEXT,
"ip_address" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "refresh_tokens_token_key" ON "refresh_tokens"("token");
-- CreateIndex
CREATE INDEX "refresh_tokens_user_id_idx" ON "refresh_tokens"("user_id");
-- AddForeignKey
ALTER TABLE "refresh_tokens" ADD CONSTRAINT "refresh_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable
ALTER TABLE "users" ADD COLUMN "max_concurrent_jobs_limit" INTEGER NOT NULL DEFAULT 3,
ADD COLUMN "max_jobs_per_day_limit" INTEGER NOT NULL DEFAULT 10,
ADD COLUMN "max_pages_limit" INTEGER NOT NULL DEFAULT 100;
-- CreateIndex
CREATE UNIQUE INDEX "crawl_pages_job_id_url_key" ON "crawl_pages"("job_id", "url");
This diff is collapsed.
-- AlterTable
ALTER TABLE "users" ALTER COLUMN "is_active" SET DEFAULT false;
-- AlterTable
ALTER TABLE "crawl_jobs" ADD COLUMN "urls" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- AlterTable
ALTER TABLE "users" ALTER COLUMN "is_active" SET DEFAULT true;
-- AlterTable
ALTER TABLE "crawl_pages" ADD COLUMN "has_sensitive_data" BOOLEAN NOT NULL DEFAULT false;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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