diff --git a/PostFundManagement.ConsoleUI/src/services/toolsService.ts b/PostFundManagement.ConsoleUI/src/services/toolsService.ts deleted file mode 100644 index b0aa4a6..0000000 --- a/PostFundManagement.ConsoleUI/src/services/toolsService.ts +++ /dev/null @@ -1,247 +0,0 @@ -import Hashids from 'hashids'; -import { useQuery } from '@tanstack/react-query'; - -const env = (typeof import.meta !== 'undefined' && import.meta.env - ? import.meta.env - : {}) as Record; - -/** - * Deterministic Hashids configuration matching pfm-api HasherSettings. - * Ensures 100% interoperability with the C# Hashids.net NuGet package on the backend. - */ -export const HASHER_SETTINGS = { - minHashLength: Number(env.VITE_HASHER_MIN_LENGTH) || 11, - salt: (env.VITE_HASHER_SALT as string) || '', -}; - -// Singleton Hashids instance initialized with salt & minHashLength -let hasher = new Hashids(HASHER_SETTINGS.salt, HASHER_SETTINGS.minHashLength); - -/** - * Dynamically reconfigures the deterministic Hashids instance from runtime configuration. - */ -export function updateHasherSettings(salt?: string, minLength?: number | string): void { - let updated = false; - if (salt && typeof salt === 'string' && salt.trim() !== '') { - HASHER_SETTINGS.salt = salt.trim(); - updated = true; - } - if (minLength !== undefined && minLength !== null && minLength !== '') { - const parsed = Number(minLength); - if (!isNaN(parsed) && parsed > 0) { - HASHER_SETTINGS.minHashLength = parsed; - updated = true; - } - } - if (updated) { - hasher = new Hashids(HASHER_SETTINGS.salt, HASHER_SETTINGS.minHashLength); - } -} - -/** - * Checks whether an incoming string is already a valid Hashid matching the configured salt and min length. - * Prevents double-encoding already-hashed route parameters or IDs. - */ -function isAlreadyHashId(candidate: string): boolean { - if (typeof candidate !== 'string' || candidate.length < HASHER_SETTINGS.minHashLength) { - return false; - } - try { - const decoded = hasher.decode(candidate); - return Array.isArray(decoded) && decoded.length > 0; - } catch { - return false; - } -} - -/** - * Deterministically encodes a numeric ID or numeric string into a HashID string. - * Synchronous and does not perform HTTP network round trips. - */ -export function encodeHashIdSync(id: number | string | null | undefined): string { - if (id === undefined || id === null || id === '') { - return ''; - } - - // If already a number - if (typeof id === 'number') { - if (isNaN(id) || !isFinite(id) || id < 0) return ''; - return hasher.encode(id); - } - - const str = String(id).trim(); - if (!str) return ''; - - // Check if string is already a valid HashID - if (isAlreadyHashId(str)) { - return str; - } - - // Check if string is pure digits (e.g. "123") - if (/^\d+$/.test(str)) { - try { - const num = parseInt(str, 10); - if (!isNaN(num) && num >= 0) { - return hasher.encode(num); - } - } catch { - // Fall through - } - } - - // Non-numeric identifiers (UUIDs, codes) returned as-is - return str; -} - -/** - * Deterministically decodes a HashID string back to original numeric ID or string. - * Synchronous and does not perform HTTP network round trips. - */ -export function decodeHashIdSync(hashId: string | number | null | undefined): number | string { - if (hashId === undefined || hashId === null || hashId === '') return ''; - if (typeof hashId === 'number') return hashId; - - const str = String(hashId).trim(); - if (!str) return ''; - - // If already pure integer string - if (/^-?\d+$/.test(str)) { - const parsed = parseInt(str, 10); - return isNaN(parsed) ? str : parsed; - } - - try { - const decoded = hasher.decode(str); - if (Array.isArray(decoded) && decoded.length > 0) { - return Number(decoded[0]); - } - } catch { - // If decoding fails, return raw string - } - - return str; -} - -/** - * Decodes a HashID or numeric value strictly to a canonical integer. - * Useful for request payloads expecting int64 integers. - */ -export function decodeToNumberSync(hashId: number | string | null | undefined): number { - if (hashId === null || hashId === undefined || hashId === '') return 0; - if (typeof hashId === 'number') { - return isNaN(hashId) ? 0 : Math.trunc(hashId); - } - const str = String(hashId).trim(); - if (/^-?\d+$/.test(str)) { - const parsed = parseInt(str, 10); - return isNaN(parsed) ? 0 : parsed; - } - try { - const decoded = hasher.decode(str); - if (Array.isArray(decoded) && decoded.length > 0) { - return Number(decoded[0]); - } - } catch {} - const fallback = Number(str); - return isNaN(fallback) ? 0 : Math.trunc(fallback); -} - -export const toolsService = { - /** - * Synchronous encoding - */ - encodeIdSync(id: number | string | null | undefined): string { - return encodeHashIdSync(id); - }, - - /** - * Synchronous decoding - */ - decodeIdSync(hashId: string | number | null | undefined): number | string { - return decodeHashIdSync(hashId); - }, - - /** - * Synchronous decoding strictly to canonical integer - */ - decodeToNumberSync(hashId: number | string | null | undefined): number { - return decodeToNumberSync(hashId); - }, - - /** - * Encodes a raw ID to a HashID (returns Promise for backward compatibility with existing callers) - */ - async encodeId(id: number | string | null | undefined, _context = 'general'): Promise { - return encodeHashIdSync(id); - }, - - /** - * Decodes a HashID string back to original identifier - */ - async decodeId(hashId: string | null | undefined, _context = 'general'): Promise { - return decodeHashIdSync(hashId); - }, - - /** - * Domain-specific convenience resolvers for entities - */ - async resolveProjectId(projectId: number | string): Promise { - return encodeHashIdSync(projectId); - }, - - async resolveOrganisationId(organisationId: number | string): Promise { - return encodeHashIdSync(organisationId); - }, - - async resolveProgrammeId(programmeId: number | string): Promise { - return encodeHashIdSync(programmeId); - }, - - async resolvePortfolioId(portfolioId: number | string): Promise { - return encodeHashIdSync(portfolioId); - }, - - async resolveAwardId(awardId: number | string): Promise { - return encodeHashIdSync(awardId); - }, - - async resolveContractId(contractId: number | string): Promise { - return encodeHashIdSync(contractId); - }, - - /** - * Resolves query parameters or path variables encoded as HashIDs before performing API calls - */ - async resolveHashedParams>( - params: T, - keysToHash: (keyof T)[] - ): Promise { - const resolved: any = { ...params }; - for (const key of keysToHash) { - const val = params[key]; - if (val !== undefined && val !== null) { - resolved[key] = encodeHashIdSync(val); - } - } - return resolved; - }, - - /** - * Compatibility cache primer (now a no-op as Hashids calculation is instantaneous client-side) - */ - async primeCache(_items: { id: number | string; context: string }[]): Promise { - // No-op - instant deterministic computation - }, -}; - -/** - * React Hook for consuming HashIDs with TanStack Query caching in components - */ -export function useHashId(id?: number | string | null, context = 'general') { - return useQuery({ - queryKey: ['hashId', context, id !== undefined && id !== null ? String(id) : ''], - queryFn: () => Promise.resolve(encodeHashIdSync(id)), - enabled: id !== undefined && id !== null && id !== '', - staleTime: Infinity, - }); -}