diff --git a/replit.nix b/replit.nix index 404c2dd..0a6dd60 100644 --- a/replit.nix +++ b/replit.nix @@ -1,5 +1,6 @@ {pkgs}: { deps = [ + pkgs.jq pkgs.postgresql ]; } diff --git a/server/query-parser.ts b/server/query-parser.ts index 7be4173..5524d74 100644 --- a/server/query-parser.ts +++ b/server/query-parser.ts @@ -257,6 +257,72 @@ export function applyQueryOptions>( }; } +/** + * 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) + } +} +\`\`\` `; } \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index 2de0a49..6c96d48 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -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 { 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 { 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 { 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 { 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 { 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); }