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:
Tiago Yamamoto 2025-12-24 11:19:26 -03:00
parent 017a34c965
commit ae4a3e5e63
6 changed files with 174 additions and 23 deletions

View file

@ -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.

View 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 $$;

View 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)';

View file

@ -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
---

View file

@ -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);

View 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 };