mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-07-26 11:59:14 +00:00
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:
@@ -1,5 +1,6 @@
|
||||
{pkgs}: {
|
||||
deps = [
|
||||
pkgs.jq
|
||||
pkgs.postgresql
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user