feat: add generateId, generateCityCode and getLatId

This commit is contained in:
vergiLgood1 2025-04-15 22:47:41 +07:00
parent 0cbcd3e636
commit 410535e1d9
1 changed files with 377 additions and 77 deletions

View File

@ -1,8 +1,22 @@
import { format } from "date-fns"; import { format } from 'date-fns';
import { redirect } from "next/navigation"; import { redirect } from 'next/navigation';
import { DateFormatOptions, DateFormatPattern } from "./types/date-format.interface"; import {
import { toast } from "sonner"; DateFormatOptions,
import { IUserSchema } from "@/src/entities/models/users/users.model"; DateFormatPattern,
} from './types/date-format.interface';
import { toast } from 'sonner';
import { IUserSchema } from '@/src/entities/models/users/users.model';
import db from '../../prisma/db';
import { v4 as uuidv4 } from 'uuid';
import * as crypto from 'crypto';
// Maintain a registry of used IDs to prevent duplicates
const usedIdRegistry = new Set<string>();
// Type definition for the global counter
declare global {
var __idCounter: number;
}
/** /**
* Redirects to a specified path with an encoded message as a query parameter. * Redirects to a specified path with an encoded message as a query parameter.
@ -12,9 +26,9 @@ import { IUserSchema } from "@/src/entities/models/users/users.model";
* @returns {never} This function doesn't return as it triggers a redirect. * @returns {never} This function doesn't return as it triggers a redirect.
*/ */
export function encodedRedirect( export function encodedRedirect(
type: "error" | "success", type: 'error' | 'success',
path: string, path: string,
message: string, message: string
) { ) {
return redirect(`${path}?${type}=${encodeURIComponent(message)}`); return redirect(`${path}?${type}=${encodeURIComponent(message)}`);
} }
@ -27,17 +41,17 @@ export function encodedRedirect(
// Helper function to ensure URLs are properly formatted // Helper function to ensure URLs are properly formatted
export function formatUrl(url: string): string { export function formatUrl(url: string): string {
// If URL starts with a slash, it's already absolute // If URL starts with a slash, it's already absolute
if (url.startsWith("/")) { if (url.startsWith('/')) {
return url; return url;
} }
// Otherwise, ensure it's properly formatted relative to root // Otherwise, ensure it's properly formatted relative to root
// Remove any potential duplicated '/dashboard' prefixes // Remove any potential duplicated '/dashboard' prefixes
if (url.startsWith("dashboard/")) { if (url.startsWith('dashboard/')) {
return "/" + url; return '/' + url;
} }
return "/" + url; return '/' + url;
} }
/** /**
@ -52,8 +66,7 @@ export function createFormData(): FormData {
} }
}); });
return data; return data;
}; }
/** /**
* Generates a unique username based on the provided email address. * Generates a unique username based on the provided email address.
@ -71,7 +84,7 @@ export function createFormData(): FormData {
* ``` * ```
*/ */
export function generateUsername(email: string): string { export function generateUsername(email: string): string {
const [localPart] = email.split("@"); const [localPart] = email.split('@');
const randomSuffix = Math.random().toString(36).substring(2, 8); // Generate a random alphanumeric string const randomSuffix = Math.random().toString(36).substring(2, 8); // Generate a random alphanumeric string
return `${localPart}.${randomSuffix}`; return `${localPart}.${randomSuffix}`;
} }
@ -84,70 +97,70 @@ export function generateUsername(email: string): string {
* @example * @example
* // Using default format * // Using default format
* formatDate("2025-03-23") * formatDate("2025-03-23")
* *
* // Using a custom format string * // Using a custom format string
* formatDate("2025-03-23", "yyyy-MM-dd") * formatDate("2025-03-23", "yyyy-MM-dd")
* *
* // Using formatting options * // Using formatting options
* formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" }) * formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" })
*/ */
export const formatDate = ( export const formatDate = (
date: string | Date | undefined | null, date: string | Date | undefined | null,
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" } options: DateFormatOptions | DateFormatPattern = { format: 'PPpp' }
): string => { ): string => {
if (!date) { if (!date) {
return typeof options === "string" return typeof options === 'string' ? '-' : options.fallback || '-';
? "-"
: (options.fallback || "-");
} }
const dateObj = date instanceof Date ? date : new Date(date); const dateObj = date instanceof Date ? date : new Date(date);
// Handle invalid dates // Handle invalid dates
if (isNaN(dateObj.getTime())) { if (isNaN(dateObj.getTime())) {
return typeof options === "string" return typeof options === 'string' ? '-' : options.fallback || '-';
? "-"
: (options.fallback || "-");
} }
if (typeof options === "string") { if (typeof options === 'string') {
return format(dateObj, options); return format(dateObj, options);
} }
const { format: formatPattern = "PPpp", locale } = options; const { format: formatPattern = 'PPpp', locale } = options;
return locale return locale
? format(dateObj, formatPattern, { locale }) ? format(dateObj, formatPattern, { locale })
: format(dateObj, formatPattern); : format(dateObj, formatPattern);
}; };
export const copyItem = (item: string, options?: { export const copyItem = (
label?: string, item: string,
onSuccess?: () => void, options?: {
onError?: (error: unknown) => void label?: string;
}) => { onSuccess?: () => void;
onError?: (error: unknown) => void;
}
) => {
if (!navigator.clipboard) { if (!navigator.clipboard) {
const error = new Error("Clipboard not supported"); const error = new Error('Clipboard not supported');
toast.error("Clipboard not supported"); toast.error('Clipboard not supported');
options?.onError?.(error); options?.onError?.(error);
return; return;
} }
if (!item) { if (!item) {
const error = new Error("Nothing to copy"); const error = new Error('Nothing to copy');
toast.error("Nothing to copy"); toast.error('Nothing to copy');
options?.onError?.(error); options?.onError?.(error);
return; return;
} }
navigator.clipboard.writeText(item) navigator.clipboard
.writeText(item)
.then(() => { .then(() => {
const label = options?.label || item; const label = options?.label || item;
toast.success(`${label} copied to clipboard`); toast.success(`${label} copied to clipboard`);
options?.onSuccess?.(); options?.onSuccess?.();
}) })
.catch((error) => { .catch((error) => {
toast.error("Failed to copy to clipboard"); toast.error('Failed to copy to clipboard');
options?.onError?.(error); options?.onError?.(error);
}); });
}; };
@ -160,67 +173,59 @@ export const copyItem = (item: string, options?: {
* @example * @example
* // Using default format * // Using default format
* formatDate("2025-03-23") * formatDate("2025-03-23")
* *
* // Using a custom format string * // Using a custom format string
* formatDate("2025-03-23", "yyyy-MM-dd") * formatDate("2025-03-23", "yyyy-MM-dd")
* *
* // Using formatting options * // Using formatting options
* formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" }) * formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" })
*/ */
export const formatDateWithFallback = ( export const formatDateWithFallback = (
date: string | Date | undefined | null, date: string | Date | undefined | null,
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" } options: DateFormatOptions | DateFormatPattern = { format: 'PPpp' }
): string => { ): string => {
if (!date) { if (!date) {
return typeof options === "string" return typeof options === 'string' ? '-' : options.fallback || '-';
? "-"
: (options.fallback || "-");
} }
const dateObj = date instanceof Date ? date : new Date(date); const dateObj = date instanceof Date ? date : new Date(date);
// Handle invalid dates // Handle invalid dates
if (isNaN(dateObj.getTime())) { if (isNaN(dateObj.getTime())) {
return typeof options === "string" return typeof options === 'string' ? '-' : options.fallback || '-';
? "-"
: (options.fallback || "-");
} }
if (typeof options === "string") { if (typeof options === 'string') {
return format(dateObj, options); return format(dateObj, options);
} }
const { format: formatPattern = "PPpp", locale } = options; const { format: formatPattern = 'PPpp', locale } = options;
return locale return locale
? format(dateObj, formatPattern, { locale }) ? format(dateObj, formatPattern, { locale })
: format(dateObj, formatPattern); : format(dateObj, formatPattern);
} };
export const formatDateWithLocale = ( export const formatDateWithLocale = (
date: string | Date | undefined | null, date: string | Date | undefined | null,
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" } options: DateFormatOptions | DateFormatPattern = { format: 'PPpp' }
): string => { ): string => {
if (!date) { if (!date) {
return typeof options === "string" return typeof options === 'string' ? '-' : options.fallback || '-';
? "-"
: (options.fallback || "-");
} }
const dateObj = date instanceof Date ? date : new Date(date); const dateObj = date instanceof Date ? date : new Date(date);
// Handle invalid dates // Handle invalid dates
if (isNaN(dateObj.getTime())) { if (isNaN(dateObj.getTime())) {
return typeof options === "string" return typeof options === 'string' ? '-' : options.fallback || '-';
? "-"
: (options.fallback || "-");
} }
if (typeof options === "string") { if (typeof options === 'string') {
return format(dateObj, options); return format(dateObj, options);
} }
const { format: formatPattern = "PPpp", locale } = options; const { format: formatPattern = 'PPpp', locale } = options;
return locale return locale
? format(dateObj, formatPattern, { locale }) ? format(dateObj, formatPattern, { locale })
@ -235,42 +240,38 @@ export const formatDateWithLocale = (
* @example * @example
* // Using default format * // Using default format
* formatDate("2025-03-23") * formatDate("2025-03-23")
* *
* // Using a custom format string * // Using a custom format string
* formatDate("2025-03-23", "yyyy-MM-dd") * formatDate("2025-03-23", "yyyy-MM-dd")
* *
* // Using formatting options * // Using formatting options
* formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" }) * formatDate("2025-03-23", { format: "MMMM do, yyyy", fallback: "Not available" })
*/ */
export const formatDateWithLocaleAndFallback = ( export const formatDateWithLocaleAndFallback = (
date: string | Date | undefined | null, date: string | Date | undefined | null,
options: DateFormatOptions | DateFormatPattern = { format: "PPpp" } options: DateFormatOptions | DateFormatPattern = { format: 'PPpp' }
): string => { ): string => {
if (!date) { if (!date) {
return typeof options === "string" return typeof options === 'string' ? '-' : options.fallback || '-';
? "-"
: (options.fallback || "-");
} }
const dateObj = date instanceof Date ? date : new Date(date); const dateObj = date instanceof Date ? date : new Date(date);
// Handle invalid dates // Handle invalid dates
if (isNaN(dateObj.getTime())) { if (isNaN(dateObj.getTime())) {
return typeof options === "string" return typeof options === 'string' ? '-' : options.fallback || '-';
? "-"
: (options.fallback || "-");
} }
if (typeof options === "string") { if (typeof options === 'string') {
return format(dateObj, options); return format(dateObj, options);
} }
const { format: formatPattern = "PPpp", locale } = options; const { format: formatPattern = 'PPpp', locale } = options;
return locale return locale
? format(dateObj, formatPattern, { locale }) ? format(dateObj, formatPattern, { locale })
: format(dateObj, formatPattern); : format(dateObj, formatPattern);
} };
/** /**
* Generates a full name from first and last names. * Generates a full name from first and last names.
@ -278,9 +279,12 @@ export const formatDateWithLocaleAndFallback = (
* @param lastName - The last name. * @param lastName - The last name.
* @returns The full name or "User" if both names are empty. * @returns The full name or "User" if both names are empty.
*/ */
export const getFullName = (firstName: string | null | undefined, lastName: string | null | undefined): string => { export const getFullName = (
return `${firstName || ""} ${lastName || ""}`.trim() || "User"; firstName: string | null | undefined,
} lastName: string | null | undefined
): string => {
return `${firstName || ''} ${lastName || ''}`.trim() || 'User';
};
/** /**
* Generates initials for a user based on their first and last names. * Generates initials for a user based on their first and last names.
@ -289,7 +293,11 @@ export const getFullName = (firstName: string | null | undefined, lastName: stri
* @param email - The email address. * @param email - The email address.
* @returns The initials or "U" if both names are empty. * @returns The initials or "U" if both names are empty.
*/ */
export const getInitials = (firstName: string, lastName: string, email: string): string => { export const getInitials = (
firstName: string,
lastName: string,
email: string
): string => {
if (firstName && lastName) { if (firstName && lastName) {
return `${firstName[0]}${lastName[0]}`.toUpperCase(); return `${firstName[0]}${lastName[0]}`.toUpperCase();
} }
@ -299,9 +307,8 @@ export const getInitials = (firstName: string, lastName: string, email: string):
if (email) { if (email) {
return email[0].toUpperCase(); return email[0].toUpperCase();
} }
return "U"; return 'U';
} };
export function calculateUserStats(users: IUserSchema[] | undefined) { export function calculateUserStats(users: IUserSchema[] | undefined) {
if (!users || !Array.isArray(users)) { if (!users || !Array.isArray(users)) {
@ -337,7 +344,10 @@ export function calculateUserStats(users: IUserSchema[] | undefined) {
* @param params - Object containing query parameters * @param params - Object containing query parameters
* @returns Formatted route with query parameters * @returns Formatted route with query parameters
*/ */
export const createRoute = (baseRoute: string, params?: Record<string, string>): string => { export const createRoute = (
baseRoute: string,
params?: Record<string, string>
): string => {
if (!params || Object.keys(params).length === 0) { if (!params || Object.keys(params).length === 0) {
return baseRoute; return baseRoute;
} }
@ -348,3 +358,293 @@ export const createRoute = (baseRoute: string, params?: Record<string, string>):
return `${baseRoute}?${queryString}`; return `${baseRoute}?${queryString}`;
}; };
/**
* Universal Custom ID Generator
* Creates structured, readable IDs for any system or entity
*
* @param {Object} options - Configuration options
* @param {string} options.prefix - Primary identifier prefix (e.g., "CRIME", "USER", "INVOICE")
* @param {Object} options.segments - Collection of ID segments to include
* @param {string[]} options.segments.codes - Array of short codes (e.g., region codes, department codes)
* @param {number} options.segments.year - Year to include in the ID
* @param {number} options.segments.sequentialDigits - Number of digits for sequential number
* @param {boolean} options.segments.includeDate - Whether to include current date
* @param {string} options.segments.dateFormat - Format for date (e.g., "yyyy-MM-dd", "dd/MM/yyyy")
* @param {boolean} options.segments.includeTime - Whether to include timestamp
* @param {string} options.format - Custom format string for ID structure
* @param {string} options.separator - Character to separate ID components
* @param {boolean} options.upperCase - Convert result to uppercase
* @returns {string} - Generated custom ID
*/
/**
* Generate a unique ID with multiple options to reduce collision risk
*/
export function generateId(
options: {
prefix?: string;
segments?: {
codes?: string[];
year?: number;
sequentialDigits?: number;
includeDate?: boolean;
dateFormat?: string;
includeTime?: boolean;
includeMilliseconds?: boolean;
};
format?: string;
separator?: string;
upperCase?: boolean;
randomSequence?: boolean;
uniquenessStrategy?: 'uuid' | 'timestamp' | 'counter' | 'hash';
retryOnCollision?: boolean;
maxRetries?: number;
} = {}
): string {
// Default options
const config = {
prefix: options.prefix || 'ID',
segments: {
codes: options.segments?.codes || [],
year: options.segments?.year,
sequentialDigits: options.segments?.sequentialDigits || 6, // Increased to 6
includeDate: options.segments?.includeDate ?? false,
dateFormat: options.segments?.dateFormat || 'yyyyMMdd',
includeTime: options.segments?.includeTime ?? false,
includeMilliseconds: options.segments?.includeMilliseconds ?? false,
},
format: options.format || null,
separator: options.separator || '-',
upperCase: options.upperCase ?? false,
randomSequence: options.randomSequence ?? true,
uniquenessStrategy: options.uniquenessStrategy || 'timestamp',
retryOnCollision: options.retryOnCollision ?? true,
maxRetries: options.maxRetries || 10,
};
// Static counter for sequential IDs (module-level)
if (typeof globalThis.__idCounter === 'undefined') {
globalThis.__idCounter = 0;
}
// Get current date and time with high precision
const now = new Date();
// Format date based on selected format
let dateString = '';
if (config.segments.includeDate) {
dateString = format(now, config.segments.dateFormat);
}
// Format time if included (with higher precision)
let timeString = '';
if (config.segments.includeTime) {
timeString = format(now, 'HHmmss');
// Add milliseconds for even more uniqueness
if (config.segments.includeMilliseconds) {
timeString += now.getMilliseconds().toString().padStart(3, '0');
}
}
// Generate sequence based on strategy
let sequentialNum: string;
switch (config.uniquenessStrategy) {
case 'uuid':
// Use first part of UUID for high uniqueness
sequentialNum = uuidv4().split('-')[0];
break;
case 'timestamp':
// Use high-precision timestamp
sequentialNum = `${now.getTime()}${Math.floor(Math.random() * 1000)}`;
sequentialNum = sequentialNum.slice(-config.segments.sequentialDigits);
break;
case 'counter':
// Use an incrementing counter
sequentialNum = (++globalThis.__idCounter)
.toString()
.padStart(config.segments.sequentialDigits, '0');
break;
case 'hash':
// Create a hash from the current time and options
const hashSource = `${now.getTime()}-${JSON.stringify(options)}-${Math.random()}`;
const hash = crypto.createHash('sha256').update(hashSource).digest('hex');
sequentialNum = hash.substring(0, config.segments.sequentialDigits);
break;
default:
// Standard random sequence with improved randomness
if (config.randomSequence) {
const randomBytes = crypto.randomBytes(4);
const randomNum = parseInt(randomBytes.toString('hex'), 16);
sequentialNum = (
randomNum % Math.pow(10, config.segments.sequentialDigits)
)
.toString()
.padStart(config.segments.sequentialDigits, '0');
} else {
sequentialNum = (++globalThis.__idCounter)
.toString()
.padStart(config.segments.sequentialDigits, '0');
}
}
// Prepare all components
const components = {
prefix: config.prefix,
codes: config.segments.codes.join(config.separator),
year: config.segments.year || format(now, 'yyyy'),
sequence: sequentialNum,
date: dateString,
time: timeString,
};
// Build the ID based on custom format if provided
let result: string;
if (config.format) {
let customID = config.format;
for (const [key, value] of Object.entries(components)) {
if (value) {
const placeholder = `{${key}}`;
customID = customID.replace(placeholder, String(value));
}
}
// Clean up any unused placeholders
customID = customID.replace(/{[^}]+}/g, '');
// Clean up any consecutive separators
const escapedSeparator = config.separator.replace(
/[-\/\\^$*+?.()|[\]{}]/g,
'\\$&'
);
const separatorRegex = new RegExp(`${escapedSeparator}+`, 'g');
customID = customID.replace(separatorRegex, config.separator);
// Remove leading/trailing separators
customID = customID.replace(
new RegExp(`^${escapedSeparator}|${escapedSeparator}$`, 'g'),
''
);
result = config.upperCase ? customID.toUpperCase() : customID;
} else {
// Default structured build if no format specified
const parts = [];
if (components.prefix) parts.push(components.prefix);
if (components.codes) parts.push(components.codes);
if (components.year) parts.push(components.year);
if (components.sequence) parts.push(components.sequence);
if (components.date) parts.push(components.date);
if (components.time) parts.push(components.time);
result = parts.join(config.separator);
if (config.upperCase) result = result.toUpperCase();
}
// Check for collisions and retry if necessary
if (config.retryOnCollision) {
let retryCount = 0;
let originalResult = result;
while (usedIdRegistry.has(result) && retryCount < config.maxRetries) {
retryCount++;
// Try adding a unique suffix
const suffix = crypto.randomBytes(2).toString('hex');
result = `${originalResult}${config.separator}${suffix}`;
}
if (retryCount >= config.maxRetries) {
console.warn(
`Warning: Max ID generation retries (${config.maxRetries}) reached for prefix ${config.prefix}`
);
}
}
// Register this ID to prevent future duplicates
usedIdRegistry.add(result);
// Periodically clean up the registry to prevent memory leaks (optional)
if (usedIdRegistry.size > 10000) {
// This is a simple implementation - in production you might want a more sophisticated strategy
const entriesToKeep = Array.from(usedIdRegistry).slice(-5000);
usedIdRegistry.clear();
entriesToKeep.forEach((id) => usedIdRegistry.add(id));
}
return result;
}
/**
* Gets the last ID from a specified table and column.
* @param tableName - The name of the table to query.
* @param columnName - The column containing the IDs.
* @returns The last ID as a string, or null if no records exist.
*/
export async function getLastId(
tableName: string,
columnName: string
): Promise<string | null> {
try {
const result = await db.$queryRawUnsafe(
`SELECT ${columnName} FROM ${tableName} ORDER BY ${columnName} DESC LIMIT 1`
);
if (Array.isArray(result) && result.length > 0) {
return result[0][columnName];
}
} catch (error) {
console.error('Error fetching last ID:', error);
}
return null;
}
/**
* Generates a unique code based on the provided name.
* @param name - The name to generate the code from.
* @returns The generated code.
*/
export function generateCode(name: string): string {
const words = name.toUpperCase().split(' ');
let code = '';
if (name.length <= 3) {
code = name.toUpperCase();
} else if (words.length > 1) {
code = words
.map((w) => w[0])
.join('')
.padEnd(3, 'X')
.slice(0, 3);
} else {
const nameClean = name.replace(/[aeiou]/gi, '');
code = (nameClean.slice(0, 3) || name.slice(0, 3)).toUpperCase();
}
return code;
}
/**
* Generates a unique sequential ID based on a base string and existing codes.
* @param base - The base string to generate the ID from.
* @param existingCodes - A set of existing codes to check against.
* @returns The generated unique sequential ID.
*/
export function getLatestSequentialId(
base: string,
existingCodes: Set<string>
): string {
let attempt = 1;
let newCode = base;
while (existingCodes.has(newCode)) {
newCode = base.slice(0, 2) + attempt;
attempt++;
}
return newCode;
}