Add pagination to API responses. Includes metadata for next/previous pages.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/a3377fb3-20c9-414f-b780-b6636141319b.jpg
This commit is contained in:
alphaeusmote
2025-04-09 14:10:05 +00:00
3 changed files with 275 additions and 6 deletions
+1
View File
@@ -1,5 +1,6 @@
{pkgs}: {
deps = [
pkgs.jq
pkgs.postgresql
];
}
+80
View File
@@ -257,6 +257,72 @@ export function applyQueryOptions<T extends PgTableWithColumns<any>>(
};
}
/**
* Generate pagination metadata for response
*/
export function generatePaginationMetadata(
totalRecords: number,
limit?: number,
offset?: number,
baseUrl?: string
) {
// If no pagination params were specified
if (limit === undefined) {
return {
pagination: {
totalRecords,
currentPage: 1,
totalPages: 1,
nextPage: null,
previousPage: null
}
};
}
// Calculate current page (1-based) and total pages
const currentPage = offset !== undefined ? Math.floor(offset / limit) + 1 : 1;
const totalPages = Math.ceil(totalRecords / limit);
// Prepare pagination metadata
const pagination = {
totalRecords,
currentPage,
totalPages,
nextPage: null as string | null,
previousPage: null as string | null
};
// Add nextPage and previousPage URLs if baseUrl is provided
if (baseUrl) {
const url = new URL(baseUrl);
const params = new URLSearchParams(url.search);
// Next page
if (currentPage < totalPages) {
const nextOffset = offset !== undefined ? offset + limit : limit;
params.set('top', String(limit));
params.set('skip', String(nextOffset));
const nextUrl = new URL(url.pathname, url.origin);
nextUrl.search = params.toString();
pagination.nextPage = nextUrl.toString();
}
// Previous page
if (currentPage > 1 && offset !== undefined) {
const prevOffset = Math.max(0, offset - limit);
params.set('top', String(limit));
params.set('skip', String(prevOffset));
const prevUrl = new URL(url.pathname, url.origin);
prevUrl.search = params.toString();
pagination.previousPage = prevUrl.toString();
}
}
return { pagination };
}
/**
* Build a cache key based on query parameters
*/
@@ -348,5 +414,19 @@ Use the \`top\` and \`skip\` parameters for pagination.
- \`skip\`: Number of records to skip
Example: \`?top=10&skip=20\` (return records 21-30)
The response will include pagination metadata with the following structure:
\`\`\`json
{
"data": [...], // The actual records
"pagination": {
"totalRecords": 100, // Total number of records
"currentPage": 3, // Current page number (1-based)
"totalPages": 10, // Total number of pages
"nextPage": "http://example.com/api/resource?top=10&skip=30", // URL to next page (or null)
"previousPage": "http://example.com/api/resource?top=10&skip=10" // URL to previous page (or null)
}
}
\`\`\`
`;
}
+194 -6
View File
@@ -3,6 +3,7 @@ import { createServer, type Server } from "http";
import { setupAuth } from "./auth";
import { setupSwagger } from "./swagger";
import { storage } from "./storage";
import { db } from "./db";
import {
apiQuerySchema,
PERMISSIONS,
@@ -10,7 +11,12 @@ import {
moveUserSchema,
addToGroupSchema,
removeFromGroupSchema,
updateManagedBySchema
updateManagedBySchema,
adUsers,
adGroups,
adOrgUnits,
adComputers,
adDomains
} from "@shared/schema";
import { ZodError } from "zod";
import rateLimit from "express-rate-limit";
@@ -21,6 +27,13 @@ import {
initializeRBAC
} from "./authorization";
import { ldapClient } from "./ldap";
import {
applyFilterConditions,
generatePaginationMetadata,
parseFilter,
parsePagination
} from "./query-parser";
import { eq, sql } from "drizzle-orm";
// Extend Express Request to include user property
interface Request extends ExpressRequest {
@@ -508,7 +521,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
const query = parseQueryParams(req);
const users = await storage.listAdUsers(connectionId, query);
res.json(users);
// Count total records for pagination metadata
const countQuery = db.select({ count: sql`count(*)` }).from(adUsers)
.where(eq(adUsers.connectionId, connectionId));
// Apply filters if present
if (query && query.filter) {
const conditions = parseFilter(query.filter);
const whereClause = applyFilterConditions(adUsers, conditions);
if (whereClause) {
countQuery.where(whereClause);
}
}
const [countResult] = await countQuery;
const totalRecords = Number(countResult?.count || 0);
// Add objectType to each result
const usersWithObjectType = users.map(user => ({
...user,
objectType: 'user'
}));
// Generate pagination metadata
const { limit, offset } = parsePagination(query?.top, query?.skip);
const paginationMetadata = generatePaginationMetadata(
totalRecords,
limit,
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
// Return data with pagination metadata
res.json({
data: usersWithObjectType,
...paginationMetadata
});
} catch (error) {
next(error);
}
@@ -1000,7 +1048,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
const query = parseQueryParams(req);
const groups = await storage.listAdGroups(connectionId, query);
res.json(groups);
// Count total records for pagination metadata
const countQuery = db.select({ count: sql`count(*)` }).from(adGroups)
.where(eq(adGroups.connectionId, connectionId));
// Apply filters if present
if (query && query.filter) {
const conditions = parseFilter(query.filter);
const whereClause = applyFilterConditions(adGroups, conditions);
if (whereClause) {
countQuery.where(whereClause);
}
}
const [countResult] = await countQuery;
const totalRecords = Number(countResult?.count || 0);
// Add objectType to each result
const groupsWithObjectType = groups.map(group => ({
...group,
objectType: 'group'
}));
// Generate pagination metadata
const { limit, offset } = parsePagination(query?.top, query?.skip);
const paginationMetadata = generatePaginationMetadata(
totalRecords,
limit,
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
// Return data with pagination metadata
res.json({
data: groupsWithObjectType,
...paginationMetadata
});
} catch (error) {
next(error);
}
@@ -1314,7 +1397,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
const query = parseQueryParams(req);
const orgUnits = await storage.listAdOrgUnits(connectionId, query);
res.json(orgUnits);
// Count total records for pagination metadata
const countQuery = db.select({ count: sql`count(*)` }).from(adOrgUnits)
.where(eq(adOrgUnits.connectionId, connectionId));
// Apply filters if present
if (query && query.filter) {
const conditions = parseFilter(query.filter);
const whereClause = applyFilterConditions(adOrgUnits, conditions);
if (whereClause) {
countQuery.where(whereClause);
}
}
const [countResult] = await countQuery;
const totalRecords = Number(countResult?.count || 0);
// Add objectType to each result
const orgUnitsWithObjectType = orgUnits.map(ou => ({
...ou,
objectType: 'organizationalUnit'
}));
// Generate pagination metadata
const { limit, offset } = parsePagination(query?.top, query?.skip);
const paginationMetadata = generatePaginationMetadata(
totalRecords,
limit,
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
// Return data with pagination metadata
res.json({
data: orgUnitsWithObjectType,
...paginationMetadata
});
} catch (error) {
next(error);
}
@@ -1492,7 +1610,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
const query = parseQueryParams(req);
const computers = await storage.listAdComputers(connectionId, query);
res.json(computers);
// Count total records for pagination metadata
const countQuery = db.select({ count: sql`count(*)` }).from(adComputers)
.where(eq(adComputers.connectionId, connectionId));
// Apply filters if present
if (query && query.filter) {
const conditions = parseFilter(query.filter);
const whereClause = applyFilterConditions(adComputers, conditions);
if (whereClause) {
countQuery.where(whereClause);
}
}
const [countResult] = await countQuery;
const totalRecords = Number(countResult?.count || 0);
// Add objectType to each result
const computersWithObjectType = computers.map(computer => ({
...computer,
objectType: 'computer'
}));
// Generate pagination metadata
const { limit, offset } = parsePagination(query?.top, query?.skip);
const paginationMetadata = generatePaginationMetadata(
totalRecords,
limit,
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
// Return data with pagination metadata
res.json({
data: computersWithObjectType,
...paginationMetadata
});
} catch (error) {
next(error);
}
@@ -1846,7 +1999,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
const query = parseQueryParams(req);
const domains = await storage.listAdDomains(connectionId, query);
res.json(domains);
// Count total records for pagination metadata
const countQuery = db.select({ count: sql`count(*)` }).from(adDomains)
.where(eq(adDomains.connectionId, connectionId));
// Apply filters if present
if (query && query.filter) {
const conditions = parseFilter(query.filter);
const whereClause = applyFilterConditions(adDomains, conditions);
if (whereClause) {
countQuery.where(whereClause);
}
}
const [countResult] = await countQuery;
const totalRecords = Number(countResult?.count || 0);
// Add objectType to each result
const domainsWithObjectType = domains.map(domain => ({
...domain,
objectType: 'domain'
}));
// Generate pagination metadata
const { limit, offset } = parsePagination(query?.top, query?.skip);
const paginationMetadata = generatePaginationMetadata(
totalRecords,
limit,
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
// Return data with pagination metadata
res.json({
data: domainsWithObjectType,
...paginationMetadata
});
} catch (error) {
next(error);
}