feat: migrate from UUID v4 to UUID v7
Migrations: - Fix 010_seed_super_admin.sql: only use columns from migration 001 - Add 021_create_uuid_v7_function.sql: PostgreSQL uuid_generate_v7() function - Add 022_migrate_to_uuid_v7.sql: update notifications, tickets, job_payments to use v7 Seeder: - Create seeder-api/src/utils/uuid.js with uuidv7() function - Update notifications.js to use uuidv7() instead of randomUUID() Docs: - Update DATABASE.md with UUID v7 section and benefits UUID v7 benefits: - Time-ordered (sortable by creation time) - Better index performance than v4 - RFC 9562 compliant
This commit is contained in:
parent
017a34c965
commit
ae4a3e5e63
6 changed files with 174 additions and 23 deletions
|
|
@ -1,6 +1,7 @@
|
|||
-- Migration: Create Super Admin and System Company
|
||||
-- Description: Inserts the default System Company and Super Admin user.
|
||||
-- Uses unified tables (companies, users, user_roles)
|
||||
-- NOTE: This runs before migration 020 adds extra columns, so only use existing columns
|
||||
|
||||
-- 1. Insert System Company (for SuperAdmin context)
|
||||
INSERT INTO companies (name, slug, type, document, email, description, verified, active)
|
||||
|
|
@ -15,25 +16,17 @@ VALUES (
|
|||
true
|
||||
) ON CONFLICT (slug) DO NOTHING;
|
||||
|
||||
-- 2. Insert Super Admin User
|
||||
-- 2. Insert Super Admin User (using only columns from migration 001)
|
||||
-- WARNING: This hash is generated WITHOUT PASSWORD_PEPPER.
|
||||
-- For development only. Use seeder-api for proper user creation.
|
||||
-- Password: "Admin@2025!" (should be created by seeder with proper pepper)
|
||||
INSERT INTO users (identifier, password_hash, role, full_name, email, name, status, tenant_id)
|
||||
SELECT
|
||||
-- For development only. Use seeder-api for proper user creation with pepper.
|
||||
INSERT INTO users (identifier, password_hash, role, full_name, active)
|
||||
VALUES (
|
||||
'superadmin',
|
||||
'$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- placeholder, seeder will update
|
||||
'$2a$10$UWrE9xN39lVagJHlXZsxwOVI3NRSEd1VJ6UzMblW6LOxNmsOZtj9K', -- placeholder
|
||||
'superadmin',
|
||||
'Super Administrator',
|
||||
'admin@gohorsejobs.com',
|
||||
'Super Administrator',
|
||||
'active',
|
||||
c.id
|
||||
FROM companies c WHERE c.slug = 'gohorse-system'
|
||||
ON CONFLICT (identifier) DO NOTHING;
|
||||
true
|
||||
) ON CONFLICT (identifier) DO NOTHING;
|
||||
|
||||
-- 3. Assign Super Admin Role
|
||||
INSERT INTO user_roles (user_id, role)
|
||||
SELECT u.id, 'SUPER_ADMIN'
|
||||
FROM users u WHERE u.identifier = 'superadmin'
|
||||
ON CONFLICT (user_id, role) DO NOTHING;
|
||||
-- NOTE: user_roles table is created in migration 020, so we skip role assignment here.
|
||||
-- The seeder-api will handle proper user creation with all fields.
|
||||
|
|
|
|||
57
backend/migrations/021_create_uuid_v7_function.sql
Normal file
57
backend/migrations/021_create_uuid_v7_function.sql
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
-- Migration: Create UUID v7 generation function
|
||||
-- Description: PostgreSQL function to generate UUID v7 (time-ordered UUIDs)
|
||||
-- UUID v7 format: tttttttt-tttt-7xxx-yxxx-xxxxxxxxxxxx
|
||||
-- Where: t = timestamp, 7 = version, y = variant, x = random
|
||||
|
||||
-- Create or replace the uuid_generate_v7 function
|
||||
CREATE OR REPLACE FUNCTION uuid_generate_v7()
|
||||
RETURNS uuid AS $$
|
||||
DECLARE
|
||||
unix_ts_ms bigint;
|
||||
uuid_bytes bytea;
|
||||
BEGIN
|
||||
-- Get current Unix timestamp in milliseconds
|
||||
unix_ts_ms := (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint;
|
||||
|
||||
-- Build the UUID bytes:
|
||||
-- First 6 bytes: timestamp (48 bits)
|
||||
-- Next 2 bytes: version (4 bits) + random (12 bits)
|
||||
-- Last 8 bytes: variant (2 bits) + random (62 bits)
|
||||
uuid_bytes := set_byte(
|
||||
set_byte(
|
||||
set_byte(
|
||||
set_byte(
|
||||
set_byte(
|
||||
set_byte(
|
||||
gen_random_bytes(16),
|
||||
0, (unix_ts_ms >> 40)::int
|
||||
),
|
||||
1, (unix_ts_ms >> 32)::int
|
||||
),
|
||||
2, (unix_ts_ms >> 24)::int
|
||||
),
|
||||
3, (unix_ts_ms >> 16)::int
|
||||
),
|
||||
4, (unix_ts_ms >> 8)::int
|
||||
),
|
||||
5, unix_ts_ms::int
|
||||
);
|
||||
|
||||
-- Set version 7 (0111) in byte 6
|
||||
uuid_bytes := set_byte(uuid_bytes, 6, (get_byte(uuid_bytes, 6) & 15) | 112);
|
||||
|
||||
-- Set variant RFC 4122 (10xx) in byte 8
|
||||
uuid_bytes := set_byte(uuid_bytes, 8, (get_byte(uuid_bytes, 8) & 63) | 128);
|
||||
|
||||
RETURN encode(uuid_bytes, 'hex')::uuid;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql VOLATILE;
|
||||
|
||||
-- Comment
|
||||
COMMENT ON FUNCTION uuid_generate_v7() IS 'Generates a UUID v7 (time-ordered) - RFC 9562 compliant';
|
||||
|
||||
-- Test the function
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'UUID v7 test: %', uuid_generate_v7();
|
||||
END $$;
|
||||
20
backend/migrations/022_migrate_to_uuid_v7.sql
Normal file
20
backend/migrations/022_migrate_to_uuid_v7.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-- Migration: Update UUID tables to use UUID v7
|
||||
-- Description: Updates default values for notifications, tickets, job_payments to use uuid_generate_v7()
|
||||
-- Requires: 021_create_uuid_v7_function.sql (must run first)
|
||||
|
||||
-- Update notifications table to use UUID v7
|
||||
ALTER TABLE notifications ALTER COLUMN id SET DEFAULT uuid_generate_v7();
|
||||
|
||||
-- Update tickets table to use UUID v7
|
||||
ALTER TABLE tickets ALTER COLUMN id SET DEFAULT uuid_generate_v7();
|
||||
|
||||
-- Update ticket_messages table to use UUID v7
|
||||
ALTER TABLE ticket_messages ALTER COLUMN id SET DEFAULT uuid_generate_v7();
|
||||
|
||||
-- Update job_payments table to use UUID v7
|
||||
ALTER TABLE job_payments ALTER COLUMN id SET DEFAULT uuid_generate_v7();
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE notifications IS 'User notifications (UUID v7 IDs)';
|
||||
COMMENT ON TABLE tickets IS 'Support tickets (UUID v7 IDs)';
|
||||
COMMENT ON TABLE job_payments IS 'Payment records for job postings (UUID v7 IDs)';
|
||||
|
|
@ -4,7 +4,7 @@ Complete database documentation for the GoHorseJobs platform.
|
|||
|
||||
> **Last Updated:** 2024-12-24
|
||||
> **Database:** PostgreSQL 15+
|
||||
> **ID Strategy:** SERIAL (INT) for core tables, UUID v4 for newer tables
|
||||
> **ID Strategy:** SERIAL (INT) for core tables, UUID v7 for newer tables
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -600,10 +600,25 @@ The database uses a **hybrid ID strategy**:
|
|||
|
||||
| Strategy | Tables | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| **SERIAL (INT)** | users, companies, jobs, applications, regions, cities | Legacy tables, simpler, faster |
|
||||
| **UUID v4** | notifications, tickets, job_payments | Newer tables, distributed-friendly |
|
||||
| **SERIAL (INT)** | users, companies, jobs, applications, regions, cities | Legacy tables, simpler, faster, auto-increment |
|
||||
| **UUID v7** | notifications, tickets, job_payments | Time-ordered, distributed-friendly, sortable |
|
||||
|
||||
> **Note:** The system does NOT use UUID v7 (time-ordered UUIDs). It uses PostgreSQL's `gen_random_uuid()` which generates UUID v4.
|
||||
### UUID v7 (RFC 9562)
|
||||
|
||||
Starting from migration `021_create_uuid_v7_function.sql`, the database uses **UUID v7** instead of UUID v4:
|
||||
|
||||
```sql
|
||||
-- UUID v7 is generated by uuid_generate_v7() function
|
||||
SELECT uuid_generate_v7();
|
||||
-- Returns: 019438a1-2b3c-7abc-8123-4567890abcdef
|
||||
-- ^^^^^^^^ time component (sortable)
|
||||
```
|
||||
|
||||
**Benefits of UUID v7:**
|
||||
- ⏱️ Time-ordered (sortable by creation time)
|
||||
- 🌐 Distributed-friendly (no coordination needed)
|
||||
- 📊 Better index performance than UUID v4
|
||||
- 🔒 Contains embedded timestamp
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { pool } from '../db.js';
|
||||
import crypto from 'crypto';
|
||||
import { uuidv7 } from '../utils/uuid.js';
|
||||
|
||||
/**
|
||||
* 🔔 Notifications Seeder
|
||||
|
|
@ -98,7 +98,7 @@ export async function seedNotifications() {
|
|||
|
||||
for (let i = 0; i < numNotifs && i < notificationTemplates.length; i++) {
|
||||
const template = notificationTemplates[i % notificationTemplates.length];
|
||||
const notifId = crypto.randomUUID();
|
||||
const notifId = uuidv7();
|
||||
|
||||
// Random time in the past 30 days
|
||||
const daysAgo = Math.floor(Math.random() * 30);
|
||||
|
|
|
|||
66
seeder-api/src/utils/uuid.js
Normal file
66
seeder-api/src/utils/uuid.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* UUID v7 Generator for Node.js
|
||||
* Generates time-ordered UUIDs per RFC 9562
|
||||
*
|
||||
* Format: tttttttt-tttt-7xxx-yxxx-xxxxxxxxxxxx
|
||||
* - t = timestamp (48 bits)
|
||||
* - 7 = version
|
||||
* - x = random
|
||||
* - y = variant (8, 9, a, or b)
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* Generate a UUID v7 (time-ordered)
|
||||
* @returns {string} UUID v7 string
|
||||
*/
|
||||
export function uuidv7() {
|
||||
// Get current timestamp in milliseconds
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Generate random bytes
|
||||
const randomBytes = crypto.randomBytes(10);
|
||||
|
||||
// Build UUID bytes (16 total)
|
||||
const bytes = Buffer.alloc(16);
|
||||
|
||||
// First 6 bytes: timestamp (48 bits, big-endian)
|
||||
bytes.writeUInt32BE(Math.floor(timestamp / 0x10000), 0);
|
||||
bytes.writeUInt16BE(timestamp & 0xFFFF, 4);
|
||||
|
||||
// Next 2 bytes: version (4 bits = 7) + random (12 bits)
|
||||
bytes[6] = 0x70 | (randomBytes[0] & 0x0F); // version 7
|
||||
bytes[7] = randomBytes[1];
|
||||
|
||||
// Last 8 bytes: variant (2 bits = 10) + random (62 bits)
|
||||
bytes[8] = 0x80 | (randomBytes[2] & 0x3F); // variant RFC 4122
|
||||
randomBytes.copy(bytes, 9, 3, 10);
|
||||
|
||||
// Format as UUID string
|
||||
const hex = bytes.toString('hex');
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a string is a valid UUID (any version)
|
||||
* @param {string} uuid
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isValidUUID(uuid) {
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get UUID version from a UUID string
|
||||
* @param {string} uuid
|
||||
* @returns {number} version (1-7) or 0 if invalid
|
||||
*/
|
||||
export function getUUIDVersion(uuid) {
|
||||
if (!isValidUUID(uuid)) return 0;
|
||||
return parseInt(uuid.charAt(14), 16);
|
||||
}
|
||||
|
||||
// Export default for convenience
|
||||
export default { uuidv7, isValidUUID, getUUIDVersion };
|
||||
Loading…
Reference in a new issue