mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-27 19:17:07 +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}: {
|
{pkgs}: {
|
||||||
deps = [
|
deps = [
|
||||||
|
pkgs.jq
|
||||||
pkgs.postgresql
|
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
|
* 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
|
- \`skip\`: Number of records to skip
|
||||||
|
|
||||||
Example: \`?top=10&skip=20\` (return records 21-30)
|
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 { setupAuth } from "./auth";
|
||||||
import { setupSwagger } from "./swagger";
|
import { setupSwagger } from "./swagger";
|
||||||
import { storage } from "./storage";
|
import { storage } from "./storage";
|
||||||
|
import { db } from "./db";
|
||||||
import {
|
import {
|
||||||
apiQuerySchema,
|
apiQuerySchema,
|
||||||
PERMISSIONS,
|
PERMISSIONS,
|
||||||
@@ -10,7 +11,12 @@ import {
|
|||||||
moveUserSchema,
|
moveUserSchema,
|
||||||
addToGroupSchema,
|
addToGroupSchema,
|
||||||
removeFromGroupSchema,
|
removeFromGroupSchema,
|
||||||
updateManagedBySchema
|
updateManagedBySchema,
|
||||||
|
adUsers,
|
||||||
|
adGroups,
|
||||||
|
adOrgUnits,
|
||||||
|
adComputers,
|
||||||
|
adDomains
|
||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit from "express-rate-limit";
|
||||||
@@ -21,6 +27,13 @@ import {
|
|||||||
initializeRBAC
|
initializeRBAC
|
||||||
} from "./authorization";
|
} from "./authorization";
|
||||||
import { ldapClient } from "./ldap";
|
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
|
// Extend Express Request to include user property
|
||||||
interface Request extends ExpressRequest {
|
interface Request extends ExpressRequest {
|
||||||
@@ -508,7 +521,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const query = parseQueryParams(req);
|
const query = parseQueryParams(req);
|
||||||
const users = await storage.listAdUsers(connectionId, query);
|
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) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -1000,7 +1048,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const query = parseQueryParams(req);
|
const query = parseQueryParams(req);
|
||||||
const groups = await storage.listAdGroups(connectionId, query);
|
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) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -1314,7 +1397,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const query = parseQueryParams(req);
|
const query = parseQueryParams(req);
|
||||||
const orgUnits = await storage.listAdOrgUnits(connectionId, query);
|
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) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -1492,7 +1610,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const query = parseQueryParams(req);
|
const query = parseQueryParams(req);
|
||||||
const computers = await storage.listAdComputers(connectionId, query);
|
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) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -1846,7 +1999,42 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const query = parseQueryParams(req);
|
const query = parseQueryParams(req);
|
||||||
const domains = await storage.listAdDomains(connectionId, query);
|
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) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user