From ae4a3e5e637e2301f2b87ac8342a986f813674e2 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Wed, 24 Dec 2025 11:19:26 -0300 Subject: [PATCH] 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 --- backend/migrations/010_seed_super_admin.sql | 27 +++----- .../021_create_uuid_v7_function.sql | 57 ++++++++++++++++ backend/migrations/022_migrate_to_uuid_v7.sql | 20 ++++++ docs/DATABASE.md | 23 +++++-- seeder-api/src/seeders/notifications.js | 4 +- seeder-api/src/utils/uuid.js | 66 +++++++++++++++++++ 6 files changed, 174 insertions(+), 23 deletions(-) create mode 100644 backend/migrations/021_create_uuid_v7_function.sql create mode 100644 backend/migrations/022_migrate_to_uuid_v7.sql create mode 100644 seeder-api/src/utils/uuid.js diff --git a/backend/migrations/010_seed_super_admin.sql b/backend/migrations/010_seed_super_admin.sql index 638abf6..adccb05 100644 --- a/backend/migrations/010_seed_super_admin.sql +++ b/backend/migrations/010_seed_super_admin.sql @@ -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. diff --git a/backend/migrations/021_create_uuid_v7_function.sql b/backend/migrations/021_create_uuid_v7_function.sql new file mode 100644 index 0000000..c66b7ec --- /dev/null +++ b/backend/migrations/021_create_uuid_v7_function.sql @@ -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 $$; diff --git a/backend/migrations/022_migrate_to_uuid_v7.sql b/backend/migrations/022_migrate_to_uuid_v7.sql new file mode 100644 index 0000000..3e7f0a2 --- /dev/null +++ b/backend/migrations/022_migrate_to_uuid_v7.sql @@ -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)'; diff --git a/docs/DATABASE.md b/docs/DATABASE.md index e1010b2..252ca6d 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -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 --- diff --git a/seeder-api/src/seeders/notifications.js b/seeder-api/src/seeders/notifications.js index 5e5693a..11de134 100644 --- a/seeder-api/src/seeders/notifications.js +++ b/seeder-api/src/seeders/notifications.js @@ -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); diff --git a/seeder-api/src/utils/uuid.js b/seeder-api/src/utils/uuid.js new file mode 100644 index 0000000..2bf27da --- /dev/null +++ b/seeder-api/src/utils/uuid.js @@ -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 };