🚧 user table pagination

This commit is contained in:
Fred KISSIE
2026-03-23 20:02:53 +01:00
parent b648aa605c
commit 6d0e10a4aa
3 changed files with 205 additions and 106 deletions

View File

@@ -5,33 +5,67 @@ import { idp, roles, userOrgs, users } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { and, sql } from "drizzle-orm";
import { and, asc, desc, like, or, sql, type SQL } from "drizzle-orm";
import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { eq } from "drizzle-orm";
import type { PaginatedResponse } from "@server/types/Pagination";
const listUsersParamsSchema = z.strictObject({
orgId: z.string()
});
const listUsersSchema = z.strictObject({
limit: z
.string()
pageSize: z.coerce
.number<string>() // for prettier formatting
.int()
.positive()
.optional()
.default("1000")
.transform(Number)
.pipe(z.int().nonnegative()),
offset: z
.string()
.catch(20)
.default(20)
.openapi({
type: "integer",
default: 20,
description: "Number of items per page"
}),
page: z.coerce
.number<string>() // for prettier formatting
.int()
.min(0)
.optional()
.default("0")
.transform(Number)
.pipe(z.int().nonnegative())
.catch(1)
.default(1)
.openapi({
type: "integer",
default: 1,
description: "Page number to retrieve"
}),
query: z.string().optional(),
sort_by: z
.enum(["username"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["username"],
description: "Field to sort by"
}),
order: z
.enum(["asc", "desc"])
.optional()
.default("asc")
.catch("asc")
.openapi({
type: "string",
enum: ["asc", "desc"],
default: "asc",
description: "Sort order"
})
});
async function queryUsers(orgId: string, limit: number, offset: number) {
return await db
function queryUsersBase() {
return db
.select({
id: users.userId,
email: users.email,
@@ -54,16 +88,12 @@ async function queryUsers(orgId: string, limit: number, offset: number) {
.leftJoin(userOrgs, eq(users.userId, userOrgs.userId))
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
.leftJoin(idp, eq(users.idpId, idp.idpId))
.leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId))
.where(eq(userOrgs.orgId, orgId))
.limit(limit)
.offset(offset);
.leftJoin(idpOidcConfig, eq(idpOidcConfig.idpId, idp.idpId));
}
export type ListUsersResponse = {
users: NonNullable<Awaited<ReturnType<typeof queryUsers>>>;
pagination: { total: number; limit: number; offset: number };
};
export type ListUsersResponse = PaginatedResponse<{
users: NonNullable<Awaited<ReturnType<typeof queryUsersBase>>>;
}>;
registry.registerPath({
method: "get",
@@ -92,7 +122,7 @@ export async function listUsers(
)
);
}
const { limit, offset } = parsedQuery.data;
const { page, pageSize, sort_by, order, query } = parsedQuery.data;
const parsedParams = listUsersParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
@@ -106,24 +136,57 @@ export async function listUsers(
const { orgId } = parsedParams.data;
const usersWithRoles = await queryUsers(
orgId.toString(),
limit,
offset
const conditions = [and(eq(userOrgs.orgId, orgId))];
if (query) {
conditions.push(
or(
like(
sql`LOWER(${users.name})`,
"%" + query.toLowerCase() + "%"
),
like(
sql`LOWER(${users.username})`,
"%" + query.toLowerCase() + "%"
),
like(
sql`LOWER(${users.email})`,
"%" + query.toLowerCase() + "%"
)
)
);
}
const countQuery = db.$count(
queryUsersBase()
.where(and(...conditions))
.as("filtered_users")
);
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(userOrgs)
.where(eq(userOrgs.orgId, orgId));
const userListQuery = queryUsersBase()
.where(and(...conditions))
.limit(pageSize)
.offset(pageSize * (page - 1))
.orderBy(
sort_by
? order === "asc"
? asc(users[sort_by])
: desc(users[sort_by])
: asc(users.name)
);
const [count, usersWithRoles] = await Promise.all([
countQuery,
userListQuery
]);
return response<ListUsersResponse>(res, {
data: {
users: usersWithRoles,
pagination: {
total: count,
limit,
offset
page,
pageSize
}
},
success: true,