Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

Commit

Permalink
feat: work in progress paging and sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
jackcmeyer committed Apr 6, 2020
1 parent b0097d1 commit 7f856c8
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 17 deletions.
4 changes: 4 additions & 0 deletions src/clients/Sort.ts
@@ -0,0 +1,4 @@
export default interface Sort {
field: string
direction: 'asc' | 'desc'
}
4 changes: 4 additions & 0 deletions src/clients/db/LabRepository.ts
Expand Up @@ -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<Lab> {
constructor() {
super(labs)
Expand Down
48 changes: 48 additions & 0 deletions src/clients/db/Page.ts
@@ -0,0 +1,48 @@
export default class Page<T> {
/** 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<Page<T>>

getPreviousPage?: () => Promise<Page<T>>

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
}
}
10 changes: 10 additions & 0 deletions 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[]
}
116 changes: 99 additions & 17 deletions 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
Expand All @@ -33,17 +26,106 @@ export default class Repository<T extends AbstractDBModel> {
return mapDocument(document)
}

async search(criteria: any): Promise<T[]> {
const response = await this.db.find(criteria)
return response.docs.map(mapDocument)
async findAll(sort = Unsorted): Promise<T[]> {
const selector = {
_id: { $gt: null },
}

return this.search({ selector }, sort)
}

async pagedFindAll(pageRequest: PageRequest, sortRequest = Unsorted): Promise<Page<T>> {
return this.pagedSearch(
{ selector: { _id: { $gt: pageRequest?.startKey } } },
pageRequest,
sortRequest,
)
}

async findAll(): Promise<T[]> {
const allDocs = await this.db.allDocs({
include_docs: true,
async search(criteria: any, sort: SortRequest = Unsorted): Promise<T[]> {
// 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<Page<T>> {
// 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<T>(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<Page<T>>
}

// 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<Page<T>>
}

return allDocs.rows.map(mapRow)
return page
}

async save(entity: T): Promise<T> {
Expand Down
7 changes: 7 additions & 0 deletions 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: [] }

0 comments on commit 7f856c8

Please sign in to comment.