diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index b4e2eb56..bb59755c 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -29,6 +29,7 @@ import { OpenAPITags, registry } from "@server/openApi"; import NodeCache from "node-cache"; import semver from "semver"; import { getUserDeviceName } from "@server/db/names"; +import type { PaginatedResponse } from "@server/types/Pagination"; const olmVersionCache = new NodeCache({ stdTTL: 3600 }); @@ -89,38 +90,29 @@ const listClientsParamsSchema = z.strictObject({ }); const listClientsSchema = z.object({ - limit: z - .string() + pageSize: z.coerce + .number() // for prettier formatting + .int() + .positive() .optional() - .default("1000") - .transform(Number) - .pipe(z.int().positive()), - offset: z - .string() + .catch(20) + .default(20), + page: z.coerce + .number() // for prettier formatting + .int() + .min(0) .optional() - .default("0") - .transform(Number) - .pipe(z.int().nonnegative()), + .catch(1) + .default(1), + query: z.string().optional(), + sort_by: z + .enum(["megabytesIn", "megabytesOut"]) + .optional() + .catch(undefined), filter: z.enum(["user", "machine"]).optional() }); -function queryClients( - orgId: string, - accessibleClientIds: number[], - filter?: "user" | "machine" -) { - const conditions = [ - inArray(clients.clientId, accessibleClientIds), - eq(clients.orgId, orgId) - ]; - - // Add filter condition based on filter type - if (filter === "user") { - conditions.push(isNotNull(clients.userId)); - } else if (filter === "machine") { - conditions.push(isNull(clients.userId)); - } - +function queryClientsBase() { return db .select({ clientId: clients.clientId, @@ -156,8 +148,7 @@ function queryClients( .leftJoin(orgs, eq(clients.orgId, orgs.orgId)) .leftJoin(olms, eq(clients.clientId, olms.clientId)) .leftJoin(users, eq(clients.userId, users.userId)) - .leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId)) - .where(and(...conditions)); + .leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId)); } async function getSiteAssociations(clientIds: number[]) { @@ -175,7 +166,7 @@ async function getSiteAssociations(clientIds: number[]) { .where(inArray(clientSitesAssociationsCache.clientId, clientIds)); } -type ClientWithSites = Awaited>[0] & { +type ClientWithSites = Awaited>[0] & { sites: Array<{ siteId: number; siteName: string | null; @@ -186,10 +177,9 @@ type ClientWithSites = Awaited>[0] & { type OlmWithUpdateAvailable = ClientWithSites; -export type ListClientsResponse = { +export type ListClientsResponse = PaginatedResponse<{ clients: Array; - pagination: { total: number; limit: number; offset: number }; -}; +}>; registry.registerPath({ method: "get", @@ -218,7 +208,7 @@ export async function listClients( ) ); } - const { limit, offset, filter } = parsedQuery.data; + const { page, pageSize, query, filter } = parsedQuery.data; const parsedParams = listClientsParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -267,28 +257,31 @@ export async function listClients( const accessibleClientIds = accessibleClients.map( (client) => client.clientId ); - const baseQuery = queryClients(orgId, accessibleClientIds, filter); + const baseQuery = queryClientsBase(); // Get client count with filter - const countConditions = [ + const conditions = [ inArray(clients.clientId, accessibleClientIds), eq(clients.orgId, orgId) ]; if (filter === "user") { - countConditions.push(isNotNull(clients.userId)); + conditions.push(isNotNull(clients.userId)); } else if (filter === "machine") { - countConditions.push(isNull(clients.userId)); + conditions.push(isNull(clients.userId)); } - const countQuery = db - .select({ count: count() }) - .from(clients) - .where(and(...countConditions)); + const countQuery = db.$count( + queryClientsBase().where(and(...conditions)) + ); - const clientsList = await baseQuery.limit(limit).offset(offset); - const totalCountResult = await countQuery; - const totalCount = totalCountResult[0].count; + const [clientsList, totalCount] = await Promise.all([ + baseQuery + .where(and(...conditions)) + .limit(page) + .offset(pageSize * (page - 1)), + countQuery + ]); // Get associated sites for all clients const clientIds = clientsList.map((client) => client.clientId); @@ -368,8 +361,8 @@ export async function listClients( clients: olmsWithUpdates, pagination: { total: totalCount, - limit, - offset + page, + pageSize } }, success: true, diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 26a0d613..cf0769ca 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -33,6 +33,7 @@ import { import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import type { PaginatedResponse } from "@server/types/Pagination"; const listResourcesParamsSchema = z.strictObject({ orgId: z.string() @@ -171,10 +172,9 @@ function queryResourcesBase() { ); } -export type ListResourcesResponse = { +export type ListResourcesResponse = PaginatedResponse<{ resources: ResourceWithTargets[]; - pagination: { total: number; pageSize: number; page: number }; -}; +}>; registry.registerPath({ method: "get", @@ -268,16 +268,15 @@ export async function listResources( (resource) => resource.resourceId ); - let conditions = and( + const conditions = [ and( inArray(resources.resourceId, accessibleResourceIds), eq(resources.orgId, orgId) ) - ); + ]; if (query) { - conditions = and( - conditions, + conditions.push( or( ilike(resources.name, "%" + query + "%"), ilike(resources.fullDomain, "%" + query + "%") @@ -285,17 +284,16 @@ export async function listResources( ); } if (typeof enabled !== "undefined") { - conditions = and(conditions, eq(resources.enabled, enabled)); + conditions.push(eq(resources.enabled, enabled)); } if (typeof authState !== "undefined") { switch (authState) { case "none": - conditions = and(conditions, eq(resources.http, false)); + conditions.push(eq(resources.http, false)); break; case "protected": - conditions = and( - conditions, + conditions.push( or( eq(resources.sso, true), eq(resources.emailWhitelistEnabled, true), @@ -306,8 +304,7 @@ export async function listResources( ); break; case "not_protected": - conditions = and( - conditions, + conditions.push( not(eq(resources.sso, true)), not(eq(resources.emailWhitelistEnabled, true)), isNull(resourceHeaderAuth.headerAuthId), @@ -318,7 +315,7 @@ export async function listResources( } } - let aggregateFilters: SQL | null | undefined = null; + let aggregateFilters: SQL | undefined = sql`1 = 1`; if (typeof healthStatus !== "undefined") { switch (healthStatus) { @@ -354,8 +351,8 @@ export async function listResources( } const baseQuery = queryResourcesBase() - .where(conditions) - .having(aggregateFilters ?? sql`1 = 1`); + .where(and(...conditions)) + .having(aggregateFilters); // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count(baseQuery.as("filtered_resources")); diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index c65f8d10..cc292499 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -22,6 +22,7 @@ import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import semver from "semver"; import cache from "@server/lib/cache"; +import type { PaginatedResponse } from "@server/types/Pagination"; async function getLatestNewtVersion(): Promise { try { @@ -111,9 +112,6 @@ const listSitesSchema = z.object({ .catch(undefined) }); -function countSitesBase() { - return db.select({ count: count() }).from(sites); -} function querySitesBase() { return db .select({ @@ -148,10 +146,9 @@ type SiteWithUpdateAvailable = Awaited>[0] & { newtUpdateAvailable?: boolean; }; -export type ListSitesResponse = { +export type ListSitesResponse = PaginatedResponse<{ sites: SiteWithUpdateAvailable[]; - pagination: { total: number; pageSize: number; page: number }; -}; +}>; registry.registerPath({ method: "get", @@ -227,13 +224,14 @@ export async function listSites( const accessibleSiteIds = accessibleSites.map((site) => site.siteId); - let conditions = and( - inArray(sites.siteId, accessibleSiteIds), - eq(sites.orgId, orgId) - ); + const conditions = [ + and( + inArray(sites.siteId, accessibleSiteIds), + eq(sites.orgId, orgId) + ) + ]; if (query) { - conditions = and( - conditions, + conditions.push( or( ilike(sites.name, "%" + query + "%"), ilike(sites.niceId, "%" + query + "%") @@ -241,13 +239,15 @@ export async function listSites( ); } if (typeof online !== "undefined") { - conditions = and(conditions, eq(sites.online, online)); + conditions.push(eq(sites.online, online)); } - const baseQuery = querySitesBase().where(conditions); + const baseQuery = querySitesBase().where(and(...conditions)); // we need to add `as` so that drizzle filters the result as a subquery - const countQuery = db.$count(querySitesBase().where(conditions)); + const countQuery = db.$count( + querySitesBase().where(and(...conditions)) + ); const siteListQuery = baseQuery .limit(pageSize) diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 2392eb76..f15d2ecc 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -9,6 +9,7 @@ import { eq, and, asc, ilike, or } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; +import type { PaginatedResponse } from "@server/types/Pagination"; const listAllSiteResourcesByOrgParamsSchema = z.strictObject({ orgId: z.string() @@ -33,14 +34,13 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({ mode: z.enum(["host", "cidr"]).optional().catch(undefined) }); -export type ListAllSiteResourcesByOrgResponse = { +export type ListAllSiteResourcesByOrgResponse = PaginatedResponse<{ siteResources: (SiteResource & { siteName: string; siteNiceId: string; siteAddress: string | null; })[]; - pagination: { total: number; pageSize: number; page: number }; -}; +}>; function querySiteResourcesBase() { return db @@ -114,10 +114,9 @@ export async function listAllSiteResourcesByOrg( const { orgId } = parsedParams.data; const { page, pageSize, query, mode } = parsedQuery.data; - let conditions = and(eq(siteResources.orgId, orgId)); + const conditions = [and(eq(siteResources.orgId, orgId))]; if (query) { - conditions = and( - conditions, + conditions.push( or( ilike(siteResources.name, "%" + query + "%"), ilike(siteResources.destination, "%" + query + "%"), @@ -129,16 +128,15 @@ export async function listAllSiteResourcesByOrg( } if (mode) { - conditions = and(conditions, eq(siteResources.mode, mode)); + conditions.push(eq(siteResources.mode, mode)); } - const baseQuery = querySiteResourcesBase().where(conditions); + const baseQuery = querySiteResourcesBase().where(and(...conditions)); const countQuery = db.$count( - querySiteResourcesBase().where(conditions) + querySiteResourcesBase().where(and(...conditions)) ); - // Get all site resources for the org with site names const [siteResourcesList, totalCount] = await Promise.all([ baseQuery .limit(pageSize) diff --git a/server/types/Pagination.ts b/server/types/Pagination.ts new file mode 100644 index 00000000..b0f5edfe --- /dev/null +++ b/server/types/Pagination.ts @@ -0,0 +1,5 @@ +export type Pagination = { total: number; pageSize: number; page: number }; + +export type PaginatedResponse = T & { + pagination: Pagination; +};