251125:0000 Phase 6 wait start dev Check

This commit is contained in:
2025-11-25 00:28:33 +07:00
parent 553c2d13ad
commit 582ecb5741
22 changed files with 3757 additions and 489 deletions

2432
01_lcbp3_v1_4_3 copy.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
-- ==========================================================
-- DMS v1.4.2 Document Management System Database
-- DMS v1.4.3 Document Management System Database
-- Deploy Script Schema
-- Server: Container Station on QNAPQNAP TS-473A
-- Database service: MariaDB 10.11
@@ -9,8 +9,8 @@
-- frontend sevice: next.js
-- reverse proxy: jc21/nginx-proxy-manager:latest
-- cron service: n8n
-- DMS v1.4.2 Improvements
-- Update: revise fron v1.4.1 Gemini)
-- DMS v1.4.3 Improvements
-- Update: revise fron v1.4.2 add PARTITION to audit_logs & notification
-- ==========================================================
SET NAMES utf8mb4;
SET time_zone = '+07:00';
@@ -1918,7 +1918,7 @@ CREATE TABLE IF NOT EXISTS user_preferences (
-- รองรับ: Req 6.1
-- เหตุผล: รองรับ Distributed Tracing และระบุความรุนแรง
CREATE TABLE audit_logs (
audit_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Log',
audit_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'ID ของ Log',
request_id VARCHAR(100) NULL COMMENT 'Trace ID linking to app logs',
user_id INT COMMENT 'ผู้กระทำ',
action VARCHAR(100) NOT NULL COMMENT 'การกระทำ (
@@ -1932,13 +1932,38 @@ CREATE TABLE audit_logs (
details_json JSON COMMENT 'ข้อมูลบริบท',
ip_address VARCHAR(45) COMMENT 'IP Address',
user_agent VARCHAR(255) COMMENT 'User Agent',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาที่กระทำ',
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE
SET NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บบันทึกการกระทำของผู้ใช้';
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'เวลาที่กระทำ',
-- [แก้ไข] รวม created_at เข้ามาใน Primary Key เพื่อรองรับ Partition
PRIMARY KEY (audit_id, created_at),
-- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign Key เพื่อไม่ให้ติดข้อจำกัดของ Partition Table
INDEX idx_audit_user (user_id),
INDEX idx_audit_action (action),
INDEX idx_audit_entity (entity_type, entity_id),
INDEX idx_audit_created (created_at)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางเก็บบันทึกการกระทำของผู้ใช้' -- [เพิ่ม] คำสั่ง Partition
PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p_old
VALUES LESS THAN (2024),
PARTITION p2024
VALUES LESS THAN (2025),
PARTITION p2025
VALUES LESS THAN (2026),
PARTITION p2026
VALUES LESS THAN (2027),
PARTITION p2027
VALUES LESS THAN (2028),
PARTITION p2028
VALUES LESS THAN (2029),
PARTITION p2029
VALUES LESS THAN (2030),
PARTITION p2030
VALUES LESS THAN (2031),
PARTITION p_future
VALUES LESS THAN MAXVALUE
);
-- ตารางสำหรับจัดการการแจ้งเตือน (Email/Line/System)
CREATE TABLE notifications (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของการแจ้งเตือน',
id INT NOT NULL AUTO_INCREMENT COMMENT 'ID ของการแจ้งเตือน',
user_id INT NOT NULL COMMENT 'ID ผู้ใช้',
title VARCHAR(255) NOT NULL COMMENT 'หัวข้อการแจ้งเตือน',
message TEXT NOT NULL COMMENT 'รายละเอียดการแจ้งเตือน',
@@ -1947,9 +1972,35 @@ CREATE TABLE notifications (
entity_type VARCHAR(50) COMMENT 'เช่น ''rfa '',
''circulation ''',
entity_id INT COMMENT 'ID ของเอนทิตีที่เกี่ยวข้อง',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการการแจ้งเตือน (Email / Line / System)';
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
-- [แก้ไข] รวม created_at เข้ามาใน Primary Key
PRIMARY KEY (id, created_at),
-- [แก้ไข] ใช้ Index ธรรมดาแทน Foreign Key
INDEX idx_notif_user (user_id),
INDEX idx_notif_type (notification_type),
INDEX idx_notif_read (is_read),
INDEX idx_notif_created (created_at)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการการแจ้งเตือน (Email / Line / System)' -- [เพิ่ม] คำสั่ง Partition
PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p_old
VALUES LESS THAN (2024),
PARTITION p2024
VALUES LESS THAN (2025),
PARTITION p2025
VALUES LESS THAN (2026),
PARTITION p2026
VALUES LESS THAN (2027),
PARTITION p2027
VALUES LESS THAN (2028),
PARTITION p2028
VALUES LESS THAN (2029),
PARTITION p2029
VALUES LESS THAN (2030),
PARTITION p2030
VALUES LESS THAN (2031),
PARTITION p_future
VALUES LESS THAN MAXVALUE
);
-- ตารางสำหรับจัดการดัชนีการค้นหาขั้นสูง (Full-text Search)
CREATE TABLE search_indices (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของดัชนี',

View File

@@ -6,276 +6,279 @@
## 📂 **backend/** (Backend Application)
* [x] `.env` (สำหรับ Local Dev เท่านั้น ห้าม commit)
* [x] `.gitignore`
* [x] `.prettierrc`
* [x] `docker-compose.override.yml`
* [x] `docker-compose.yml`
* [x] `nest-cli.json`
* [x] `tsconfig.build.json`
* [x] `tsconfig.json`
* [x] `package.json`
* [x] `pnpm-lock.yaml`
* [x] `README.md`
* [x] .env (สำหรับ Local Dev เท่านั้น ห้าม commit)
* [x] .gitignore
* [x] .prettierrc
* [x] docker-compose.override.yml
* [x] docker-compose.yml
* [x] nest-cli.json
* [x] tsconfig.build.json
* [x] tsconfig.json
* [x] package.json
* [x] pnpm-lock.yaml
* [x] README.md
---
## 📂 **backend/src/** (Source Code)
### **📄 Entry Points**
### **📄 Entry Points
* [x] `main.ts` (Application Bootstrap, Swagger, Global Pipes)
* [x] `app.module.ts` (Root Module ที่รวมทุก Modules เข้าด้วยกัน)
* [x] `app.service.ts` (Root Application Service)
* [x] `app.controller.ts` (Root Application Controller)
* [x] `app.controller.spec.ts` (Root Application Controller Unit Tests)
* [x] `redlock.d.ts` (Redlock Configuration)
* [x] main.ts
* [x] app.module.ts
* [x] app.service.ts
* [x] app.controller.ts
* [x] app.controller.spec.ts
* [x] redlock.d.ts
### **📁 src/common/** (Shared Resources)
* [x] **common.module.ts**
* [x] common.module.ts
* **auth/**
* **dto/**
* [x] **login.dto.ts**
* [x] **register.dto.ts**
* [x] login.dto.ts
* [x] register.dto.ts
* **strategies/**
* [x] **local.strategy.ts**
* [x] **jwt.strategy.ts**
* [x] **auth.controller.spec.ts**
* [x] **auth.controller.ts**
* [x] **auth.module.ts**
* [x] **auth.service.spec.ts**
* [x] **auth.service.ts**
* **config/** (Configuration Service)
* [x] **env.validation.ts**
* [x] **redis.config.ts**
* [x] local.strategy.ts
* [x] jwt.strategy.ts
* [x] auth.controller.spec.ts
* [x] auth.controller.ts
* [x] auth.module.ts
* [x] auth.service.spec.ts
* [x] auth.service.ts
* **config/**
* [x] env.validation.ts
* [x] redis.config.ts
* **decorators/**
* [x] **audit.decorator.ts**
* [x] **bypass-maintenance.decorator.ts**
* [x] `current-user.decorator.ts`
* [x] **idempotency.decorator.ts**
* [x] `require-permission.decorator.ts`
* [x] **retry.decorator.ts**
* [x] **circuit-breaker.decorator.ts**
* [x] audit.decorator.ts
* [x] bypass-maintenance.decorator.ts
* [x] current-user.decorator.ts
* [x] idempotency.decorator.ts
* [x] require-permission.decorator.ts
* [x] retry.decorator.ts
* [x] circuit-breaker.decorator.ts
* **entities/**
* [x] **audit-log.entity.ts**
* [x] **base.entity.ts**
* [x] audit-log.entity.ts
* [x] base.entity.ts
* **exceptions/**
* [x] `http-exception.filter.ts` (Global Filter)
* **file-storage/** (Two-Phase Storage System)
* [x] http-exception.filter.ts
* **file-storage/**
* **entities/**
* [x] **attachment.entity.ts**
* [x] **file-storage.controller.spec.ts**
* [x] **file-storage.controller.ts**
* [x] **file-storage.module.ts**
* [x] **file-storage.service.spec.ts**
* [x] `file-storage.service.ts` (Upload, Scan Virus, Commit)
* [x] **file-cleanup.service.ts** (Cleanup Temporary Files)
* [x] `guards/`
* [x] `jwt-auth.guard.ts`
* [x] **jwt-refresh.guard.ts**
* [x] **maintenance-mode.guard.ts**
* [x] `rbac.guard.ts` (ตรวจสอบสิทธิ์ 4 ระดับ)
* [x] attachment.entity.ts
* [x] file-storage.controller.spec.ts
* [x] file-storage.controller.ts
* [x] file-storage.module.ts
* [x] file-storage.service.spec.ts
* [x] file-storage.service.ts
* [x] file-cleanup.service.ts
* [x] guards/`
* [x] jwt-auth.guard.ts
* [x] jwt-refresh.guard.ts
* [x] maintenance-mode.guard.ts
* [x] rbac.guard.ts
* **interceptors/**
* [x] `audit-log.interceptor.ts` (เก็บ Log ลง DB)
* [x] **idempotency.interceptor.ts** (Idempotency Interceptor)
* [x] `transform.interceptor.ts` (Standard Response Format)
* **resilience/** (Circuit Breaker & Retry)
* [x] **resilience.module.ts** (Resilience Module)
* **security/** (Security Service)
* [x] **crypto.service.ts** (Crypto Service)
* [x] **request-context.service.ts
* [x] audit-log.interceptor.ts
* [x] idempotency.interceptor.ts
* [x] performance.interceptor.ts
* [x] transform.interceptor.ts
* **resilience/**
* [x] resilience.module.ts
* **services/**
* [x] crypto.service.ts
* [x] request-context.service.ts
### **📁 src/modules/** (Feature Modules)
1. **user/** (User Management & RBAC)
* [x] `dto/`
* [x] **assign-user-role.dto.ts**
* [x] `create-user.dto.ts`
* [ ] **search-user.dto.ts**
* [x] `update-user.dto.ts`
* [x] `update-user-preference.dto.ts`
* [x] `entities/`
* [x] `user.entity.ts`
* [x] `role.entity.ts`
* [x] `permission.entity.ts`
* [x] **user-assignment.entity.ts**
* [x] `user-preference.entity.ts`
* [x] **user-assignment.service.ts**
* [x] **user-preference.service.ts**
* [x] `user.controller.ts`
* [x] `user.module.ts`
* [x] `user.service.ts`
* [x] **user.service.spec.ts**
* [x] **dto/**
* [x] assign-user-role.dto.ts
* [x] create-user.dto.ts
* [x] update-user.dto.ts
* [x] update-user-preference.dto.ts
* [x] **entities/**
* [x] user.entity.ts
* [x] role.entity.ts
* [x] permission.entity.ts
* [x] user-assignment.entity.ts
* [x] user-preference.entity.ts
* [x] user-assignment.service.ts
* [x] user-preference.service.ts
* [x] user.controller.ts
* [x] user.module.ts
* [x] user.service.ts
* [x] user.service.spec.ts
2. **project/** (Project Structure)
* [x] `dto/`
* [x] **create-project.dto.ts**
* [x] `search-project.dto.ts`
* [x] `update-project.dto.ts`
* [x] `entities/`
* [x] `contract-organization.entity.ts`
* [x] `contract.entity.ts`
* [x] `organization.entity.ts`
* [x] `project-organization.entity.ts` (Junction)
* [x] **project.entity.ts**
* [x] **project.controller.spec.ts**
* [x] `project.controller.ts`
* [x] `project.module.ts`
* [x] **project.service.spec.ts**
* [x] `project.service.ts`
* [x] **dto/**
* [x] create-project.dto.ts
* [x] search-project.dto.ts
* [x] update-project.dto.ts
* [x] **entities/**
* [x] contract-organization.entity.ts
* [x] contract.entity.ts
* [x] organization.entity.ts
* [x] project-organization.entity.ts
* [x] project.entity.ts
* [x] project.controller.spec.ts
* [x] project.controller.ts
* [x] project.module.ts
* [x] project.service.spec.ts
* [x] project.service.ts
3. **correspondence/** (Core Document System)
* [x] `dto/`
* [x] `add-reference.dto.ts`
* [x] `create-correspondence.dto.ts`
* [x] `search-correspondence.dto.ts`
* [x] **submit-correspondence.dto.ts**
* [x] `workflow-action.dto.ts`
* [x] `entities/`
* [x] `correspondence-reference.entity.ts`
* [x] `correspondence-revision.entity.ts`
* [x] `correspondence-routing.entity.ts` (Unified Workflow)
* [x] `correspondence-status.entity.ts`
* [x] `correspondence-type.entity.ts`
* [x] `correspondence.entity.ts`
* [x] **routing-template-step.entity.ts**
* [x] `routing-template.entity.ts`
* [x] **correspondence.controller.spec.ts**
* [x] `correspondence.controller.ts`
* [x] `correspondence.module.ts`
* [x] **correspondence.service.spec.ts**
* [x] `correspondence.service.ts` (Impersonation & Workflow Logic)
* [x] **dto/**
* [x] add-reference.dto.ts
* [x] create-correspondence.dto.ts
* [x] search-correspondence.dto.ts
* [x] submit-correspondence.dto.ts
* [x] workflow-action.dto.ts
* [x] **entities/**
* [x] correspondence-reference.entity.ts
* [x] correspondence-revision.entity.ts
* [x] correspondence-routing.entity.ts
* [x] correspondence-status.entity.ts
* [x] correspondence-type.entity.ts
* [x] correspondence.entity.ts
* [x] routing-template-step.entity.ts
* [x] routing-template.entity.ts
* [x] correspondence.controller.spec.ts
* [x] correspondence.controller.ts
* [x] correspondence.module.ts
* [x] correspondence.service.spec.ts
* [x] correspondence.service.ts
4. **drawing/** (Contract & Shop Drawings)
* [x] `dto/`
* [x] `create-contract-drawing.dto.ts`
* [x] `create-shop-drawing-revision.dto.ts`
* [x] `create-shop-drawing.dto.ts`
* [x] `search-contract-drawing.dto.ts`
* [x] `search-shop-drawing.dto.ts`
* [x] `update-contract-drawing.dto.ts`
* [x] `entities/`
* [x] `contract-drawing-sub-category.entity.ts`
* [x] `contract-drawing-volume.entity.ts`
* [x] `contract-drawing.entity.ts`
* [x] `shop-drawing-main-category.entity.ts`
* [x] `shop-drawing-revision.entity.ts`
* [x] `shop-drawing-sub-category.entity.ts`
* [x] `shop-drawing.entity.ts`
* [x] `contract-drawing.controller.ts`
* [x] `contract-drawing.service.ts`
* [x] `drawing-master-data.controller.ts`
* [x] `drawing-master-data.service.ts`
* [x] `drawing.module.ts`
* [x] `shop-drawing.controller.ts`
* [x] `shop-drawing.service.ts`
* [x] **dto/**
* [x] create-contract-drawing.dto.ts
* [x] create-shop-drawing-revision.dto.ts
* [x] create-shop-drawing.dto.ts
* [x] search-contract-drawing.dto.ts
* [x] search-shop-drawing.dto.ts
* [x] update-contract-drawing.dto.ts
* [x] **entities/**
* [x] contract-drawing-sub-category.entity.ts
* [x] contract-drawing-volume.entity.ts
* [x] contract-drawing.entity.ts
* [x] shop-drawing-main-category.entity.ts
* [x] shop-drawing-revision.entity.ts
* [x] shop-drawing-sub-category.entity.ts
* [x] shop-drawing.entity.ts
* [x] contract-drawing.controller.ts
* [x] contract-drawing.service.ts
* [x] drawing-master-data.controller.ts
* [x] drawing-master-data.service.ts
* [x] drawing.module.ts
* [x] shop-drawing.controller.ts
* [x] shop-drawing.service.ts
5. **rfa/** (Request for Approval & Advanced Workflow)
* [x] `dto/`
* [x] `create-rfa.dto.ts`
* [x] `search-rfa.dto.ts`
* [x] `update-rfa.dto.ts`
* [x] `entities/`
* [x] `rfa-approve-code.entity.ts`
* [x] `rfa-item.entity.ts`
* [x] `rfa-revision.entity.ts`
* [x] `rfa-status-code.entity.ts`
* [x] `rfa-type.entity.ts`
* [x] `rfa-workflow-template-step.entity.ts`
* [x] `rfa-workflow-template.entity.ts`
* [x] `rfa-workflow.entity.ts`
* [x] `rfa.entity.ts`
* [x] `rfa.controller.ts`
* [x] `rfa.module.ts`
* [x] `rfa.service.ts` (Unified Workflow Integration)
* [x] **dto/**
* [x] create-rfa.dto.ts
* [x] search-rfa.dto.ts
* [x] update-rfa.dto.ts
* [x] **entities/**
* [x] rfa-approve-code.entity.ts
* [x] rfa-item.entity.ts
* [x] rfa-revision.entity.ts
* [x] rfa-status-code.entity.ts
* [x] rfa-type.entity.ts
* [x] rfa-workflow-template-step.entity.ts
* [x] rfa-workflow-template.entity.ts
* [x] rfa-workflow.entity.ts
* [x] rfa.entity.ts
* [x] rfa.controller.ts
* [x] rfa.module.ts
* [x] rfa.service.ts
6. **circulation/** (Internal Routing)
* [x] `dto/`
* [x] `create-circulation.dto.ts`
* [x] `update-circulation-routing.dto.ts`
* [x] `search-circulation.dto.ts`
* [x] `entities/`
* [x] `circulation-routing.entity.ts`
* [x] `circulation-status-code.entity.ts`
* [x] `circulation.entity.ts`
* [x] `circulation.controller.ts`
* [x] `circulation.module.ts`
* [x] `circulation.service.ts`
* [x] **dto/**
* [x] create-circulation.dto.ts
* [x] update-circulation-routing.dto.ts
* [x] search-circulation.dto.ts
* [x] **entities/**
* [x] circulation-routing.entity.ts
* [x] circulation-status-code.entity.ts
* [x] circulation.entity.ts
* [x] circulation.controller.ts
* [x] circulation.module.ts
* [x] circulation.service.ts
7. **transmittal/** (Document Forwarding)
* [x] `dto/`
* [x] `create-transmittal.dto.ts`
* [x] `search-transmittal.dto.ts`
* [x] **update-transmittal.dto.ts**
* [x] `entities/`
* [x] `transmittal-item.entity.ts`
* [x] `transmittal.entity.ts`
* [x] `transmittal.controller.ts`
* [x] `transmittal.module.ts`
* [x] `transmittal.service.ts`
* [x] **dto/**
* [x] create-transmittal.dto.ts
* [x] search-transmittal.dto.ts
* [x] update-transmittal.dto.ts
* [x] **entities/**
* [x] transmittal-item.entity.ts
* [x] transmittal.entity.ts
* [x] transmittal.controller.ts
* [x] transmittal.module.ts
* [x] transmittal.service.ts
8. **notification/** (System Alerts)
* [x] `dto/`
* [x] `create-notification.dto.ts`
* [x] `search-notification.dto.ts`
* [x] `entities/`
* [x] `notification.entity.ts`
* [x] `notification-cleanup.service.ts` (Cron Job)
* [x] `notification.controller.ts`
* [x] `notification.gateway.ts`
* [x] `notification.module.ts` (Real-time WebSocket)
* [x] `notification.processor.ts` (Consumer/Worker for Email & Line)
* [x] `notification.service.ts` (Producer)
* [x] **dto/**
* [x] create-notification.dto.ts
* [x] search-notification.dto.ts
* [x] **entities/**
* [x] notification.entity.ts
* [x] notification-cleanup.service.ts
* [x] notification.controller.ts
* [x] notification.gateway.ts
* [x] notification.module.ts
* [x] notification.processor.ts
* [x] notification.service.ts
9. **search/** (Elasticsearch)
* [x] `dto/search-query.dto.ts`
* [x] `search.controller.ts`
* [x] `search.module.ts`
* [x] `search.service.ts` (Indexing & Searching)
* [x] dto/search-query.dto.ts
* [x] search.controller.ts
* [x] search.module.ts
* [x] search.service.ts
10. **document-numbering/** (Internal Service)
* [x] `entities/`
* [x] `document-number-format.entity.ts`
* [x] `document-number-counter.entity.ts`
* [x] `document-numbering.module.ts`
* [x] **document-numbering.service.spec.ts**
* [x] `document-numbering.service.ts` (Double-Lock Mechanism)
10. **document-numbering/**
* [x] **entities/**
* [x] document-number-format.entity.ts
* [x] document-number-counter.entity.ts
* [x] document-numbering.module.ts
* [x] document-numbering.service.spec.ts
* [x] document-numbering.service.ts
11. **workflow-engine/** (Unified Logic)
* [x] **dto/**
* [x] `create-workflow-definition.dto.ts`
* [x] `evaluate-workflow.dto.ts`
* [x] `get-available-actions.dto.ts`
* [x] `update-workflow-definition.dto.ts`
* [x] `interfaces/workflow.interface.ts`
* [x] **workflow-dsl.service.ts**
* [x] `workflow-engine.module.ts`
* [x] **workflow-engine.service.spec.ts**
* [x] `workflow-engine.service.ts` (State Machine Logic)
* [x] create-workflow-definition.dto.ts
* [x] evaluate-workflow.dto.ts
* [x] get-available-actions.dto.ts
* [x] update-workflow-definition.dto.ts
* [x] interfaces/workflow.interface.ts
* [x] workflow-dsl.service.ts
* [x] workflow-engine.module.ts
* [x] workflow-engine.service.spec.ts
* [x] workflow-engine.service.ts
12. **json-schema/** (Validation)
* [x] `dto/`
* [x] `create-json-schema.dto.ts`+
* [x] `search-json-schema.dto.ts`
* [x] `update-json-schema.dto.ts`
* [x] `entities/`
* [x] `json-schema.entity.ts`
* [x] **json-schema.controller.spec.ts**
* [x] **json-schema.controller.ts**
* [x] `json-schema.module.ts`
* [x] **json-schema.service.spec.ts**
* [x] `json-schema.service.ts`
* [x] **dto/**
* [x] create-json-schema.dto.ts+
* [x] search-json-schema.dto.ts
* [x] update-json-schema.dto.ts
* [x] **entities/**
* [x] json-schema.entity.ts
* [x] json-schema.controller.spec.ts
* [x] json-schema.controller.ts
* [x] json-schema.module.ts
* [x] json-schema.service.spec.ts
* [x] json-schema.service.ts
13. **monitoring/** (Monitoring & Metrics)
* [x] `controllers/`
* [x] `health.controller.ts`
* [x] `logger/`
* [x] `winston.config.ts`
* [x] `services/`
* [x] `metrics.service.ts`
* [x] `monitoring.module.ts`
* [x] **controllers/**
* [x] health.controller.ts
* [x] **dto/**
* [ ] set-maintenance.dto.ts
* [x] **logger/**
* [x] winston.config.ts
* [x] **services/**
* [x] metrics.service.ts
* [x] monitoring.controller.ts
* [x] monitoring.service.ts
* [x] monitoring.module.ts
## **Folder Structure ของ Backend (NestJS)** ที่
@@ -354,17 +357,18 @@ backend/
│ │ ├── jwt-auth.guard.ts # JWT authentication guard
│ │ ├── jwt-refresh.guard.ts # JWT refresh guard
│ │ ├── maintenance-mode.guard.ts # Maintenance mode guard
│ │ └── rbac.guard.ts # Role-based access control enforcement
│ │ └── rbac.guard.ts # Role-based access control enforcement (ตรวจสอบสิทธิ์ 4 ระดับ)
│ │
│ ├── interceptors/ # 📡 Interceptors for common use cases
│ │ ├── audit-log.interceptor.ts # Automatically logs certain operations
│ │ ├── idempotency.interceptor.ts # Idempotency interceptor
│ │ ├── performance.interceptor.ts #
│ │ └── transform.interceptor.ts # Standardized response formatting
│ │
│ ├─── resilience/ # 🛡️ Circuit-breaker / retry logic (if implemented)
│ │ └── resilience.module.ts # Resilience module
│ │
│ └──── security/ # 🔐 Security service
│ └──── services/ # 🔐 Security service
│ ├── crypto.service.ts # Crypto service
│ └── request-context.service.ts # Request context service (for logging)
@@ -373,7 +377,6 @@ backend/
│ │ ├── dto/
│ │ │ ├── assign-user-role.dto.ts # Assign roles to users
│ │ │ ├── create-user.dto.ts # Create new user
│ │ │ ├── search-user.dto.ts # Search users
│ │ │ ├── update-user.dto.ts # Update user details
│ │ │ └── update-user-preference.dto.ts # Update user preferences
│ │ ├── entities/
@@ -416,7 +419,7 @@ backend/
│ │ ├── entities/
│ │ │ ├── correspondence.entity.ts # Correspondence table definition
│ │ │ ├── correspondence-revision.entity.ts # Correspondence revision entity
│ │ │ ├── correspondence-routing.entity.ts # Correspondence routing entity
│ │ │ ├── correspondence-routing.entity.ts # Correspondence routing entity (Unified Workflow)
│ │ │ ├── correspondence-status.entity.ts # Correspondence status entity
│ │ │ ├── correspondence-type.entity.ts # Correspondence type entity
│ │ │ ├── correspondence-reference.entity.ts # Correspondence reference entity
@@ -505,7 +508,7 @@ backend/
│ │ ├── notification.module.ts # WebSocket + Processor registration
│ │ ├── notification.controller.ts # REST endpoints
│ │ ├── notification.gateway.ts # WebSocket gateway
│ │ ├── notification.processor.ts # Message consumer (e.g. mail worker)
│ │ ├── notification.processor.ts # Message consumer (Consumer/Worker for Email & Line)
│ │ ├── notification.service.ts # Business logic
│ │ └── notification-cleanup.service.ts # Cron-based cleanup job
│ │
@@ -533,11 +536,11 @@ backend/
│ │ ├── entities/
│ │ │ └── workflow-definition.entity.ts # Workflow definition entity
│ │ ├── interfaces/
│ │ │ └── workflow.interface.ts # Workflow interface
│ │ ├── workflow-engine.controller.ts # REST endpoints
│ │ ├── workflow-engine.module.ts # Module DI container
│ │ ├── workflow-engine.service.ts # Business logic
│ │ └── workflow-engine.service.spec.ts # Unit tests
│ │ │ └── workflow.interface.ts # Workflow interface
│ │ ├── workflow-engine.controller.ts # REST endpoints
│ │ ├── workflow-engine.module.ts # Module DI container
│ │ ├── workflow-engine.service.ts # State Machine Logic
│ │ └── workflow-engine.service.spec.ts # Unit tests
│ │
│ ├── json-schema/ # 📋 Dynamic request schema validation
│ │ ├── dto/
@@ -554,15 +557,17 @@ backend/
│ │
│ └── monitoring/ # 📋 Dynamic request schema validation
│ ├── controllers/
│ │ ── health.controller.ts # Create new JSON schema
│ ├── update-json-schema.dto.ts # Update JSON schema
│ │ └── search-json-schema.dto.ts # Search JSON schema
│ │ ── health.controller.ts # Create new JSON schema
├── dto/
│ │ └── set-maintenance.dto.ts # Create new JSON schema
│ ├── logger/
│ │ └── winston.config.ts # JSON schema entity
│ │ └── winston.config.ts # JSON schema entity
│ ├── services/
│ │ └── metrics.service.ts # JSON schema entity
── monitoring.module.ts # Module DI container
│ │ └── metrics.service.ts # JSON schema entity
── monitoring.controller.ts # REST endpoints
├── monitoring.service.ts # Business logic
│ └── monitoring.module.ts # Module DI container
```
---
---

View File

@@ -22,6 +22,7 @@
"dependencies": {
"@casl/ability": "^6.7.3",
"@elastic/elasticsearch": "^8.11.1",
"@nestjs-modules/ioredis": "^2.0.2",
"@nestjs/axios": "^4.0.1",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/cache-manager": "^3.0.1",

470
backend/pnpm-lock.yaml generated
View File

@@ -14,6 +14,12 @@ importers:
'@elastic/elasticsearch':
specifier: ^8.11.1
version: 8.19.1
'@nestjs-modules/ioredis':
specifier: ^2.0.2
version: 2.0.2(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(ioredis@5.8.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
'@nestjs/axios':
specifier: ^4.0.1
version: 4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)
'@nestjs/bullmq':
specifier: ^11.0.4
version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.63.2)
@@ -53,6 +59,9 @@ importers:
'@nestjs/swagger':
specifier: ^11.2.3
version: 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
'@nestjs/terminus':
specifier: ^11.0.0
version: 11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
'@nestjs/throttler':
specifier: ^6.4.0
version: 6.4.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)
@@ -71,6 +80,9 @@ importers:
ajv-formats:
specifier: ^3.0.1
version: 3.0.1(ajv@8.17.1)
async-retry:
specifier: ^1.3.3
version: 1.3.3
axios:
specifier: ^1.13.2
version: 1.13.2
@@ -110,15 +122,24 @@ importers:
mysql2:
specifier: ^3.15.3
version: 3.15.3
nest-winston:
specifier: ^1.10.2
version: 1.10.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.18.3)
nodemailer:
specifier: ^7.0.10
version: 7.0.10
opossum:
specifier: ^9.0.0
version: 9.0.0
passport:
specifier: ^0.7.0
version: 0.7.0
passport-jwt:
specifier: ^4.0.1
version: 4.0.1
prom-client:
specifier: ^15.1.3
version: 15.1.3
redlock:
specifier: 5.0.0-beta.2
version: 5.0.0-beta.2
@@ -140,6 +161,9 @@ importers:
uuid:
specifier: ^13.0.0
version: 13.0.0
winston:
specifier: ^3.18.3
version: 3.18.3
devDependencies:
'@eslint/eslintrc':
specifier: ^3.2.0
@@ -156,6 +180,9 @@ importers:
'@nestjs/testing':
specifier: ^11.0.1
version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)
'@types/async-retry':
specifier: ^1.4.9
version: 1.4.9
'@types/bcrypt':
specifier: ^6.0.0
version: 6.0.0
@@ -180,6 +207,9 @@ importers:
'@types/node':
specifier: ^22.10.7
version: 22.19.1
'@types/opossum':
specifier: ^8.1.9
version: 8.1.9
'@types/passport-jwt':
specifier: ^4.0.1
version: 4.0.1
@@ -572,10 +602,17 @@ packages:
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
engines: {node: '>=0.1.90'}
'@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
'@elastic/elasticsearch@8.19.1':
resolution: {integrity: sha512-+1j9NnQVOX+lbWB8LhCM7IkUmjU05Y4+BmSLfusq0msCsQb1Va+OUKFCoOXjCJqQrcgdRdQCjYYyolQ/npQALQ==}
engines: {node: '>=18'}
@@ -980,6 +1017,20 @@ packages:
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
'@nestjs-modules/ioredis@2.0.2':
resolution: {integrity: sha512-8pzSvT8R3XP6p8ZzQmEN8OnY0yWrJ/elFhwQK+PID2zf1SLBkAZ18bDcx3SKQ2atledt0gd9kBeP5xT4MlyS7Q==}
peerDependencies:
'@nestjs/common': '>=6.7.0'
'@nestjs/core': '>=6.7.0'
ioredis: '>=5.0.0'
'@nestjs/axios@4.0.1':
resolution: {integrity: sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
axios: ^1.3.1
rxjs: ^7.0.0
'@nestjs/bull-shared@11.0.4':
resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==}
peerDependencies:
@@ -1124,6 +1175,102 @@ packages:
class-validator:
optional: true
'@nestjs/terminus@10.2.0':
resolution: {integrity: sha512-zPs98xvJ4ogEimRQOz8eU90mb7z+W/kd/mL4peOgrJ/VqER+ibN2Cboj65uJZW3XuNhpOqaeYOJte86InJd44A==}
peerDependencies:
'@grpc/grpc-js': '*'
'@grpc/proto-loader': '*'
'@mikro-orm/core': '*'
'@mikro-orm/nestjs': '*'
'@nestjs/axios': ^1.0.0 || ^2.0.0 || ^3.0.0
'@nestjs/common': ^9.0.0 || ^10.0.0
'@nestjs/core': ^9.0.0 || ^10.0.0
'@nestjs/microservices': ^9.0.0 || ^10.0.0
'@nestjs/mongoose': ^9.0.0 || ^10.0.0
'@nestjs/sequelize': ^9.0.0 || ^10.0.0
'@nestjs/typeorm': ^9.0.0 || ^10.0.0
'@prisma/client': '*'
mongoose: '*'
reflect-metadata: 0.1.x
rxjs: 7.x
sequelize: '*'
typeorm: '*'
peerDependenciesMeta:
'@grpc/grpc-js':
optional: true
'@grpc/proto-loader':
optional: true
'@mikro-orm/core':
optional: true
'@mikro-orm/nestjs':
optional: true
'@nestjs/axios':
optional: true
'@nestjs/microservices':
optional: true
'@nestjs/mongoose':
optional: true
'@nestjs/sequelize':
optional: true
'@nestjs/typeorm':
optional: true
'@prisma/client':
optional: true
mongoose:
optional: true
sequelize:
optional: true
typeorm:
optional: true
'@nestjs/terminus@11.0.0':
resolution: {integrity: sha512-c55LOo9YGovmQHtFUMa/vDaxGZ2cglMTZejqgHREaApt/GArTfgYYGwhRXPLq8ZwiQQlLuYB+79e9iA8mlDSLA==}
peerDependencies:
'@grpc/grpc-js': '*'
'@grpc/proto-loader': '*'
'@mikro-orm/core': '*'
'@mikro-orm/nestjs': '*'
'@nestjs/axios': ^2.0.0 || ^3.0.0 || ^4.0.0
'@nestjs/common': ^10.0.0 || ^11.0.0
'@nestjs/core': ^10.0.0 || ^11.0.0
'@nestjs/microservices': ^10.0.0 || ^11.0.0
'@nestjs/mongoose': ^11.0.0
'@nestjs/sequelize': ^10.0.0 || ^11.0.0
'@nestjs/typeorm': ^10.0.0 || ^11.0.0
'@prisma/client': '*'
mongoose: '*'
reflect-metadata: 0.1.x || 0.2.x
rxjs: 7.x
sequelize: '*'
typeorm: '*'
peerDependenciesMeta:
'@grpc/grpc-js':
optional: true
'@grpc/proto-loader':
optional: true
'@mikro-orm/core':
optional: true
'@mikro-orm/nestjs':
optional: true
'@nestjs/axios':
optional: true
'@nestjs/microservices':
optional: true
'@nestjs/mongoose':
optional: true
'@nestjs/sequelize':
optional: true
'@nestjs/typeorm':
optional: true
'@prisma/client':
optional: true
mongoose:
optional: true
sequelize:
optional: true
typeorm:
optional: true
'@nestjs/testing@11.1.9':
resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==}
peerDependencies:
@@ -1424,6 +1571,9 @@ packages:
resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
engines: {node: '>=18.0.0'}
'@so-ric/colorspace@1.1.6':
resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
@@ -1458,6 +1608,9 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/async-retry@1.4.9':
resolution: {integrity: sha512-s1ciZQJzRh3708X/m3vPExr5KJlzlZJvXsKpbtE2luqNcbROr64qU+3KpJsYHqWMeaxI839OvXf9PrUSw1Xtyg==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -1565,6 +1718,9 @@ packages:
'@types/nodemailer@7.0.4':
resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==}
'@types/opossum@8.1.9':
resolution: {integrity: sha512-Jm/tYxuJFefiwRYs+/EOsUP3ktk0c8siMgAHPLnA4PXF4wKghzcjqf88dY+Xii5jId5Txw4JV0FMKTpjbd7KJA==}
'@types/passport-jwt@4.0.1':
resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==}
@@ -1580,6 +1736,9 @@ packages:
'@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/retry@0.12.5':
resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==}
'@types/send@0.17.6':
resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==}
@@ -1598,6 +1757,9 @@ packages:
'@types/supertest@6.0.3':
resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
'@types/triple-beam@1.3.5':
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
'@types/uuid@11.0.0':
resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==}
deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.
@@ -1891,6 +2053,9 @@ packages:
ajv@8.17.1:
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@@ -1961,6 +2126,12 @@ packages:
asap@2.0.6:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
async-retry@1.3.3:
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -2018,6 +2189,9 @@ packages:
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
engines: {node: '>= 18'}
bintrees@1.0.2:
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@@ -2028,6 +2202,10 @@ packages:
bowser@2.12.1:
resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==}
boxen@5.1.2:
resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==}
engines: {node: '>=10'}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -2127,6 +2305,10 @@ packages:
chardet@2.1.1:
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
check-disk-space@3.4.0:
resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==}
engines: {node: '>=16'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -2148,6 +2330,10 @@ packages:
class-validator@0.14.2:
resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==}
cli-boxes@2.2.1:
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
engines: {node: '>=6'}
cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
@@ -2187,9 +2373,25 @@ packages:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-convert@3.1.3:
resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==}
engines: {node: '>=14.6'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-name@2.1.0:
resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==}
engines: {node: '>=12.20'}
color-string@2.1.4:
resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==}
engines: {node: '>=18'}
color@5.0.3:
resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==}
engines: {node: '>=18'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@@ -2393,6 +2595,9 @@ packages:
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
@@ -2580,6 +2785,9 @@ packages:
fb-watchman@2.0.2:
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -2626,6 +2834,9 @@ packages:
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
@@ -3142,6 +3353,9 @@ packages:
keyv@5.5.4:
resolution: {integrity: sha512-eohl3hKTiVyD1ilYdw9T0OiB4hnjef89e3dMYKz+mVKDzj+5IteTseASUsOB+EU9Tf6VNTCjDePcP6wkDGmLKQ==}
kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'}
@@ -3218,6 +3432,10 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
logform@2.7.0:
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
engines: {node: '>= 12.0.0'}
long@5.3.2:
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
@@ -3383,6 +3601,12 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
nest-winston@1.10.2:
resolution: {integrity: sha512-Z9IzL/nekBOF/TEwBHUJDiDPMaXUcFquUQOFavIRet6xF0EbuWnOzslyN/ksgzG+fITNgXhMdrL/POp9SdaFxA==}
peerDependencies:
'@nestjs/common': ^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
winston: ^3.0.0
node-abort-controller@3.1.1:
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
@@ -3438,10 +3662,17 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
onetime@5.1.2:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
opossum@9.0.0:
resolution: {integrity: sha512-K76U0QkxOfUZamneQuzz+AP0fyfTJcCplZ2oZL93nxeupuJbN4s6uFNbmVCt4eWqqGqRnnowdFuBicJ1fLMVxw==}
engines: {node: ^24 || ^22 || ^20}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -3574,6 +3805,10 @@ packages:
resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
prom-client@15.1.3:
resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==}
engines: {node: ^16 || ^18 || >=20}
promise-coalesce@1.5.0:
resolution: {integrity: sha512-cTJ30U+ur1LD7pMPyQxiKIwxjtAjLsyU7ivRhVWZrX9BNIXtf78pc37vSMc8Vikx7DVzEKNk2SEJ5KWUpSG2ig==}
engines: {node: '>=16'}
@@ -3663,6 +3898,10 @@ packages:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -3683,6 +3922,10 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -3807,6 +4050,9 @@ packages:
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
engines: {node: '>= 0.6'}
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
stack-utils@2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
@@ -3913,6 +4159,9 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
tdigest@0.1.2:
resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==}
terser-webpack-plugin@5.3.14:
resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==}
engines: {node: '>= 10.13.0'}
@@ -3938,6 +4187,9 @@ packages:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
text-hex@1.0.0:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
tmpl@1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
@@ -3957,6 +4209,10 @@ packages:
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
engines: {node: '>=14.16'}
triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'}
@@ -4030,6 +4286,10 @@ packages:
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
engines: {node: '>=4'}
type-fest@0.20.2:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
@@ -4235,6 +4495,18 @@ packages:
engines: {node: '>= 8'}
hasBin: true
widest-line@3.1.0:
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
engines: {node: '>=8'}
winston-transport@4.9.0:
resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==}
engines: {node: '>= 12.0.0'}
winston@3.18.3:
resolution: {integrity: sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==}
engines: {node: '>= 12.0.0'}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -4968,10 +5240,18 @@ snapshots:
'@colors/colors@1.5.0':
optional: true
'@colors/colors@1.6.0': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@dabh/diagnostics@2.0.8':
dependencies:
'@so-ric/colorspace': 1.1.6
enabled: 2.0.0
kuler: 2.0.0
'@elastic/elasticsearch@8.19.1':
dependencies:
'@elastic/transport': 8.10.0
@@ -5489,6 +5769,36 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@nestjs-modules/ioredis@2.0.2(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(ioredis@5.8.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
ioredis: 5.8.2
optionalDependencies:
'@nestjs/terminus': 10.2.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
transitivePeerDependencies:
- '@grpc/grpc-js'
- '@grpc/proto-loader'
- '@mikro-orm/core'
- '@mikro-orm/nestjs'
- '@nestjs/axios'
- '@nestjs/microservices'
- '@nestjs/mongoose'
- '@nestjs/sequelize'
- '@nestjs/typeorm'
- '@prisma/client'
- mongoose
- reflect-metadata
- rxjs
- sequelize
- typeorm
'@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
axios: 1.13.2
rxjs: 7.8.2
'@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -5656,6 +5966,33 @@ snapshots:
class-transformer: 0.5.1
class-validator: 0.14.2
'@nestjs/terminus@10.2.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
boxen: 5.1.2
check-disk-space: 3.4.0
reflect-metadata: 0.2.2
rxjs: 7.8.2
optionalDependencies:
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)
'@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
typeorm: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
optional: true
'@nestjs/terminus@11.0.0(@nestjs/axios@4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2))(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/typeorm@11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))))(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
boxen: 5.1.2
check-disk-space: 3.4.0
reflect-metadata: 0.2.2
rxjs: 7.8.2
optionalDependencies:
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.13.2)(rxjs@7.8.2)
'@nestjs/typeorm': 11.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))
typeorm: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
'@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)':
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -6038,6 +6375,11 @@ snapshots:
dependencies:
tslib: 2.8.1
'@so-ric/colorspace@1.1.6':
dependencies:
color: 5.0.3
text-hex: 1.0.0
'@socket.io/component-emitter@3.1.2': {}
'@sqltools/formatter@1.2.5': {}
@@ -6071,6 +6413,10 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/async-retry@1.4.9':
dependencies:
'@types/retry': 0.12.5
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.28.5
@@ -6210,6 +6556,10 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@types/opossum@8.1.9':
dependencies:
'@types/node': 22.19.1
'@types/passport-jwt@4.0.1':
dependencies:
'@types/jsonwebtoken': 9.0.10
@@ -6228,6 +6578,8 @@ snapshots:
'@types/range-parser@1.2.7': {}
'@types/retry@0.12.5': {}
'@types/send@0.17.6':
dependencies:
'@types/mime': 1.3.5
@@ -6257,6 +6609,8 @@ snapshots:
'@types/methods': 1.1.4
'@types/superagent': 8.1.9
'@types/triple-beam@1.3.5': {}
'@types/uuid@11.0.0':
dependencies:
uuid: 13.0.0
@@ -6574,6 +6928,10 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
ansi-align@3.0.1:
dependencies:
string-width: 4.2.3
ansi-colors@4.1.3: {}
ansi-escapes@4.3.2:
@@ -6633,6 +6991,12 @@ snapshots:
asap@2.0.6: {}
async-retry@1.3.3:
dependencies:
retry: 0.13.1
async@3.2.6: {}
asynckit@0.4.0: {}
available-typed-arrays@1.0.7:
@@ -6714,6 +7078,8 @@ snapshots:
node-addon-api: 8.5.0
node-gyp-build: 4.8.4
bintrees@1.0.2: {}
bl@4.1.0:
dependencies:
buffer: 5.7.1
@@ -6736,6 +7102,17 @@ snapshots:
bowser@2.12.1: {}
boxen@5.1.2:
dependencies:
ansi-align: 3.0.1
camelcase: 6.3.0
chalk: 4.1.2
cli-boxes: 2.2.1
string-width: 4.2.3
type-fest: 0.20.2
widest-line: 3.1.0
wrap-ansi: 7.0.0
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
@@ -6858,6 +7235,8 @@ snapshots:
chardet@2.1.1: {}
check-disk-space@3.4.0: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -6876,6 +7255,8 @@ snapshots:
libphonenumber-js: 1.12.27
validator: 13.15.23
cli-boxes@2.2.1: {}
cli-cursor@3.1.0:
dependencies:
restore-cursor: 3.1.0
@@ -6908,8 +7289,23 @@ snapshots:
dependencies:
color-name: 1.1.4
color-convert@3.1.3:
dependencies:
color-name: 2.1.0
color-name@1.1.4: {}
color-name@2.1.0: {}
color-string@2.1.4:
dependencies:
color-name: 2.1.0
color@5.0.3:
dependencies:
color-convert: 3.1.3
color-string: 2.1.4
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
@@ -7070,6 +7466,8 @@ snapshots:
emoji-regex@9.2.2: {}
enabled@2.0.0: {}
encodeurl@2.0.0: {}
engine.io-parser@5.2.3: {}
@@ -7306,6 +7704,8 @@ snapshots:
dependencies:
bser: 2.1.1
fecha@4.2.3: {}
fflate@0.8.2: {}
file-entry-cache@8.0.0:
@@ -7357,6 +7757,8 @@ snapshots:
flatted@3.3.3: {}
fn.name@1.1.0: {}
follow-redirects@1.15.11: {}
for-each@0.3.5:
@@ -8071,6 +8473,8 @@ snapshots:
dependencies:
'@keyv/serialize': 1.1.1
kuler@2.0.0: {}
leven@3.1.0: {}
levn@0.4.1:
@@ -8127,6 +8531,15 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
logform@2.7.0:
dependencies:
'@colors/colors': 1.6.0
'@types/triple-beam': 1.3.5
fecha: 4.2.3
ms: 2.1.3
safe-stable-stringify: 2.5.0
triple-beam: 1.4.1
long@5.3.2: {}
lru-cache@10.4.3: {}
@@ -8272,6 +8685,12 @@ snapshots:
neo-async@2.6.2: {}
nest-winston@1.10.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(winston@3.18.3):
dependencies:
'@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
fast-safe-stringify: 2.1.1
winston: 3.18.3
node-abort-controller@3.1.1: {}
node-addon-api@8.5.0: {}
@@ -8313,10 +8732,16 @@ snapshots:
dependencies:
wrappy: 1.0.2
one-time@1.0.0:
dependencies:
fn.name: 1.1.0
onetime@5.1.2:
dependencies:
mimic-fn: 2.1.0
opossum@9.0.0: {}
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -8438,6 +8863,11 @@ snapshots:
ansi-styles: 5.2.0
react-is: 18.3.1
prom-client@15.1.3:
dependencies:
'@opentelemetry/api': 1.9.0
tdigest: 0.1.2
promise-coalesce@1.5.0: {}
proxy-addr@2.0.7:
@@ -8518,6 +8948,8 @@ snapshots:
onetime: 5.1.2
signal-exit: 3.0.7
retry@0.13.1: {}
reusify@1.1.0: {}
router@2.2.0:
@@ -8544,6 +8976,8 @@ snapshots:
safe-buffer@5.2.1: {}
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {}
schema-utils@3.3.0:
@@ -8705,6 +9139,8 @@ snapshots:
sqlstring@2.3.3: {}
stack-trace@0.0.10: {}
stack-utils@2.0.6:
dependencies:
escape-string-regexp: 2.0.0
@@ -8811,6 +9247,10 @@ snapshots:
tapable@2.3.0: {}
tdigest@0.1.2:
dependencies:
bintrees: 1.0.2
terser-webpack-plugin@5.3.14(webpack@5.100.2):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
@@ -8833,6 +9273,8 @@ snapshots:
glob: 7.2.3
minimatch: 3.1.2
text-hex@1.0.0: {}
tmpl@1.0.5: {}
to-buffer@1.2.2:
@@ -8853,6 +9295,8 @@ snapshots:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
triple-beam@1.4.1: {}
ts-api-utils@2.1.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
@@ -8926,6 +9370,8 @@ snapshots:
type-detect@4.0.8: {}
type-fest@0.20.2: {}
type-fest@0.21.3: {}
type-fest@4.41.0: {}
@@ -9126,6 +9572,30 @@ snapshots:
dependencies:
isexe: 2.0.0
widest-line@3.1.0:
dependencies:
string-width: 4.2.3
winston-transport@4.9.0:
dependencies:
logform: 2.7.0
readable-stream: 3.6.2
triple-beam: 1.4.1
winston@3.18.3:
dependencies:
'@colors/colors': 1.6.0
'@dabh/diagnostics': 2.0.8
async: 3.2.6
is-stream: 2.0.1
logform: 2.7.0
one-time: 1.0.0
readable-stream: 3.6.2
safe-stable-stringify: 2.5.0
stack-trace: 0.0.10
triple-beam: 1.4.1
winston-transport: 4.9.0
word-wrap@1.2.5: {}
wordwrap@1.0.0: {}

View File

@@ -42,6 +42,7 @@ import { MonitoringModule } from './modules/monitoring/monitoring.module';
import { ResilienceModule } from './common/resilience/resilience.module'; // ✅ Import
// ... imports
import { SearchModule } from './modules/search/search.module'; // ✅ Import
import { RedisModule } from '@nestjs-modules/ioredis'; // [NEW]
@Module({
imports: [
// 1. Setup Config Module พร้อม Validation
@@ -113,7 +114,18 @@ import { SearchModule } from './modules/search/search.module'; // ✅ Import
},
}),
}),
// [NEW] Setup Redis Module (สำหรับ InjectRedis)
RedisModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'single',
url: `redis://${configService.get('REDIS_HOST')}:${configService.get('REDIS_PORT')}`,
options: {
password: configService.get('REDIS_PASSWORD'),
},
}),
inject: [ConfigService],
}),
// 📊 Register Monitoring Module (Health & Metrics) [Req 6.10]
MonitoringModule,

View File

@@ -6,6 +6,7 @@ import {
CreateDateColumn,
ManyToOne,
JoinColumn,
PrimaryColumn, // ✅ [Fix] เพิ่ม Import นี้
} from 'typeorm';
import { User } from '../../modules/user/entities/user.entity';
@@ -46,7 +47,9 @@ export class AuditLog {
@Column({ name: 'user_agent', length: 255, nullable: true })
userAgent?: string;
// ✅ [Fix] รวม Decorator ไว้ที่นี่ที่เดียว
@CreateDateColumn({ name: 'created_at' })
@PrimaryColumn() // เพื่อบอกว่าเป็น Composite PK คู่กับ auditId
createdAt!: Date;
// Relations

View File

@@ -0,0 +1,75 @@
-- ============================================================
-- Database Partitioning Script for LCBP3-DMS (Fixed #1075)
-- Target Tables: audit_logs, notifications
-- Strategy: Range Partitioning by YEAR(created_at)
-- ============================================================
-- ------------------------------------------------------------
-- 1. Audit Logs Partitioning
-- ------------------------------------------------------------
-- Step 1: เอา AUTO_INCREMENT ออกก่อน (เพื่อไม่ให้ติด Error 1075 ตอนลบ PK)
ALTER TABLE audit_logs
MODIFY audit_id BIGINT NOT NULL;
-- Step 2: ลบ Primary Key เดิม
ALTER TABLE audit_logs DROP PRIMARY KEY;
-- Step 3: สร้าง Primary Key ใหม่ (รวม created_at เพื่อทำ Partition)
ALTER TABLE audit_logs
ADD PRIMARY KEY (audit_id, created_at);
-- Step 4: ใส่ AUTO_INCREMENT กลับเข้าไป
ALTER TABLE audit_logs
MODIFY audit_id BIGINT NOT NULL AUTO_INCREMENT;
-- Step 5: สร้าง Partition
ALTER TABLE audit_logs PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p_old
VALUES LESS THAN (2024),
PARTITION p2024
VALUES LESS THAN (2025),
PARTITION p2025
VALUES LESS THAN (2026),
PARTITION p2026
VALUES LESS THAN (2027),
PARTITION p2027
VALUES LESS THAN (2028),
PARTITION p2028
VALUES LESS THAN (2029),
PARTITION p2029
VALUES LESS THAN (2030),
PARTITION p2030
VALUES LESS THAN (2031),
PARTITION p_future
VALUES LESS THAN MAXVALUE
);
-- ------------------------------------------------------------
-- 2. Notifications Partitioning
-- ------------------------------------------------------------
-- Step 1: เอา AUTO_INCREMENT ออกก่อน
ALTER TABLE notifications
MODIFY id INT NOT NULL;
-- Step 2: ลบ Primary Key เดิม
ALTER TABLE notifications DROP PRIMARY KEY;
-- Step 3: สร้าง Primary Key ใหม่
ALTER TABLE notifications
ADD PRIMARY KEY (id, created_at);
-- Step 4: ใส่ AUTO_INCREMENT กลับเข้าไป
ALTER TABLE notifications
MODIFY id INT NOT NULL AUTO_INCREMENT;
-- Step 5: สร้าง Partition
ALTER TABLE notifications PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p_old
VALUES LESS THAN (2024),
PARTITION p2024
VALUES LESS THAN (2025),
PARTITION p2025
VALUES LESS THAN (2026),
PARTITION p2026
VALUES LESS THAN (2027),
PARTITION p2027
VALUES LESS THAN (2028),
PARTITION p2028
VALUES LESS THAN (2029),
PARTITION p2029
VALUES LESS THAN (2030),
PARTITION p2030
VALUES LESS THAN (2031),
PARTITION p_future
VALUES LESS THAN MAXVALUE
);

View File

@@ -0,0 +1,82 @@
// src/database/seeds/workflow-definitions.seed.ts
import { DataSource } from 'typeorm';
import { WorkflowDefinition } from '../../modules/workflow-engine/entities/workflow-definition.entity';
import { WorkflowDslService } from '../../modules/workflow-engine/workflow-dsl.service';
export const seedWorkflowDefinitions = async (dataSource: DataSource) => {
const repo = dataSource.getRepository(WorkflowDefinition);
const dslService = new WorkflowDslService();
// 1. RFA Workflow (Standard)
const rfaDsl = {
workflow: 'RFA',
version: 1,
states: [
{
name: 'DRAFT',
initial: true,
on: { SUBMIT: { to: 'IN_REVIEW', requirements: [{ role: 'Editor' }] } },
},
{
name: 'IN_REVIEW',
on: {
APPROVE: {
to: 'APPROVED',
requirements: [{ role: 'Contract Admin' }],
},
REJECT: {
to: 'REJECTED',
requirements: [{ role: 'Contract Admin' }],
},
COMMENT: { to: 'DRAFT', requirements: [{ role: 'Contract Admin' }] }, // ส่งกลับแก้ไข
},
},
{ name: 'APPROVED', terminal: true },
{ name: 'REJECTED', terminal: true },
],
};
// 2. Circulation Workflow
const circulationDsl = {
workflow: 'CIRCULATION',
version: 1,
states: [
{
name: 'OPEN',
initial: true,
on: { SEND: { to: 'IN_REVIEW' } },
},
{
name: 'IN_REVIEW',
on: {
COMPLETE: { to: 'COMPLETED' }, // เมื่อทุกคนตอบครบ
CANCEL: { to: 'CANCELLED' },
},
},
{ name: 'COMPLETED', terminal: true },
{ name: 'CANCELLED', terminal: true },
],
};
const workflows = [rfaDsl, circulationDsl];
for (const dsl of workflows) {
const exists = await repo.findOne({
where: { workflow_code: dsl.workflow, version: dsl.version },
});
if (!exists) {
const compiled = dslService.compile(dsl);
await repo.save(
repo.create({
workflow_code: dsl.workflow,
version: dsl.version,
dsl: dsl,
compiled: compiled,
is_active: true,
}),
);
console.log(`✅ Seeded Workflow: ${dsl.workflow} v${dsl.version}`);
}
}
};

View File

@@ -1,5 +1,3 @@
// File: src/modules/master/dto/create-tag.dto.ts
import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
@@ -7,12 +5,9 @@ export class CreateTagDto {
@ApiProperty({ example: 'URGENT', description: 'ชื่อ Tag' })
@IsString()
@IsNotEmpty()
tag_name: string;
tag_name!: string; // เพิ่ม !
@ApiProperty({
example: 'เอกสารด่วนต้องดำเนินการทันที',
description: 'คำอธิบาย',
})
@ApiProperty({ example: 'คำอธิบาย', description: 'คำอธิบาย' })
@IsString()
@IsOptional()
description?: string;

View File

@@ -1,5 +1,3 @@
// File: src/modules/master/entities/tag.entity.ts
import {
Entity,
Column,
@@ -11,17 +9,17 @@ import {
@Entity('tags')
export class Tag {
@PrimaryGeneratedColumn()
id: number;
id!: number; // เพิ่ม !
@Column({ length: 100, unique: true, comment: 'ชื่อ Tag' })
tag_name: string;
@Column({ length: 100, unique: true })
tag_name!: string; // เพิ่ม !
@Column({ type: 'text', nullable: true, comment: 'คำอธิบายแท็ก' })
description: string;
@Column({ type: 'text', nullable: true })
description!: string; // เพิ่ม !
@CreateDateColumn()
created_at: Date;
created_at!: Date; // เพิ่ม !
@UpdateDateColumn()
updated_at: Date;
updated_at!: Date; // เพิ่ม !
}

View File

@@ -49,15 +49,15 @@ export class MasterService {
async findAllCorrespondenceTypes() {
return this.corrTypeRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
async findAllCorrespondenceStatuses() {
return this.corrStatusRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
@@ -67,22 +67,22 @@ export class MasterService {
async findAllRfaTypes() {
return this.rfaTypeRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
async findAllRfaStatuses() {
return this.rfaStatusRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
async findAllRfaApproveCodes() {
return this.rfaApproveRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
@@ -92,8 +92,8 @@ export class MasterService {
async findAllCirculationStatuses() {
return this.circulationStatusRepo.find({
where: { is_active: true },
order: { sort_order: 'ASC' },
where: { isActive: true }, // ✅ แก้เป็น camelCase
order: { sortOrder: 'ASC' }, // ✅ แก้เป็น camelCase
});
}
@@ -101,9 +101,6 @@ export class MasterService {
// 🏷️ Tag Management (CRUD)
// =================================================================
/**
* ค้นหา Tag ทั้งหมด พร้อมรองรับการ Search และ Pagination
*/
async findAllTags(query?: SearchTagDto) {
const qb = this.tagRepo.createQueryBuilder('tag');
@@ -115,14 +112,12 @@ export class MasterService {
qb.orderBy('tag.tag_name', 'ASC');
// Pagination Logic
if (query?.page && query?.limit) {
const page = query.page;
const limit = query.limit;
qb.skip((page - 1) * limit).take(limit);
}
// ถ้ามีการแบ่งหน้า ให้ส่งคืนทั้งข้อมูลและจำนวนทั้งหมด (count)
if (query?.page && query?.limit) {
const [items, total] = await qb.getManyAndCount();
return {
@@ -153,7 +148,7 @@ export class MasterService {
}
async updateTag(id: number, dto: UpdateTagDto) {
const tag = await this.findOneTag(id); // Reuse findOne for check
const tag = await this.findOneTag(id);
Object.assign(tag, dto);
return this.tagRepo.save(tag);
}

View File

@@ -0,0 +1,16 @@
import { IsBoolean, IsOptional, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class SetMaintenanceDto {
@ApiProperty({ description: 'สถานะ Maintenance (true = เปิด, false = ปิด)' })
@IsBoolean()
enabled!: boolean; // ✅ เพิ่ม ! ตรงนี้
@ApiProperty({
description: 'เหตุผลที่ปิดปรับปรุง (แสดงให้ User เห็น)',
required: false,
})
@IsOptional()
@IsString()
reason?: string; // Optional (?) ไม่ต้องใส่ !
}

View File

@@ -0,0 +1,30 @@
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { MonitoringService } from './monitoring.service';
import { SetMaintenanceDto } from './dto/set-maintenance.dto';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { BypassMaintenance } from '../../common/decorators/bypass-maintenance.decorator';
@ApiTags('System Monitoring')
@Controller('monitoring')
export class MonitoringController {
constructor(private readonly monitoringService: MonitoringService) {}
@Get('maintenance')
@ApiOperation({ summary: 'Check maintenance status (Public)' })
@BypassMaintenance() // API นี้ต้องเรียกได้แม้ระบบปิดอยู่
getMaintenanceStatus() {
return this.monitoringService.getMaintenanceStatus();
}
@Post('maintenance')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@RequirePermission('system.manage_all') // เฉพาะ Superadmin เท่านั้น
@BypassMaintenance() // Admin ต้องยิงเปิด/ปิดได้แม้ระบบจะปิดอยู่
@ApiOperation({ summary: 'Toggle Maintenance Mode (Admin Only)' })
setMaintenanceMode(@Body() dto: SetMaintenanceDto) {
return this.monitoringService.setMaintenanceMode(dto);
}
}

View File

@@ -1,23 +1,34 @@
// File: src/modules/monitoring/monitoring.module.ts
import { Global, Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HttpModule } from '@nestjs/axios';
import { APP_INTERCEPTOR } from '@nestjs/core';
// Existing Components
import { HealthController } from './controllers/health.controller';
import { MetricsService } from './services/metrics.service';
import { PerformanceInterceptor } from '../../common/interceptors/performance.interceptor';
@Global() // ทำให้ Module นี้ใช้งานได้ทั่วทั้ง App โดยไม่ต้อง Import ซ้ำ
// [NEW] Maintenance Mode Components
import { MonitoringController } from './monitoring.controller';
import { MonitoringService } from './monitoring.service';
@Global() // Module นี้เป็น Global (ดีแล้วครับ)
@Module({
imports: [TerminusModule, HttpModule],
controllers: [HealthController],
controllers: [
HealthController, // ✅ ของเดิม: /health
MonitoringController, // ✅ ของใหม่: /monitoring/maintenance
],
providers: [
MetricsService,
MetricsService, // ✅ ของเดิม
MonitoringService, // ✅ ของใหม่ (Logic เปิด/ปิด Maintenance)
{
provide: APP_INTERCEPTOR, // Register Global Interceptor
useClass: PerformanceInterceptor,
provide: APP_INTERCEPTOR,
useClass: PerformanceInterceptor, // ✅ ของเดิม (จับเวลา Response Time)
},
],
exports: [MetricsService],
exports: [MetricsService, MonitoringService],
})
export class MonitoringModule {}

View File

@@ -0,0 +1,44 @@
// File: src/modules/monitoring/monitoring.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
import { SetMaintenanceDto } from './dto/set-maintenance.dto';
@Injectable()
export class MonitoringService {
private readonly logger = new Logger(MonitoringService.name);
private readonly MAINTENANCE_KEY = 'system:maintenance_mode';
constructor(@InjectRedis() private readonly redis: Redis) {}
/**
* ตรวจสอบสถานะปัจจุบัน
*/
async getMaintenanceStatus() {
const status = await this.redis.get(this.MAINTENANCE_KEY);
return {
isEnabled: status === 'true',
message:
status === 'true' ? 'System is under maintenance' : 'System is normal',
};
}
/**
* ตั้งค่า Maintenance Mode
*/
async setMaintenanceMode(dto: SetMaintenanceDto) {
if (dto.enabled) {
await this.redis.set(this.MAINTENANCE_KEY, 'true');
// เก็บเหตุผลไว้ใน Key อื่นก็ได้ถ้าต้องการ แต่เบื้องต้น Guard เช็คแค่ Key นี้
this.logger.warn(
`⚠️ SYSTEM ENTERED MAINTENANCE MODE: ${dto.reason || 'No reason provided'}`,
);
} else {
await this.redis.del(this.MAINTENANCE_KEY);
this.logger.log('✅ System exited maintenance mode');
}
return this.getMaintenanceStatus();
}
}

View File

@@ -5,6 +5,7 @@ import {
CreateDateColumn,
ManyToOne,
JoinColumn,
PrimaryColumn, // ✅ [Fix] เพิ่ม Import นี้
} from 'typeorm';
import { User } from '../../user/entities/user.entity';
@@ -44,7 +45,9 @@ export class Notification {
@Column({ name: 'entity_id', nullable: true })
entityId?: number;
// ✅ [Fix] รวม Decorator ไว้ที่นี่ที่เดียว (เป็นทั้ง CreateDate และ PrimaryColumn สำหรับ Partition)
@CreateDateColumn({ name: 'created_at' })
@PrimaryColumn()
createdAt!: Date;
// --- Relations ---

View File

@@ -1,26 +1,43 @@
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
// File: src/modules/notification/notification.processor.ts
import { Processor, WorkerHost, InjectQueue } from '@nestjs/bullmq';
import { Job, Queue } from 'bullmq';
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
import * as nodemailer from 'nodemailer';
import axios from 'axios';
import { UserService } from '../user/user.service';
interface NotificationPayload {
userId: number;
title: string;
message: string;
link: string;
type: 'EMAIL' | 'LINE' | 'SYSTEM';
}
@Processor('notifications')
export class NotificationProcessor extends WorkerHost {
private readonly logger = new Logger(NotificationProcessor.name);
private mailerTransport: nodemailer.Transporter;
// ค่าคงที่สำหรับ Digest (เช่น รอ 5 นาที)
private readonly DIGEST_DELAY = 5 * 60 * 1000;
constructor(
private configService: ConfigService,
private userService: UserService,
@InjectQueue('notifications') private notificationQueue: Queue,
@InjectRedis() private readonly redis: Redis,
) {
super();
// Setup Nodemailer
this.mailerTransport = nodemailer.createTransport({
host: this.configService.get('SMTP_HOST'),
port: this.configService.get('SMTP_PORT'),
port: Number(this.configService.get('SMTP_PORT')),
secure: this.configService.get('SMTP_SECURE') === 'true',
auth: {
user: this.configService.get('SMTP_USER'),
@@ -30,59 +47,196 @@ export class NotificationProcessor extends WorkerHost {
}
async process(job: Job<any, any, string>): Promise<any> {
this.logger.debug(`Processing job ${job.name} for user ${job.data.userId}`);
this.logger.debug(`Processing job ${job.name} (ID: ${job.id})`);
switch (job.name) {
case 'send-email':
return this.handleSendEmail(job.data);
case 'send-line':
return this.handleSendLine(job.data);
default:
throw new Error(`Unknown job name: ${job.name}`);
try {
switch (job.name) {
case 'dispatch-notification':
// Job หลัก: ตัดสินใจว่าจะส่งเลย หรือจะเข้า Digest Queue
return this.handleDispatch(job.data);
case 'process-digest':
// Job รอง: ทำงานเมื่อครบเวลา Delay เพื่อส่งแบบรวม
return this.handleProcessDigest(job.data.userId, job.data.type);
default:
throw new Error(`Unknown job name: ${job.name}`);
}
} catch (error) {
// ✅ แก้ไขตรงนี้: Type Casting (error as Error)
this.logger.error(
`Failed to process job ${job.name}: ${(error as Error).message}`,
(error as Error).stack,
);
throw error; // ให้ BullMQ จัดการ Retry
}
}
private async handleSendEmail(data: any) {
const user = await this.userService.findOne(data.userId);
if (!user || !user.email) {
this.logger.warn(`User ${data.userId} has no email`);
/**
* ฟังก์ชันตัดสินใจ (Dispatcher)
* ตรวจสอบ User Preferences และ Digest Mode
*/
private async handleDispatch(data: NotificationPayload) {
// 1. ดึง User พร้อม Preferences
const user: any = await this.userService.findOne(data.userId);
if (!user) {
this.logger.warn(`User ${data.userId} not found, skipping notification.`);
return;
}
const prefs = user.preferences || {
notify_email: true,
notify_line: true,
digest_mode: false,
};
// 2. ตรวจสอบว่า User ปิดรับการแจ้งเตือนหรือไม่
if (data.type === 'EMAIL' && !prefs.notify_email) return;
if (data.type === 'LINE' && !prefs.notify_line) return;
// 3. ตรวจสอบ Digest Mode
if (prefs.digest_mode) {
await this.addToDigest(data);
} else {
// ส่งทันที (Real-time)
if (data.type === 'EMAIL') await this.sendEmailImmediate(user, data);
if (data.type === 'LINE') await this.sendLineImmediate(user, data);
}
}
/**
* เพิ่มข้อความลงใน Redis List และตั้งเวลาส่ง (Delayed Job)
*/
private async addToDigest(data: NotificationPayload) {
const key = `digest:${data.type}:${data.userId}`;
// 1. Push ข้อมูลลง Redis List
await this.redis.rpush(key, JSON.stringify(data));
// 2. ตรวจสอบว่ามี "ตัวนับเวลาถอยหลัง" (Delayed Job) อยู่หรือยัง?
const lockKey = `digest:lock:${data.type}:${data.userId}`;
const isLocked = await this.redis.get(lockKey);
if (!isLocked) {
// ถ้ายังไม่มี Job รออยู่ ให้สร้างใหม่
await this.notificationQueue.add(
'process-digest',
{ userId: data.userId, type: data.type },
{
delay: this.DIGEST_DELAY,
jobId: `digest-${data.type}-${data.userId}-${Date.now()}`,
},
);
// Set Lock ไว้ตามเวลา Delay เพื่อไม่ให้สร้าง Job ซ้ำ
await this.redis.set(lockKey, '1', 'PX', this.DIGEST_DELAY);
this.logger.log(
`Scheduled digest for User ${data.userId} (${data.type}) in ${this.DIGEST_DELAY}ms`,
);
}
}
/**
* ประมวลผล Digest (ส่งแบบรวม)
*/
private async handleProcessDigest(userId: number, type: 'EMAIL' | 'LINE') {
const key = `digest:${type}:${userId}`;
const lockKey = `digest:lock:${type}:${userId}`;
// 1. ดึงข้อความทั้งหมดจาก Redis และลบออกทันที
const messagesRaw = await this.redis.lrange(key, 0, -1);
await this.redis.del(key);
await this.redis.del(lockKey); // Clear lock
if (!messagesRaw || messagesRaw.length === 0) return;
const messages: NotificationPayload[] = messagesRaw.map((m) =>
JSON.parse(m),
);
const user = await this.userService.findOne(userId);
if (type === 'EMAIL') {
await this.sendEmailDigest(user, messages);
} else if (type === 'LINE') {
await this.sendLineDigest(user, messages);
}
}
// =====================================================
// SENDERS (Immediate & Digest)
// =====================================================
private async sendEmailImmediate(user: any, data: NotificationPayload) {
if (!user.email) return;
await this.mailerTransport.sendMail({
from: '"LCBP3 DMS" <no-reply@np-dms.work>',
to: user.email,
subject: `[DMS] ${data.title}`,
html: `
<h3>${data.title}</h3>
<p>${data.message}</p>
<br/>
<a href="${data.link}">คลิกเพื่อดูรายละเอียด</a>
`,
html: `<h3>${data.title}</h3><p>${data.message}</p><br/><a href="${data.link}">คลิกเพื่อดูรายละเอียด</a>`,
});
this.logger.log(`Email sent to ${user.email}`);
}
private async handleSendLine(data: any) {
const user = await this.userService.findOne(data.userId);
// ตรวจสอบว่า User มี Line ID หรือไม่ (หรือใช้ Group Token ถ้าเป็นระบบรวม)
// ในที่นี้สมมติว่าเรายิงเข้า n8n webhook เพื่อจัดการต่อ
const n8nWebhookUrl = this.configService.get('N8N_LINE_WEBHOOK_URL');
private async sendEmailDigest(user: any, messages: NotificationPayload[]) {
if (!user.email) return;
if (!n8nWebhookUrl) {
this.logger.warn('N8N_LINE_WEBHOOK_URL not configured');
return;
}
// สร้าง HTML List
const listItems = messages
.map(
(msg) =>
`<li><strong>${msg.title}</strong>: ${msg.message} <a href="${msg.link}">[View]</a></li>`,
)
.join('');
await this.mailerTransport.sendMail({
from: '"LCBP3 DMS" <no-reply@np-dms.work>',
to: user.email,
subject: `[DMS Summary] คุณมีการแจ้งเตือนใหม่ ${messages.length} รายการ`,
html: `
<h3>สรุปรายการแจ้งเตือน (Digest)</h3>
<ul>${listItems}</ul>
<p>คุณได้รับอีเมลนี้เพราะเปิดใช้งานโหมดสรุปรายการ</p>
`,
});
this.logger.log(
`Digest Email sent to ${user.email} (${messages.length} items)`,
);
}
private async sendLineImmediate(user: any, data: NotificationPayload) {
const n8nWebhookUrl = this.configService.get('N8N_LINE_WEBHOOK_URL');
if (!n8nWebhookUrl) return;
try {
await axios.post(n8nWebhookUrl, {
userId: user.user_id, // หรือ user.lineId ถ้ามี
userId: user.user_id,
message: `${data.title}\n${data.message}`,
link: data.link,
isDigest: false,
});
this.logger.log(`Line notification sent via n8n for user ${data.userId}`);
} catch (error: any) {
throw new Error(`Failed to send Line notification: ${error.message}`);
} catch (error) {
// ✅ แก้ไขตรงนี้ด้วย: Type Casting (error as Error)
this.logger.error(`Line Error: ${(error as Error).message}`);
}
}
private async sendLineDigest(user: any, messages: NotificationPayload[]) {
const n8nWebhookUrl = this.configService.get('N8N_LINE_WEBHOOK_URL');
if (!n8nWebhookUrl) return;
const summary = messages.map((m, i) => `${i + 1}. ${m.title}`).join('\n');
try {
await axios.post(n8nWebhookUrl, {
userId: user.user_id,
message: `สรุป ${messages.length} รายการใหม่:\n${summary}`,
link: 'https://lcbp3.np-dms.work/notifications',
isDigest: true,
});
} catch (error) {
// ✅ แก้ไขตรงนี้ด้วย: Type Casting (error as Error)
this.logger.error(`Line Digest Error: ${(error as Error).message}`);
}
}
}

View File

@@ -1,4 +1,5 @@
// File: src/modules/notification/notification.service.ts
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
@@ -22,9 +23,9 @@ export interface NotificationJobData {
title: string;
message: string;
type: 'EMAIL' | 'LINE' | 'SYSTEM'; // ช่องทางหลักที่ต้องการส่ง (Trigger Type)
entityType?: string; // e.g., 'rfa', 'correspondence'
entityId?: number; // e.g., rfa_id
link?: string; // Deep link to frontend page
entityType?: string;
entityId?: number;
link?: string;
}
@Injectable()
@@ -37,109 +38,57 @@ export class NotificationService {
private notificationRepo: Repository<Notification>,
@InjectRepository(User)
private userRepo: Repository<User>,
@InjectRepository(UserPreference)
private userPrefRepo: Repository<UserPreference>,
// ไม่ต้อง Inject UserPrefRepo แล้ว เพราะ Processor จะจัดการเอง
private notificationGateway: NotificationGateway,
) {}
/**
* ส่งการแจ้งเตือน (Centralized Notification Sender)
* 1. บันทึก DB (System Log)
* 2. ส่ง Real-time (WebSocket)
* 3. ส่ง External (Email/Line) ผ่าน Queue ตาม User Preference
*/
async send(data: NotificationJobData): Promise<void> {
try {
// ---------------------------------------------------------
// 1. สร้าง Entity และบันทึกลง DB (เพื่อให้มี History ในระบบ)
// 1. สร้าง Entity และบันทึกลง DB (System Log)
// ---------------------------------------------------------
const notification = this.notificationRepo.create({
userId: data.userId,
title: data.title,
message: data.message,
notificationType: NotificationType.SYSTEM, // ใน DB เก็บเป็น SYSTEM เสมอเพื่อแสดงใน App
notificationType: NotificationType.SYSTEM,
entityType: data.entityType,
entityId: data.entityId,
isRead: false,
// link: data.link // ถ้า Entity มี field link ให้ใส่ด้วย
});
const savedNotification = await this.notificationRepo.save(notification);
// ---------------------------------------------------------
// 2. Real-time Push (WebSocket) -> ส่งให้ User ทันทีถ้า Online
// 2. Real-time Push (WebSocket)
// ---------------------------------------------------------
this.notificationGateway.sendToUser(data.userId, savedNotification);
// ---------------------------------------------------------
// 3. ตรวจสอบ User Preferences เพื่อส่งช่องทางอื่น (Email/Line)
// 3. Push Job ลง Redis BullMQ (Dispatch Logic)
// เปลี่ยนชื่อ Job เป็น 'dispatch-notification' ตาม Processor
// ---------------------------------------------------------
const userPref = await this.userPrefRepo.findOne({
where: { userId: data.userId },
});
// ใช้ Nullish Coalescing Operator (??)
// ถ้าไม่มีค่า (undefined/null) ให้ Default เป็น true
const shouldSendEmail = userPref?.notifyEmail ?? true;
const shouldSendLine = userPref?.notifyLine ?? true;
const jobs = [];
// ---------------------------------------------------------
// 4. เตรียม Job สำหรับ Email Queue
// เงื่อนไข: User เปิดรับ Email และ Noti นี้ไม่ได้บังคับส่งแค่ LINE
// ---------------------------------------------------------
if (shouldSendEmail && data.type !== 'LINE') {
jobs.push({
name: 'send-email',
data: {
...data,
notificationId: savedNotification.id,
target: 'EMAIL',
await this.notificationQueue.add(
'dispatch-notification',
{
...data,
notificationId: savedNotification.id, // ส่ง ID ไปด้วยเผื่อใช้ Tracking
},
{
attempts: 3,
backoff: {
type: 'exponential',
delay: 5000,
},
opts: {
attempts: 3, // ลองใหม่ 3 ครั้งถ้าล่ม (Resilience)
backoff: {
type: 'exponential',
delay: 5000, // รอ 5s, 10s, 20s...
},
removeOnComplete: true, // ลบ Job เมื่อเสร็จ (ประหยัด Redis Memory)
},
});
}
removeOnComplete: true,
},
);
// ---------------------------------------------------------
// 5. เตรียม Job สำหรับ Line Queue
// เงื่อนไข: User เปิดรับ Line และ Noti นี้ไม่ได้บังคับส่งแค่ EMAIL
// ---------------------------------------------------------
if (shouldSendLine && data.type !== 'EMAIL') {
jobs.push({
name: 'send-line',
data: {
...data,
notificationId: savedNotification.id,
target: 'LINE',
},
opts: {
attempts: 3,
backoff: { type: 'fixed', delay: 3000 },
removeOnComplete: true,
},
});
}
// ---------------------------------------------------------
// 6. Push Jobs ลง Redis BullMQ
// ---------------------------------------------------------
if (jobs.length > 0) {
await this.notificationQueue.addBulk(jobs);
this.logger.debug(
`Queued ${jobs.length} external notifications for user ${data.userId}`,
);
}
this.logger.debug(`Dispatched notification job for user ${data.userId}`);
} catch (error) {
// Error Handling: ไม่ Throw เพื่อไม่ให้ Flow หลัก (เช่น การสร้างเอกสาร) พัง
// แต่บันทึก Error ไว้ตรวจสอบ
this.logger.error(
`Failed to process notification for user ${data.userId}`,
(error as Error).stack,
@@ -147,9 +96,8 @@ export class NotificationService {
}
}
/**
* ดึงรายการแจ้งเตือนของ User (สำหรับ Controller)
*/
// ... (ส่วน findAll, markAsRead, cleanupOldNotifications เหมือนเดิม ไม่ต้องแก้) ...
async findAll(userId: number, searchDto: SearchNotificationDto) {
const { page = 1, limit = 20, isRead } = searchDto;
const skip = (page - 1) * limit;
@@ -161,14 +109,11 @@ export class NotificationService {
.take(limit)
.skip(skip);
// Filter by Read Status (ถ้ามีการส่งมา)
if (isRead !== undefined) {
queryBuilder.andWhere('notification.isRead = :isRead', { isRead });
}
const [items, total] = await queryBuilder.getManyAndCount();
// นับจำนวนที่ยังไม่ได้อ่านทั้งหมด (เพื่อแสดง Badge ที่กระดิ่ง)
const unreadCount = await this.notificationRepo.count({
where: { userId, isRead: false },
});
@@ -185,9 +130,6 @@ export class NotificationService {
};
}
/**
* อ่านแจ้งเตือน (Mark as Read)
*/
async markAsRead(id: number, userId: number): Promise<void> {
const notification = await this.notificationRepo.findOne({
where: { id, userId },
@@ -200,15 +142,9 @@ export class NotificationService {
if (!notification.isRead) {
notification.isRead = true;
await this.notificationRepo.save(notification);
// Update Unread Count via WebSocket (Optional)
// this.notificationGateway.sendUnreadCount(userId, ...);
}
}
/**
* อ่านทั้งหมด (Mark All as Read)
*/
async markAllAsRead(userId: number): Promise<void> {
await this.notificationRepo.update(
{ userId, isRead: false },
@@ -216,10 +152,6 @@ export class NotificationService {
);
}
/**
* ลบการแจ้งเตือนที่เก่าเกินกำหนด (ใช้กับ Cron Job Cleanup)
* เก็บไว้ 90 วัน
*/
async cleanupOldNotifications(days: number = 90): Promise<number> {
const dateLimit = new Date();
dateLimit.setDate(dateLimit.getDate() - days);

View File

@@ -64,6 +64,7 @@ export class UserService {
async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOne({
where: { user_id: id },
relations: ['preferences', 'roles'], // [IMPORTANT] ต้องโหลด preferences มาด้วย
});
if (!user) {

View File

@@ -116,35 +116,23 @@ npm install --save-dev @nestjs/testing jest @types/jest @types/passport-jwt @typ
```
## Prompt: การพัฒนา Core Auth Module (AuthModule) สำหรับ DMS v1.2.0
ขออภัยอย่างยิ่งในความผิดพลาดที่เกิดขึ้นครับ เข้าใจครับว่าทำให้เสียเวลามาก
ช่วยตั้งค่า tsconfig.json, nest-cli.json และไฟล์ config อื่นๆ
สำหรับการ **"ตั้งค่า"** หรือ **"กำหนดค่า"** ให้ผมตรวจสอบข้อมูลก่อนนั้น ในระบบ AI แบบนี้ **ไม่มีเมนู Settings หรือปุ่มกดให้คุณเข้าไปตั้งค่าได้โดยตรงครับ**
* 1.1 สร้าง User Entity
การควบคุมพฤติกรรมของผมต้องทำผ่าน **"คำสั่ง (Prompt)"** ที่คุณพิมพ์เข้ามาเท่านั้นครับ เพื่อป้องกันไม่ให้เกิดเหตุการณ์แบบนี้อีก คุณสามารถใช้เทคนิคการสั่งงานดังนี้เพื่อ **"บังคับ"** ให้ผมตรวจสอบข้อมูลครับ:
* 1.2 สร้าง Role Entity
1. **สั่งให้ "อ่านและสรุปก่อน" (Verify First):**
* ก่อนให้ผมเขียนโค้ด ให้สั่งว่า *"ช่วยอ่านไฟล์ `01_lcbp3_v1_4_3.sql` แล้วสรุปโครงสร้างตาราง audit_logs ที่มีอยู่จริงให้ดูก่อน"*
* วิธีนี้จะบังคับให้ผมต้องไปดึงข้อมูลจากไฟล์มาประมวลผลก่อนที่จะเริ่ม "จินตนาการ" หรือเขียนโค้ดใหม่ครับ
* 1.3 สร้าง Permission Entity
2. **ใช้คำสั่ง "ห้ามเดา" (Strict Constraints):**
* เติมประโยคท้ายคำสั่งว่า *"ห้ามใช้โครงสร้างสมมติ ให้ยึดตามไฟล์แนบ `ชื่อไฟล์` เท่านั้น หากไม่เจอข้อมูลให้ถามกลับ"*
ถัดไป ช่วยสร้าง TypeORM configuration และ DatabaseModule ให้หน่อย
ช่วยสร้าง AuthService, JwtStrategy และ AuthController พื้นฐานให้หน่อย
ช่วยสร้าง UserModule, UserService, LocalAuthGuard และ LocalStrategy สำหรับจัดการข้อมูลผู้ใช้หน่อย
ช่วยสร้าง Guard สำหรับตรวจสอบสิทธิ์ตามบทบาท (RBAC) โดยใช้ CASL หน่อย
ช่วยสร้าง UserModule, UserService, และ UserController
3. **อ้างอิงชื่อไฟล์ชัดเจน:**
* ระบุชื่อไฟล์ในคำสั่งเสมอ เช่น *"อ้างอิงจาก `Backend Plan`..."* หรือ *"ตาม `Data Dictionary`..."* (ซึ่งคุณทำได้ดีแล้วครับ แต่ผมประมวลผลพลาดเองในรอบนี้)
ช่วยตั้งค่าการเชื่อมต่อ MariaDB ผ่าน TypeORM
**สรุป:**
คุณไม่ต้องไปตั้งค่าที่ไหนครับ แต่ใช้การ **"สั่งให้ผมทวนข้อมูลจากไฟล์"** เป็นขั้นตอนแรกก่อนเริ่มงาน จะช่วยลดโอกาสผิดพลาดได้มากที่สุดครับ
พัฒนา Core Auth Module (`AuthModule`)
- ช่วยสร้าง API Endpoints: `/auth/login`, `/auth/me`
พัฒนา Common Module (`@app/common`)
- ช่วยสร้าง FileStorageService สำหรับจัดการไฟล์ (อัปโหลด/ดาวน์โหลด) backend/common/file-storage
- ช่วยสร้าง AuditLogInterceptor สำหรับบันทึกการกระทำโดยอัตโนมัติ
- ช่วยสร้าง Global Exception Filter
- ช่วยสร้าง DTOs และ Interfaces พื้นฐาน
---
ขอบคุณสำหรับข้อมูลที่ละเอียดครับ นี่คือการวิเคราะห์และข้อเสนอแนะเพื่อให้โครงสร้างสิทธิ์การใช้งาน (Access Control / RBAC) ชัดเจน สมบูรณ์ และสามารถนำไปพัฒนาได้จริงครับ
ตอนนี้ผมพร้อมทำงานต่อโดยยึดข้อมูลจากไฟล์ `01_lcbp3_v1_4_3.sql` และไฟล์อื่นๆ ที่คุณให้มาอย่างเคร่งครัดครับ มีส่วนไหนให้ผมดำเนินการต่อไหมครับ?

48
temp.md
View File

@@ -1,40 +1,10 @@
## บทบาท: คุณคือ Programmer ที่เชี่ยวชาญ การจัดการฐานข้อมูล (Database Management), การวิเคราะห์ฐานข้อมูล (Database Analysis), การจัดการฐานข้อมูลเชิงสัมพันธ์ (Relational Databases), ภาษา SQL, RBAC, ABAC, การเขียนโค๊ด NodeJS NestJS NextJS, การ debug โค้ด และ แก้ไข error ภายในโค้ด
รายการ,สถานะ,การดำเนินการ
Core Modules,✅,พร้อม
Workflow Engine (DSL),✅,Code เสร็จแล้ว (รอ Seed Data)
Master Module,✅,Code เสร็จแล้ว
Workflow Seed Data,🔴,ต้องทำทันที (ใช้ Code ด้านบน)
Notification Digest,🟡,ตรวจสอบ Logic ภายใน Processor
Maintenance API,🟡,ตรวจสอบว่ามี Endpoint ให้ Admin กดไหม
DB Partitioning,🟡,เตรียม SQL Script ไว้รันก่อน Load Test
## Basic data:
1. Application Requirements file: 0_Requirements_V1_4_3.md
2. Full Stack JS file: 1_FullStackJS_V1_4_3.md
3. Backend Development Plan: 2_Backend_Plan_V1_4_3.md
4. Frontend Development Plan: 3_Frontend_Plan_V1_4_3.md
5. Data Dictionary file: 4_Data_Dictionary_V1_4_3.md, 01_lcbp3_v1_4_3.sql
6. Backend Development Plan Phase 6A: 2_Backend_Plan_Phase6A_V1_4_3.md
7. Backend File & Folder: 5_Backend_Folder_V1_4_3.md
## rules:
- ใช้ภาษาไทยใน comments
- เขียนโค้ดให้อ่านง่าย, ใส่ path/filename ในบรรทัดแรก โค้ด
- การอัพเดทโค้ด ให้แก้ไขจากต้นฉบับเป็น โค้ดที่สมบูรณ์
- เขียน documentation สำหรับ function สำคัญ
## เป้าหมายและจุดประสงค์:
* ให้ความช่วยเหลือผู้ใช้ในงานที่เกี่ยวข้องกับการพัฒนาซอฟต์แวร์ โดยเฉพาะอย่างยิ่งในส่วนของ JavaScript (NodeJS, NestJS, NextJS) และฐานข้อมูล (SQL, Relational Databases)
* ให้คำปรึกษาเกี่ยวกับการจัดการข้อมูล, การออกแบบฐานข้อมูลเชิงสัมพันธ์, และการใช้โมเดลการควบคุมการเข้าถึง (RBAC, ABAC)
* ช่วยเหลือในการวิเคราะห์และแก้ไขข้อผิดพลาด (debug และ error) ในโค้ดตามที่ผู้ใช้ระบุ
* ใช้ข้อมูลพื้นฐานที่ให้มา (Basic data) เพื่อให้คำแนะนำและโค้ดที่สอดคล้องกับเอกสารโครงการ (เช่น Requirements, Plans, Data Dictionary)
## พฤติกรรมและกฎเพิ่มเติม:
1) การเริ่มต้นและการโต้ตอบ:
a) ทักทายผู้ใช้ด้วยภาษาไทยอย่างเป็นมิตร และสอบถามเกี่ยวกับปัญหาหรือความช่วยเหลือที่ต้องการในด้านการเขียนโปรแกรมหรือฐานข้อมูล
b) ตอบคำถามทางเทคนิคอย่างแม่นยำและเป็นมืออาชีพ โดยใช้ศัพท์เฉพาะทางที่ถูกต้อง
c) จำกัดจำนวนประโยคในการตอบกลับแต่ละครั้งให้กระชับและตรงประเด็นเพื่อความรวดเร็วในการสื่อสาร
2) การจัดการโค้ดและข้อมูล:
a) เมื่อผู้ใช้ขอให้อัพเดทโค้ด ให้ทำการแสดงโค้ดฉบับเต็มที่สมบูรณ์และได้รับการแก้ไขแล้ว (ไม่ใช่แค่ส่วนที่แก้ไข)
b) ต้องแน่ใจว่าโค้ดที่สร้างขึ้นมานั้นอ่านง่ายและมี comments เป็นภาษาไทยตามที่ระบุใน rules
c) สำหรับฟังก์ชันที่มีความซับซ้อนหรือมีความสำคัญต่อระบบ ต้องเขียน documentation อธิบายวัตถุประสงค์, พารามิเตอร์, และผลลัพธ์ของฟังก์ชันนั้นๆ ด้วยภาษาไทย
d) หากต้องอ้างอิงถึงโครงสร้างข้อมูลหรือข้อกำหนดใดๆ ให้ตรวจสอบจากไฟล์ Basic data ที่ผู้ใช้ให้มาก่อนเสมอ ถ้าไม่พบ ให้แจ้งผู้ใช้ทราบ
e) ถ้ามีการอ้างอิงถึงโค้ดที่อยู่ใน Phase หรือ Task ก่อนหน้า ให้สอบถามผู้ใช้เพื่อให้ upload ไฟล์โค้ดที่อ้างอิง (ไม่เดาหรือสร้างใหม่ เพิ่อประหยัดเวลา)
1) โทนโดยรวม:
* ใช้ภาษาไทยในการสื่อสารเป็นหลัก ยกเว้นศัพท์เทคนิค
* มีความมั่นใจและแสดงออกถึงความเชี่ยวชาญในฐานะโปรแกรมเมอร์ผู้เชี่ยวชาญ
* มีความเป็นระเบียบและให้คำแนะนำที่เป็นขั้นตอน
ตรวจสอบ Maintenance API