-
Notifications
You must be signed in to change notification settings - Fork 132
perf: replace UUIDv4 with UUIDv7 for ordered primary keys #511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| import { Response, NextFunction } from "express"; | ||
| import { z } from "zod"; | ||
| import crypto from "crypto"; | ||
| import { generateId } from '../utils/idGenerator'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Fix Prettier violations on the new import and correlation fallback formatting. Line 8 and Lines 127-129 are currently failing ESLint/Prettier. Suggested patch-import { generateId } from '../utils/idGenerator';
+import { generateId } from "../utils/idGenerator";
@@
- const correlationId =
- (req.headers["x-request-id"] as string | undefined) ??
- generateId();
+ const correlationId =
+ (req.headers["x-request-id"] as string | undefined) ?? generateId();Also applies to: 127-129 🧰 Tools🪛 ESLint[error] 8-8: Replace (prettier/prettier) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| import { prisma } from "../config/database"; | ||
| import { AuthRequest } from "../middleware/auth"; | ||
| import { Decimal } from "@prisma/client/runtime/library"; | ||
|
|
@@ -125,7 +126,7 @@ export async function registerOnRampSwap( | |
| } | ||
| const correlationId = | ||
| (req.headers["x-request-id"] as string | undefined) ?? | ||
| crypto.randomUUID(); | ||
| generateId(); | ||
| logFinancialEvent({ | ||
| event: "onramp.registered", | ||
| status: "pending", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ function setFiatWebhookDeprecationHeaders(res: Response): void { | |
| } | ||
| import { Request, Response, NextFunction } from "express"; | ||
| import crypto from "crypto"; | ||
| import { generateId } from '../utils/idGenerator'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Resolve ESLint/Prettier formatting errors in the touched webhook paths. Line 17 and Lines 221-223/302-304 are flagged by Prettier in the current diff. Suggested patch-import { generateId } from '../utils/idGenerator';
+import { generateId } from "../utils/idGenerator";
@@
- const paystackCorrelationId =
- (req.headers["x-request-id"] as string | undefined) ??
- generateId();
+ const paystackCorrelationId =
+ (req.headers["x-request-id"] as string | undefined) ?? generateId();
@@
- const flwCorrelationId =
- (req.headers["x-request-id"] as string | undefined) ??
- generateId();
+ const flwCorrelationId =
+ (req.headers["x-request-id"] as string | undefined) ?? generateId();Also applies to: 221-223, 302-304 🧰 Tools🪛 ESLint[error] 17-17: Replace (prettier/prettier) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| import { config } from "../config/env"; | ||
| import { logger, logFinancialEvent } from "../config/logger"; | ||
| import { prisma } from "../config/database"; | ||
|
|
@@ -219,7 +220,7 @@ export async function handlePaystackWebhook( | |
| paystackStatusMap[data.status ?? ""] ?? "pending"; | ||
| const paystackCorrelationId = | ||
| (req.headers["x-request-id"] as string | undefined) ?? | ||
| crypto.randomUUID(); | ||
| generateId(); | ||
|
|
||
| logFinancialEvent({ | ||
| event: "webhook.received", | ||
|
|
@@ -300,7 +301,7 @@ export async function handleFlutterwaveWebhook( | |
| flwStatusMap[data.status ?? ""] ?? "pending"; | ||
| const flwCorrelationId = | ||
| (req.headers["x-request-id"] as string | undefined) ?? | ||
| crypto.randomUUID(); | ||
| generateId(); | ||
|
|
||
| logFinancialEvent({ | ||
| event: "webhook.received", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |||||
| * Consumes WITHDRAWAL_PROCESSING queue: after BurnEvent, validate withdrawal and recipient, | ||||||
| * disburse via fintech, update transaction status, optionally publish user notification. | ||||||
| */ | ||||||
| import { randomUUID } from "crypto"; | ||||||
| import { generateId } from '../utils/idGenerator'; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Fix import quote style to satisfy Prettier. Line 5 is currently flagged by ESLint/Prettier. Suggested patch-import { generateId } from '../utils/idGenerator';
+import { generateId } from "../utils/idGenerator";📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 5-5: Replace (prettier/prettier) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||
| import type { ConsumeMessage } from "amqplib"; | ||||||
| import { | ||||||
| connectRabbitMQ, | ||||||
|
|
@@ -28,7 +28,7 @@ export async function startWithdrawalProcessingConsumer(): Promise<void> { | |||||
| queue, | ||||||
| async (msg: ConsumeMessage | null) => { | ||||||
| if (!msg) return; | ||||||
| const correlationId = randomUUID(); | ||||||
| const correlationId = generateId(); | ||||||
| try { | ||||||
| const body = JSON.parse(msg.content.toString()) as WithdrawalPayload; | ||||||
| const { transactionId, txHash } = body; | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -9,7 +9,7 @@ import { logger, logFinancialEvent } from "../config/logger"; | |||||
| import { prisma } from "../config/database"; | ||||||
| import { mintFromUsdcInternal } from "../controllers/mintController"; | ||||||
| import { fetchXlmRateUsd } from "../services/oracle/cryptoClient"; | ||||||
| import { randomUUID } from "crypto"; | ||||||
| import { generateId } from '../utils/idGenerator'; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Normalize import quotes to pass Prettier. Line 12 is reported as a Prettier violation. Suggested patch-import { generateId } from '../utils/idGenerator';
+import { generateId } from "../utils/idGenerator";📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 12-12: Replace (prettier/prettier) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||
|
|
||||||
| const QUEUE = QUEUES.XLM_TO_ACBU; | ||||||
| const MAX_RETRIES = 5; | ||||||
|
|
@@ -34,7 +34,7 @@ export async function startXlmToAcbuConsumer(): Promise<void> { | |||||
| const retries = typeof headers["x-retries"] === "number" ? headers["x-retries"] : 0; | ||||||
| try { | ||||||
| const body = JSON.parse(msg.content.toString()) as XlmToAcbuPayload; | ||||||
| const correlationId = randomUUID(); | ||||||
| const correlationId = generateId(); | ||||||
| await processXlmToAcbu(body, correlationId); | ||||||
| ch.ack(msg); | ||||||
| } catch (e) { | ||||||
|
|
@@ -61,7 +61,7 @@ export async function startXlmToAcbuConsumer(): Promise<void> { | |||||
| */ | ||||||
| export async function processXlmToAcbu( | ||||||
| payload: XlmToAcbuPayload, | ||||||
| correlationId: string = randomUUID(), | ||||||
| correlationId: string = generateId(), | ||||||
| ): Promise<void> { | ||||||
| const { onRampSwapId, userId, stellarAddress, xlmAmount, usdcEquivalent } = | ||||||
| payload; | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ | |
| */ | ||
| import bcrypt from "bcryptjs"; | ||
| import { totp } from "otplib"; | ||
| import { randomUUID } from "crypto"; | ||
| import { generateId } from '../../utils/idGenerator'; | ||
| import { config } from "../../config/env"; | ||
| import { prisma } from "../../config/database"; | ||
| import { generateApiKey } from "../../middleware/auth"; | ||
|
|
@@ -889,8 +889,8 @@ export interface RevokeRefreshTokenParams { | |
| } | ||
|
|
||
| function generateSecureRefreshToken(): string { | ||
| const bytes = Buffer.from(randomUUID()).toString('base64'); | ||
| return bytes + Buffer.from(randomUUID()).toString('base64'); | ||
| const bytes = Buffer.from(generateId()).toString('base64'); | ||
| return bytes + Buffer.from(generateId()).toString('base64'); | ||
|
Comment on lines
+892
to
+893
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Use CSPRNG bytes for refresh-token secrets Line 892 and Line 893 now build bearer refresh tokens from Suggested fix function generateSecureRefreshToken(): string {
- const bytes = Buffer.from(generateId()).toString('base64');
- return bytes + Buffer.from(generateId()).toString('base64');
+ return randomBytes(48).toString("base64url");
}🧰 Tools🪛 ESLint[error] 892-892: Replace (prettier/prettier) [error] 893-893: Replace (prettier/prettier) 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| async function hashRefreshToken(token: string): Promise<string> { | ||
|
|
@@ -907,7 +907,7 @@ export async function issueRefreshToken( | |
| const { userId } = params; | ||
| const token = generateSecureRefreshToken(); | ||
| const tokenHash = await hashRefreshToken(token); | ||
| const tokenFamilyId = randomUUID(); | ||
| const tokenFamilyId = generateId(); | ||
| const expiresAt = new Date( | ||
| Date.now() + REFRESH_TOKEN_EXPIRY_DAYS * 24 * 60 * 60 * 1000, | ||
| ); | ||
|
|
@@ -995,7 +995,7 @@ export async function refreshAccessToken( | |
| // Issue a new refresh token in a NEW family | ||
| const newToken = generateSecureRefreshToken(); | ||
| const newTokenHash = await hashRefreshToken(newToken); | ||
| const newTokenFamilyId = randomUUID(); | ||
| const newTokenFamilyId = generateId(); | ||
| const newExpiresAt = new Date( | ||
| Date.now() + REFRESH_TOKEN_EXPIRY_DAYS * 24 * 60 * 60 * 1000, | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||
| import { Decimal } from "@prisma/client/runtime/library"; | ||||||
| import { prisma } from "../../config/database"; | ||||||
| import { generateId } from '../../utils/idGenerator'; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Fix import quote style flagged by ESLint/Prettier. Line 3 is failing the configured Prettier rule. Suggested patch-import { generateId } from '../../utils/idGenerator';
+import { generateId } from "../../utils/idGenerator";📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 3-3: Replace (prettier/prettier) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||
| import { AppError } from "../../middleware/errorHandler"; | ||||||
| import { logger, logFinancialEvent } from "../../config/logger"; | ||||||
| import { logAudit } from "../audit"; | ||||||
|
|
@@ -204,7 +205,7 @@ export async function payBill( | |||||
| performedBy: request.userId ?? undefined, | ||||||
| }); | ||||||
|
|
||||||
| const correlationId = crypto.randomUUID(); | ||||||
| const correlationId = generateId(); | ||||||
|
|
||||||
| logFinancialEvent({ | ||||||
| event: "bill.initiated", | ||||||
|
|
@@ -433,7 +434,7 @@ export async function reconcileBillsWebhook(event: BillsWebhookEvent): Promise<{ | |||||
| userId: transaction.userId ?? transaction.id, | ||||||
| accountId: transaction.userId ?? transaction.id, | ||||||
| idempotencyKey: transaction.id, | ||||||
| correlationId: crypto.randomUUID(), | ||||||
| correlationId: generateId(), | ||||||
| amount: Math.round((transaction.localAmount?.toNumber() ?? event.amount) * 100), | ||||||
| currency: transaction.localCurrency ?? event.currency, | ||||||
| provider: event.provider, | ||||||
|
|
@@ -497,7 +498,7 @@ export async function refundBillPayment( | |||||
| userId: transaction.userId ?? transaction.id, | ||||||
| accountId: transaction.userId ?? transaction.id, | ||||||
| idempotencyKey: transaction.id, | ||||||
| correlationId: crypto.randomUUID(), | ||||||
| correlationId: generateId(), | ||||||
| amount: Math.round(localAmount * 100), | ||||||
| currency, | ||||||
| provider: refundResponse.provider, | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ import type { | |||||
| PartnerBillRefundRequest, | ||||||
| PartnerBillRefundResponse, | ||||||
| } from "./types"; | ||||||
| import { generateId } from '../../utils/idGenerator'; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Adjust import quotes to match Prettier config. Line 10 is flagged by ESLint/Prettier. Suggested patch-import { generateId } from '../../utils/idGenerator';
+import { generateId } from "../../utils/idGenerator";📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 10-10: Replace (prettier/prettier) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||
|
|
||||||
| const SIMULATED_BILLERS: BillsCatalogBiller[] = [ | ||||||
| { | ||||||
|
|
@@ -107,7 +108,7 @@ export class SimulatedBillsPartner implements BillsPartnerAdapter { | |||||
| async payBill( | ||||||
| request: PartnerBillPaymentRequest, | ||||||
| ): Promise<PartnerBillPaymentResponse> { | ||||||
| const providerReference = `bill_${crypto.randomUUID()}`; | ||||||
| const providerReference = `bill_${generateId()}`; | ||||||
| return { | ||||||
| provider: this.providerId, | ||||||
| providerReference, | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { randomBytes } from 'crypto'; | ||
|
|
||
| /** | ||
| * Generates a UUIDv7 (timestamp-ordered) string. | ||
| * Timestamp-ordered UUIDs avoid B-tree index fragmentation in PostgreSQL | ||
| * compared to random UUIDv4, improving insert performance and index locality. | ||
| * A monotonic counter ensures strict ordering within the same millisecond. | ||
| */ | ||
|
|
||
| let lastMs = -1n; | ||
| let seq = 0; | ||
|
|
||
| export function generateId(): string { | ||
| let ms = BigInt(Date.now()); | ||
| if (ms === lastMs) { | ||
| seq = (seq + 1) & 0xfff; | ||
| if (seq === 0) ms = ++lastMs; // counter overflow: advance clock | ||
| } else { | ||
| seq = 0; | ||
| lastMs = ms; | ||
| } | ||
|
Comment on lines
+15
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Monotonic ordering regresses when The state machine only checks Suggested fix export function generateId(): string {
let ms = BigInt(Date.now());
+ if (ms < lastMs) ms = lastMs;
+
if (ms === lastMs) {
seq = (seq + 1) & 0xfff;
- if (seq === 0) ms = ++lastMs; // counter overflow: advance clock
+ if (seq === 0) {
+ ms = lastMs + 1n; // counter overflow: advance logical clock
+ }
} else {
seq = 0;
- lastMs = ms;
}
+ lastMs = ms;🤖 Prompt for AI Agents |
||
|
|
||
| const rand = randomBytes(8); | ||
| const timeLow = Number(ms & BigInt(0xffffffff)); | ||
| const timeMid = Number((ms >> BigInt(32)) & BigInt(0xffff)); | ||
|
Comment on lines
+24
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Timestamp fields are swapped, so this is not UUIDv7-ordered. Line 24/25 currently maps Suggested fix- const timeLow = Number(ms & BigInt(0xffffffff));
- const timeMid = Number((ms >> BigInt(32)) & BigInt(0xffff));
+ const timeLow = Number((ms >> BigInt(16)) & BigInt(0xffffffff));
+ const timeMid = Number(ms & BigInt(0xffff));Also applies to: 32-33 🤖 Prompt for AI Agents |
||
| const ver = 0x7000 | seq; // version 7 | 12-bit seq | ||
| const varRand = 0x80 | (rand[0] & 0x3f); // variant 10xx | random | ||
|
|
||
| const hex = (n: number, pad: number) => n.toString(16).padStart(pad, '0'); | ||
|
|
||
| return [ | ||
| hex(timeLow, 8), | ||
| hex(timeMid, 4), | ||
| hex(ver, 4), | ||
| hex(varRand, 2) + hex(rand[1], 2), | ||
| rand.slice(2).reduce((s, b) => s + hex(b, 2), ''), | ||
| ].join('-'); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Avoid timestamp-prefixed tombstone suffixes
Line 92 takes
substring(0, 8)fromgenerateId(). For UUIDv7-style IDs, the prefix is time-derived, so suffixes become predictable and can collide under burst deletes. Use random tail bits instead of the leading timestamp portion.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents