From 7f856c84ca94808b4a85d6ae6abf6ecde1351d77 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Sun, 5 Apr 2020 20:01:03 -0500 Subject: [PATCH] feat: work in progress paging and sorting --- src/clients/Sort.ts | 4 ++ src/clients/db/LabRepository.ts | 4 ++ src/clients/db/Page.ts | 48 +++++++++++++ src/clients/db/PageRequest.ts | 10 +++ src/clients/db/Repository.ts | 116 +++++++++++++++++++++++++++----- src/clients/db/SortRequest.ts | 7 ++ 6 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 src/clients/Sort.ts create mode 100644 src/clients/db/Page.ts create mode 100644 src/clients/db/PageRequest.ts create mode 100644 src/clients/db/SortRequest.ts diff --git a/src/clients/Sort.ts b/src/clients/Sort.ts new file mode 100644 index 0000000000..cd82eb0c37 --- /dev/null +++ b/src/clients/Sort.ts @@ -0,0 +1,4 @@ +export default interface Sort { + field: string + direction: 'asc' | 'desc' +} diff --git a/src/clients/db/LabRepository.ts b/src/clients/db/LabRepository.ts index c1b292be93..742c15cb84 100644 --- a/src/clients/db/LabRepository.ts +++ b/src/clients/db/LabRepository.ts @@ -2,6 +2,10 @@ import Lab from 'model/Lab' import Repository from './Repository' import { labs } from '../../config/pouchdb' +labs.createIndex({ + index: { fields: ['requestedOn'] }, +}) + export class LabRepository extends Repository { constructor() { super(labs) diff --git a/src/clients/db/Page.ts b/src/clients/db/Page.ts new file mode 100644 index 0000000000..2cd63ff25f --- /dev/null +++ b/src/clients/db/Page.ts @@ -0,0 +1,48 @@ +export default class Page { + /** the content for this page */ + content: T[] + + /** the total number of elements that match the search */ + totalElements: number + + /** the size of the current page */ + size: number + + /** the current page number */ + number: number + + getNextPage?: () => Promise> + + getPreviousPage?: () => Promise> + + constructor(content: T[], totalElements: number, size: number, number: number) { + this.content = content + this.totalElements = totalElements + this.size = size + this.number = number + } + + getContent(): T[] { + return this.content + } + + getTotalElements(): number { + return this.totalElements + } + + getSize(): number { + return this.size + } + + getNumber(): number { + return this.number + } + + hasNext(): boolean { + return this.getNextPage !== undefined + } + + hasPrevious(): boolean { + return this.getPreviousPage !== undefined + } +} diff --git a/src/clients/db/PageRequest.ts b/src/clients/db/PageRequest.ts new file mode 100644 index 0000000000..96870d387b --- /dev/null +++ b/src/clients/db/PageRequest.ts @@ -0,0 +1,10 @@ +export default interface PageRequest { + /** the page number requested */ + number: number + /** the size of the pages */ + size: number + + startKey?: string + + previousStartKeys?: string[] +} diff --git a/src/clients/db/Repository.ts b/src/clients/db/Repository.ts index f1d5f52877..8b1681ab1d 100644 --- a/src/clients/db/Repository.ts +++ b/src/clients/db/Repository.ts @@ -1,16 +1,9 @@ /* eslint "@typescript-eslint/camelcase": "off" */ import { v4 as uuidv4 } from 'uuid' import AbstractDBModel from '../../model/AbstractDBModel' - -function mapRow(row: any): any { - const { value, doc } = row - const { id, _rev, _id, rev, ...restOfDoc } = doc - return { - id: _id, - rev: value.rev, - ...restOfDoc, - } -} +import PageRequest from './PageRequest' +import Page from './Page' +import SortRequest, { Unsorted } from './SortRequest' function mapDocument(document: any): any { const { _id, _rev, ...values } = document @@ -33,17 +26,106 @@ export default class Repository { return mapDocument(document) } - async search(criteria: any): Promise { - const response = await this.db.find(criteria) - return response.docs.map(mapDocument) + async findAll(sort = Unsorted): Promise { + const selector = { + _id: { $gt: null }, + } + + return this.search({ selector }, sort) + } + + async pagedFindAll(pageRequest: PageRequest, sortRequest = Unsorted): Promise> { + return this.pagedSearch( + { selector: { _id: { $gt: pageRequest?.startKey } } }, + pageRequest, + sortRequest, + ) } - async findAll(): Promise { - const allDocs = await this.db.allDocs({ - include_docs: true, + async search(criteria: any, sort: SortRequest = Unsorted): Promise { + // hack to get around the requirement that any sorted field must be in the selector list + sort.sorts.forEach((s) => { + criteria.selector[s.field] = { $gt: null } }) + const allCriteria = { + ...criteria, + sort: sort.sorts.map((s) => ({ [s.field]: s.direction })), + } + const response = await this.db.find(allCriteria) + return response.docs.map(mapDocument) + } + + async pagedSearch( + criteria: any, + pageRequest: PageRequest, + sortRequest = Unsorted, + ): Promise> { + // eslint-disable-next-line + criteria.selector._id = { $gt: pageRequest?.startKey } + criteria.selector.requestedOn = { $gt: null } + const allCriteria = { + ...criteria, + limit: pageRequest?.size, + // if the page request has a start key included, then do not use skip due to its performance drawbacks + // documented here: https://pouchdb.com/2014/04/14/pagination-strategies-with-pouchdb.html + // if the start key was provided, make the skip 1. This will get the next document after the one with the given id defined + // by the start key + skip: pageRequest && !pageRequest.startKey ? pageRequest.size * pageRequest.number : 1, + sort: sortRequest.sorts.length > 0 ? sortRequest.sorts.map((s) => s.field) : undefined, + } + + const info = await this.db.allDocs({ limit: 0 }) + const result = await this.db.find(allCriteria) + + const rows = result.docs.map(mapDocument) + const page = new Page(rows, info.total_rows, rows.length, pageRequest.number) + + const lastPageNumber = + Math.floor(info.total_rows / pageRequest.size) + (info.total_rows % pageRequest.size) + + // if it's not the last page, calculate the next page + if (lastPageNumber !== pageRequest.number + 1) { + // add the current start key to the previous start keys list + // this is to keep track of the previous start key in order to do "linked list paging" + // for performance reasons + // the current page's start key will become the previous start key + const previousStartKeys = pageRequest.previousStartKeys || [] + if (pageRequest.startKey) { + previousStartKeys.push(pageRequest.startKey) + } + + page.getNextPage = async () => + this.pagedSearch( + criteria, + { + size: pageRequest.size, + // the start key is the last row returned on the current page + startKey: rows[rows.length - 1].id, + previousStartKeys, + number: pageRequest.number + 1, + }, + sortRequest, + ) as Promise> + } + + // if it's not the first page, calculate the previous page + if (pageRequest.number !== 0) { + // pop a start key off the list to get the previous start key + const previousStartKey = pageRequest.previousStartKeys?.pop() + page.getPreviousPage = async () => + this.pagedSearch( + criteria, + { + size: pageRequest.size, + number: pageRequest.number - 1, + startKey: previousStartKey, + previousStartKeys: pageRequest.previousStartKeys, + }, + sortRequest, + ) as Promise> + } - return allDocs.rows.map(mapRow) + return page } async save(entity: T): Promise { diff --git a/src/clients/db/SortRequest.ts b/src/clients/db/SortRequest.ts new file mode 100644 index 0000000000..3079fd01c5 --- /dev/null +++ b/src/clients/db/SortRequest.ts @@ -0,0 +1,7 @@ +import Sort from 'clients/Sort' + +export default interface SortRequest { + sorts: Sort[] +} + +export const Unsorted: SortRequest = { sorts: [] }