Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add users collection pagination #636

Merged
merged 12 commits into from
Feb 22, 2025
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DB_DRIVER=sqlite
DB_FILENAME=:memory:
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ docker-run:
docker run -it --rm --name $(DOCKER_IMAGE_NAME)-01 $(DOCKER_IMAGE_NAME)

test:
npx tsx --test ${TEST_FILES}
NODE_ENV=test npx tsx --test ${TEST_FILES}

lint:
npx tsc --noemit
Expand Down
8 changes: 7 additions & 1 deletion src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ if (process.env.PUBLIC_URI === undefined) {
// that we may be missing a .env file.
//
// This is the only required environment variable.
dotenv.config({path: dirname(fileURLToPath(import.meta.url)) + '/../.env'});
if(process.env.NODE_ENV === 'test'){
dotenv.config({ path: './.env.test' });
}
else{
dotenv.config({path: dirname(fileURLToPath(import.meta.url)) + '/../.env'});
}

} else {
console.warn('/env.js was loaded twice?');
}
45 changes: 38 additions & 7 deletions src/principal/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BasePrincipal,
Group,
NewPrincipal,
PaginatedResult,
Principal,
PrincipalIdentity,
PrincipalStats,
Expand Down Expand Up @@ -46,26 +47,57 @@ export class PrincipalService {

}

async findAll(type: 'user'): Promise<User[]>;
async findAll(type: 'user', page: number): Promise<PaginatedResult<User>>;
async findAll(type: 'group'): Promise<Group[]>;
async findAll(type: 'app'): Promise<App[]>;
async findAll(): Promise<Principal[]>;
async findAll(type?: PrincipalType): Promise<Principal[]> {
async findAll(type?: PrincipalType, page?: number): Promise<Principal[] | PaginatedResult<Principal>> {

this.privileges.require('a12n:principals:list');
const filters: Record<string, any> = {};
if (type) {
filters.type = userTypeToInt(type);
}

const result = await db('principals')
.where(filters);
const pageSize = 100;
const total = (await getPrincipalStats()).user;

let result: PrincipalsRecord[] = [];
let hasNextPage = false;

if(type && page !== undefined){

page = page < 1 ? 1 : page;
const offset = (page - 1) * pageSize;

hasNextPage = (offset + pageSize) < total;

result = await db('principals')
.where(filters)
.limit(pageSize)
.offset(offset);

} else {
result = await db('principals')
.where(filters);
}

const principals: Principal[] = [];
for (const principal of result) {
principals.push(recordToModel(principal));
}
return principals;

if(type && page !== undefined){
return {
principals,
total,
page,
pageSize,
hasNextPage,
};
} else {
return principals;
}

}

Expand Down Expand Up @@ -403,8 +435,7 @@ export async function getPrincipalStats(): Promise<PrincipalStats> {

}


function recordToModel(user: PrincipalsRecord): Principal {
export function recordToModel(user: PrincipalsRecord): Principal {

return {
id: user.id,
Expand Down
11 changes: 11 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@ export type PrincipalStats = {
group: number;
};

/**
* Paginated Result
*/
export type PaginatedResult<T> = {
principals: T[];
total: number;
page: number;
pageSize: number;
hasNextPage: boolean;
}

/**
* The App Client refers to a single set of credentials for an app.
*
Expand Down
9 changes: 7 additions & 2 deletions src/user/controller/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ class UserCollectionController extends Controller {
async get(ctx: Context) {

const principalService = new services.principal.PrincipalService(ctx.privileges);
const users = await principalService.findAll('user');

const page = +ctx.request.query.page || 1;

const paginatedResult = await principalService.findAll('user', page);
const users = paginatedResult.principals;

const embed = ctx.request.prefer('transclude').toString().includes('item') || ctx.query.embed?.includes('item');

const embeddedUsers: HalResource[] = [];
Expand All @@ -36,7 +41,7 @@ class UserCollectionController extends Controller {
}
}

ctx.response.body = hal.collection(users, embeddedUsers);
ctx.response.body = hal.collection(embeddedUsers, paginatedResult);

}

Expand Down
25 changes: 21 additions & 4 deletions src/user/formats/hal.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { PrivilegeMap } from '../../privilege/types.ts';
import { Principal, Group, User, PrincipalIdentity } from '../../types.ts';
import { Principal, Group, User, PrincipalIdentity, PaginatedResult } from '../../types.ts';
import { HalResource } from 'hal-types';
import { LazyPrivilegeBox } from '../../privilege/service.ts';
import { UserNewResult } from '../../api-types.ts';

export function collection(users: User[], embeddedUsers: HalResource[]): HalResource {
export function collection(embeddedUsers: HalResource[], paginatedResult: PaginatedResult<User>): HalResource {

const { principals: users, page: currentPage, total, pageSize, hasNextPage } = paginatedResult;

const totalPages = Math.ceil(total / pageSize);

const hal: HalResource = {
_links: {
'self': { href: '/user' },
'self': { href: `/user?page=${currentPage}` },
'item': users.map( user => ({
href: user.href,
title: user.nickname,
Expand All @@ -20,9 +24,22 @@ export function collection(users: User[], embeddedUsers: HalResource[]): HalReso
templated: true,
},
},
total: users.length,
total,
currentPage,
totalPages,
};

if(hasNextPage){
const nextPage = currentPage + 1;
hal._links['next'] = { href: `/user?page=${nextPage}` };
}

const hasPrevPage = currentPage > 1;
if(hasPrevPage){
const prevPage = currentPage - 1;
hal._links['previous'] = { href: `/user?page=${prevPage}` };
}

if (embeddedUsers.length) {
hal._embedded = {
item: embeddedUsers
Expand Down
Loading