197 lines
5.5 KiB
TypeScript
Raw Normal View History

/**
* Domain Toolkit - Validation Helpers
*
* Common validation utilities that can be reused across all domains.
*/
import { z } from "zod";
// ============================================================================
// ID Validation
// ============================================================================
/**
* Validate that an ID is a positive integer
*/
export function isValidPositiveId(id: number): boolean {
return Number.isInteger(id) && id > 0;
}
/**
* Validate Salesforce ID (18 characters, alphanumeric)
*/
export function isValidSalesforceId(id: string): boolean {
return /^[A-Za-z0-9]{18}$/.test(id);
}
// ============================================================================
// Pagination Validation
// ============================================================================
/**
* Validate and sanitize pagination parameters
*/
export function sanitizePagination(options: {
page?: number;
limit?: number;
minLimit?: number;
maxLimit?: number;
defaultLimit?: number;
}): {
page: number;
limit: number;
} {
const { page = 1, limit = options.defaultLimit ?? 10, minLimit = 1, maxLimit = 100 } = options;
return {
page: Math.max(1, Math.floor(page)),
limit: Math.max(minLimit, Math.min(maxLimit, Math.floor(limit))),
};
}
/**
* Check if pagination offset is valid
*/
export function isValidPaginationOffset(offset: number): boolean {
return Number.isInteger(offset) && offset >= 0;
}
// ============================================================================
// String Validation
// ============================================================================
/**
* Check if string is non-empty after trimming
*/
export function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
/**
* Check if value is a valid enum member
*/
export function isValidEnumValue<T extends Record<string, string | number>>(
value: unknown,
enumObj: T
): value is T[keyof T] {
return Object.values(enumObj).some(enumValue => enumValue === value);
}
// ============================================================================
// Array Validation
// ============================================================================
/**
* Check if array is non-empty
*/
export function isNonEmptyArray<T>(value: unknown): value is T[] {
return Array.isArray(value) && value.length > 0;
}
/**
* Check if all array items are unique
*/
export function hasUniqueItems<T>(items: T[]): boolean {
return new Set(items).size === items.length;
}
// ============================================================================
// Number Validation
// ============================================================================
/**
* Check if number is within range (inclusive)
*/
export function isInRange(value: number, min: number, max: number): boolean {
return value >= min && value <= max;
}
/**
* Check if number is a valid positive integer
*/
export function isPositiveInteger(value: unknown): value is number {
return typeof value === "number" && Number.isInteger(value) && value > 0;
}
/**
* Check if number is a valid non-negative integer
*/
export function isNonNegativeInteger(value: unknown): value is number {
return typeof value === "number" && Number.isInteger(value) && value >= 0;
}
// ============================================================================
// Date Validation
// ============================================================================
/**
* Check if string is a valid ISO date time
*/
export function isValidIsoDateTime(value: string): boolean {
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
if (!isoRegex.test(value)) return false;
const date = new Date(value);
return !isNaN(date.getTime());
}
/**
* Check if string is a valid date in YYYYMMDD format
*/
export function isValidYYYYMMDD(value: string): boolean {
return /^\d{8}$/.test(value);
}
// ============================================================================
// Zod Schema Helpers
// ============================================================================
/**
* Create a schema for a positive integer ID
*/
export const positiveIdSchema = z.number().int().positive();
/**
* Create a schema for pagination parameters
*/
export function createPaginationSchema(options?: {
minLimit?: number;
maxLimit?: number;
defaultLimit?: number;
}) {
const { minLimit = 1, maxLimit = 100, defaultLimit = 10 } = options ?? {};
return z.object({
page: z.coerce.number().int().positive().optional().default(1),
limit: z.coerce.number().int().min(minLimit).max(maxLimit).optional().default(defaultLimit),
offset: z.coerce.number().int().nonnegative().optional(),
});
}
/**
* Create a schema for sortable queries
*/
export const sortableQuerySchema = z.object({
sortBy: z.string().optional(),
sortOrder: z.enum(["asc", "desc"]).optional(),
});
// ============================================================================
// Type Guards
// ============================================================================
/**
* Type guard for checking if value is a record
*/
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
* Type guard for checking if error is a Zod error
*/
export function isZodError(error: unknown): error is z.ZodError {
if (!isRecord(error)) return false;
return Array.isArray(error.issues);
}