add pagination type responses

This commit is contained in:
Nate Kelley 2025-07-15 17:23:32 -06:00
parent fb0e79065f
commit 094fc4c251
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
10 changed files with 712 additions and 35 deletions

View File

@ -14,12 +14,14 @@ const app = new Hono().get(
const { id: userId } = c.get('busterUser'); const { id: userId } = c.get('busterUser');
const { page, page_size, filters } = c.req.valid('json'); const { page, page_size, filters } = c.req.valid('json');
const users = await getUserToOrganization({ const result = await getUserToOrganization({
userId, userId,
page,
page_size,
filters, filters,
}); });
const response: GetUserToOrganizationResponse = users; const response: GetUserToOrganizationResponse = result;
return c.json(response); return c.json(response);
} }

View File

@ -23,7 +23,8 @@
"linter": { "linter": {
"rules": { "rules": {
"suspicious": { "suspicious": {
"noConsoleLog": "off" "noConsoleLog": "off",
"noExplicitAny": "warn"
} }
} }
} }

View File

@ -0,0 +1,215 @@
# Pagination Utilities
This directory contains reusable pagination utilities for Drizzle ORM queries.
## Overview
The pagination utilities provide a consistent way to add offset-based pagination to your database queries. They handle:
- Page calculation and offset computation
- Total count queries
- Consistent response format
- Type safety
## Recommended Usage
### Primary Method: `withPaginationMeta` (Handles Count Automatically)
This is the **recommended approach** for most use cases as it handles the count query for you:
```typescript
import { eq, desc } from 'drizzle-orm';
import { db } from '@/database';
import { users } from '@/schema';
import { withPaginationMeta } from '@/queries/shared-types';
const result = await withPaginationMeta({
query: db
.select()
.from(users)
.where(eq(users.active, true))
.$dynamic(),
orderBy: desc(users.createdAt),
page: 2,
page_size: 10,
countFrom: users,
countWhere: eq(users.active, true), // Optional: same conditions as main query
});
// Result shape:
// {
// data: User[],
// pagination: {
// page: number,
// page_size: number,
// total: number, // Automatically calculated
// total_pages: number // Automatically calculated
// }
// }
```
### Simple Pagination: `withPagination` (No Count Query)
Use this when you only need paginated results without metadata:
```typescript
import { db } from '@/database';
import { users } from '@/schema';
import { withPagination } from '@/queries/shared-types';
// Your base query needs to use .$dynamic()
const query = db.select().from(users).$dynamic();
// Apply pagination
const paginatedQuery = withPagination(query, users.createdAt, 2, 10);
const results = await paginatedQuery;
// Note: This only returns the data, no pagination metadata
```
### Special Cases: `createPaginatedResponse` (Manual Count)
Only use this when you:
- Already have the total count from another source
- Need to transform data after querying
- Are working with non-database data
```typescript
import { createPaginatedResponse } from '@/queries/shared-types';
// Example: When you already have count from elsewhere
const transformedData = existingData.map(item => ({
...item,
displayName: item.name || 'Unnamed'
}));
const response = createPaginatedResponse({
data: transformedData,
page: 1,
page_size: 10,
total: existingTotalCount, // You must provide this
});
```
## API Reference
### Types
#### `PaginationInput`
```typescript
interface PaginationInput {
page?: number; // Current page (default: 1)
page_size?: number; // Items per page (default: 250, max: 1000)
}
```
#### `PaginationMetadata`
```typescript
interface PaginationMetadata {
page: number; // Current page
page_size: number; // Items per page
total: number; // Total number of items
total_pages: number; // Total number of pages
}
```
#### `PaginatedResponse<T>`
```typescript
interface PaginatedResponse<T> {
data: T[];
pagination: PaginationMetadata;
}
```
### Functions
#### `withPaginationMeta(options)` ⭐ Recommended
Executes a paginated query and automatically handles the count query.
#### `withPagination(query, orderBy, page?, pageSize?)`
Adds pagination to a dynamic query without executing it or counting.
#### `createPaginatedResponse(options)`
Creates a paginated response when you already have the total count.
#### `createPaginationMetadata(options)`
Creates just the pagination metadata from count and page info.
## Best Practices
1. **Use `withPaginationMeta` by default**: It handles counting automatically
2. **Always use ordering**: Pagination without ordering can lead to inconsistent results
3. **Match WHERE conditions**: When using `countWhere`, ensure it matches your query's WHERE clause
4. **Consider performance**: For very large tables, consider cursor-based pagination
5. **Set reasonable limits**: Default page size is 250, max is 1000 to prevent performance issues
## Common Patterns
### API Endpoint with Automatic Count
```typescript
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const page_size = parseInt(searchParams.get('per_page') || '25');
// This handles everything for you
const result = await withPaginationMeta({
query: db.select().from(users).$dynamic(),
orderBy: users.createdAt,
page,
page_size,
countFrom: users,
});
return Response.json(result);
}
```
### With Filters
```typescript
const activeUsers = await withPaginationMeta({
query: db
.select()
.from(users)
.where(and(
eq(users.active, true),
like(users.email, '%@company.com')
))
.$dynamic(),
orderBy: users.email,
page,
page_size,
countFrom: users,
countWhere: and( // Same conditions as query
eq(users.active, true),
like(users.email, '%@company.com')
),
});
```
## Migration Guide
If you're updating existing code:
1. **Replace manual pagination logic** with `withPaginationMeta`:
```typescript
// Before: Manual offset/limit and count
const offset = (page - 1) * pageSize;
const data = await db.select().from(users).limit(pageSize).offset(offset);
const [{ count }] = await db.select({ count: count() }).from(users);
// After: Automatic with withPaginationMeta
const result = await withPaginationMeta({
query: db.select().from(users).$dynamic(),
orderBy: users.id,
page,
page_size: pageSize,
countFrom: users,
});
```
2. **Add `.$dynamic()`** to your query builder
3. **Update response types** to use `PaginatedResponse<T>`
See `pagination.example.ts` for more complete examples.

