260222:1053 20260222 refactor specs/ #1 03-Data-and-Storage
Build and Deploy / deploy (push) Successful in 1m0s
Build and Deploy / deploy (push) Successful in 1m0s
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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 ได้สูงสุดครับ
|
||||
@@ -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)
|
||||
@@ -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 |
|
||||
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';
|
||||
@@ -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);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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