260316:1117 20260316:1100 Refactor UUID
Build and Deploy / deploy (push) Successful in 9m24s

This commit is contained in:
admin
2026-03-16 11:17:15 +07:00
parent b93cd91325
commit c5c3ed9016
92 changed files with 1726 additions and 620 deletions
@@ -104,7 +104,7 @@ UNIQUE | Role name (
* * Purpose **: MASTER TABLE storing ALL organizations involved IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ----------------- | ------------ | ----------------------------------- | ---------------------------------------- |
| id | INT | PRIMARY KEY,
AUTO_INCREMENT | UNIQUE identifier FOR organization | | organization_code | VARCHAR(20) | NOT NULL,
AUTO_INCREMENT | UNIQUE identifier FOR organization | | uuid | UUID | NOT NULL, UNIQUE, DEFAULT UUID() | UUID Public Identifier (ADR-019) | | organization_code | VARCHAR(20) | NOT NULL,
UNIQUE | Organization code (e.g., 'กทท.', 'TEAM') | | organization_name | VARCHAR(255) | NOT NULL | FULL organization name | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS (1 = active, 0 = inactive) | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last
UPDATE timestamp |
| deleted_at | DATETIME | NULL | Soft delete timestamp | ** INDEXES **: - PRIMARY KEY (id) - UNIQUE (organization_code) - INDEX (is_active) ** Relationships **: - Referenced by: users,
@@ -117,7 +117,7 @@ UPDATE timestamp |
* * Purpose **: MASTER TABLE FOR ALL projects IN the system | COLUMN Name | Data TYPE | Constraints | Description | | ------------ | ------------ | --------------------------- | ----------------------------- |
| id | INT | PRIMARY KEY,
AUTO_INCREMENT | UNIQUE identifier FOR project | | project_code | VARCHAR(50) | NOT NULL,
AUTO_INCREMENT | UNIQUE identifier FOR project | | uuid | UUID | NOT NULL, UNIQUE, DEFAULT UUID() | UUID Public Identifier (ADR-019) | | project_code | VARCHAR(50) | NOT NULL,
UNIQUE | Project code (e.g., 'LCBP3') | | project_name | VARCHAR(255) | NOT NULL | FULL project name | | is_active | TINYINT(1) | DEFAULT 1 | Active STATUS |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last update timestamp |
@@ -131,7 +131,7 @@ UPDATE timestamp |
* * Purpose **: MASTER TABLE FOR contracts within projects | COLUMN Name | Data TYPE | Constraints | Description | | ------------- | ------------ | ----------------------------------- | ------------------------------ |
| id | INT | PRIMARY KEY,
AUTO_INCREMENT | UNIQUE identifier FOR contract | | project_id | INT | NOT NULL,
AUTO_INCREMENT | UNIQUE identifier FOR contract | | uuid | UUID | NOT NULL, UNIQUE, DEFAULT UUID() | UUID Public Identifier (ADR-019) | | project_id | INT | NOT NULL,
FK | Reference TO projects TABLE | | contract_code | VARCHAR(50) | NOT NULL,
UNIQUE | Contract code | | contract_name | VARCHAR(255) | NOT NULL | FULL contract name | | description | TEXT | NULL | Contract description | | start_date | DATE | NULL | Contract START date | | end_date | DATE | NULL | Contract
END date | | is_active | BOOLEAN | DEFAULT TRUE | Active STATUS | | created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | Record creation timestamp | | updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE | Last
@@ -152,7 +152,7 @@ UPDATE timestamp |
* * Purpose **: MASTER TABLE storing ALL system users | COLUMN Name | Data TYPE | Constraints | Description | | ----------------------- | ------------ | ----------------------------------- | -------------------------------- |
| user_id | INT | PRIMARY KEY,
AUTO_INCREMENT | UNIQUE identifier FOR user | | username | VARCHAR(50) | NOT NULL,
AUTO_INCREMENT | UNIQUE identifier FOR user | | uuid | UUID | NOT NULL, UNIQUE, DEFAULT UUID() | UUID Public Identifier (ADR-019) | | username | VARCHAR(50) | NOT NULL,
UNIQUE | Login username | | password_hash | VARCHAR(255) | NOT NULL | Hashed PASSWORD (bcrypt) | | first_name | VARCHAR(50) | NULL | User 's first name |
| last_name | VARCHAR(50) | NULL | User' s last name | | email | VARCHAR(100) | NOT NULL,
UNIQUE | Email address | | line_id | VARCHAR(100) | NULL | LINE messenger ID | | primary_organization_id | INT | NULL,
@@ -335,6 +335,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| ------------------------- | ------------ | --------------------------- | ------------------------------------------ |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Master correspondence ID |
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
| correspondence_number | VARCHAR(100) | NOT NULL | Document number (from numbering system) |
| correspondence_type_id | INT | NOT NULL, FK | Reference to correspondence_types |
| **discipline_id** | **INT** | **NULL, FK** | **[NEW] สาขางาน (ถ้ามี)** |
@@ -354,6 +355,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
* FOREIGN KEY (originator_id) REFERENCES organizations(id) ON DELETE SET NULL
* FOREIGN KEY (created_by) REFERENCES users(user_id) ON DELETE SET NULL
* UNIQUE KEY (project_id, correspondence_number)
* UNIQUE INDEX idx_correspondences_uuid (uuid)
* INDEX (correspondence_type_id)
* INDEX (originator_id)
* INDEX (deleted_at)
@@ -372,6 +374,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| ------------------------ | ------------ | --------------------------------- | -------------------------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
| correspondence_id | INT | NOT NULL, FK | Master correspondence ID |
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, 1.1...) |
@@ -824,6 +827,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| --------------- | ------------ | ----------------------------------- | ---------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID |
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
| project_id | INT | NOT NULL, FK | Reference to projects |
| condwg_no | VARCHAR(255) | NOT NULL | Contract drawing number |
| title | VARCHAR(255) | NOT NULL | Drawing title |
@@ -843,6 +847,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
* FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes(id) ON DELETE RESTRICT
* FOREIGN KEY (updated_by) REFERENCES users(user_id)
* UNIQUE KEY (project_id, condwg_no)
* UNIQUE INDEX idx_contract_drawings_uuid (uuid)
* INDEX (map_cat_id)
* INDEX (volume_id)
* INDEX (deleted_at)
@@ -942,6 +947,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| ---------------- | ------------ | ----------------------------------- | -------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID |
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
| project_id | INT | NOT NULL, FK | Reference to projects |
| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | Shop drawing number |
| main_category_id | INT | NOT NULL, FK | Reference to main category |
@@ -955,6 +961,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
* PRIMARY KEY (id)
* UNIQUE (drawing_number)
* UNIQUE INDEX idx_shop_drawings_uuid (uuid)
* FOREIGN KEY (project_id) REFERENCES projects(id)
* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id)
* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id)
@@ -986,6 +993,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| ------------------------- | ---------------- | --------------------------- | ---------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
| shop_drawing_id | INT | NOT NULL, FK | Master shop drawing ID |
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) |
@@ -1000,6 +1008,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
* PRIMARY KEY (id)
* FOREIGN KEY (shop_drawing_id) REFERENCES shop_drawings(id) ON DELETE CASCADE
* UNIQUE KEY (shop_drawing_id, revision_number)
* UNIQUE INDEX idx_shop_drawing_revisions_uuid (uuid)
* INDEX (revision_date)
**Relationships**:
@@ -1053,6 +1062,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| ---------------- | ------------ | ----------------------------------- | -------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique drawing ID |
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
| project_id | INT | NOT NULL, FK | Reference to projects |
| drawing_number | VARCHAR(100) | NOT NULL, UNIQUE | AS Built drawing number |
| main_category_id | INT | NOT NULL, FK | Reference to main category |
@@ -1066,6 +1076,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
* PRIMARY KEY (id)
* UNIQUE (drawing_number)
* UNIQUE INDEX idx_asbuilt_drawings_uuid (uuid)
* FOREIGN KEY (project_id) REFERENCES projects(id)
* FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories(id)
* FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories(id)
@@ -1097,6 +1108,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| --------------------- | ------------ | --------------------------- | ------------------------------ |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique revision ID |
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
| asbuilt_drawing_id | INT | NOT NULL, FK | Master AS Built drawing ID |
| revision_number | INT | NOT NULL | Revision sequence (0, 1, 2...) |
| revision_label | VARCHAR(10) | NULL | Display revision (A, B, C...) |
@@ -1111,6 +1123,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
* PRIMARY KEY (id)
* FOREIGN KEY (asbuilt_drawing_id) REFERENCES asbuilt_drawings(id) ON DELETE CASCADE
* UNIQUE KEY (asbuilt_drawing_id, revision_number)
* UNIQUE INDEX idx_asbuilt_drawing_revisions_uuid (uuid)
* INDEX (revision_date)
**Relationships**:
@@ -1229,6 +1242,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| ----------------------- | ------------ | ----------------------------------- | ----------------------------------------- |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique circulation ID |
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
| correspondence_id | INT | UNIQUE, FK | Link to correspondence (1:1 relationship) |
| organization_id | INT | NOT NULL, FK | Organization that owns this circulation |
| circulation_no | VARCHAR(100) | NOT NULL | Circulation sheet number |
@@ -1249,6 +1263,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
* FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes(code)
* FOREIGN KEY (created_by_user_id) REFERENCES users(user_id)
* INDEX (organization_id)
* UNIQUE INDEX idx_circulations_uuid (uuid)
* INDEX (circulation_status_code)
* INDEX (created_by_user_id)
@@ -1338,6 +1353,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| ------------------- | ------------ | --------------------------- | ------------------------------------------------------------------------ |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique attachment ID |
| uuid | UUID | NOT NULL, UNIQUE, DEFAULT | UUID Public Identifier (ADR-019) |
| original_filename | VARCHAR(255) | NOT NULL | Original filename from upload |
| stored_filename | VARCHAR(255) | NOT NULL | System-generated unique filename |
| file_path | VARCHAR(500) | NOT NULL | Full file path on server (/share/dms-data/) |
@@ -1358,6 +1374,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
* INDEX (stored_filename)
* INDEX (mime_type)
* INDEX (uploaded_by_user_id)
* UNIQUE INDEX idx_attachments_uuid (uuid)
* INDEX (created_at)
* INDEX (reference_date)
@@ -1820,6 +1837,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
| Column Name | Data Type | Constraints | Description |
| :---------------- | :----------- | :-------------------------- | :------------------------ |
| id | INT | PRIMARY KEY, AUTO_INCREMENT | Unique notification ID |
| uuid | UUID | NOT NULL, DEFAULT | UUID Public Identifier (ADR-019) |
| user_id | INT | NOT NULL, FK | Recipient user ID |
| title | VARCHAR(255) | NOT NULL | Notification title |
| message | TEXT | NOT NULL | Notification body |
@@ -1836,6 +1854,7 @@ SET NULL - INDEX (is_active) - INDEX (email) ** Relationships **: - Parent: orga
* INDEX idx_notif_type (notification_type)
* INDEX idx_notif_read (is_read)
* INDEX idx_notif_created (created_at)
* INDEX idx_notifications_uuid (uuid)
**Partitioning**:
* **PARTITION BY RANGE (YEAR(created_at))**: แบ่ง Partition รายปี
@@ -23,6 +23,7 @@ CREATE TABLE organization_roles (
-- ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ
CREATE TABLE organizations (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
organization_code VARCHAR(20) NOT NULL UNIQUE COMMENT 'รหัสองค์กร',
organization_name VARCHAR(255) NOT NULL COMMENT 'ชื่อองค์กร',
role_id INT COMMENT 'บทบาทขององค์กร',
@@ -31,12 +32,14 @@ CREATE TABLE organizations (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
FOREIGN KEY (role_id) REFERENCES organization_roles (id) ON DELETE
SET NULL
SET NULL,
UNIQUE INDEX idx_organizations_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลองค์กรทั้งหมดที่เกี่ยวข้องในระบบ';
-- ตาราง Master เก็บข้อมูลโครงการ
CREATE TABLE projects (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
project_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสโครงการ',
project_name VARCHAR(255) NOT NULL COMMENT 'ชื่อโครงการ',
-- parent_project_id INT COMMENT 'รหัสโครงการหลัก (ถ้ามี)',
@@ -46,12 +49,14 @@ CREATE TABLE projects (
-- FOREIGN KEY (contractor_organization_id) REFERENCES organizations(id) ON DELETE SET NULL
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)'
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
UNIQUE INDEX idx_projects_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลโครงการ';
-- ตาราง Master เก็บข้อมูลสัญญา
CREATE TABLE contracts (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
project_id INT NOT NULL,
contract_code VARCHAR(50) NOT NULL UNIQUE COMMENT 'รหัสสัญญา',
contract_name VARCHAR(255) NOT NULL COMMENT 'ชื่อสัญญา',
@@ -62,7 +67,8 @@ CREATE TABLE contracts (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'วันที่สร้าง',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
deleted_at DATETIME NULL COMMENT 'วันที่ลบ (Soft Delete)',
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
UNIQUE INDEX idx_contracts_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลสัญญา';
-- =====================================================
@@ -71,6 +77,7 @@ CREATE TABLE contracts (
-- ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
username VARCHAR(50) NOT NULL UNIQUE COMMENT 'ชื่อผู้ใช้งาน',
password_hash VARCHAR(255) NOT NULL COMMENT 'รหัสผ่าน (Hashed)',
first_name VARCHAR(50) COMMENT 'ชื่อจริง',
@@ -86,7 +93,8 @@ CREATE TABLE users (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'วันที่แก้ไขล่าสุด',
deleted_at DATETIME NULL DEFAULT NULL COMMENT 'วันที่ลบ',
FOREIGN KEY (primary_organization_id) REFERENCES organizations (id) ON DELETE
SET NULL
SET NULL,
UNIQUE INDEX idx_users_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูลผู้ใช้งาน (User)';
-- ตารางเก็บ Refresh Tokens สำหรับ Authentication
@@ -258,6 +266,7 @@ CREATE TABLE correspondence_status (
-- ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision
CREATE TABLE correspondences (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง (นี่คือ "Master ID" ที่ใช้เชื่อมโยง)',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
correspondence_type_id INT NOT NULL COMMENT 'ประเภทเอกสาร ใช้แบ่งแยกว่าเป็น RFA หรือ อื่นๆ',
correspondence_number VARCHAR(100) NOT NULL COMMENT 'เลขที่เอกสาร (สร้างจาก DocumentNumberingModule)',
discipline_id INT NULL COMMENT 'สาขางาน (ถ้ามี)',
@@ -279,7 +288,8 @@ CREATE TABLE correspondences (
-- Foreign Key ที่รวมเข้ามาจาก ALTER (ระบุชื่อ Constraint ตามที่ต้องการ)
CONSTRAINT fk_corr_discipline FOREIGN KEY (discipline_id) REFERENCES disciplines (id) ON DELETE
SET NULL,
UNIQUE KEY uq_corr_no_per_project (project_id, correspondence_number)
UNIQUE KEY uq_corr_no_per_project (project_id, correspondence_number),
UNIQUE INDEX idx_correspondences_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของเอกสารโต้ตอบ เก็บข้อมูลที่ไม่เปลี่ยนตาม Revision';
-- ตารางเชื่อมผู้รับ (TO/CC) สำหรับเอกสารแต่ละฉบับ (M:N)
@@ -299,6 +309,7 @@ CREATE TABLE correspondence_recipients (
-- ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1:N)
CREATE TABLE correspondence_revisions (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
correspondence_id INT NOT NULL COMMENT 'Master ID',
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (0, 1, 2...)',
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
@@ -338,7 +349,8 @@ CREATE TABLE correspondence_revisions (
UNIQUE KEY uq_master_revision_number (correspondence_id, revision_number),
UNIQUE KEY uq_master_current (correspondence_id, is_current),
INDEX idx_corr_rev_v_project (v_ref_project_id),
INDEX idx_corr_rev_v_subtype (v_doc_subtype)
INDEX idx_corr_rev_v_subtype (v_doc_subtype),
UNIQUE INDEX idx_correspondence_revisions_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติการแก้ไข (Revisions) ของ correspondences (1 :N)';
-- ตาราง Master เก็บ Tags ทั้งหมดที่ใช้ในระบบ
@@ -529,6 +541,7 @@ CREATE TABLE contract_drawing_subcat_cat_maps (
-- ตาราง Master เก็บข้อมูล "แบบคู่สัญญา"
CREATE TABLE contract_drawings (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
project_id INT NOT NULL COMMENT 'โครงการ',
condwg_no VARCHAR(255) NOT NULL COMMENT 'เลขที่แบบสัญญา',
title VARCHAR(255) NOT NULL COMMENT 'ชื่อแบบสัญญา',
@@ -542,7 +555,8 @@ CREATE TABLE contract_drawings (
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
FOREIGN KEY (map_cat_id) REFERENCES contract_drawing_subcat_cat_maps (id) ON DELETE RESTRICT,
FOREIGN KEY (volume_id) REFERENCES contract_drawing_volumes (id) ON DELETE RESTRICT,
UNIQUE KEY ux_condwg_no_project (project_id, condwg_no)
UNIQUE KEY ux_condwg_no_project (project_id, condwg_no),
UNIQUE INDEX idx_contract_drawings_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบคู่สัญญา"';
-- ตาราง Master สำหรับ "หมวดหมู่หลัก" ของแบบก่อสร้าง
@@ -576,6 +590,7 @@ CREATE TABLE shop_drawing_sub_categories (
-- ตาราง Master เก็บข้อมูล "แบบก่อสร้าง"
CREATE TABLE shop_drawings (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
project_id INT NOT NULL COMMENT 'โครงการ',
drawing_number VARCHAR(100) NOT NULL COMMENT 'เลขที่ Shop Drawing',
main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก',
@@ -587,12 +602,14 @@ CREATE TABLE shop_drawings (
FOREIGN KEY (project_id) REFERENCES projects (id),
FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id),
FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id),
UNIQUE KEY ux_shop_dwg_no_project (project_id, drawing_number)
UNIQUE KEY ux_shop_dwg_no_project (project_id, drawing_number),
UNIQUE INDEX idx_shop_drawings_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบก่อสร้าง"';
-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N)
CREATE TABLE shop_drawing_revisions (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
shop_drawing_id INT NOT NULL COMMENT 'Master ID',
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)',
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
@@ -610,7 +627,8 @@ CREATE TABLE shop_drawing_revisions (
FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE
SET NULL,
UNIQUE KEY ux_sd_rev_drawing_revision (shop_drawing_id, revision_number),
UNIQUE KEY uq_sd_current (shop_drawing_id, is_current)
UNIQUE KEY uq_sd_current (shop_drawing_id, is_current),
UNIQUE INDEX idx_shop_drawing_revisions_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ shop_drawings (1:N)';
-- ตารางเชื่อมระหว่าง shop_drawing_revisions กับ contract_drawings (M:N)
@@ -628,6 +646,7 @@ CREATE TABLE shop_drawing_revision_contract_refs (
-- ตาราง Master เก็บข้อมูล "AS Built"
CREATE TABLE asbuilt_drawings (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตาราง',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
project_id INT NOT NULL COMMENT 'โครงการ',
drawing_number VARCHAR(100) NOT NULL COMMENT 'เลขที่ AS Built Drawing',
main_category_id INT NOT NULL COMMENT 'หมวดหมู่หลัก',
@@ -639,12 +658,14 @@ CREATE TABLE asbuilt_drawings (
FOREIGN KEY (project_id) REFERENCES projects (id),
FOREIGN KEY (main_category_id) REFERENCES shop_drawing_main_categories (id),
FOREIGN KEY (sub_category_id) REFERENCES shop_drawing_sub_categories (id),
UNIQUE KEY ux_asbuilt_no_project (project_id, drawing_number)
UNIQUE KEY ux_asbuilt_no_project (project_id, drawing_number),
UNIQUE INDEX idx_asbuilt_drawings_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง Master เก็บข้อมูล "แบบ AS Built"';
-- ตาราง "ลูก" เก็บประวัติ (Revisions) ของ AS Built (1:N)
CREATE TABLE asbuilt_drawing_revisions (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของ Revision',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
asbuilt_drawing_id INT NOT NULL COMMENT 'Master ID',
revision_number INT NOT NULL COMMENT 'หมายเลข Revision (เช่น 0, 1, 2...)',
revision_label VARCHAR(10) COMMENT 'Revision ที่แสดง (เช่น A, B, 1.1)',
@@ -662,7 +683,8 @@ CREATE TABLE asbuilt_drawing_revisions (
FOREIGN KEY (updated_by) REFERENCES users (user_id) ON DELETE
SET NULL,
UNIQUE KEY ux_asbuilt_rev_drawing_revision (asbuilt_drawing_id, revision_number),
UNIQUE KEY uq_asbuilt_current (asbuilt_drawing_id, is_current)
UNIQUE KEY uq_asbuilt_current (asbuilt_drawing_id, is_current),
UNIQUE INDEX idx_asbuilt_drawing_revisions_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "ลูก" เก็บประวัติ (Revisions) ของ asbuilt_drawings (1:N)';
-- ตารางเชื่อมระหว่าง asbuilt_drawing_revisions กับ shop_drawing_revisions (M:N)
@@ -744,6 +766,7 @@ CREATE TABLE circulation_status_codes (
-- ตาราง "แม่" ของใบเวียนเอกสารภายใน
CREATE TABLE circulations (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของตารางใบเวียน',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
correspondence_id INT UNIQUE COMMENT 'ID ของเอกสาร (จากตาราง correspondences)',
organization_id INT NOT NULL COMMENT 'ID ขององค์กรณ์ที่เป็นเจ้าของใบเวียนนี้',
circulation_no VARCHAR(100) NOT NULL COMMENT 'เลขที่ใบเวียน',
@@ -757,7 +780,8 @@ CREATE TABLE circulations (
FOREIGN KEY (correspondence_id) REFERENCES correspondences (id),
FOREIGN KEY (organization_id) REFERENCES organizations (id),
FOREIGN KEY (circulation_status_code) REFERENCES circulation_status_codes (code),
FOREIGN KEY (created_by_user_id) REFERENCES users (user_id)
FOREIGN KEY (created_by_user_id) REFERENCES users (user_id),
UNIQUE INDEX idx_circulations_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "แม่" ของใบเวียนเอกสารภายใน';
-- =====================================================
@@ -800,6 +824,7 @@ CREATE TABLE transmittal_items (
-- เหตุผล: จัดการไฟล์ขยะ (Orphan Files) และตรวจสอบความถูกต้องไฟล์
CREATE TABLE attachments (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID ของไฟล์แนบ',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
original_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ดั้งเดิมตอนอัปโหลด',
stored_filename VARCHAR(255) NOT NULL COMMENT 'ชื่อไฟล์ที่เก็บจริงบน Server (ป้องกันชื่อซ้ำ)',
file_path VARCHAR(500) NOT NULL COMMENT 'Path ที่เก็บไฟล์ (บน QNAP / share / dms - data /)',
@@ -813,7 +838,8 @@ CREATE TABLE attachments (
CHECKSUM VARCHAR(64) NULL COMMENT 'SHA-256 Checksum',
reference_date DATE NULL COMMENT 'Date used for folder structure (e.g. Issue Date) to prevent broken paths',
FOREIGN KEY (uploaded_by_user_id) REFERENCES users (user_id) ON DELETE CASCADE,
INDEX idx_attachments_reference_date (reference_date)
INDEX idx_attachments_reference_date (reference_date),
UNIQUE INDEX idx_attachments_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตาราง "กลาง" เก็บไฟล์แนบทั้งหมดของระบบ';
-- ตารางเชื่อม correspondences กับ attachments (M:N)
@@ -1198,6 +1224,7 @@ PARTITION BY RANGE (YEAR(created_at)) (
-- ตารางสำหรับจัดการการแจ้งเตือน (Email/Line/System)
CREATE TABLE notifications (
id INT NOT NULL AUTO_INCREMENT COMMENT 'ID ของการแจ้งเตือน',
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
user_id INT NOT NULL COMMENT 'ID ผู้ใช้',
title VARCHAR(255) NOT NULL COMMENT 'หัวข้อการแจ้งเตือน',
message TEXT NOT NULL COMMENT 'รายละเอียดการแจ้งเตือน',
@@ -1213,7 +1240,8 @@ CREATE TABLE notifications (
INDEX idx_notif_user (user_id),
INDEX idx_notif_type (notification_type),
INDEX idx_notif_read (is_read),
INDEX idx_notif_created (created_at)
INDEX idx_notif_created (created_at),
INDEX idx_notifications_uuid (uuid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'ตารางสำหรับจัดการการแจ้งเตือน (Email / Line / System)' -- [เพิ่ม] คำสั่ง Partition
PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p_old
@@ -120,17 +120,21 @@ CREATE INDEX idx_audit_request_id ON audit_logs (request_id);
-- View แสดง Revision "ปัจจุบัน" ของ correspondences ทั้งหมด (ที่ไม่ใช่ RFA)
CREATE VIEW v_current_correspondences AS
SELECT c.id AS correspondence_id,
c.uuid AS correspondence_uuid,
c.correspondence_number,
c.correspondence_type_id,
ct.type_code AS correspondence_type_code,
ct.type_name AS correspondence_type_name,
c.project_id,
p.uuid AS project_uuid,
p.project_code,
p.project_name,
c.originator_id,
org.uuid AS originator_uuid,
org.organization_code AS originator_code,
org.organization_name AS originator_name,
cr.id AS revision_id,
cr.uuid AS revision_uuid,
cr.revision_number,
cr.revision_label,
cr.subject,
@@ -162,6 +166,7 @@ WHERE cr.is_current = TRUE
-- View แสดง Revision "ปัจจุบัน" ของ rfa_revisions ทั้งหมด
CREATE VIEW v_current_rfas AS
SELECT r.id AS rfa_id,
c.uuid AS correspondence_uuid,
r.rfa_type_id,
rt.type_code AS rfa_type_code,
rt.type_name_th AS rfa_type_name_th,
@@ -172,11 +177,14 @@ SELECT r.id AS rfa_id,
d.discipline_code,
-- ✅ Join เพิ่มเพื่อแสดง code
c.project_id,
p.uuid AS project_uuid,
p.project_code,
p.project_name,
c.originator_id,
org.uuid AS originator_uuid,
org.organization_name AS originator_name,
rr.id AS revision_id,
cr.uuid AS revision_uuid,
cr.revision_number,
cr.revision_label,
cr.subject,
@@ -211,12 +219,15 @@ WHERE cr.is_current = TRUE
-- View แสดงความสัมพันธ์ทั้งหมดระหว่าง Contract, Project, และ Organization
CREATE VIEW v_contract_parties_all AS
SELECT c.id AS contract_id,
c.uuid AS contract_uuid,
c.contract_code,
c.contract_name,
p.id AS project_id,
p.uuid AS project_uuid,
p.project_code,
p.project_name,
o.id AS organization_id,
o.uuid AS organization_uuid,
o.organization_code,
o.organization_name,
co.role_in_contract
@@ -380,8 +391,10 @@ WHERE p.is_active = 1
CREATE VIEW v_documents_with_attachments AS
SELECT 'CORRESPONDENCE' AS document_type,
c.id AS document_id,
c.uuid AS document_uuid,
c.correspondence_number AS document_number,
c.project_id,
p.uuid AS project_uuid,
p.project_code,
p.project_name,
COUNT(ca.attachment_id) AS attachment_count,
@@ -399,8 +412,10 @@ GROUP BY c.id,
UNION ALL
SELECT 'CIRCULATION' AS document_type,
circ.id AS document_id,
circ.uuid AS document_uuid,
circ.circulation_no AS document_number,
corr.project_id,
p.uuid AS project_uuid,
p.project_code,
p.project_name,
COUNT(ca.attachment_id) AS attachment_count,
@@ -418,8 +433,10 @@ GROUP BY circ.id,
UNION ALL
SELECT 'SHOP_DRAWING' AS document_type,
sdr.id AS document_id,
sdr.uuid AS document_uuid,
sd.drawing_number AS document_number,
sd.project_id,
p.uuid AS project_uuid,
p.project_code,
p.project_name,
COUNT(sdra.attachment_id) AS attachment_count,
@@ -438,8 +455,10 @@ GROUP BY sdr.id,
UNION ALL
SELECT 'CONTRACT_DRAWING' AS document_type,
cd.id AS document_id,
cd.uuid AS document_uuid,
cd.condwg_no AS document_number,
cd.project_id,
p.uuid AS project_uuid,
p.project_code,
p.project_name,
COUNT(cda.attachment_id) AS attachment_count,
@@ -0,0 +1,295 @@
# Implementation Plan: Hybrid UUID Strategy (ADR-019)
**Version:** 1.8.1
**Created:** 2026-03-16
**Related ADR:** [ADR-019](../06-Decision-Records/ADR-019-hybrid-identifier-strategy.md)
---
## Overview
This document outlines the step-by-step implementation plan to integrate UUIDv7 public identifiers into the LCBP3-DMS backend, following the hybrid strategy defined in ADR-019.
**Scope:** 14 public-facing tables now have `uuid UUID` columns (MariaDB native type, stored as BINARY(16) internally) in the schema. This plan covers backend code changes to expose UUIDs through the API while keeping INT PKs for internal operations.
---
## Phase 1: Database Foundation (✅ COMPLETED)
- [x] Create ADR-019 document
- [x] Add `uuid UUID` columns (MariaDB native type) to 14 public-facing tables in schema SQL
- [x] Add UNIQUE INDEX on each uuid column
- [x] Update data dictionary with uuid column documentation
- [x] Update AGENTS.md with ADR-019 reference
### Affected Tables (14)
| # | Table | PK Column | UUID Index |
|---|-------|-----------|------------|
| 1 | organizations | id | idx_organizations_uuid |
| 2 | projects | id | idx_projects_uuid |
| 3 | contracts | id | idx_contracts_uuid |
| 4 | users | user_id | idx_users_uuid |
| 5 | correspondences | id | idx_correspondences_uuid |
| 6 | correspondence_revisions | id | idx_correspondence_revisions_uuid |
| 7 | circulations | id | idx_circulations_uuid |
| 8 | shop_drawings | id | idx_shop_drawings_uuid |
| 9 | shop_drawing_revisions | id | idx_shop_drawing_revisions_uuid |
| 10 | contract_drawings | id | idx_contract_drawings_uuid |
| 11 | asbuilt_drawings | id | idx_asbuilt_drawings_uuid |
| 12 | asbuilt_drawing_revisions | id | idx_asbuilt_drawing_revisions_uuid |
| 13 | attachments | id | idx_attachments_uuid |
| 14 | notifications | id | idx_notifications_uuid |
### Excluded Tables (Shared-PK / Junction — inherit UUID from parent)
- `rfas` — shared PK with `correspondences`
- `rfa_revisions` — shared PK with `correspondence_revisions`
- `transmittals` — shared PK with `correspondences`
- `rfa_items` — junction table (composite PK, no own identity)
---
## Phase 2: Backend — TypeORM Base Entity & UUID Utilities
> **Simplified by MariaDB Native UUID Type:** MariaDB 10.7+ stores UUID as `BINARY(16)` internally but auto-converts to/from string format. No manual binary conversion utilities or TypeORM transformers needed.
### 2.1 Create Base Entity with UUID
**File:** `backend/src/common/entities/uuid-base.entity.ts`
```typescript
import { Column, BeforeInsert } from 'typeorm';
import { v7 as uuidv7 } from 'uuid';
export abstract class UuidBaseEntity {
@Column({
type: 'uuid',
unique: true,
nullable: false,
comment: 'UUID Public Identifier (ADR-019)',
})
uuid: string;
@BeforeInsert()
generateUuid(): void {
if (!this.uuid) {
this.uuid = uuidv7();
}
}
}
```
> **Note:** MariaDB native `UUID` type handles string ↔ binary conversion automatically.
> TypeORM reads/writes UUID as standard string format (8-4-4-4-12) — no transformer required.
> DB `DEFAULT UUID()` generates UUID v1 as fallback; app generates UUIDv7 via `@BeforeInsert()`.
### 2.2 Install uuid Package
```bash
cd backend
npm install uuid
npm install -D @types/uuid
```
---
## Phase 3: Backend — Update Existing Entities
For each of the 14 public-facing entities, extend or mix in the UUID column:
### Pattern: Extend UuidBaseEntity
```typescript
// Example: correspondence.entity.ts
import { Entity, PrimaryGeneratedColumn } from 'typeorm';
import { UuidBaseEntity } from '../../common/entities/uuid-base.entity';
@Entity('correspondences')
export class Correspondence extends UuidBaseEntity {
@PrimaryGeneratedColumn()
id: number;
// ... existing columns (uuid + @BeforeInsert inherited from UuidBaseEntity)
}
```
### Entities to Update
| Entity File | Table |
|-------------|-------|
| `organization.entity.ts` | organizations |
| `project.entity.ts` | projects |
| `contract.entity.ts` | contracts |
| `user.entity.ts` | users |
| `correspondence.entity.ts` | correspondences |
| `correspondence-revision.entity.ts` | correspondence_revisions |
| `circulation.entity.ts` | circulations |
| `shop-drawing.entity.ts` | shop_drawings |
| `shop-drawing-revision.entity.ts` | shop_drawing_revisions |
| `contract-drawing.entity.ts` | contract_drawings |
| `asbuilt-drawing.entity.ts` | asbuilt_drawings |
| `asbuilt-drawing-revision.entity.ts` | asbuilt_drawing_revisions |
| `attachment.entity.ts` | attachments |
| `notification.entity.ts` | notifications |
---
## Phase 4: Backend — API Layer Changes
### 4.1 UUID Pipe (Parameter Validation)
**File:** `backend/src/common/pipes/parse-uuid.pipe.ts`
```typescript
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { validate as uuidValidate, version as uuidVersion } from 'uuid';
@Injectable()
export class ParseUuidPipe implements PipeTransform<string> {
transform(value: string): string {
if (!uuidValidate(value) || uuidVersion(value) !== 7) {
throw new BadRequestException(`Invalid UUID: ${value}`);
}
return value;
}
}
```
### 4.2 Controller Pattern — UUID in URLs
```typescript
// BEFORE (INT): GET /api/correspondences/123
// AFTER (UUID): GET /api/correspondences/01912345-6789-7abc-...
@Get(':uuid')
findOne(@Param('uuid', ParseUuidPipe) uuid: string) {
return this.service.findByUuid(uuid);
}
```
### 4.3 Service Pattern — Internal UUID Lookup
```typescript
async findByUuid(uuid: string): Promise<CorrespondenceDto> {
const entity = await this.repository.findOne({
where: { uuid },
relations: ['revisions', 'recipients'],
});
if (!entity) throw new NotFoundException();
return this.mapToDto(entity);
}
```
### 4.4 DTO Pattern — UUID Exposure
```typescript
// Response DTO exposes uuid, hides id
export class CorrespondenceResponseDto {
uuid: string; // ✅ Public identifier
correspondenceNumber: string;
// id: number; // ❌ Never expose INT id
}
```
### 4.5 Migration Helper — findByUuidOrId
During transition, support both identifiers:
```typescript
async findByUuidOrId(identifier: string): Promise<Entity> {
const isUuid = uuidValidate(identifier);
if (isUuid) {
return this.repository.findOne({ where: { uuid: identifier } });
}
// Fallback to INT (internal/admin use only)
const id = parseInt(identifier, 10);
if (isNaN(id)) throw new BadRequestException();
return this.repository.findOne({ where: { id } });
}
```
---
## Phase 5: Frontend — UUID Integration
### 5.1 API Client Updates
- Update all API calls to use UUID in URL paths instead of INT id
- Update TanStack Query cache keys to use UUID
- Update Zustand stores to key by UUID
### 5.2 Route Parameters
```typescript
// BEFORE: /correspondences/[id]
// AFTER: /correspondences/[uuid]
```
### 5.3 Form Handling
- Hidden `uuid` field in forms for edit operations
- No changes needed for create operations (UUID generated server-side)
---
## Phase 6: Testing & Verification
### 6.1 Unit Tests
- UUID generation produces valid UUIDv7
- UuidBaseEntity `@BeforeInsert()` auto-generates UUID when not provided
- ParseUuidPipe rejects invalid UUIDs
- MariaDB native UUID column stores and retrieves string format correctly
### 6.2 Integration Tests
- Entity creation auto-generates UUID
- API endpoints accept UUID parameters
- UUID lookup returns correct records
- Duplicate UUID detection (unique constraint)
### 6.3 Performance Verification
- Benchmark: UUID lookup via UNIQUE INDEX vs INT PK lookup
- Acceptable threshold: < 2x overhead on single-row lookups
- Verify B-tree ordering with time-sorted UUIDv7
---
## Implementation Order (Priority)
| Order | Task | Effort | Depends On |
|-------|------|--------|------------|
| 1 | UuidBaseEntity (no transformer needed — MariaDB native UUID) | S | Phase 1 |
| 2 | Install `uuid` package | XS | — |
| 3 | Update 14 entity files with uuid column | M | Task 1 |
| 4 | Create ParseUuidPipe | S | — |
| 5 | Update controllers to use UUID params | L | Tasks 3, 4 |
| 6 | Update services with findByUuid methods | L | Task 3 |
| 7 | Update DTOs to expose uuid, hide id | M | Task 3 |
| 8 | Update frontend API calls | L | Tasks 5, 6, 7 |
| 9 | Update frontend routes | M | Task 8 |
| 10 | Write unit + integration tests | M | Tasks 1-7 |
**Estimated Total Effort:** ~3-5 days for backend, ~2-3 days for frontend
---
## Rollback Strategy
If issues arise:
1. **Schema:** UUID columns have `DEFAULT` — existing inserts still work without app changes
2. **API:** INT-based endpoints can be restored by reverting controller/service changes
3. **Data:** No data loss — UUID column is additive (no existing columns modified)
4. **Frontend:** Route parameter changes are reversible
---
## Notes
- **Seed files** do not need UUID values — the `DEFAULT UUID()` clause auto-generates UUIDs at INSERT time
- **Notifications table** uses a non-unique INDEX (not UNIQUE) for uuid because of its partitioned composite PK `(id, created_at)`
- **Workflow engine tables** (`workflow_instances`, `workflow_tasks`) already use `CHAR(36)` UUIDs — no changes needed
- **Shared-PK tables** (`rfas`, `rfa_revisions`, `transmittals`) inherit their parent's UUID via the correspondence relationship
@@ -0,0 +1,478 @@
# ADR-019: Hybrid Identifier Strategy (INT + UUIDv7)
**Status:** Accepted
**Date:** 2026-03-12
**Version:** 1.8.1
**Decision Makers:** Development Team, Database Architect
**Related Documents:**
- [Data Dictionary](../03-Data-and-Storage/03-01-data-dictionary.md)
- [Database Schema](../03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql)
- [ADR-005: Technology Stack](ADR-005-technology-stack.md)
- [ADR-009: Database Migration Strategy](ADR-009-database-migration-strategy.md)
- [ADR-016: Security & Authentication](ADR-016-security-authentication.md)
---
## Context and Problem Statement
ระบบ LCBP3-DMS ใช้ `INT AUTO_INCREMENT` เป็น Primary Key ทุกตาราง ซึ่งทำงานได้ดีสำหรับ Internal JOIN/FK แต่มีปัญหาด้านความปลอดภัยและ Scalability:
1. **ID Enumeration Attack:** Sequential INT IDs ถูกเดาได้ง่าย (เช่น `/api/correspondences/1`, `/api/correspondences/2`) ทำให้ผู้ไม่ประสงค์ดีสามารถ Enumerate ข้อมูลได้
2. **Information Leakage:** INT IDs เปิดเผยจำนวนข้อมูลในระบบ (เช่น `user_id=5` แปลว่ามีผู้ใช้ 5 คน)
3. **Cross-System Integration:** หากในอนาคตต้องการ Sync ข้อมูลข้ามระบบ INT ID จะชนกัน
4. **API Security:** OWASP BOLA (Broken Object Level Authorization) แนะนำให้ใช้ Opaque Identifier แทน Sequential ID
ทั้งนี้ ระบบมีข้อจำกัดด้าน Hardware (QNAP NAS) ที่ต้องพิจารณาเรื่อง Performance
---
## Decision Drivers
- **Security:** ป้องกัน ID Enumeration และลดความเสี่ยง OWASP BOLA
- **Performance:** INT PK ยังคงเป็น Primary Key เพื่อ JOIN/FK Performance บน InnoDB
- **Backward Compatibility:** ไม่ต้อง Migrate ข้อมูลหรือเปลี่ยน FK Relationships ที่มีอยู่
- **Simplicity:** เปลี่ยนเฉพาะ Public-Facing Tables (ไม่ใช่ทุกตาราง)
- **Standards:** UUIDv7 (RFC 9562) เป็น Time-ordered UUID ที่ B-tree friendly
---
## Considered Options
### Option 1: Replace INT with UUID as Primary Key
**Pros:**
- ✅ Opaque identifier ทุกที่
**Cons:**
- ❌ FK ทั้งหมดต้องเปลี่ยนเป็น BINARY(16) — Migration ซับซ้อนมาก
- ❌ JOIN Performance แย่ลง (16 bytes vs 4 bytes)
- ❌ InnoDB Clustered Index ไม่เรียงลำดับตาม INSERT Time (UUIDv4)
- ❌ ต้อง Rewrite Backend ทั้งหมด (Entity, DTO, Controller, Service)
- ❌ Breaking Change กับ Frontend ที่ใช้ INT ID อยู่
### Option 2: UUID as String Column (CHAR(36))
**Pros:**
- ✅ Human-readable
**Cons:**
- ❌ ใช้พื้นที่ 36 bytes ต่อ row (vs 16 bytes สำหรับ BINARY)
- ❌ Index ใหญ่ ช้ากว่า BINARY(16) อย่างมีนัยสำคัญ
- ❌ Collation issues กับ case-sensitivity
### Option 3: Hybrid INT + UUID (MariaDB Native) ⭐ (Selected)
**Pros:**
- ✅ INT PK ยังเป็น Internal ID → Performance ไม่เปลี่ยน
- ✅ UUID เป็น External ID → ปลอดภัย + Space-efficient (BINARY(16) ภายใน)
- ✅ ไม่ต้อง Migrate FK Relationships
- ✅ UUIDv7 Time-ordered → B-tree friendly, Index Performance ดี
- ✅ Backward Compatible — Frontend ค่อยๆ Migrate ได้
- ✅ ไม่กระทบ Migration Tables (Temporary)
**Cons:**
- ❌ ต้องเพิ่ม Column ใหม่ + UNIQUE INDEX ทุก Public-Facing Table
- ❌ Application Layer ต้อง Generate UUIDv7 ตอน INSERT
- ❌ API Layer ต้อง Resolve UUID → INT สำหรับ Internal Queries
---
## Decision Outcome
**Chosen Option:** Option 3 — Hybrid INT + UUID (MariaDB Native Type)
**Rationale:** เป็นแนวทางที่ Balance ระหว่าง Security, Performance และ Migration Effort ดีที่สุด ไม่ต้อง Rewrite FK ทั้งหมด ไม่ต้อง Migrate ข้อมูล และเพิ่มความปลอดภัยของ API
---
## Technical Specification
### 1. UUID Format
| Property | Value |
|----------|-------|
| **Type** | MariaDB Native `UUID` (available since 10.7) |
| **Storage** | `BINARY(16)` internally (automatic) |
| **DB Default** | `UUID()` — generates UUID v1 (time-based, fallback for seed data) |
| **App Generation** | NestJS `@BeforeInsert()` generates UUIDv7 (RFC 9562) for time-ordering |
| **Display** | Auto-converts to string format (8-4-4-4-12) — no conversion function needed |
| **Index** | `UNIQUE INDEX` on `uuid` column |
### 2. Column Specification
```sql
-- Column definition for all public-facing tables (MariaDB 10.7+)
uuid UUID NOT NULL DEFAULT UUID() COMMENT 'UUID Public Identifier (ADR-019)',
UNIQUE INDEX idx_{table}_uuid (uuid)
```
> **Note:** MariaDB native `UUID` type เก็บเป็น `BINARY(16)` ภายใน แต่แสดงผลเป็น String format อัตโนมัติ ไม่ต้องใช้ `BIN_TO_UUID()` / `UUID_TO_BIN()`
>
> **DB Default:** `UUID()` สร้าง UUID v1 (สำหรับ Seed Data และ Fallback)
>
> **Application Override:** NestJS Entity จะ Generate UUIDv7 เองก่อน INSERT เพื่อให้ได้ True UUIDv7 (Time-ordered, B-tree friendly)
### 3. Tables Requiring UUID Column
#### Tier 1 — Core Entity Tables (Own UUID Column)
| # | Table Name | Current PK | UUID Column | Notes |
|---|-----------|-----------|-------------|-------|
| 1 | `users` | `user_id INT AI` | `uuid UUID` | User profiles |
| 2 | `organizations` | `id INT AI` | `uuid UUID` | Organization data |
| 3 | `projects` | `id INT AI` | `uuid UUID` | Project data |
| 4 | `contracts` | `id INT AI` | `uuid UUID` | Contract data |
| 5 | `correspondences` | `id INT AI` | `uuid UUID` | Main document entity |
| 6 | `correspondence_revisions` | `id INT AI` | `uuid UUID` | Document versions |
| 7 | `circulations` | `id INT AI` | `uuid UUID` | Internal circulations |
| 8 | `shop_drawings` | `id INT AI` | `uuid UUID` | Shop drawing master |
| 9 | `shop_drawing_revisions` | `id INT AI` | `uuid UUID` | Shop drawing versions |
| 10 | `contract_drawings` | `id INT AI` | `uuid UUID` | Contract drawing master |
| 11 | `asbuilt_drawings` | `id INT AI` | `uuid UUID` | As-built drawing master |
| 12 | `asbuilt_drawing_revisions` | `id INT AI` | `uuid UUID` | As-built drawing versions |
| 13 | `attachments` | `id INT AI` | `uuid UUID` | File attachments |
| 14 | `notifications` | `id INT AI` (partitioned) | `uuid UUID` | User notifications |
#### Tier 2 — Shared-PK Tables (Inherit UUID from Parent)
| # | Table Name | Shared PK Source | UUID Resolution |
|---|-----------|-----------------|-----------------|
| 1 | `rfas` | `correspondences.id` | Use `correspondences.uuid` |
| 2 | `rfa_revisions` | `correspondence_revisions.id` | Use `correspondence_revisions.uuid` |
| 3 | `transmittals` | `correspondences.id` | Use `correspondences.uuid` |
#### Already Using UUID — No Changes Needed
| Table Name | Current PK |
|-----------|-----------|
| `workflow_definitions` | `CHAR(36) UUID` |
| `workflow_instances` | `CHAR(36) UUID` |
| `workflow_histories` | `CHAR(36) UUID` |
#### Excluded Tables (Internal/Master/Junction)
ตารางต่อไปนี้ **ไม่ต้อง** เพิ่ม UUID Column เพราะเป็น Internal-use only:
- **Master/Lookup:** `organization_roles`, `disciplines`, `correspondence_types`, `correspondence_sub_types`, `correspondence_status`, `rfa_types`, `rfa_status_codes`, `rfa_approve_codes`, `circulation_status_codes`, `tags`
- **RBAC:** `roles`, `permissions`, `user_assignments`
- **Junction/Mapping:** `project_organizations`, `contract_organizations`, `correspondence_recipients`, `correspondence_tags`, `correspondence_references`, `contract_drawing_subcat_cat_maps`, `shop_drawing_revision_contract_refs`, `asbuilt_revision_shop_revisions_refs`, all `*_attachments` junction tables
- **Drawing Categories:** `contract_drawing_volumes`, `contract_drawing_cats`, `contract_drawing_sub_cats`, `shop_drawing_main_categories`, `shop_drawing_sub_categories`
- **Document Numbering:** `document_number_formats`, `document_number_counters`, `document_number_audit`, `document_number_errors`, `document_number_reservations`
- **System/Logs:** `json_schemas`, `user_preferences`, `audit_logs`, `search_indices`, `backup_logs`, `refresh_tokens`
- **Migration (Temporary):** `migration_progress`, `migration_review_queue`, `migration_errors`, `migration_fallback_state`, `import_transactions`, `migration_daily_summary`
---
## TypeORM Entity Pattern
### Base Entity with UUID
```typescript
// src/common/entities/base-uuid.entity.ts
import { Column, BeforeInsert } from 'typeorm';
import { v7 as uuidv7 } from 'uuid';
export abstract class BaseUuidEntity {
@Column({
type: 'uuid',
unique: true,
nullable: false,
comment: 'UUID Public Identifier (ADR-019)',
})
uuid!: string;
@BeforeInsert()
generateUuid(): void {
if (!this.uuid) {
this.uuid = uuidv7();
}
}
}
```
> **Note:** MariaDB native `UUID` type ทำให้ TypeORM ไม่ต้องใช้ transformer อีกต่อไป — ค่าเข้า/ออกเป็น string format เสมอ
### Entity Usage Example
```typescript
// Example: correspondence.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { BaseUuidEntity } from '../../common/entities/base-uuid.entity';
@Entity('correspondences')
export class Correspondence extends BaseUuidEntity {
@PrimaryGeneratedColumn()
id!: number;
// ... existing columns
}
```
---
## API Layer Changes
### URL Pattern
```
// Before (INT — vulnerable to enumeration)
GET /api/correspondences/42
GET /api/users/5
// After (UUID — opaque identifier)
GET /api/correspondences/019505a1-7c3e-7000-8000-abc123def456
GET /api/users/019505a1-8b2f-7000-8000-abc123def456
```
### Controller Pattern
```typescript
@Get(':uuid')
async findOne(@Param('uuid', ParseUUIDPipe) uuid: string) {
return this.service.findByUuid(uuid);
}
```
### Service Pattern
```typescript
async findByUuid(uuid: string): Promise<CorrespondenceDto> {
const entity = await this.repository.findOne({
where: { uuid },
});
if (!entity) throw new NotFoundException();
return this.toDto(entity);
}
```
### DTO Pattern — Never Expose INT ID
```typescript
export class CorrespondenceResponseDto {
// ✅ Expose UUID as 'id' in API response
@Expose({ name: 'id' })
uuid!: string;
// ❌ Never expose internal INT id
// id: number; — REMOVED from response
// ... other fields
// For FK references, also use UUID
@Expose({ name: 'project_id' })
projectUuid!: string;
}
```
---
## Migration SQL Script
```sql
-- =====================================================
-- ADR-019: Add UUIDv7 columns to public-facing tables
-- Strategy: Non-destructive — ADD COLUMN only
-- Rollback: DROP COLUMN uuid
-- =====================================================
-- Tier 1: Core Entity Tables
ALTER TABLE users
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_users_uuid (uuid);
ALTER TABLE organizations
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_organizations_uuid (uuid);
ALTER TABLE projects
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_projects_uuid (uuid);
ALTER TABLE contracts
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_contracts_uuid (uuid);
ALTER TABLE correspondences
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_correspondences_uuid (uuid);
ALTER TABLE correspondence_revisions
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_correspondence_revisions_uuid (uuid);
ALTER TABLE circulations
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_circulations_uuid (uuid);
ALTER TABLE shop_drawings
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_shop_drawings_uuid (uuid);
ALTER TABLE shop_drawing_revisions
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_shop_drawing_revisions_uuid (uuid);
ALTER TABLE contract_drawings
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_contract_drawings_uuid (uuid);
ALTER TABLE asbuilt_drawings
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_asbuilt_drawings_uuid (uuid);
ALTER TABLE asbuilt_drawing_revisions
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_asbuilt_drawing_revisions_uuid (uuid);
ALTER TABLE attachments
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD UNIQUE INDEX idx_attachments_uuid (uuid);
ALTER TABLE notifications
ADD COLUMN uuid UUID NOT NULL DEFAULT UUID()
COMMENT 'UUID Public Identifier (ADR-019)',
ADD INDEX idx_notifications_uuid (uuid);
-- Note: UNIQUE constraint on partitioned table requires uuid in partition key
-- Using regular INDEX instead
```
### Rollback SQL
```sql
-- Rollback: Remove UUID columns (Non-destructive reverse)
ALTER TABLE users DROP INDEX idx_users_uuid, DROP COLUMN uuid;
ALTER TABLE organizations DROP INDEX idx_organizations_uuid, DROP COLUMN uuid;
ALTER TABLE projects DROP INDEX idx_projects_uuid, DROP COLUMN uuid;
ALTER TABLE contracts DROP INDEX idx_contracts_uuid, DROP COLUMN uuid;
ALTER TABLE correspondences DROP INDEX idx_correspondences_uuid, DROP COLUMN uuid;
ALTER TABLE correspondence_revisions DROP INDEX idx_correspondence_revisions_uuid, DROP COLUMN uuid;
ALTER TABLE circulations DROP INDEX idx_circulations_uuid, DROP COLUMN uuid;
ALTER TABLE shop_drawings DROP INDEX idx_shop_drawings_uuid, DROP COLUMN uuid;
ALTER TABLE shop_drawing_revisions DROP INDEX idx_shop_drawing_revisions_uuid, DROP COLUMN uuid;
ALTER TABLE contract_drawings DROP INDEX idx_contract_drawings_uuid, DROP COLUMN uuid;
ALTER TABLE asbuilt_drawings DROP INDEX idx_asbuilt_drawings_uuid, DROP COLUMN uuid;
ALTER TABLE asbuilt_drawing_revisions DROP INDEX idx_asbuilt_drawing_revisions_uuid, DROP COLUMN uuid;
ALTER TABLE attachments DROP INDEX idx_attachments_uuid, DROP COLUMN uuid;
ALTER TABLE notifications DROP INDEX idx_notifications_uuid, DROP COLUMN uuid;
```
---
## Storage Impact Analysis
| Item | Size |
|------|------|
| UUID (BINARY(16) internal) per row | 16 bytes |
| UNIQUE INDEX per row | ~16 bytes (key) + ~6 bytes (pointer) ≈ 22 bytes |
| **Total per row** | **~38 bytes** |
| Estimated rows (all 14 tables combined) | ~100,000 (Year 1) |
| **Total additional storage** | **~3.8 MB** |
| Impact on QNAP NAS | **Negligible** |
---
## Performance Considerations
### UUIDv7 vs UUIDv4 for B-tree Index
| Property | UUIDv4 | UUIDv7 |
|----------|--------|--------|
| Ordering | Random | Time-ordered |
| B-tree insert | Random page splits | Sequential append |
| Index fragmentation | High | Low |
| Cache efficiency | Poor | Good |
**UUIDv7 ถูกเลือกเพราะ Time-ordering** ทำให้ INSERT ไม่ทำให้เกิด Random Page Split บน InnoDB B-tree ซึ่งสำคัญมากสำหรับ QNAP NAS ที่มี I/O จำกัด
### Query Pattern
```sql
-- Internal query (JOINs still use INT — no performance change)
SELECT c.*, cr.*
FROM correspondences c
JOIN correspondence_revisions cr ON c.id = cr.correspondence_id
WHERE c.id = 42;
-- API query (UUID lookup via UNIQUE INDEX — O(log n))
SELECT c.*, cr.*
FROM correspondences c
JOIN correspondence_revisions cr ON c.id = cr.correspondence_id
WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
```
---
## Security Benefits
| Threat | Before (INT) | After (Hybrid) |
|--------|-------------|----------------|
| ID Enumeration | ❌ Vulnerable (`/api/users/1,2,3...`) | ✅ Opaque UUID |
| Record Count Leak | ❌ `id=500` reveals ~500 records | ✅ UUID reveals nothing |
| BOLA Attack Surface | ❌ Predictable IDs | ✅ 2^122 possible values |
| Cross-System Collision | ❌ Possible | ✅ Globally unique |
---
## Compatibility with Existing ADRs
| ADR | Impact | Notes |
|-----|--------|-------|
| ADR-002 (Doc Numbering) | ✅ None | Document numbers (VARCHAR) are business identifiers, unaffected |
| ADR-005 (Tech Stack) | ✅ Compatible | uuid npm package + MariaDB native UUID type |
| ADR-006 (Redis Caching) | ⚠️ Minor | Cache keys should use UUID instead of INT for public-facing data |
| ADR-009 (DB Migration) | ✅ Compatible | ADD COLUMN is a safe, non-destructive migration |
| ADR-016 (Security) | ✅ Enhanced | Strengthens OWASP BOLA defense |
| ADR-017 (Ollama Migration) | ✅ None | Migration tables are temporary, excluded from UUID scope |
---
## Transition Strategy
### Phase 1: Database (Schema Change)
- เพิ่ม `uuid UUID` column (MariaDB native type) กับ UNIQUE INDEX ใน 14 ตาราง
- Existing rows ได้รับ UUID อัตโนมัติจาก DB DEFAULT
### Phase 2: Backend (Dual-Mode)
- เพิ่ม `uuid` field ใน TypeORM Entities
- สร้าง `BaseUuidEntity` class
- API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern
- API Response รวม UUID เป็น `id` field
### Phase 3: Frontend (Gradual Migration)
- Frontend เปลี่ยนจากใช้ `id` (INT) เป็น `id` (UUID) ใน API response
- URL parameters เปลี่ยนเป็น UUID
- ไม่ต้อง Big-Bang migration — ค่อยๆ เปลี่ยนทีละ Module
### Phase 4: Cleanup
- ลบ INT ID จาก API Response (DTO)
- ลบ INT-based route handlers
- Update API Documentation
---
## Final Assessment
| Area | Status |
|------|--------|
| Security | ✅ Eliminates ID enumeration |
| Performance | ✅ No impact on internal JOINs |
| Migration Risk | ✅ Low — ADD COLUMN only |
| Storage Impact | ✅ Negligible (~3.8 MB) |
| Backward Compatibility | ✅ Dual-mode transition |
| ADR Compliance | ✅ Compatible with all existing ADRs |
---
*สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน ADR-019-implementation-plan.md*