Skip to content

Commit

Permalink
feat(users): add pagination to users endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
Rafatcb committed Apr 26, 2024
1 parent ec409f0 commit d514068
Show file tree
Hide file tree
Showing 6 changed files with 524 additions and 90 deletions.
24 changes: 4 additions & 20 deletions models/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { v4 as uuidV4 } from 'uuid';
import { ForbiddenError, ValidationError } from 'errors';
import database from 'infra/database.js';
import balance from 'models/balance.js';
import pagination from 'models/pagination.js';
import prestige from 'models/prestige';
import user from 'models/user.js';
import validator from 'models/validator.js';
Expand Down Expand Up @@ -259,27 +260,10 @@ async function findWithStrategy(options = {}) {
}
}

async function getPagination(values, options = {}) {
async function getPagination(values, options) {
values.count = true;

const totalRows = values.totalRows ?? (await findAll(values, options))[0]?.total_rows ?? 0;
const perPage = values.per_page;
const firstPage = 1;
const lastPage = Math.ceil(totalRows / values.per_page);
const nextPage = values.page >= lastPage ? null : values.page + 1;
const previousPage = values.page <= 1 ? null : values.page > lastPage ? lastPage : values.page - 1;
const strategy = values.strategy;

return {
currentPage: values.page,
totalRows: totalRows,
perPage: perPage,
firstPage: firstPage,
nextPage: nextPage,
previousPage: previousPage,
lastPage: lastPage,
strategy: strategy,
};
values.totalRows = values.totalRows ?? (await findAll(values, options))[0]?.total_rows ?? 0;
return pagination.getPagination(values);
}

async function create(postedContent, options = {}) {
Expand Down
29 changes: 17 additions & 12 deletions models/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,27 @@ function logRequest(request, response, next) {

function injectPaginationHeaders(pagination, endpoint, response) {
const links = [];
const baseUrl = `${webserver.host}${endpoint}?strategy=${pagination.strategy}`;
const baseUrl = `${webserver.host}${endpoint}`;

if (pagination.firstPage) {
links.push(`<${baseUrl}&page=${pagination.firstPage}&per_page=${pagination.perPage}>; rel="first"`);
}

if (pagination.previousPage) {
links.push(`<${baseUrl}&page=${pagination.previousPage}&per_page=${pagination.perPage}>; rel="prev"`);
}
const searchParams = new URLSearchParams();

if (pagination.nextPage) {
links.push(`<${baseUrl}&page=${pagination.nextPage}&per_page=${pagination.perPage}>; rel="next"`);
if (pagination.strategy) {
searchParams.set('strategy', pagination.strategy);
}

if (pagination.lastPage) {
links.push(`<${baseUrl}&page=${pagination.lastPage}&per_page=${pagination.perPage}>; rel="last"`);
const pages = [
{ page: pagination.firstPage, rel: 'first' },
{ page: pagination.previousPage, rel: 'prev' },
{ page: pagination.nextPage, rel: 'next' },
{ page: pagination.lastPage, rel: 'last' },
];

for (const { page, rel } of pages) {
if (page) {
searchParams.set('page', page);
searchParams.set('per_page', pagination.perPage);
links.push(`<${baseUrl}?${searchParams.toString()}>; rel="${rel}"`);
}
}

const linkHeaderString = links.join(', ');
Expand Down
26 changes: 26 additions & 0 deletions models/pagination.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
async function getPagination({ totalRows, page, per_page, strategy }) {
const firstPage = 1;
const lastPage = Math.ceil(totalRows / per_page);
const nextPage = page >= lastPage ? null : page + 1;
const previousPage = page <= 1 ? null : page > lastPage ? lastPage : page - 1;

const pagination = {
currentPage: page,
totalRows: totalRows,
perPage: per_page,
firstPage: firstPage,
nextPage: nextPage,
previousPage: previousPage,
lastPage: lastPage,
};

if (strategy) {
pagination.strategy = strategy;
}

return pagination;
}

export default Object.freeze({
getPagination,
});
51 changes: 51 additions & 0 deletions models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import database from 'infra/database.js';
import authentication from 'models/authentication.js';
import balance from 'models/balance.js';
import emailConfirmation from 'models/email-confirmation.js';
import pagination from 'models/pagination.js';
import validator from 'models/validator.js';

async function findAll() {
Expand All @@ -25,6 +26,55 @@ async function findAll() {
return results.rows;
}

async function findAllWithPagination(values) {
const offset = (values.page - 1) * values.per_page;

const query = {
text: `
WITH user_window AS (
SELECT
COUNT(*) OVER()::INTEGER as total_rows,
id
FROM users
ORDER BY updated_at DESC
LIMIT $1 OFFSET $2
)
SELECT
*
FROM
users
INNER JOIN
user_window ON users.id = user_window.id
CROSS JOIN LATERAL (
SELECT
get_user_current_tabcoins(users.id) as tabcoins,
get_user_current_tabcash(users.id) as tabcash
) as balance
ORDER BY updated_at DESC
`,
values: [values.limit || values.per_page, offset],
};

const queryResults = await database.query(query);

const results = {
rows: queryResults.rows,
};

values.totalRows = results.rows[0]?.total_rows ?? (await countTotalRows());

results.pagination = await pagination.getPagination(values);

return results;
}

async function countTotalRows() {
const countQuery = `SELECT COUNT(*) OVER()::INTEGER as total_rows FROM users`;
const countResult = await database.query(countQuery);
return countResult.rows[0].total_rows;
}

async function findOneByUsername(username, options = {}) {
const baseQuery = `
WITH user_found AS (
Expand Down Expand Up @@ -446,6 +496,7 @@ async function updateRewardedAt(userId, options) {
export default Object.freeze({
create,
findAll,
findAllWithPagination,
findOneByUsername,
findOneByEmail,
findOneById,
Expand Down
22 changes: 20 additions & 2 deletions pages/api/v1/users/index.public.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,39 @@ export default nextConnect({
.use(authentication.injectAnonymousOrUser)
.use(controller.logRequest)
.use(cacheControl.noCache)
.get(authorization.canRequest('read:user:list'), getHandler)
.get(getValidationHandler, authorization.canRequest('read:user:list'), getHandler)
.post(
postValidationHandler,
authorization.canRequest('create:user'),
firewall.canRequest('create:user'),
postHandler,
);

function getValidationHandler(request, response, next) {
const cleanValues = validator(request.query, {
page: 'optional',
per_page: 'optional',
});

request.query = cleanValues;

next();
}

async function getHandler(request, response) {
const userTryingToList = request.context.user;

const userList = await user.findAll();
const results = await user.findAllWithPagination({
page: request.query.page,
per_page: request.query.per_page,
});

const userList = results.rows;

const secureOutputValues = authorization.filterOutput(userTryingToList, 'read:user:list', userList);

controller.injectPaginationHeaders(results.pagination, '/api/v1/users', response);

return response.status(200).json(secureOutputValues);
}

Expand Down
Loading

0 comments on commit d514068

Please sign in to comment.