/** * @fileoverview Pagination & Filtering Standards * v0.18.0 Enhancement: Standardized query parameters for all list endpoints * Purpose: Consistent pagination behavior, no one-off logic per team */ import { z } from 'zod'; // Standard sort direction export const SortDirection = z.enum(['asc', 'desc']); // Standard pagination parameters export const PaginationParams = z.object({ limit: z.number().int().positive().max(1000).default(50), offset: z.number().int().nonnegative().default(0), sort: z.string().optional(), // field:direction format order: SortDirection.optional() // Alternative to sort direction }).strict(); // Standard filtering parameters export const FilterParams = z.object({ // Common filters across domains symbol: z.string().optional(), timeframe: z.string().optional(), owner: z.string().optional(), status: z.string().optional(), // Date range filtering created_after: z.string().datetime().optional(), created_before: z.string().datetime().optional(), updated_after: z.string().datetime().optional(), updated_before: z.string().datetime().optional(), // Generic key-value filters (filter[key]=value) filter: z.record(z.string()).optional() }).strict(); // Combined query parameters export const StandardQueryParams = PaginationParams.merge(FilterParams); // Pagination metadata in responses export const PaginationMeta = z.object({ limit: z.number().int().positive(), offset: z.number().int().nonnegative(), total: z.number().int().nonnegative(), count: z.number().int().nonnegative(), // Items in current page has_more: z.boolean(), has_previous: z.boolean(), // Optional cursor-based pagination next_cursor: z.string().optional(), previous_cursor: z.string().optional() }).strict(); // Standard list response envelope export const PaginatedResponse = (itemSchema: T) => z.object({ success: z.literal(true), data: z.object({ items: z.array(itemSchema), pagination: PaginationMeta }).strict(), requestId: z.string(), timestamp: z.string().datetime(), // Enhanced metadata (Management improvement #5) meta: z.object({ contractsVersion: z.string(), traceId: z.string().optional(), latencyMs: z.number().int().nonnegative().optional(), source: z.string().optional(), query_time_ms: z.number().int().nonnegative().optional(), cache_hit: z.boolean().optional() }).strict().optional() }).strict(); // Specific paginated response types export const PaginatedSignalsResponse = PaginatedResponse(z.object({ id: z.string().uuid(), symbol: z.string(), decision: z.enum(['BUY', 'SELL', 'HOLD']), confidence: z.number().min(0).max(1), created_at: z.string().datetime() })); export const PaginatedJobsResponse = PaginatedResponse(z.object({ id: z.string().uuid(), type: z.string(), status: z.enum(['pending', 'queued', 'running', 'succeeded', 'failed', 'cancelled']), created_at: z.string().datetime(), updated_at: z.string().datetime() })); export const PaginatedInvestmentsResponse = PaginatedResponse(z.object({ id: z.string().uuid(), symbol: z.string(), asset_type: z.enum(['stock', 'etf', 'fund', 'crypto', 'bond']), quantity: z.number(), cost_basis: z.number(), created_at: z.string().datetime() })); // Helper functions export function buildSortClause(sort?: string, order?: 'asc' | 'desc'): string | null { if (!sort) return null; // Handle field:direction format if (sort.includes(':')) { return sort; } // Use order parameter if provided const direction = order || 'desc'; return `${sort}:${direction}`; } export function validatePaginationParams(params: any): z.infer { return PaginationParams.parse(params); } export function validateFilterParams(params: any): z.infer { return FilterParams.parse(params); } // Type exports export type SortDirectionType = z.infer; export type PaginationParamsType = z.infer; export type FilterParamsType = z.infer; export type StandardQueryParamsType = z.infer; export type PaginationMetaType = z.infer; export type PaginatedSignalsResponseType = z.infer; export type PaginatedJobsResponseType = z.infer; export type PaginatedInvestmentsResponseType = z.infer;