todo,todo-backend: switch to and implement offset/limit pagination

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-03-10 00:37:48 +01:00
parent 2eeb884353
commit cf193329a4
7 changed files with 75 additions and 81 deletions
@@ -49,28 +49,40 @@ export class TodoReaderService implements TodoService {
this.defaultPageSize = options.defaultPageSize ?? DEFAULT_DEFAULT_PAGE_SIZE;
}
async listTodos({
entity: entityName,
cursor,
}: ListTodosRequest): Promise<ListTodosResponse> {
if (!entityName) {
async listTodos(req: ListTodosRequest): Promise<ListTodosResponse> {
if (!req.entity) {
throw new InputError('entity filter is required to list todos');
}
const entity = await this.catalogClient.getEntityByName(entityName);
const entity = await this.catalogClient.getEntityByName(req.entity);
if (!entity) {
throw new NotFoundError(
`Entity not found, ${serializeEntityRef(entityName)}`,
`Entity not found, ${serializeEntityRef(req.entity)}`,
);
}
const url = this.getEntitySourceUrl(entity);
const todos = await this.todoReader.readTodos({ url });
const totalCount = todos.items.length;
let limit = req.limit ?? this.defaultPageSize;
if (limit < 0) {
limit = 0;
} else if (limit > this.maxPageSize) {
limit = this.maxPageSize;
}
let offset = req.offset ?? 0;
if (offset < 0) {
offset = 0;
} else if (offset - limit > totalCount) {
offset = totalCount - limit;
}
const { offset, limit } = this.parseCursor(cursor);
return {
items: todos.items.slice(offset, offset + limit),
totalCount: todos.items.length,
cursors: this.calculateCursors(offset, limit, todos.items.length),
totalCount,
offset,
limit,
};
}
@@ -105,44 +117,4 @@ export class TodoReaderService implements TodoService {
`No entity location annotation found for ${serializeEntityRef(entity)}`,
);
}
private parseCursor(
cursor: string | undefined,
): { offset: number; limit: number } {
if (!cursor) {
return { offset: 0, limit: this.defaultPageSize };
}
const [offsetStr, limitStr] = cursor.split(',');
const offset = parseInt(offsetStr, 10);
if (!Number.isInteger(offset) || offset < 0) {
throw new InputError(`Invalid cursor, ${cursor}`);
}
let limit = parseInt(limitStr, 10);
if (!Number.isInteger(limit) || limit < 0) {
throw new InputError(`Invalid cursor, ${cursor}`);
}
if (limit > this.maxPageSize) {
limit = this.maxPageSize;
}
return { offset, limit };
}
private calculateCursors(
offset: number,
limit: number,
total: number,
): ListTodosResponse['cursors'] {
const prevOffset = Math.max(offset - limit, 0);
const nextOffset = Math.min(offset + limit, total - limit);
return {
prev: `${prevOffset},${limit}`,
self: `${offset},${limit}`,
next: `${nextOffset},${limit}`,
};
}
}
+18 -6
View File
@@ -33,15 +33,13 @@ export async function createRouter(
router.use(express.json());
router.get('/v1/todos', async (req, res) => {
const { entity: entityRef, cursor } = req.query;
const offset = parseIntegerParam(req.query.offset, 'offset query');
const limit = parseIntegerParam(req.query.limit, 'limit query');
const entityRef = req.query.entity;
if (entityRef && typeof entityRef !== 'string') {
throw new InputError(`entity query must be a string`);
}
if (cursor && typeof cursor !== 'string') {
throw new InputError(`cursor query must be a string`);
}
let entity: EntityName | undefined = undefined;
if (entityRef) {
try {
@@ -51,9 +49,23 @@ export async function createRouter(
}
}
const todos = await todoService.listTodos({ entity, cursor });
const todos = await todoService.listTodos({ entity, offset, limit });
res.json(todos);
});
return router;
}
function parseIntegerParam(str: unknown, ctx: string): number | undefined {
if (str === undefined) {
return undefined;
}
if (typeof str !== 'string') {
throw new InputError(`invalid ${ctx}, must be a string`);
}
const parsed = parseInt(str, 10);
if (!Number.isInteger(parsed)) {
throw new InputError(`invalid ${ctx}, not an integer`);
}
return parsed;
}
+4 -6
View File
@@ -19,17 +19,15 @@ import { TodoItem } from '../lib';
export type ListTodosRequest = {
entity?: EntityName;
cursor?: string;
offset?: number;
limit?: number;
};
export type ListTodosResponse = {
items: TodoItem[];
totalCount: number;
cursors: {
prev: string;
self: string;
next: string;
};
offset: number;
limit: number;
};
export interface TodoService {
+2 -5
View File
@@ -55,11 +55,8 @@ const mockedApi = {
},
],
totalCount: 15,
cursors: {
prev: 'prev',
self: 'self',
next: 'next',
},
offset: 0,
limit: 10,
}),
};
+7 -3
View File
@@ -34,7 +34,8 @@ export class TodoClient implements TodoApi {
async listTodos({
entity,
cursor,
offset,
limit,
}: TodoListOptions): Promise<TodoListResult> {
const baseUrl = await this.discoveryApi.getBaseUrl('todo');
const token = await this.identityApi.getIdToken();
@@ -43,8 +44,11 @@ export class TodoClient implements TodoApi {
if (entity) {
query.set('entity', serializeEntityRef(entity) as string);
}
if (cursor) {
query.set('cursor', cursor);
if (typeof offset === 'number') {
query.set('offset', String(offset));
}
if (typeof limit === 'number') {
query.set('limit', String(limit));
}
const res = await fetch(`${baseUrl}/v1/todos?${query}`, {
+4 -6
View File
@@ -26,17 +26,15 @@ export type TodoItem = {
export type TodoListOptions = {
entity?: Entity;
cursor?: string;
offset?: number;
limit?: number;
};
export type TodoListResult = {
items: TodoItem[];
totalCount: number;
cursors: {
prev: string;
self: string;
next: string;
};
offset: number;
limit: number;
};
export interface TodoApi {
@@ -17,7 +17,7 @@
import { Progress, Table, TableColumn, useApi } from '@backstage/core';
import { useEntity } from '@backstage/plugin-catalog-react';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import React, { useState } from 'react';
import { useAsync } from 'react-use';
import { todoApiRef } from '../../api';
import { TodoItem } from '../../api/types';
@@ -52,10 +52,17 @@ const columns: TableColumn<TodoItem>[] = [
export const TodoList = () => {
const { entity } = useEntity();
const todoApi = useApi(todoApiRef);
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(10);
const { value, loading, error } = useAsync(
async () => todoApi.listTodos({ entity }),
[todoApi, entity],
async () =>
todoApi.listTodos({
entity,
offset: page * pageSize,
limit: pageSize,
}),
[todoApi, entity, page, pageSize],
);
if (loading) {
@@ -67,9 +74,15 @@ export const TodoList = () => {
return (
<Table
title="TODOs"
options={{ search: false }}
page={3}
options={{
search: false,
pageSize,
}}
page={page}
columns={columns}
totalCount={value!.totalCount}
onChangePage={setPage}
onChangeRowsPerPage={setPageSize}
data={value!.items}
/>
);