217 lines
5.6 KiB
TypeScript
217 lines
5.6 KiB
TypeScript
/**
|
|
* 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).includes(value as any);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 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 {
|
|
return (
|
|
typeof error === "object" &&
|
|
error !== null &&
|
|
"issues" in error &&
|
|
Array.isArray((error as any).issues)
|
|
);
|
|
}
|
|
|