View File

@ -0,0 +1,3 @@
// Export pagination types and utilities
export * from './pagination.types';
export * from './with-pagination';

View File

@ -0,0 +1,126 @@
/**
* Example usage of pagination utilities
*/
import { and, count, desc, eq, like } from 'drizzle-orm';
import { db } from '../../connection';
import { dashboards, users } from '../../schema';
import { createPaginatedResponse, withPagination, withPaginationMeta } from './index';
// Example 1: RECOMMENDED - Pagination with metadata (handles count automatically)
export async function getUsersWithMeta(page = 1, pageSize = 10) {
const query = db.select().from(users).where(eq(users.name, 'John')).$dynamic();
// This is the recommended approach - it handles the count query for you
return await withPaginationMeta({
query,
orderBy: desc(users.createdAt),
page,
page_size: pageSize,
countFrom: users,
countWhere: eq(users.name, 'John'), // Same condition as the main query
});
}
// Example 2: Simple pagination without metadata (when you don't need count)
export async function getUsers(page = 1, pageSize = 10) {
const query = db.select().from(users).$dynamic();
const paginatedQuery = withPagination(query, users.createdAt, page, pageSize);
return await paginatedQuery;
}
// Example 3: Using in API endpoints
export async function apiHandler(request: { query: { page?: string; per_page?: string } }) {
const page = request.query.page ? Number.parseInt(request.query.page) : 1;
const pageSize = request.query.per_page ? Number.parseInt(request.query.per_page) : 25;
// withPaginationMeta handles everything for you
const result = await withPaginationMeta({
query: db.select().from(users).$dynamic(),
orderBy: desc(users.createdAt),
page,
page_size: pageSize,
countFrom: users,
});
// The result has the shape:
// {
// data: User[],
// pagination: {
// page: number,
// page_size: number,
// total: number, // Automatically calculated
// total_pages: number // Automatically calculated
// }
// }
return result;
}
// Example 4: With complex filters
export async function getFilteredDashboards(
organizationId: string,
searchTerm: string,
page = 1,
pageSize = 20
) {
const whereCondition = and(
eq(dashboards.organizationId, organizationId),
like(dashboards.name, `%${searchTerm}%`)
);
const paginationOptions = {
query: db.select().from(dashboards).where(whereCondition).$dynamic(),
orderBy: dashboards.createdAt,
page,
page_size: pageSize,
countFrom: dashboards,
...(whereCondition && { countWhere: whereCondition }),
};
return await withPaginationMeta(paginationOptions);
}
// Example 5: SPECIAL CASE - Manual pagination with custom transformation
// Only use when you need to transform data or already have the count
export async function getDashboardsWithCustomResponse(
organizationId: string,
page = 1,
pageSize = 20
) {
// Sometimes you might already have the count from another source
// or need to do custom transformations
const query = db
.select()
.from(dashboards)
.where(eq(dashboards.organizationId, organizationId))
.$dynamic();
const paginatedQuery = withPagination(query, dashboards.createdAt, page, pageSize);
const results = await paginatedQuery;
// Maybe you got the count from somewhere else or cached it
const cachedTotal = await getCachedDashboardCount(organizationId);
// Transform results with custom logic
const transformedData = results.map((dashboard) => ({
...dashboard,
displayName: dashboard.name || 'Untitled Dashboard',
isRecent: new Date(dashboard.createdAt) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
}));
// Only use createPaginatedResponse when you already have the total
return createPaginatedResponse({
data: transformedData,
page,
page_size: pageSize,
total: cachedTotal,
});
}
// Helper function (simulated)
async function getCachedDashboardCount(_organizationId: string): Promise<number> {
// This would normally come from cache or another source
// In a real app, you'd use the organizationId to look up cached count
return 42;
}

