260222:1053 20260222 refactor specs/ #1 03-Data-and-Storage
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
This commit is contained in:
2115
specs/03-Data-and-Storage/03-01-data-dictionary.md
Normal file
2115
specs/03-Data-and-Storage/03-01-data-dictionary.md
Normal file
File diff suppressed because it is too large
Load Diff
131
specs/03-Data-and-Storage/03-02-db-indexing.md
Normal file
131
specs/03-Data-and-Storage/03-02-db-indexing.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Database Indexing & Performance Strategy
|
||||
**Version:** 1.0.0
|
||||
**Context:** Production-scale (100k+ documents, High Concurrency)
|
||||
**Database:** MySQL 8.x (On-Premise via Docker)
|
||||
|
||||
## 1. Core Principles (หลักการสำคัญ)
|
||||
ในการออกแบบ Database Index สำหรับระบบ DMS ให้ยึดหลักการตัดสินใจดังนี้:
|
||||
1. **Data Integrity First:** ใช้ `UNIQUE INDEX` เพื่อเป็นปราการด่านสุดท้ายป้องกันการเกิด Duplicate Document Number และ Revision ซ้ำซ้อน (แม้ Application Layer จะมี Logic ดักไว้แล้วก็ตาม)
|
||||
2. **Soft-Delete Awareness:** ทุก Index ที่เกี่ยวข้องกับความถูกต้องของข้อมูล ต้องคำนึงถึงคอลัมน์ `deleted_at` เพื่อไม่ให้เอกสารที่ถูกลบไปแล้ว มาขัดขวางการสร้างเอกสารใหม่ที่ใช้เลขเดิม
|
||||
3. **Foreign Key Performance:** สร้าง B-Tree Index ให้กับ Foreign Key (FK) ทุกตัว เพื่อรองรับการ JOIN ข้อมูลที่รวดเร็ว โดยเฉพาะการดึง Workflow และ Routing
|
||||
4. **Write-Heavy Resilience:** ตารางประเภท `audit_logs` ให้เน้น Index เฉพาะที่จำเป็น (`created_at`, `user_id`, `action`) เพื่อไม่ให้กระทบประสิทธิภาพการ Insert
|
||||
|
||||
---
|
||||
|
||||
## 2. Document Control Indexes (ป้องกัน Duplicate & Conflict)
|
||||
หัวใจของ DMS คือห้ามมีเอกสารเลขซ้ำในระบบที่ Active อยู่
|
||||
|
||||
### 2.1 Unique Document Number & Revision
|
||||
เพื่อรองรับระบบ Soft Delete (`deleted_at`) ใน MySQL การตั้ง Unique Index จำเป็นต้องมีเทคนิคเพื่อจัดการกับค่า `NULL` (เนื่องจาก MySQL มองว่า `NULL` ไม่เท่ากับ `NULL` จึงอาจทำให้เกิด Duplicate ได้ถ้าตั้งค่าไม่รัดกุม)
|
||||
|
||||
**SQL Recommendation (Functional Index - MySQL 8.0+):**
|
||||
```sql
|
||||
-- ป้องกันการสร้าง Document No และ Revision ซ้ำ สำหรับเอกสารที่ยังไม่ถูกลบ (Active)
|
||||
ALTER TABLE `documents`
|
||||
ADD UNIQUE INDEX `idx_unique_active_doc_rev` (
|
||||
`document_no`,
|
||||
`revision`,
|
||||
(IF(`deleted_at` IS NULL, 1, NULL))
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
*เหตุผล:* โครงสร้างนี้รับประกันว่าจะมี `document_no` + `revision` ที่ Active ได้เพียง 1 รายการเท่านั้น แต่สามารถมีรายการที่ถูกลบ (`deleted_at` มีค่า) ซ้ำกันได้
|
||||
|
||||
### 2.2 Current/Superseded Flag Index
|
||||
|
||||
การค้นหาว่าเอกสารไหนเป็น "Latest Revision" จะเกิดขึ้นบ่อยมาก
|
||||
|
||||
```sql
|
||||
-- ใช้สำหรับ Filter เอกสารที่เป็นเวอร์ชันล่าสุดอย่างรวดเร็ว
|
||||
ALTER TABLE `documents`
|
||||
ADD INDEX `idx_doc_status_is_current` (`is_current`, `status`, `project_id`);
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. High-Concurrency Search Indexes (รองรับ 100k+ Docs)
|
||||
|
||||
สำหรับการทำ Filter และ Search บนหน้า Dashboard หรือรายการเอกสาร
|
||||
|
||||
### 3.1 Pagination & Sorting
|
||||
|
||||
การ Query ข้อมูลแบบแบ่งหน้า (Pagination) พร้อมเรียงลำดับวันที่ มักเกิดปัญหา "Filesort" ที่ทำให้ CPU โหลดหนัก
|
||||
|
||||
```sql
|
||||
-- สำหรับหน้า Dashboard ที่เรียงตามวันที่อัปเดตล่าสุด
|
||||
ALTER TABLE `documents`
|
||||
ADD INDEX `idx_project_updated` (`project_id`, `updated_at` DESC);
|
||||
|
||||
-- สำหรับ Inbox / Pending Tasks ของ User
|
||||
ALTER TABLE `workflow_instances`
|
||||
ADD INDEX `idx_assignee_status` (`assignee_id`, `status`, `created_at` DESC);
|
||||
|
||||
```
|
||||
|
||||
### 3.2 Full-Text Search (ทางเลือกเบื้องต้นก่อนใช้ Elasticsearch)
|
||||
|
||||
หากผู้ใช้ต้องการค้นหาจากชื่อเอกสาร (`title`) หรือเนื้อหาบางส่วน
|
||||
|
||||
```sql
|
||||
-- สร้าง Full-Text Index สำหรับคำค้นหา
|
||||
ALTER TABLE `documents`
|
||||
ADD FULLTEXT INDEX `ft_idx_doc_title` (`title`, `subject`);
|
||||
|
||||
```
|
||||
|
||||
*(หมายเหตุ: หากอนาคตมีระบบ OCR หรือค้นหาในเนื้อหาไฟล์ PDF ให้พิจารณาขยับไปใช้ Elasticsearch แยกต่างหาก ไม่ควรเก็บ Full-Text ขนาดใหญ่ไว้ใน MySQL)*
|
||||
|
||||
---
|
||||
|
||||
## 4. RBAC & Security Indexes
|
||||
|
||||
เพื่อป้องกันปัญหาคอขวด (Bottleneck) ตอนเช็คสิทธิ์ (RBAC validation) ก่อนให้เข้าถึงเอกสาร
|
||||
|
||||
```sql
|
||||
-- ตาราง user_permissions
|
||||
ALTER TABLE `user_permissions`
|
||||
ADD UNIQUE INDEX `idx_user_role_project` (`user_id`, `role_id`, `project_id`);
|
||||
|
||||
-- ตาราง document_access_logs (Audit)
|
||||
ALTER TABLE `audit_logs`
|
||||
ADD INDEX `idx_audit_user_action` (`user_id`, `action`, `created_at`);
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Audit Log Strategy (การจัดการตารางประวัติ)
|
||||
|
||||
ตาราง `audit_logs` จะโตเร็วมาก (Insert-only) คาดว่าจะมีหลักล้าน Record อย่างรวดเร็ว
|
||||
|
||||
**คำแนะนำสำหรับ On-Premise:**
|
||||
|
||||
1. **Partitioning:** แนะนำให้ทำ Table Partitioning ตามเดือน (Monthly) หรือปี (Yearly) บนคอลัมน์ `created_at`
|
||||
2. **Minimal Indexing:** ห้ามสร้าง Index เยอะเกินความจำเป็นในตารางนี้ แนะนำแค่:
|
||||
* `INDEX(document_id, created_at)` สำหรับดู History ของเอกสารนั้นๆ
|
||||
* `INDEX(user_id, created_at)` สำหรับตรวจสอบพฤติกรรมผู้ใช้ต้องสงสัย (Security Audit)
|
||||
|
||||
|
||||
|
||||
```sql
|
||||
-- ตัวอย่างการ Index สำหรับดูกระแสของเอกสาร
|
||||
ALTER TABLE `audit_logs`
|
||||
ADD INDEX `idx_entity_history` (`entity_type`, `entity_id`, `created_at` DESC);
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Maintenance & Optimization (DevOps/Admin)
|
||||
|
||||
เนื่องจากระบบอยู่บน On-Prem NAS (QNAP/ASUSTOR) ทรัพยากร I/O ของดิสก์มีจำกัด (Disk IOPS)
|
||||
|
||||
* **Index Defragmentation:** ให้กำหนด Scheduled Task (ผ่าน Cronjob หรือ MySQL Event) มารัน `OPTIMIZE TABLE` ทุกๆ ไตรมาส สำหรับตารางที่มีการ Delete/Update บ่อย (ช่วยคืนพื้นที่ดิสก์และลด I/O)
|
||||
* **Slow Query Monitoring:** ใน `04-infrastructure-ops/04-01-docker-compose.md` ต้องเปิดใช้งาน `slow_query_log=1` และตั้ง `long_query_time=2` เพื่อตรวจสอบว่ามี Query ใดทำงานแบบ Full Table Scan (ไม่ใช้ Index) หรือไม่
|
||||
|
||||
|
||||
## 💡 คำแนะนำเพิ่มเติมจาก Architect (Architect's Notes):
|
||||
1. **เรื่อง Soft Delete กับ Unique Constraint:** เป็นจุดที่นักพัฒนาพลาดกันบ่อยที่สุด ถ้าระบบอนุญาตให้ลบ `DOC-001 Rev.0` แล้วสร้าง `DOC-001 Rev.0` ใหม่ได้ การจัดการ Unique Constraint บน MySQL ต้องใช้ Functional Index (ตามตัวอย่างในข้อ 2.1) เพื่อป้องกันการตีกันของค่า `NULL` ในฐานข้อมูล
|
||||
2. **ลดภาระ QNAP/ASUSTOR:** อุปกรณ์จำพวก NAS On-Premise มักจะมีปัญหาเรื่อง Random Read/Write Disk I/O การใช้ **Composite Index** แบบครบคลุม (Covering Index) จะช่วยให้ MySQL คืนค่าได้จาก Index Tree โดยตรง ไม่ต้องกระโดดไปอ่าน Data File จริง ซึ่งจะช่วยรีด Performance ของ NAS ได้สูงสุดครับ
|
||||
315
specs/03-Data-and-Storage/03-03-file-storage.md
Normal file
315
specs/03-Data-and-Storage/03-03-file-storage.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# 3.3 File Storage and Handling
|
||||
|
||||
---
|
||||
title: 'Data & Storage: File Storage and Handling (Two-Phase)'
|
||||
version: 1.8.0
|
||||
status: drafted
|
||||
owner: Nattanin Peancharoen
|
||||
last_updated: 2026-02-22
|
||||
related:
|
||||
- specs/01-requirements/01-03.10-file-handling.md (Merged)
|
||||
- specs/03-Data-and-Storage/ADR-003-file-storage-approach.md (Merged)
|
||||
- specs/02-architecture/02-01-system-architecture.md
|
||||
- ADR-006-security-best-practices
|
||||
---
|
||||
|
||||
## 1. Overview and Core Infrastructure Requirements
|
||||
|
||||
เอกสารฉบับนี้รวบรวมข้อกำหนดการจัดการไฟล์และการจัดเก็บไฟล์ (File Storage Approach) สำหรับ LCBP3-DMS โดยมีข้อบังคับด้าน Infrastructure และ Security ที่สำคัญมากดังต่อไปนี้:
|
||||
|
||||
### 1.1 Infrastructure Requirement (การจัดเก็บและ Mount Volume)
|
||||
**สำคัญ (CRITICAL SPECIFICATION):**
|
||||
1. **Outside Webroot:** ไฟล์รูปและเอกสารทั้งหมดต้องถูกจัดเก็บไว้ **ภายนอก Webroot ของ Application** ห้ามเก็บไฟล์รูปหรือเอกสารไว้ใน Container หรือโฟลเดอร์ Webroot เด็ดขาด เพื่อป้องกันการเข้าถึงไฟล์โดยตรงจากสาธารณะ (Direct Public Access)
|
||||
2. **QNAP Volume Mount:** ต้องใช้ **QNAP Volume Mount เข้า Docker** (Mount external volume from QNAP NAS to Docker container) สำหรับเป็นพื้นที่เก็บไฟล์ Storage ให้ Container ดึงไปใช้งาน
|
||||
3. **Authenticated Endpoint:** ไฟล์ต้องถูกเข้าถึงและให้บริการดาวน์โหลดผ่าน Authenticated Endpoint ในฝั่ง Backend เท่านั้น โดยต้องผ่านการตรวจสอบสิทธิ์ (RBAC / Junction Table) เสียก่อน
|
||||
|
||||
### 1.2 Access & Security Rules
|
||||
- **Virus Scan:** ต้องมีการ scan virus สำหรับไฟล์ที่อัปโหลดทั้งหมด โดยใช้ ClamAV หรือบริการ third-party ก่อนการบันทึก
|
||||
- **Whitelist File Types:** อนุญาตเฉพาะเอกสารตามที่กำหนด: PDF, DWG, DOCX, XLSX, ZIP
|
||||
- **Max File Size:** ขนาดไฟล์สูงสุดไม่เกิน 50MB ต่อไฟล์ (Total max 500MB per form submission)
|
||||
- **Expiration Time:** Download links ที่สร้างขึ้นต้องมี expiration time (default: 24 ชั่วโมง)
|
||||
- **Integrity Check:** ต้องมี file integrity check (checksum เป็น SHA-256) เพื่อป้องกันการแก้ไขไฟล์ภายหลัง
|
||||
- **Audit Logging:** ต้องบันทึก audit log ทุกครั้งที่มีการดาวน์โหลดไฟล์สำคัญ
|
||||
|
||||
---
|
||||
|
||||
## 2. Two-Phase File Storage Approach (ADR-003)
|
||||
|
||||
### 2.1 Context and Problem Statement
|
||||
LCBP3-DMS ต้องจัดการ File Uploads สำหรับ Attachments ของเอกสาร (PDF, DWG, DOCX, etc.) โดยต้องรับมือกับปัญหา:
|
||||
1. **Orphan Files:** User อัปโหลดไฟล์แล้วไม่ Submit Form ทำให้ไฟล์ค้างใน Storage
|
||||
2. **Transaction Integrity:** ถ้า Database Transaction Rollback ไฟล์ยังอยู่ใน Storage ต้องสอดคล้องกับ Database Record
|
||||
3. **Virus Scanning:** ต้อง Scan ไฟล์ก่อน Save เข้าระบบถาวร
|
||||
4. **File Validation:** ตรวจสอบ Type, Size, และสร้าง Checksum
|
||||
5. **Storage Organization:** จัดเก็บไฟล์แยกเป็นสัดส่วน (เพื่อไม่ให้ QNAP Storage กระจัดกระจายและจำกัดขนาดได้)
|
||||
|
||||
### 2.2 Decision Drivers
|
||||
- **Data Integrity:** File และ Database Record ต้อง Consistent
|
||||
- **Security:** ป้องกัน Virus และ Malicious Files
|
||||
- **User Experience:** Upload ต้องรวดเร็ว ไม่ Block UI (ถ้าอัปโหลดพร้อม Submit อาจทำให้ระบบดูค้าง)
|
||||
- **Storage Efficiency:** ไม่เก็บไฟล์ที่ไม่ถูกใช้งาน (Orphan files)
|
||||
- **Auditability:** ติดตามประวัติ File Operations ได้
|
||||
|
||||
### 2.3 Considered Options & Decision
|
||||
- **Option 1:** Direct Upload to Permanent Storage (ทิ้งไฟล์ถ้า Transaction Fail / ได้ Orphan Files) - ❌
|
||||
- **Option 2:** Upload after Form Submission (UX แย่ ผู้ใช้ต้องรออัปโหลดรวดเดียวท้ายสุด) - ❌
|
||||
- **Option 3: Two-Phase Storage (Temp → Permanent) ⭐ (Selected Option)** - ✅
|
||||
|
||||
**แนวทาง Two-Phase Storage (Temp → Permanent):**
|
||||
1. **Phase 1 (Upload):** ไฟล์ถูกอัปโหลดเข้าโฟลเดอร์ `temp/` ได้รับ `temp_id`
|
||||
2. **Phase 2 (Commit):** เมื่อ User กด Submit ฟอร์มสำเร็จ ระบบจะย้ายไฟล์จาก `temp/` ไปยัง `permanent/{YYYY}/{MM}/` และบันทึกลง Database ใน Transaction เดียวกัน
|
||||
3. **Cleanup:** มี Cron Job ทำหน้าที่ลบไฟล์ใน `temp/` ที่ค้างเกินกำหนด (เช่น 24 ชั่วโมง)
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Details
|
||||
|
||||
### 3.1 Database Schema
|
||||
```sql
|
||||
CREATE TABLE attachments (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
original_filename VARCHAR(255) NOT NULL,
|
||||
stored_filename VARCHAR(255) NOT NULL, -- UUID-based
|
||||
file_path VARCHAR(500) NOT NULL, -- QNAP Mount path
|
||||
mime_type VARCHAR(100) NOT NULL,
|
||||
file_size INT NOT NULL,
|
||||
checksum VARCHAR(64) NULL, -- SHA-256
|
||||
|
||||
-- Two-Phase Fields
|
||||
is_temporary BOOLEAN DEFAULT TRUE,
|
||||
temp_id VARCHAR(100) NULL, -- UUID for temp reference
|
||||
expires_at DATETIME NULL, -- Temp file expiration
|
||||
|
||||
uploaded_by_user_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (uploaded_by_user_id) REFERENCES users(user_id),
|
||||
INDEX idx_temp_files (is_temporary, expires_at)
|
||||
);
|
||||
```
|
||||
|
||||
### 3.2 Two-Phase Storage Flow
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as Client
|
||||
participant BE as Backend
|
||||
participant Virus as ClamAV
|
||||
participant TempStorage as Temp Storage (QNAP Volume)
|
||||
participant PermStorage as Permanent Storage (QNAP Volume)
|
||||
participant DB as Database
|
||||
|
||||
Note over User,DB: Phase 1: Upload to Temporary Storage
|
||||
User->>BE: POST /attachments/upload (file)
|
||||
BE->>BE: Validate file type, size
|
||||
BE->>Virus: Scan virus
|
||||
|
||||
alt File is CLEAN
|
||||
Virus-->>BE: CLEAN
|
||||
BE->>BE: Generate temp_id (UUID)
|
||||
BE->>BE: Calculate SHA-256 checksum
|
||||
BE->>TempStorage: Save to temp/{temp_id}
|
||||
BE->>DB: INSERT attachment<br/>(is_temporary=TRUE, expires_at=NOW+24h)
|
||||
BE-->>User: { temp_id, expires_at }
|
||||
else File is INFECTED
|
||||
Virus-->>BE: INFECTED
|
||||
BE-->>User: Error: Virus detected
|
||||
end
|
||||
|
||||
Note over User,DB: Phase 2: Commit to Permanent Storage
|
||||
User->>BE: POST /correspondences<br/>{ temp_file_ids: [temp_id] }
|
||||
BE->>DB: BEGIN Transaction
|
||||
BE->>DB: INSERT correspondence
|
||||
|
||||
loop For each temp_file_id
|
||||
BE->>TempStorage: Read temp file
|
||||
BE->>PermStorage: Move to permanent/{YYYY}/{MM}/{UUID}
|
||||
BE->>DB: UPDATE attachment<br/>(is_temporary=FALSE, file_path=new_path)
|
||||
BE->>DB: INSERT correspondence_attachments
|
||||
BE->>TempStorage: DELETE temp file
|
||||
end
|
||||
|
||||
BE->>DB: COMMIT Transaction
|
||||
BE-->>User: Success
|
||||
|
||||
Note over BE,TempStorage: Cleanup Job (Every 6 hours)
|
||||
BE->>DB: SELECT expired temp files
|
||||
BE->>TempStorage: DELETE expired physical files
|
||||
BE->>DB: DELETE attachment records
|
||||
```
|
||||
|
||||
### 3.3 NestJS Service Implementation
|
||||
|
||||
```typescript
|
||||
// file-storage.service.ts
|
||||
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { createHash } from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { LessThan } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class FileStorageService {
|
||||
private readonly TEMP_DIR: string;
|
||||
private readonly PERMANENT_DIR: string;
|
||||
private readonly TEMP_EXPIRY_HOURS = 24;
|
||||
private readonly logger = new Logger(FileStorageService.name);
|
||||
|
||||
constructor(private config: ConfigService) {
|
||||
// 💡 Must point to the QNAP Volume mount inside the container!
|
||||
this.TEMP_DIR = this.config.get('STORAGE_PATH') + '/temp';
|
||||
this.PERMANENT_DIR = this.config.get('STORAGE_PATH') + '/permanent';
|
||||
}
|
||||
|
||||
// Phase 1: Upload to Temporary
|
||||
async uploadToTemp(file: Express.Multer.File): Promise<UploadResult> {
|
||||
// 1. Validate file (Size & Type)
|
||||
this.validateFile(file);
|
||||
|
||||
// 2. Virus scan (ClamAV)
|
||||
await this.virusScan(file);
|
||||
|
||||
// 3. Generate temp ID and File paths
|
||||
const tempId = uuidv4();
|
||||
const storedFilename = `${tempId}_${file.originalname}`;
|
||||
const tempPath = path.join(this.TEMP_DIR, storedFilename);
|
||||
|
||||
// 4. Calculate checksum
|
||||
const checksum = await this.calculateChecksum(file.buffer);
|
||||
|
||||
// 5. Save to temp directory (Outside Webroot via volume mount)
|
||||
await fs.writeFile(tempPath, file.buffer);
|
||||
|
||||
// 6. Create attachment record in DB (Example assuming typeorm usage)
|
||||
const attachment = await this.attachmentRepo.save({
|
||||
original_filename: file.originalname,
|
||||
stored_filename: storedFilename,
|
||||
file_path: tempPath,
|
||||
mime_type: file.mimetype,
|
||||
file_size: file.size,
|
||||
checksum,
|
||||
is_temporary: true,
|
||||
temp_id: tempId,
|
||||
expires_at: new Date(Date.now() + this.TEMP_EXPIRY_HOURS * 3600 * 1000),
|
||||
uploaded_by_user_id: this.currentUserId,
|
||||
});
|
||||
|
||||
return {
|
||||
temp_id: tempId,
|
||||
expires_at: attachment.expires_at,
|
||||
filename: file.originalname,
|
||||
size: file.size,
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 2: Commit to Permanent (within Transaction Manager)
|
||||
async commitFiles(tempIds: string[], entityId: number, entityType: string, manager: EntityManager): Promise<Attachment[]> {
|
||||
const attachments = [];
|
||||
|
||||
for (const tempId of tempIds) {
|
||||
const tempAttachment = await manager.findOne(Attachment, { where: { temp_id: tempId, is_temporary: true } });
|
||||
if (!tempAttachment) throw new Error(`Temporary file not found: ${tempId}`);
|
||||
if (tempAttachment.expires_at < new Date()) throw new Error(`Temporary file expired: ${tempId}`);
|
||||
|
||||
// Generate permanent path: permanent/YYYY/MM
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = (now.getMonth() + 1).toString().padStart(2, '0');
|
||||
const permanentDir = path.join(this.PERMANENT_DIR, year.toString(), month);
|
||||
await fs.ensureDir(permanentDir);
|
||||
|
||||
const permanentFilename = `${uuidv4()}_${tempAttachment.original_filename}`;
|
||||
const permanentPath = path.join(permanentDir, permanentFilename);
|
||||
|
||||
// Move file physically in QNAP Volume
|
||||
await fs.move(tempAttachment.file_path, permanentPath);
|
||||
|
||||
// Update Database record
|
||||
await manager.update(Attachment, { id: tempAttachment.id }, {
|
||||
file_path: permanentPath,
|
||||
stored_filename: permanentFilename,
|
||||
is_temporary: false,
|
||||
temp_id: null,
|
||||
expires_at: null,
|
||||
});
|
||||
|
||||
attachments.push(tempAttachment);
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
// Phase 3: Cleanup Job (Cron)
|
||||
@Cron('0 */6 * * *') // Every 6 hours
|
||||
async cleanupExpiredFiles(): Promise<void> {
|
||||
const expiredFiles = await this.attachmentRepo.find({
|
||||
where: { is_temporary: true, expires_at: LessThan(new Date()) },
|
||||
});
|
||||
|
||||
for (const file of expiredFiles) {
|
||||
try {
|
||||
await fs.remove(file.file_path);
|
||||
await this.attachmentRepo.remove(file);
|
||||
this.logger.log(`Cleaned up expired file: ${file.temp_id}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to cleanup file: ${file.temp_id}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateFile(file: Express.Multer.File): void {
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
// ... (DOCX, WHiteListed Mimetypes + XLSX, DWG, ZIP)
|
||||
];
|
||||
const maxSize = 50 * 1024 * 1024; // 50MB
|
||||
if (!allowedTypes.includes(file.mimetype)) throw new BadRequestException('Invalid file type');
|
||||
if (file.size > maxSize) throw new BadRequestException('File too large (max 50MB)');
|
||||
// 💡 Add Magic Number Verification logic in real implementation to avoid simple extension spoofing
|
||||
}
|
||||
|
||||
private async virusScan(file: Express.Multer.File): Promise<void> {
|
||||
// ClamAV integration example
|
||||
// const scanner = await this.clamAV.scan(file.buffer);
|
||||
// if (scanner.isInfected) throw new BadRequestException('Virus detected in file');
|
||||
}
|
||||
|
||||
private async calculateChecksum(buffer: Buffer): Promise<string> {
|
||||
return createHash('sha256').update(buffer).digest('hex');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 API Controller Context
|
||||
ในส่วนของตัว Controller ฝ่ายรับข้อมูลจะต้องแยกระหว่าง Uploading กับ Comit:
|
||||
1. `POST /attachments/upload` ใช้เพื่อรับไฟล์และ Return `temp_id` แก่ User ทันที
|
||||
2. `POST /correspondences` หรือ Object อื่นๆ ใช้เพื่อ Commit Database โดยจะรับ `temp_file_ids: []` พ่วงมากับ Body form
|
||||
|
||||
---
|
||||
|
||||
## 4. Consequences & Mitigation Strategies
|
||||
|
||||
### Positive Consequences
|
||||
1. ✅ **Fast Upload UX:** User upload แบบ Async ก่อน Submit ดำเนินการลื่นไหล
|
||||
2. ✅ **No Orphan Files:** เกิดระบบ Auto-cleanup จัดการไฟล์หมดอายุโดยอัตโนมัติ ไม่เปลืองสเปซ QNAP
|
||||
3. ✅ **Transaction Safe:** Rollback ได้สมบูรณ์หากบันทึกฐานข้อมูลผิดพลาด ไฟล์จะถูก Cron จัดการให้ทีหลังไม่ตกค้างในระบบ
|
||||
4. ✅ **Security:** Virus scan ไฟล์ก่อน Commit เข้าถึงข้อมูล Sensitive Area
|
||||
5. ✅ **Audit Trail:** ติดตามประวัติ Operations ต่างๆ เพื่อความโปร่งใส
|
||||
6. ✅ **Storage Organization:** จัดเก็บอย่างเป็นระเบียบ ด้วยรูปแบบ YYYY/MM ลดคอขวด IO Operations ในระบบ
|
||||
|
||||
### Negative Consequences & Mitigations
|
||||
1. ❌ **Complexity:** ต้อง Implement 2 phases ซึ่งซับซ้อนขึ้น
|
||||
👉 *Mitigation:* รวบ Logic ทุกอย่างให้เป็น Service ชั้นเดียว (`FileStorageService`) เพื่อให้จัดการง่ายและเรียกใช้ง่ายที่สุด
|
||||
2. ❌ **Extra Storage:** ต้องใช้พื้นที่ QNAP ในส่วน Temp directory ควบคู่ไปกับแบบ Permanent
|
||||
👉 *Mitigation:* คอย Monitor และปรับรอบความถี่ของการ Cleanup หากไฟล์มีปริมาณไหลเวียนเยอะมาก
|
||||
3. ❌ **Edge Cases:** อาจเกิดประเด็นเรื่อง File lock หรือ missing temp files
|
||||
👉 *Mitigation:* ทำ Proper error handling พร้อม Logging ให้ตรวจสอบได้ง่าย
|
||||
|
||||
---
|
||||
|
||||
## 5. Performance Optimization Consideration
|
||||
- **Streaming:** ใช้ multipart/form-data streaming เพิ่อลดภาระ Memory ของฝั่งเครื่องเซิฟเวอร์ (NestJS) ขณะสูบไฟล์ใหญ่ๆ
|
||||
- **Compression:** พิจารณาเรื่องการบีบอัดสำหรับไฟล์ขนาดใหญ่หรือบางประเภท
|
||||
- **Deduplication Check:** สามารถใช้งาน Field `checksum` ดักการ Commit ด้วยข้อมูลชุดเดิมที่เคยถูกอัปโหลดเพื่อประหยัดพื้นที่จัดเก็บ (Deduplicate)
|
||||
652
specs/03-Data-and-Storage/99-archives/02-03-data-model.md
Normal file
652
specs/03-Data-and-Storage/99-archives/02-03-data-model.md
Normal file
@@ -0,0 +1,652 @@
|
||||
# Data Model Architecture
|
||||
|
||||
---
|
||||
|
||||
title: 'Data Model Architecture'
|
||||
version: 1.5.0
|
||||
status: first-draft
|
||||
owner: Nattanin Peancharoen
|
||||
last_updated: 2025-11-30
|
||||
related:
|
||||
|
||||
- specs/01-requirements/02-architecture.md
|
||||
- specs/01-requirements/03-functional-requirements.md
|
||||
- docs/4_Data_Dictionary_V1_4_5.md
|
||||
- docs/8_lcbp3_v1_4_5.sql
|
||||
|
||||
---
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
เอกสารนี้อธิบายสถาปัตยกรรมของ Data Model สำหรับระบบ LCBP3-DMS โดยครอบคลุมโครงสร้างฐานข้อมูล, ความสัมพันธ์ระหว่างตาราง, และหลักการออกแบบที่สำคัญ
|
||||
|
||||
## 🎯 Design Principles
|
||||
|
||||
### 1. Separation of Concerns
|
||||
|
||||
- **Master-Revision Pattern**: แยกข้อมูลที่ไม่เปลี่ยนแปลง (Master) จากข้อมูลที่มีการแก้ไข (Revisions)
|
||||
- `correspondences` (Master) ↔ `correspondence_revisions` (Revisions)
|
||||
- `rfas` (Master) ↔ `rfa_revisions` (Revisions)
|
||||
- `shop_drawings` (Master) ↔ `shop_drawing_revisions` (Revisions)
|
||||
|
||||
### 2. Data Integrity
|
||||
|
||||
- **Foreign Key Constraints**: ใช้ FK ทุกความสัมพันธ์เพื่อรักษาความสมบูรณ์ของข้อมูล
|
||||
- **Soft Delete**: ใช้ `deleted_at` แทนการลบข้อมูลจริง เพื่อรักษาประวัติ
|
||||
- **Optimistic Locking**: ใช้ `version` column ใน `document_number_counters` ป้องกัน Race Condition
|
||||
|
||||
### 3. Flexibility & Extensibility
|
||||
|
||||
- **JSON Details Field**: เก็บข้อมูลเฉพาะประเภทใน `correspondence_revisions.details`
|
||||
- **Virtual Columns**: สร้าง Index จาก JSON fields สำหรับ Performance
|
||||
- **Master Data Tables**: แยกข้อมูล Master (Types, Status, Codes) เพื่อความยืดหยุ่น
|
||||
|
||||
### 4. Security & Audit
|
||||
|
||||
- **RBAC (Role-Based Access Control)**: ระบบสิทธิ์แบบ Hierarchical Scope
|
||||
- **Audit Trail**: บันทึกผู้สร้าง/แก้ไข และเวลาในทุกตาราง
|
||||
- **Two-Phase File Upload**: ป้องกันไฟล์ขยะด้วย Temporary Storage
|
||||
|
||||
## 🗂️ Database Schema Overview
|
||||
|
||||
### Entity Relationship Diagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
%% Core Entities
|
||||
organizations ||--o{ users : "employs"
|
||||
projects ||--o{ contracts : "contains"
|
||||
projects ||--o{ correspondences : "manages"
|
||||
|
||||
%% RBAC
|
||||
users ||--o{ user_assignments : "has"
|
||||
roles ||--o{ user_assignments : "assigned_to"
|
||||
roles ||--o{ role_permissions : "has"
|
||||
permissions ||--o{ role_permissions : "granted_by"
|
||||
|
||||
%% Correspondences
|
||||
correspondences ||--o{ correspondence_revisions : "has_revisions"
|
||||
correspondence_types ||--o{ correspondences : "categorizes"
|
||||
correspondence_status ||--o{ correspondence_revisions : "defines_state"
|
||||
disciplines ||--o{ correspondences : "classifies"
|
||||
|
||||
%% RFAs
|
||||
rfas ||--o{ rfa_revisions : "has_revisions"
|
||||
rfa_types ||--o{ rfas : "categorizes"
|
||||
rfa_status_codes ||--o{ rfa_revisions : "defines_state"
|
||||
rfa_approve_codes ||--o{ rfa_revisions : "defines_result"
|
||||
disciplines ||--o{ rfas : "classifies"
|
||||
|
||||
%% Drawings
|
||||
shop_drawings ||--o{ shop_drawing_revisions : "has_revisions"
|
||||
shop_drawing_main_categories ||--o{ shop_drawings : "categorizes"
|
||||
shop_drawing_sub_categories ||--o{ shop_drawings : "sub_categorizes"
|
||||
|
||||
%% Attachments
|
||||
attachments ||--o{ correspondence_attachments : "attached_to"
|
||||
correspondences ||--o{ correspondence_attachments : "has"
|
||||
```
|
||||
|
||||
## 📊 Data Model Categories
|
||||
|
||||
### 1. 🏢 Core & Master Data
|
||||
|
||||
#### 1.1 Organizations & Projects
|
||||
|
||||
**Tables:**
|
||||
|
||||
- `organization_roles` - บทบาทขององค์กร (OWNER, DESIGNER, CONSULTANT, CONTRACTOR)
|
||||
- `organizations` - องค์กรทั้งหมดในระบบ
|
||||
- `projects` - โครงการ
|
||||
- `contracts` - สัญญาภายใต้โครงการ
|
||||
- `project_organizations` - M:N ระหว่าง Projects และ Organizations
|
||||
- `contract_organizations` - M:N ระหว่าง Contracts และ Organizations พร้อม Role
|
||||
|
||||
**Key Relationships:**
|
||||
|
||||
```
|
||||
projects (1) ──→ (N) contracts
|
||||
projects (N) ←→ (N) organizations [via project_organizations]
|
||||
contracts (N) ←→ (N) organizations [via contract_organizations]
|
||||
```
|
||||
|
||||
**Business Rules:**
|
||||
|
||||
- Organization code ต้องไม่ซ้ำกันในระบบ
|
||||
- Contract ต้องผูกกับ Project เสมอ (ON DELETE CASCADE)
|
||||
- Soft delete ใช้ `is_active` flag
|
||||
|
||||
---
|
||||
|
||||
### 2. 👥 Users & RBAC
|
||||
|
||||
#### 2.1 User Management
|
||||
|
||||
**Tables:**
|
||||
|
||||
- `users` - ผู้ใช้งานระบบ
|
||||
- `roles` - บทบาทพร้อม Scope (Global, Organization, Project, Contract)
|
||||
- `permissions` - สิทธิ์การใช้งาน (49 permissions)
|
||||
- `role_permissions` - M:N mapping
|
||||
- `user_assignments` - การมอบหมายบทบาทพร้อม Scope Context
|
||||
|
||||
**Scope Hierarchy:**
|
||||
|
||||
```
|
||||
Global (ทั้งระบบ)
|
||||
↓
|
||||
Organization (ระดับองค์กร)
|
||||
↓
|
||||
Project (ระดับโครงการ)
|
||||
↓
|
||||
Contract (ระดับสัญญา)
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **Hierarchical Scope**: User สามารถมีหลาย Role ในหลาย Scope
|
||||
- **Scope Inheritance**: สิทธิ์ระดับบนครอบคลุมระดับล่าง
|
||||
- **Account Security**: Failed login tracking, Account locking, Password hashing (bcrypt)
|
||||
|
||||
**Example User Assignment:**
|
||||
|
||||
```sql
|
||||
-- User A เป็น Editor ในองค์กร TEAM
|
||||
INSERT INTO user_assignments (user_id, role_id, organization_id)
|
||||
VALUES (1, 4, 3);
|
||||
|
||||
-- User B เป็น Project Manager ในโครงการ LCBP3
|
||||
INSERT INTO user_assignments (user_id, role_id, project_id)
|
||||
VALUES (2, 6, 1);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. ✉️ Correspondences (เอกสารโต้ตอบ)
|
||||
|
||||
#### 3.1 Master-Revision Pattern
|
||||
|
||||
**Master Table: `correspondences`**
|
||||
|
||||
เก็บข้อมูลที่ไม่เปลี่ยนแปลง:
|
||||
|
||||
- `correspondence_number` - เลขที่เอกสาร (Unique per Project)
|
||||
- `correspondence_type_id` - ประเภทเอกสาร (RFA, RFI, TRANSMITTAL, etc.)
|
||||
- `discipline_id` - สาขางาน (GEN, STR, ARC, etc.) [NEW v1.4.5]
|
||||
- `project_id`, `originator_id` - โครงการและองค์กรผู้ส่ง
|
||||
|
||||
**Revision Table: `correspondence_revisions`**
|
||||
|
||||
เก็บข้อมูลที่เปลี่ยนแปลงได้:
|
||||
|
||||
- `revision_number` - หมายเลข Revision (0, 1, 2...)
|
||||
- `is_current` - Flag สำหรับ Revision ปัจจุบัน (UNIQUE constraint)
|
||||
- `title`, `description` - เนื้อหาเอกสาร
|
||||
- `correspondence_status_id` - สถานะ (DRAFT, SUBOWN, REPCSC, etc.)
|
||||
- `details` - JSON field สำหรับข้อมูลเฉพาะประเภท
|
||||
- Virtual Columns: `v_ref_project_id`, `v_ref_type`, `v_doc_subtype` (Indexed)
|
||||
|
||||
**Supporting Tables:**
|
||||
|
||||
- `correspondence_types` - Master ประเภทเอกสาร (10 types)
|
||||
- `correspondence_status` - Master สถานะ (23 status codes)
|
||||
- `correspondence_sub_types` - ประเภทย่อยสำหรับ Document Numbering [NEW v1.4.5]
|
||||
- `disciplines` - สาขางาน (GEN, STR, ARC, etc.) [NEW v1.4.5]
|
||||
- `correspondence_recipients` - M:N ผู้รับ (TO/CC)
|
||||
- `correspondence_tags` - M:N Tags
|
||||
- `correspondence_references` - M:N Cross-references
|
||||
|
||||
**Example Query - Get Current Revision:**
|
||||
|
||||
```sql
|
||||
SELECT c.correspondence_number, cr.title, cr.revision_label, cs.status_name
|
||||
FROM correspondences c
|
||||
JOIN correspondence_revisions cr ON c.id = cr.correspondence_id
|
||||
JOIN correspondence_status cs ON cr.correspondence_status_id = cs.id
|
||||
WHERE cr.is_current = TRUE
|
||||
AND c.deleted_at IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 📐 RFAs (Request for Approval)
|
||||
|
||||
#### 4.1 RFA Structure
|
||||
|
||||
**Master Table: `rfas`**
|
||||
|
||||
- `rfa_type_id` - ประเภท RFA (DWG, DOC, MAT, SPC, etc.)
|
||||
- `discipline_id` - สาขางาน [NEW v1.4.5]
|
||||
|
||||
**Revision Table: `rfa_revisions`**
|
||||
|
||||
- `correspondence_id` - Link กับ Correspondence (RFA เป็น Correspondence ประเภทหนึ่ง)
|
||||
- `rfa_status_code_id` - สถานะ (DFT, FAP, FRE, FCO, ASB, OBS, CC)
|
||||
- `rfa_approve_code_id` - ผลการอนุมัติ (1A, 1C, 1N, 1R, 3C, 3R, 4X, 5N)
|
||||
- `approved_date` - วันที่อนุมัติ
|
||||
|
||||
**Supporting Tables:**
|
||||
|
||||
- `rfa_types` - 11 ประเภท (Shop Drawing, Document, Material, etc.)
|
||||
- `rfa_status_codes` - 7 สถานะ
|
||||
- `rfa_approve_codes` - 8 รหัสผลการอนุมัติ
|
||||
- `rfa_items` - M:N เชื่อม RFA (ประเภท DWG) กับ Shop Drawing Revisions
|
||||
|
||||
**RFA Workflow States:**
|
||||
|
||||
```
|
||||
DFT (Draft)
|
||||
↓
|
||||
FAP (For Approve) / FRE (For Review)
|
||||
↓
|
||||
[Approval Process]
|
||||
↓
|
||||
FCO (For Construction) / ASB (As-Built) / 3R (Revise) / 4X (Reject)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 📐 Drawings (แบบก่อสร้าง)
|
||||
|
||||
#### 5.1 Contract Drawings (แบบคู่สัญญา)
|
||||
|
||||
**Tables:**
|
||||
|
||||
- `contract_drawing_volumes` - เล่มแบบ
|
||||
- `contract_drawing_cats` - หมวดหมู่หลัก
|
||||
- `contract_drawing_sub_cats` - หมวดหมู่ย่อย
|
||||
- `contract_drawing_subcat_cat_maps` - M:N Mapping
|
||||
- `contract_drawings` - แบบคู่สัญญา
|
||||
|
||||
**Hierarchy:**
|
||||
|
||||
```
|
||||
Volume (เล่ม) - has volume_page
|
||||
└─ Category (หมวดหมู่หลัก)
|
||||
└─ Sub-Category (หมวดหมู่ย่อย) -- Linked via Map Table
|
||||
└─ Drawing (แบบ)
|
||||
```
|
||||
|
||||
#### 5.2 Shop Drawings (แบบก่อสร้าง)
|
||||
|
||||
**Tables:**
|
||||
|
||||
- `shop_drawing_main_categories` - หมวดหมู่หลัก (Project Specific)
|
||||
- `shop_drawing_sub_categories` - หมวดหมู่ย่อย (Project Specific)
|
||||
- `shop_drawings` - Master แบบก่อสร้าง (No title, number only)
|
||||
- `shop_drawing_revisions` - Revisions (Holds Title & Legacy Number)
|
||||
- `shop_drawing_revision_contract_refs` - M:N อ้างอิงแบบคู่สัญญา
|
||||
|
||||
**Revision Tracking:**
|
||||
|
||||
```sql
|
||||
-- Get latest revision of a shop drawing
|
||||
SELECT sd.shop_drawing_number, sdr.revision_label, sdr.revision_date
|
||||
FROM shop_drawings sd
|
||||
JOIN shop_drawing_revisions sdr ON sd.id = sdr.shop_drawing_id
|
||||
WHERE sd.shop_drawing_number = 'SD-STR-001'
|
||||
ORDER BY sdr.revision_number DESC
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
#### 5.3 As Built Drawings (แบบสร้างจริง) [NEW v1.7.0]
|
||||
|
||||
**Tables:**
|
||||
|
||||
- `asbuilt_drawings` - Master แบบสร้างจริง
|
||||
- `asbuilt_drawing_revisions` - Revisions history
|
||||
- `asbuilt_revision_shop_revisions_refs` - เชื่อมโยงกับ Shop Drawing Revision source
|
||||
- `asbuilt_drawing_revision_attachments` - ไฟล์แนบ (PDF/DWG)
|
||||
|
||||
**Business Rules:**
|
||||
|
||||
- As Built 1 ใบ อาจมาจาก Shop Drawing หลายใบ (Many-to-Many via refs table)
|
||||
- แยก Counter distinct จาก Shop Drawing และ Contract Drawing
|
||||
- รองรับไฟล์แนบหลายประเภท (PDF, DWG, SOURCE)
|
||||
|
||||
---
|
||||
|
||||
### 6. 🔄 Circulations & Transmittals
|
||||
|
||||
#### 6.1 Circulations (ใบเวียนภายใน)
|
||||
|
||||
**Tables:**
|
||||
|
||||
- `circulation_status_codes` - สถานะ (OPEN, IN_REVIEW, COMPLETED, CANCELLED)
|
||||
- `circulations` - ใบเวียน (1:1 กับ Correspondence)
|
||||
|
||||
**Workflow:**
|
||||
|
||||
```
|
||||
OPEN → IN_REVIEW → COMPLETED
|
||||
↓
|
||||
CANCELLED
|
||||
```
|
||||
|
||||
#### 6.2 Transmittals (เอกสารนำส่ง)
|
||||
|
||||
**Tables:**
|
||||
|
||||
- `transmittals` - ข้อมูล Transmittal (1:1 กับ Correspondence)
|
||||
- `transmittal_items` - M:N รายการเอกสารที่นำส่ง
|
||||
|
||||
**Purpose Types:**
|
||||
|
||||
- FOR_APPROVAL
|
||||
- FOR_INFORMATION
|
||||
- FOR_REVIEW
|
||||
- OTHER
|
||||
|
||||
---
|
||||
|
||||
### 7. 📎 File Management
|
||||
|
||||
#### 7.1 Two-Phase Storage Pattern
|
||||
|
||||
**Table: `attachments`**
|
||||
|
||||
**Phase 1: Temporary Upload**
|
||||
|
||||
```sql
|
||||
INSERT INTO attachments (
|
||||
original_filename, stored_filename, file_path,
|
||||
mime_type, file_size, is_temporary, temp_id,
|
||||
uploaded_by_user_id, expires_at, checksum
|
||||
)
|
||||
VALUES (
|
||||
'document.pdf', 'uuid-document.pdf', '/temp/uuid-document.pdf',
|
||||
'application/pdf', 1024000, TRUE, 'temp-uuid-123',
|
||||
1, NOW() + INTERVAL 1 HOUR, 'sha256-hash'
|
||||
);
|
||||
```
|
||||
|
||||
**Phase 2: Commit to Permanent**
|
||||
|
||||
```sql
|
||||
-- Update attachment to permanent
|
||||
UPDATE attachments
|
||||
SET is_temporary = FALSE, expires_at = NULL
|
||||
WHERE temp_id = 'temp-uuid-123';
|
||||
|
||||
-- Link to correspondence
|
||||
INSERT INTO correspondence_attachments (correspondence_id, attachment_id, is_main_document)
|
||||
VALUES (1, 123, TRUE);
|
||||
```
|
||||
|
||||
**Junction Tables:**
|
||||
|
||||
- `correspondence_attachments` - M:N
|
||||
- `circulation_attachments` - M:N
|
||||
- `shop_drawing_revision_attachments` - M:N (with file_type)
|
||||
- `contract_drawing_attachments` - M:N (with file_type)
|
||||
|
||||
**Security Features:**
|
||||
|
||||
- Checksum validation (SHA-256)
|
||||
- Automatic cleanup of expired temporary files
|
||||
- File type validation via `mime_type`
|
||||
|
||||
---
|
||||
|
||||
### 8. 🔢 Document Numbering
|
||||
|
||||
#### 8.1 Format & Counter System
|
||||
|
||||
**Tables:**
|
||||
|
||||
- `document_number_formats` - Template รูปแบบเลขที่เอกสาร
|
||||
- `document_number_counters` - Running Number Counter with Optimistic Locking
|
||||
|
||||
**Format Template Example:**
|
||||
|
||||
```
|
||||
{ORG_CODE}-{TYPE_CODE}-{DISCIPLINE_CODE}-{YEAR}-{SEQ:4}
|
||||
→ TEAM-RFA-STR-2025-0001
|
||||
```
|
||||
|
||||
**Counter Table Structure:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE document_number_counters (
|
||||
project_id INT,
|
||||
originator_organization_id INT,
|
||||
correspondence_type_id INT,
|
||||
discipline_id INT DEFAULT 0, -- NEW v1.4.5
|
||||
current_year INT,
|
||||
version INT DEFAULT 0, -- Optimistic Lock
|
||||
last_number INT DEFAULT 0,
|
||||
PRIMARY KEY (
|
||||
project_id,
|
||||
originator_organization_id,
|
||||
recipient_organization_id,
|
||||
correspondence_type_id,
|
||||
sub_type_id,
|
||||
rfa_type_id,
|
||||
discipline_id,
|
||||
reset_scope
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
**Optimistic Locking Pattern:**
|
||||
|
||||
```sql
|
||||
-- Get next number with version check
|
||||
UPDATE document_number_counters
|
||||
SET last_number = last_number + 1,
|
||||
version = version + 1
|
||||
WHERE project_id = 1
|
||||
AND originator_organization_id = 3
|
||||
AND correspondence_type_id = 1
|
||||
AND discipline_id = 2
|
||||
AND current_year = 2025
|
||||
AND version = @current_version; -- Optimistic lock check
|
||||
|
||||
-- If affected rows = 0, retry (conflict detected)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security & Audit
|
||||
|
||||
### 1. Audit Logging
|
||||
|
||||
**Table: `audit_logs`**
|
||||
|
||||
บันทึกการเปลี่ยนแปลงสำคัญ:
|
||||
|
||||
- User actions (CREATE, UPDATE, DELETE)
|
||||
- Entity type และ Entity ID
|
||||
- Old/New values (JSON)
|
||||
- IP Address, User Agent
|
||||
|
||||
### 2. User Preferences
|
||||
|
||||
**Table: `user_preferences`**
|
||||
|
||||
เก็บการตั้งค่าส่วนตัว:
|
||||
|
||||
- Language preference
|
||||
- Notification settings
|
||||
- UI preferences (JSON)
|
||||
|
||||
### 3. JSON Schema Validation
|
||||
|
||||
**Table: `json_schemas`**
|
||||
|
||||
เก็บ Schema สำหรับ Validate JSON fields:
|
||||
|
||||
- `correspondence_revisions.details`
|
||||
- `user_preferences.preferences`
|
||||
|
||||
---
|
||||
|
||||
## 📈 Performance Optimization
|
||||
|
||||
### 1. Indexing Strategy
|
||||
|
||||
**Primary Indexes:**
|
||||
|
||||
- Primary Keys (AUTO_INCREMENT)
|
||||
- Foreign Keys (automatic in InnoDB)
|
||||
- Unique Constraints (business keys)
|
||||
|
||||
**Secondary Indexes:**
|
||||
|
||||
```sql
|
||||
-- Correspondence search
|
||||
CREATE INDEX idx_corr_type_status ON correspondence_revisions(correspondence_type_id, correspondence_status_id);
|
||||
CREATE INDEX idx_corr_date ON correspondence_revisions(document_date);
|
||||
|
||||
-- Virtual columns for JSON
|
||||
CREATE INDEX idx_v_ref_project ON correspondence_revisions(v_ref_project_id);
|
||||
CREATE INDEX idx_v_doc_subtype ON correspondence_revisions(v_doc_subtype);
|
||||
|
||||
-- User lookup
|
||||
CREATE INDEX idx_user_email ON users(email);
|
||||
CREATE INDEX idx_user_org ON users(primary_organization_id, is_active);
|
||||
```
|
||||
|
||||
### 2. Virtual Columns
|
||||
|
||||
ใช้ Virtual Columns สำหรับ Index JSON fields:
|
||||
|
||||
```sql
|
||||
ALTER TABLE correspondence_revisions
|
||||
ADD COLUMN v_ref_project_id INT GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(details, '$.ref_project_id'))) VIRTUAL,
|
||||
ADD INDEX idx_v_ref_project(v_ref_project_id);
|
||||
```
|
||||
|
||||
### 3. Partitioning (Future)
|
||||
|
||||
พิจารณา Partition ตาราง `audit_logs` ตามปี:
|
||||
|
||||
```sql
|
||||
ALTER TABLE audit_logs
|
||||
PARTITION BY RANGE (YEAR(created_at)) (
|
||||
PARTITION p2024 VALUES LESS THAN (2025),
|
||||
PARTITION p2025 VALUES LESS THAN (2026),
|
||||
PARTITION p_future VALUES LESS THAN MAXVALUE
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Migration Strategy
|
||||
|
||||
### 1. TypeORM Migrations
|
||||
|
||||
ใช้ TypeORM Migration สำหรับ Schema Changes:
|
||||
|
||||
```typescript
|
||||
// File: backend/src/migrations/1234567890-AddDisciplineToCorrespondences.ts
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddDisciplineToCorrespondences1234567890
|
||||
implements MigrationInterface
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE correspondences
|
||||
ADD COLUMN discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)'
|
||||
AFTER correspondence_type_id
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE correspondences
|
||||
ADD CONSTRAINT fk_corr_discipline
|
||||
FOREIGN KEY (discipline_id) REFERENCES disciplines(id)
|
||||
ON DELETE SET NULL
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE correspondences DROP FOREIGN KEY fk_corr_discipline`
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE correspondences DROP COLUMN discipline_id`
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Data Seeding
|
||||
|
||||
ใช้ Seed Scripts สำหรับ Master Data:
|
||||
|
||||
```typescript
|
||||
// File: backend/src/seeds/1-organizations.seed.ts
|
||||
export class OrganizationSeeder implements Seeder {
|
||||
public async run(dataSource: DataSource): Promise<void> {
|
||||
const repository = dataSource.getRepository(Organization);
|
||||
await repository.save([
|
||||
{
|
||||
organization_code: 'กทท.',
|
||||
organization_name: 'Port Authority of Thailand',
|
||||
},
|
||||
{
|
||||
organization_code: 'TEAM',
|
||||
organization_name: 'TEAM Consulting Engineering',
|
||||
},
|
||||
// ...
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Best Practices
|
||||
|
||||
### 1. Naming Conventions
|
||||
|
||||
- **Tables**: `snake_case`, plural (e.g., `correspondences`, `users`)
|
||||
- **Columns**: `snake_case` (e.g., `correspondence_number`, `created_at`)
|
||||
- **Foreign Keys**: `{referenced_table_singular}_id` (e.g., `project_id`, `user_id`)
|
||||
- **Junction Tables**: `{table1}_{table2}` (e.g., `correspondence_tags`)
|
||||
|
||||
### 2. Timestamp Columns
|
||||
|
||||
ทุกตารางควรมี:
|
||||
|
||||
- `created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`
|
||||
- `updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`
|
||||
|
||||
### 3. Soft Delete
|
||||
|
||||
ใช้ `deleted_at DATETIME NULL` แทนการลบจริง:
|
||||
|
||||
```sql
|
||||
-- Soft delete
|
||||
UPDATE correspondences SET deleted_at = NOW() WHERE id = 1;
|
||||
|
||||
-- Query active records
|
||||
SELECT * FROM correspondences WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
### 4. JSON Field Guidelines
|
||||
|
||||
- ใช้สำหรับข้อมูลที่ไม่ต้อง Query บ่อย
|
||||
- สร้าง Virtual Columns สำหรับ fields ที่ต้อง Index
|
||||
- Validate ด้วย JSON Schema
|
||||
- Document structure ใน Data Dictionary
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Documentation
|
||||
|
||||
- [System Architecture](../01-requirements/01-02-architecture.md) - สถาปัตยกรรมระบบโดยรวม
|
||||
- [API Design](02-02-api-design.md) - การออกแบบ API
|
||||
- [Data Dictionary v1.4.5](../../docs/4_Data_Dictionary_V1_4_5.md) - รายละเอียดตารางทั้งหมด
|
||||
- [SQL Schema v1.4.5](../../docs/8_lcbp3_v1_4_5.sql) - SQL Script สำหรับสร้างฐานข้อมูล
|
||||
- [Functional Requirements](../01-requirements/01-03-functional-requirements.md) - ความต้องการด้านฟังก์ชัน
|
||||
|
||||
---
|
||||
|
||||
## 📝 Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
| ------- | ---------- | -------------------- | ---------------------------------------------- |
|
||||
| 1.5.0 | 2025-11-30 | Nattanin Peancharoen | Initial data model documentation |
|
||||
| 1.4.5 | 2025-11-29 | System | Added disciplines and correspondence_sub_types |
|
||||
1919
specs/03-Data-and-Storage/99-archives/data-dictionary-v1.7.0.md
Normal file
1919
specs/03-Data-and-Storage/99-archives/data-dictionary-v1.7.0.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `attachments`
|
||||
ADD COLUMN `reference_date` DATE NULL COMMENT 'Date used for folder structure (e.g. Issue Date) to prevent broken paths';
|
||||
ALTER TABLE `attachments`
|
||||
ADD INDEX `idx_attachments_reference_date` (`reference_date`);
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Add permission for Bulk RBAC Update
|
||||
-- Fix: Use existing 'user.manage_assignments' instead of creating new invalid permission
|
||||
-- This permission (ID 25) is for assigning roles/projects
|
||||
-- Grant to ADMIN (ID 2) and DC (ID 3) roles if not already present
|
||||
-- Use INSERT IGNORE to avoid duplicates
|
||||
INSERT IGNORE INTO `role_permissions` (`role_id`, `permission_id`)
|
||||
SELECT r.role_id,
|
||||
p.permission_id
|
||||
FROM `roles` r,
|
||||
`permissions` p
|
||||
WHERE r.role_name IN ('ADMIN', 'Org Admin', 'DC', 'Document Control')
|
||||
AND p.permission_name = 'user.manage_assignments';
|
||||
29
specs/03-Data-and-Storage/fix-project-permissions.sql
Normal file
29
specs/03-Data-and-Storage/fix-project-permissions.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- Fix Project Permissions
|
||||
-- File: specs/07-database/fix-project-permissions.sql
|
||||
-- 1. Ensure project.view permission exists
|
||||
INSERT IGNORE INTO permissions (
|
||||
permission_id,
|
||||
permission_name,
|
||||
description,
|
||||
module,
|
||||
is_active
|
||||
)
|
||||
VALUES (
|
||||
202,
|
||||
'project.view',
|
||||
'ดูรายการโครงการ',
|
||||
'project',
|
||||
1
|
||||
);
|
||||
-- 2. Grant project.view to Superadmin (Role 1)
|
||||
INSERT IGNORE INTO role_permissions (role_id, permission_id)
|
||||
VALUES (1, 202);
|
||||
-- 3. Grant project.view to Organization Admin (Role 2)
|
||||
INSERT IGNORE INTO role_permissions (role_id, permission_id)
|
||||
VALUES (2, 202);
|
||||
-- 4. Grant project.view to Project Manager (Role 6)
|
||||
INSERT IGNORE INTO role_permissions (role_id, permission_id)
|
||||
VALUES (6, 202);
|
||||
-- 5. Grant project.view to Viewer (Role 5)
|
||||
INSERT IGNORE INTO role_permissions (role_id, permission_id)
|
||||
VALUES (5, 202);
|
||||
2056
specs/03-Data-and-Storage/lcbp3-v1.7.0-schema.sql
Normal file
2056
specs/03-Data-and-Storage/lcbp3-v1.7.0-schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
2174
specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-basic.sql
Normal file
2174
specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-basic.sql
Normal file
File diff suppressed because it is too large
Load Diff
2372
specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-contractdrawing.sql
Normal file
2372
specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-contractdrawing.sql
Normal file
File diff suppressed because it is too large
Load Diff
1067
specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-permissions.sql
Normal file
1067
specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-permissions.sql
Normal file
File diff suppressed because it is too large
Load Diff
5626
specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-shopdrawing.sql
Normal file
5626
specs/03-Data-and-Storage/lcbp3-v1.7.0-seed-shopdrawing.sql
Normal file
File diff suppressed because it is too large
Load Diff
276
specs/03-Data-and-Storage/permissions-verification.sql
Normal file
276
specs/03-Data-and-Storage/permissions-verification.sql
Normal file
@@ -0,0 +1,276 @@
|
||||
-- ==========================================================
|
||||
-- Permission System Verification Queries
|
||||
-- File: specs/07-database/permissions-verification.sql
|
||||
-- Purpose: Verify permissions setup after seed data deployment
|
||||
-- ==========================================================
|
||||
-- ==========================================================
|
||||
-- 1. COUNT PERMISSIONS PER CATEGORY
|
||||
-- ==========================================================
|
||||
SELECT CASE
|
||||
WHEN permission_id BETWEEN 1 AND 10 THEN '001-010: System & Global'
|
||||
WHEN permission_id BETWEEN 11 AND 20 THEN '011-020: Organization Management'
|
||||
WHEN permission_id BETWEEN 21 AND 40 THEN '021-040: User & Role Management'
|
||||
WHEN permission_id BETWEEN 41 AND 50 THEN '041-050: Master Data Management'
|
||||
WHEN permission_id BETWEEN 51 AND 70 THEN '051-070: Document Management (Generic)'
|
||||
WHEN permission_id BETWEEN 71 AND 80 THEN '071-080: Correspondence Module'
|
||||
WHEN permission_id BETWEEN 81 AND 90 THEN '081-090: RFA Module'
|
||||
WHEN permission_id BETWEEN 91 AND 100 THEN '091-100: Drawing Module'
|
||||
WHEN permission_id BETWEEN 101 AND 110 THEN '101-110: Circulation Module'
|
||||
WHEN permission_id BETWEEN 111 AND 120 THEN '111-120: Transmittal Module'
|
||||
WHEN permission_id BETWEEN 121 AND 130 THEN '121-130: Workflow Engine'
|
||||
WHEN permission_id BETWEEN 131 AND 140 THEN '131-140: Document Numbering'
|
||||
WHEN permission_id BETWEEN 141 AND 150 THEN '141-150: Search & Reporting'
|
||||
WHEN permission_id BETWEEN 151 AND 160 THEN '151-160: Notification & Dashboard'
|
||||
WHEN permission_id BETWEEN 161 AND 170 THEN '161-170: JSON Schema Management'
|
||||
WHEN permission_id BETWEEN 171 AND 180 THEN '171-180: Monitoring & Admin Tools'
|
||||
WHEN permission_id BETWEEN 201 AND 220 THEN '201-220: Project & Contract Management'
|
||||
ELSE 'Unknown Range'
|
||||
END AS category_range,
|
||||
COUNT(*) AS permission_count
|
||||
FROM permissions
|
||||
GROUP BY CASE
|
||||
WHEN permission_id BETWEEN 1 AND 10 THEN '001-010: System & Global'
|
||||
WHEN permission_id BETWEEN 11 AND 20 THEN '011-020: Organization Management'
|
||||
WHEN permission_id BETWEEN 21 AND 40 THEN '021-040: User & Role Management'
|
||||
WHEN permission_id BETWEEN 41 AND 50 THEN '041-050: Master Data Management'
|
||||
WHEN permission_id BETWEEN 51 AND 70 THEN '051-070: Document Management (Generic)'
|
||||
WHEN permission_id BETWEEN 71 AND 80 THEN '071-080: Correspondence Module'
|
||||
WHEN permission_id BETWEEN 81 AND 90 THEN '081-090: RFA Module'
|
||||
WHEN permission_id BETWEEN 91 AND 100 THEN '091-100: Drawing Module'
|
||||
WHEN permission_id BETWEEN 101 AND 110 THEN '101-110: Circulation Module'
|
||||
WHEN permission_id BETWEEN 111 AND 120 THEN '111-120: Transmittal Module'
|
||||
WHEN permission_id BETWEEN 121 AND 130 THEN '121-130: Workflow Engine'
|
||||
WHEN permission_id BETWEEN 131 AND 140 THEN '131-140: Document Numbering'
|
||||
WHEN permission_id BETWEEN 141 AND 150 THEN '141-150: Search & Reporting'
|
||||
WHEN permission_id BETWEEN 151 AND 160 THEN '151-160: Notification & Dashboard'
|
||||
WHEN permission_id BETWEEN 161 AND 170 THEN '161-170: JSON Schema Management'
|
||||
WHEN permission_id BETWEEN 171 AND 180 THEN '171-180: Monitoring & Admin Tools'
|
||||
WHEN permission_id BETWEEN 201 AND 220 THEN '201-220: Project & Contract Management'
|
||||
ELSE 'Unknown Range'
|
||||
END
|
||||
ORDER BY MIN(permission_id);
|
||||
|
||||
-- ==========================================================
|
||||
-- 2. COUNT PERMISSIONS PER ROLE
|
||||
-- ==========================================================
|
||||
SELECT r.role_id,
|
||||
r.role_name,
|
||||
r.scope,
|
||||
COUNT(rp.permission_id) AS permission_count
|
||||
FROM roles r
|
||||
LEFT JOIN role_permissions rp ON r.role_id = rp.role_id
|
||||
GROUP BY r.role_id,
|
||||
r.role_name,
|
||||
r.scope
|
||||
ORDER BY r.role_id;
|
||||
|
||||
-- ==========================================================
|
||||
-- 3. CHECK TOTAL PERMISSION COUNT
|
||||
-- ==========================================================
|
||||
SELECT 'Total Permissions' AS metric,
|
||||
COUNT(*) AS COUNT
|
||||
FROM permissions
|
||||
UNION ALL
|
||||
SELECT 'Active Permissions',
|
||||
COUNT(*)
|
||||
FROM permissions
|
||||
WHERE is_active = 1;
|
||||
|
||||
-- ==========================================================
|
||||
-- 4. CHECK FOR MISSING PERMISSIONS (Used in Code but Not in DB)
|
||||
-- ==========================================================
|
||||
-- List of permissions actually used in controllers
|
||||
WITH code_permissions AS (
|
||||
SELECT 'system.manage_all' AS permission_name
|
||||
UNION
|
||||
SELECT 'system.impersonate'
|
||||
UNION
|
||||
SELECT 'organization.view'
|
||||
UNION
|
||||
SELECT 'organization.create'
|
||||
UNION
|
||||
SELECT 'user.create'
|
||||
UNION
|
||||
SELECT 'user.view'
|
||||
UNION
|
||||
SELECT 'user.edit'
|
||||
UNION
|
||||
SELECT 'user.delete'
|
||||
UNION
|
||||
SELECT 'user.manage_assignments'
|
||||
UNION
|
||||
SELECT 'role.assign_permissions'
|
||||
UNION
|
||||
SELECT 'project.create'
|
||||
UNION
|
||||
SELECT 'project.view'
|
||||
UNION
|
||||
SELECT 'project.edit'
|
||||
UNION
|
||||
SELECT 'project.delete'
|
||||
UNION
|
||||
SELECT 'contract.create'
|
||||
UNION
|
||||
SELECT 'contract.view'
|
||||
UNION
|
||||
SELECT 'contract.edit'
|
||||
UNION
|
||||
SELECT 'contract.delete'
|
||||
UNION
|
||||
SELECT 'master_data.view'
|
||||
UNION
|
||||
SELECT 'master_data.manage'
|
||||
UNION
|
||||
SELECT 'master_data.drawing_category.manage'
|
||||
UNION
|
||||
SELECT 'master_data.tag.manage'
|
||||
UNION
|
||||
SELECT 'document.view'
|
||||
UNION
|
||||
SELECT 'document.create'
|
||||
UNION
|
||||
SELECT 'document.edit'
|
||||
UNION
|
||||
SELECT 'document.delete'
|
||||
UNION
|
||||
SELECT 'correspondence.create'
|
||||
UNION
|
||||
SELECT 'rfa.create'
|
||||
UNION
|
||||
SELECT 'drawing.create'
|
||||
UNION
|
||||
SELECT 'drawing.view'
|
||||
UNION
|
||||
SELECT 'circulation.create'
|
||||
UNION
|
||||
SELECT 'circulation.respond'
|
||||
UNION
|
||||
SELECT 'workflow.action_review'
|
||||
UNION
|
||||
SELECT 'workflow.manage_definitions'
|
||||
UNION
|
||||
SELECT 'search.advanced'
|
||||
UNION
|
||||
SELECT 'json_schema.view'
|
||||
UNION
|
||||
SELECT 'json_schema.manage'
|
||||
UNION
|
||||
SELECT 'monitoring.manage_maintenance'
|
||||
)
|
||||
SELECT cp.permission_name,
|
||||
CASE
|
||||
WHEN p.permission_id IS NULL THEN '❌ MISSING'
|
||||
ELSE '✅ EXISTS'
|
||||
END AS STATUS,
|
||||
p.permission_id
|
||||
FROM code_permissions cp
|
||||
LEFT JOIN permissions p ON cp.permission_name = p.permission_name
|
||||
ORDER BY STATUS DESC,
|
||||
cp.permission_name;
|
||||
|
||||
-- ==========================================================
|
||||
-- 5. LIST PERMISSIONS FOR EACH ROLE
|
||||
-- ==========================================================
|
||||
SELECT r.role_name,
|
||||
r.scope,
|
||||
GROUP_CONCAT(
|
||||
p.permission_name
|
||||
ORDER BY p.permission_id SEPARATOR ', '
|
||||
) AS permissions
|
||||
FROM roles r
|
||||
LEFT JOIN role_permissions rp ON r.role_id = rp.role_id
|
||||
LEFT JOIN permissions p ON rp.permission_id = p.permission_id
|
||||
GROUP BY r.role_id,
|
||||
r.role_name,
|
||||
r.scope
|
||||
ORDER BY r.role_id;
|
||||
|
||||
-- ==========================================================
|
||||
-- 6. CHECK SUPERADMIN HAS ALL PERMISSIONS
|
||||
-- ==========================================================
|
||||
SELECT 'Superadmin Permission Coverage' AS metric,
|
||||
CONCAT(
|
||||
COUNT(DISTINCT rp.permission_id),
|
||||
' / ',
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM permissions
|
||||
WHERE is_active = 1
|
||||
),
|
||||
' (',
|
||||
ROUND(
|
||||
COUNT(DISTINCT rp.permission_id) * 100.0 / (
|
||||
SELECT COUNT(*)
|
||||
FROM permissions
|
||||
WHERE is_active = 1
|
||||
),
|
||||
1
|
||||
),
|
||||
'%)'
|
||||
) AS coverage
|
||||
FROM role_permissions rp
|
||||
WHERE rp.role_id = 1;
|
||||
|
||||
-- Superadmin
|
||||
-- ==========================================================
|
||||
-- 7. CHECK FOR DUPLICATE PERMISSIONS
|
||||
-- ==========================================================
|
||||
SELECT permission_name,
|
||||
COUNT(*) AS duplicate_count
|
||||
FROM permissions
|
||||
GROUP BY permission_name
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- ==========================================================
|
||||
-- 8. CHECK PERMISSIONS WITHOUT ROLE ASSIGNMENTS
|
||||
-- ==========================================================
|
||||
SELECT p.permission_id,
|
||||
p.permission_name,
|
||||
p.description
|
||||
FROM permissions p
|
||||
LEFT JOIN role_permissions rp ON p.permission_id = rp.permission_id
|
||||
WHERE rp.permission_id IS NULL
|
||||
AND p.is_active = 1
|
||||
ORDER BY p.permission_id;
|
||||
|
||||
-- ==========================================================
|
||||
-- 9. CHECK USER PERMISSION VIEW (v_user_all_permissions)
|
||||
-- ==========================================================
|
||||
-- Test with user_id = 1 (Superadmin)
|
||||
SELECT 'User 1 (Superadmin) Permissions' AS metric,
|
||||
COUNT(*) AS permission_count
|
||||
FROM v_user_all_permissions
|
||||
WHERE user_id = 1;
|
||||
|
||||
-- List first 10 permissions for user 1
|
||||
SELECT user_id,
|
||||
permission_name
|
||||
FROM v_user_all_permissions
|
||||
WHERE user_id = 1
|
||||
ORDER BY permission_name
|
||||
LIMIT 10;
|
||||
|
||||
-- ==========================================================
|
||||
-- 10. CHECK SPECIFIC CRITICAL PERMISSIONS
|
||||
-- ==========================================================
|
||||
SELECT permission_name,
|
||||
permission_id,
|
||||
CASE
|
||||
WHEN permission_id IS NOT NULL THEN '✅ Exists'
|
||||
ELSE '❌ Missing'
|
||||
END AS STATUS
|
||||
FROM (
|
||||
SELECT 'system.manage_all' AS permission_name
|
||||
UNION
|
||||
SELECT 'document.view'
|
||||
UNION
|
||||
SELECT 'user.create'
|
||||
UNION
|
||||
SELECT 'master_data.manage'
|
||||
UNION
|
||||
SELECT 'drawing.view'
|
||||
UNION
|
||||
SELECT 'workflow.action_review'
|
||||
) required_perms
|
||||
LEFT JOIN permissions p USING (permission_name)
|
||||
ORDER BY permission_name;
|
||||
Reference in New Issue
Block a user