/*! * @license * Copyright Squiz Australia Pty Ltd. All Rights Reserved. */ import { AttributeValue } from '@aws-sdk/client-dynamodb'; import { QUERY } from '../../../constants/QueryParams'; import { BadRequestError, InternalServerError, ERROR_MESSAGES } from '../../../errors'; import { PageQuery } from '../model/PageQuery'; import { PaginationInfo } from '../model/PaginationInfo'; import { PaginationLinks, PaginationTokens } from '../model/PaginationLinks'; import { PaginationFormattedResponse, PaginationResponse } from '../model/PaginationResponse'; import { PAGINATION_DIRECTION_TYPE_AFTER, PAGINATION_DIRECTION_TYPE_BEFORE, PaginationPageLimit, } from '../model/PaginationTypes'; /** * Decode the token string included in a pagination query parameter. * @param {string} token A base64 encoded token string. * @returns {Record} The decoded token item. */ export const decodeTokenString = (token: string): Record => { try { return JSON.parse(Buffer.from(token, 'base64').toString()); } catch (e) { throw new BadRequestError(ERROR_MESSAGES.paginationInvalidToken); } }; /** * Convert a DynamoDB LastEvaluatedKey into a base64 encoded token string. * @param {SettingsPrimaryKey} lastEvaluatedKey The LastEvaluatedKey returned from a dynamo query. * @returns {string} The encoded string. */ export const encodeTokenString = (lastEvaluatedKey: T): string => { try { return Buffer.from(JSON.stringify(lastEvaluatedKey)).toString('base64'); } catch (e) { throw new InternalServerError(ERROR_MESSAGES.paginationEncodeFailed); } }; /** * Get the pagination info from the page query param. * @param {string} requestUrl The URL for the request. * @param {PageQuery} page The page query param object. * @returns {PaginationInfo} The pagination info. */ export const getPaginationInfo = (requestUrl: string, page: PageQuery): PaginationInfo => { const { after, before, size } = page; if (after && before) { throw new BadRequestError(ERROR_MESSAGES.paginationOnlyOneDirection); } let token: string | null = null; if (after) { token = decodeURIComponent(after); } else if (before) { token = decodeURIComponent(before); } validateTokenHasCorrectFormat(token); return { direction: before ? PAGINATION_DIRECTION_TYPE_BEFORE : PAGINATION_DIRECTION_TYPE_AFTER, limit: size ?? PaginationPageLimit.large, requestUrl: decodeURIComponent(requestUrl), token, }; }; /** * Generate the pagination links for a response. * @param {string} requestUrl The URL of the request. * @param {PaginationTokens} tokens The next/prev tokens. * @returns {PaginationLinks} The pagination links. */ export const getPaginationLinks = (requestUrl: string, tokens: PaginationTokens): PaginationLinks => { let { next, prev } = tokens; let basePaginationUrl = requestUrl.replace(/\/api/g, ''); basePaginationUrl = removeParamsByName([QUERY.PAGE.AFTER, QUERY.PAGE.BEFORE], basePaginationUrl); if (basePaginationUrl.indexOf('?') !== -1) { next = next ? `${basePaginationUrl}&${QUERY.PAGE.AFTER}=${next}` : null; prev = prev ? `${basePaginationUrl}&${QUERY.PAGE.BEFORE}=${prev}` : null; } else { next = next ? `${basePaginationUrl}?${QUERY.PAGE.AFTER}=${next}` : null; prev = prev ? `${basePaginationUrl}?${QUERY.PAGE.BEFORE}=${prev}` : null; } return { next, prev, }; }; /** * Remove specified query params from a URL string. * @param {Array} keys The keys for each param to remove. * @param {string} requestUrl The URL of the request. * @returns {string} The updated URL. */ export const removeParamsByName = (keys: Array, requestUrl: string): string => { let result = requestUrl.split('?')[0]; const queryString = requestUrl.indexOf('?') !== -1 ? requestUrl.split('?')[1] : ''; if (queryString !== '') { const params = queryString.split('&'); for (let i = params.length - 1; i >= 0; i -= 1) { const param = params[i].split('=')[0]; if (keys.indexOf(param) !== -1) { params.splice(i, 1); } } if (params.length) { result = result + '?' + params.join('&'); } } return result; }; /** * Validate token corresponds to a valid DynamoDB primary key. * @param {string | null} token The token to validate. */ export const validateTokenHasCorrectFormat = (token: string | null): void => { if (token === null) { return; } const decodedToken = decodeTokenString(token); if (!decodedToken.pk || !decodedToken.sk) { throw new BadRequestError(ERROR_MESSAGES.paginationInvalidToken); } }; export const getFormattedResponse = (results: PaginationResponse): PaginationFormattedResponse => { const { links, data } = results; const response: PaginationFormattedResponse = { data }; if (links) { if (links.next || links.prev) { response.links = links; } } return response; };