View File

@ -0,0 +1,30 @@
import { z } from 'zod';
// Pagination input schema for validation
export const PaginationInputSchema = z.object({
page: z.number().min(1).optional().default(1),
page_size: z.number().min(1).max(1000).optional().default(250),
});
export type PaginationInput = z.infer<typeof PaginationInputSchema>;
// Pagination metadata that's returned with results
export interface PaginationMetadata {
page: number;
page_size: number;
total: number;
total_pages: number;
}
// Generic paginated response type
export interface PaginatedResponse<T> {
data: T[];
pagination: PaginationMetadata;
}
// Type helper for creating paginated API responses
export type WithPagination<T> = {
[K in keyof T]: T[K];
} & {
pagination: PaginationMetadata;
};

View File

@ -0,0 +1,152 @@
import { SQL, count } from 'drizzle-orm';
import { PgColumn, PgSelect, PgTable, TableConfig } from 'drizzle-orm/pg-core';
import { db } from '../../connection';
import type { PaginatedResponse, PaginationInput, PaginationMetadata } from './pagination.types';
/**
* Adds pagination to a Drizzle query using the dynamic query builder pattern
*
* @example
* ```typescript
* const query = db.select().from(users).$dynamic();
* const paginatedQuery = withPagination(query, users.id, 2, 10);
* const results = await paginatedQuery;
* ```
*/
export function withPagination<T extends PgSelect>(
qb: T,
orderByColumn: PgColumn | SQL | SQL.Aliased,
page = 1,
pageSize = 250
) {
return qb
.orderBy(orderByColumn)
.limit(pageSize)
.offset((page - 1) * pageSize);
}
/**
* Executes a paginated query and returns results with pagination metadata
* This is the recommended approach as it handles the count query automatically.
*
* @example
* ```typescript
* const result = await withPaginationMeta({
* query: db.select().from(users).where(eq(users.active, true)).$dynamic(),
* orderBy: users.id,
* page: 2,
* page_size: 10,
* countFrom: users,
* countWhere: eq(users.active, true)
* });
* ```
*/
export async function withPaginationMeta<T extends PgSelect>({
query,
orderBy,
page = 1,
page_size = 250,
countFrom,
countWhere,
}: {
query: T;
orderBy: PgColumn | SQL | SQL.Aliased;
page?: number;
page_size?: number;
countFrom: PgTable<TableConfig>;
countWhere?: SQL;
}): Promise<PaginatedResponse<Awaited<ReturnType<T['execute']>>[number]>> {
// Apply pagination to the query
const paginatedQuery = withPagination(query, orderBy, page, page_size);
// Execute the paginated query
const results = await paginatedQuery;
// Get total count
const countQuery = db.select({ count: count() }).from(countFrom);
const countWithWhere = countWhere ? countQuery.where(countWhere) : countQuery;
const totalResult = await countWithWhere;
const total = Number(totalResult[0]?.count || 0);
const total_pages = Math.ceil(total / page_size);
return {
data: results,
pagination: {
page,
page_size,
total,
total_pages,
},
};
}
/**
* Creates pagination metadata from count and pagination parameters
* Useful when you already have the total count
*
* @example
* ```typescript
* const metadata = createPaginationMetadata({
* total: 100,
* page: 2,
* page_size: 10
* });
* ```
*/
export function createPaginationMetadata({
total,
page,
page_size,
}: {
total: number;
page: number;
page_size: number;
}): PaginationMetadata {
return {
page,
page_size,
total,
total_pages: Math.ceil(total / page_size),
};
}
/**
* Helper function to create a paginated response from existing data and count.
*
* Use this when:
* - You already have both the data and total count from separate queries
* - You need to transform the data before creating the response
* - You're working with data that's not directly from a database query
*
* For most cases, prefer `withPaginationMeta` which handles the count query automatically.
*
* @example
* ```typescript
* // Only use when you already have the count or need custom transformation
* const users = await customUserQuery();
* const total = await customCountQuery();
*
* return createPaginatedResponse({
* data: users.map(u => ({ ...u, displayName: u.name })),
* page: 1,
* page_size: 10,
* total
* });
* ```
*/
export function createPaginatedResponse<T>({
data,
page,
page_size,
total,
}: {
data: T[];
page: number;
page_size: number;
total: number;
}): PaginatedResponse<T> {
return {
data,
pagination: createPaginationMetadata({ total, page, page_size }),
};
}

View File

@ -1,5 +1,6 @@
import { beforeEach, describe, expect, test, vi } from 'vitest'; import { beforeEach, describe, expect, test, vi } from 'vitest';
import { getUserOrganizationId } from '../organizations/organizations'; import { getUserOrganizationId } from '../organizations/organizations';
import { withPaginationMeta } from '../shared-types';
import { getUserToOrganization } from './users-to-organizations'; import { getUserToOrganization } from './users-to-organizations';
// Mock the organizations module // Mock the organizations module
@ -7,13 +8,20 @@ vi.mock('../organizations/organizations', () => ({
getUserOrganizationId: vi.fn(), getUserOrganizationId: vi.fn(),
})); }));
// Mock the shared-types module
vi.mock('../shared-types', () => ({
withPaginationMeta: vi.fn(),
}));
// Mock the database connection // Mock the database connection
vi.mock('../../connection', () => ({ vi.mock('../../connection', () => ({
db: { db: {
select: vi.fn(() => ({ select: vi.fn(() => ({
from: vi.fn(() => ({ from: vi.fn(() => ({
innerJoin: vi.fn(() => ({ innerJoin: vi.fn(() => ({
where: vi.fn(() => Promise.resolve([])), where: vi.fn(() => ({
$dynamic: vi.fn(() => 'mock-dynamic-query'),
})),
})), })),
})), })),
})), })),
@ -22,6 +30,7 @@ vi.mock('../../connection', () => ({
describe('getUserToOrganization', () => { describe('getUserToOrganization', () => {
const mockGetUserOrganizationId = vi.mocked(getUserOrganizationId); const mockGetUserOrganizationId = vi.mocked(getUserOrganizationId);
const mockWithPaginationMeta = vi.mocked(withPaginationMeta);
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@ -63,16 +72,16 @@ describe('getUserToOrganization', () => {
role: 'querier', role: 'querier',
}); });
const { db } = await import('../../connection'); // Mock withPaginationMeta to return the expected structure
const mockDb = vi.mocked(db); mockWithPaginationMeta.mockResolvedValue({
data: mockUsers,
mockDb.select.mockReturnValue({ pagination: {
from: vi.fn().mockReturnValue({ page: 1,
innerJoin: vi.fn().mockReturnValue({ page_size: 250,
where: vi.fn().mockResolvedValue(mockUsers), total: 2,
}), total_pages: 1,
}), },
} as unknown as ReturnType<typeof db.select>); });
// Act // Act
const result = await getUserToOrganization({ const result = await getUserToOrganization({
@ -80,8 +89,28 @@ describe('getUserToOrganization', () => {
}); });
// Assert // Assert
expect(result).toEqual(mockUsers); expect(result).toEqual({
users: mockUsers,
pagination: {
page: 1,
page_size: 250,
total: 2,
total_pages: 1,
},
});
expect(mockGetUserOrganizationId).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000'); expect(mockGetUserOrganizationId).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000');
expect(mockWithPaginationMeta).toHaveBeenCalledWith(
expect.objectContaining({
query: 'mock-dynamic-query',
orderBy: expect.anything(),
page: 1,
page_size: 250,
countFrom: expect.objectContaining({
// Just check that it has a table name symbol
[Symbol.for('drizzle:Name')]: expect.any(String),
}),
})
);
}); });
test('should apply userName filter when provided', async () => { test('should apply userName filter when provided', async () => {
@ -102,16 +131,15 @@ describe('getUserToOrganization', () => {
role: 'querier', role: 'querier',
}); });
const { db } = await import('../../connection'); mockWithPaginationMeta.mockResolvedValue({
const mockDb = vi.mocked(db); data: mockUsers,
pagination: {
mockDb.select.mockReturnValue({ page: 1,
from: vi.fn().mockReturnValue({ page_size: 250,
innerJoin: vi.fn().mockReturnValue({ total: 1,
where: vi.fn().mockResolvedValue(mockUsers), total_pages: 1,
}), },
}), });
} as unknown as ReturnType<typeof db.select>);
// Act // Act
const result = await getUserToOrganization({ const result = await getUserToOrganization({
@ -120,8 +148,87 @@ describe('getUserToOrganization', () => {
}); });
// Assert // Assert
expect(result).toEqual(mockUsers); expect(result).toEqual({
users: mockUsers,
pagination: {
page: 1,
page_size: 250,
total: 1,
total_pages: 1,
},
});
expect(mockGetUserOrganizationId).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000'); expect(mockGetUserOrganizationId).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000');
expect(mockWithPaginationMeta).toHaveBeenCalledWith(
expect.objectContaining({
query: 'mock-dynamic-query',
orderBy: expect.anything(),
page: 1,
page_size: 250,
countFrom: expect.objectContaining({
[Symbol.for('drizzle:Name')]: expect.any(String),
}),
countWhere: expect.anything(),
})
);
});
test('should handle pagination correctly', async () => {
// Arrange
const mockUsers = [
{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'John Doe',
email: 'john@example.com',
avatarUrl: null,
role: 'querier',
status: 'active',
},
];
mockGetUserOrganizationId.mockResolvedValue({
organizationId: 'org-123',
role: 'querier',
});
mockWithPaginationMeta.mockResolvedValue({
data: mockUsers,
pagination: {
page: 2,
page_size: 10,
total: 100,
total_pages: 10,
},
});
// Act
const result = await getUserToOrganization({
userId: '123e4567-e89b-12d3-a456-426614174000',
page: 2,
page_size: 10,
});
// Assert
expect(result).toEqual({
users: mockUsers,
pagination: {
page: 2,
page_size: 10,
total: 100,
total_pages: 10,
},
});
expect(mockGetUserOrganizationId).toHaveBeenCalledWith('123e4567-e89b-12d3-a456-426614174000');
expect(mockWithPaginationMeta).toHaveBeenCalledWith(
expect.objectContaining({
query: 'mock-dynamic-query',
orderBy: expect.anything(),
page: 2,
page_size: 10,
countFrom: expect.objectContaining({
[Symbol.for('drizzle:Name')]: expect.any(String),
}),
})
);
}); });
test('should validate input with invalid UUID', async () => { test('should validate input with invalid UUID', async () => {

View File

@ -1,8 +1,9 @@
import { type InferSelectModel, and, eq, isNull, like } from 'drizzle-orm'; import { type InferSelectModel, and, asc, eq, isNull, like } from 'drizzle-orm';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../connection'; import { db } from '../../connection';
import { users, usersToOrganizations } from '../../schema'; import { users, usersToOrganizations } from '../../schema';
import { getUserOrganizationId } from '../organizations/organizations'; import { getUserOrganizationId } from '../organizations/organizations';
import { withPaginationMeta } from '../shared-types';
type RawOrganizationUser = InferSelectModel<typeof usersToOrganizations>; type RawOrganizationUser = InferSelectModel<typeof usersToOrganizations>;
type RawUser = InferSelectModel<typeof users>; type RawUser = InferSelectModel<typeof users>;
@ -10,6 +11,8 @@ type RawUser = InferSelectModel<typeof users>;
// Input schema for type safety // Input schema for type safety
const GetUserToOrganizationInputSchema = z.object({ const GetUserToOrganizationInputSchema = z.object({
userId: z.string().uuid('User ID must be a valid UUID'), userId: z.string().uuid('User ID must be a valid UUID'),
page: z.number().optional().default(1),
page_size: z.number().optional().default(250),
filters: z filters: z
.object({ .object({
userName: z.string().optional(), userName: z.string().optional(),
@ -23,12 +26,24 @@ export type GetUserToOrganizationInput = z.infer<typeof GetUserToOrganizationInp
export type OrganizationUser = Pick<RawUser, 'id' | 'name' | 'email' | 'avatarUrl'> & export type OrganizationUser = Pick<RawUser, 'id' | 'name' | 'email' | 'avatarUrl'> &
Pick<RawOrganizationUser, 'role' | 'status'>; Pick<RawOrganizationUser, 'role' | 'status'>;
export type GetUserToOrganizationResult = {
users: OrganizationUser[];
pagination: {
page: number;
page_size: number;
total: number;
total_pages: number;
};
};
export const getUserToOrganization = async ({ export const getUserToOrganization = async ({
userId, userId,
page = 1,
page_size = 250,
filters, filters,
}: GetUserToOrganizationInput): Promise<OrganizationUser[]> => { }: GetUserToOrganizationInput): Promise<GetUserToOrganizationResult> => {
// Validate input // Validate input
const validated = GetUserToOrganizationInputSchema.parse({ userId, filters }); const validated = GetUserToOrganizationInputSchema.parse({ userId, page, page_size, filters });
// Get the user's organization ID // Get the user's organization ID
const userOrg = await getUserOrganizationId(validated.userId); const userOrg = await getUserOrganizationId(validated.userId);
@ -52,8 +67,8 @@ export const getUserToOrganization = async ({
...filterConditions ...filterConditions
); );
// Execute the query // Build the query with dynamic
const results = await db const query = db
.select({ .select({
id: users.id, id: users.id,
name: users.name, name: users.name,
@ -64,8 +79,24 @@ export const getUserToOrganization = async ({
}) })
.from(users) .from(users)
.innerJoin(usersToOrganizations, eq(users.id, usersToOrganizations.userId)) .innerJoin(usersToOrganizations, eq(users.id, usersToOrganizations.userId))
.where(whereCondition); .where(whereCondition)
.$dynamic();
// Validate and return results // Use withPaginationMeta to handle pagination and count automatically
return results; const paginationOptions = {
query,
orderBy: asc(users.name), // Order by name for consistent results
page: validated.page,
page_size: validated.page_size,
countFrom: users,
...(whereCondition && { countWhere: whereCondition }),
};
const result = await withPaginationMeta(paginationOptions);
// Transform to match expected format
return {
users: result.data,
pagination: result.pagination,
};
}; };

View File

@ -27,7 +27,17 @@ export const UserListResponseSchema = z.array(
export const UserFavoriteResponseSchema = z.array(UserFavoriteSchema); export const UserFavoriteResponseSchema = z.array(UserFavoriteSchema);
export const GetUserToOrganizationResponseSchema = z.array(OrganizationUserSchema); const PaginationSchema = z.object({
page: z.number(),
page_size: z.number(),
total: z.number(),
total_pages: z.number(),
});
export const GetUserToOrganizationResponseSchema = z.object({
users: z.array(OrganizationUserSchema),
pagination: PaginationSchema,
});
export type UserResponse = z.infer<typeof UserResponseSchema>; export type UserResponse = z.infer<typeof UserResponseSchema>;
export type UserListResponse = z.infer<typeof UserListResponseSchema>; export type UserListResponse = z.infer<typeof UserListResponseSchema>;