From cf193329a446ff3f8c0acb8be4f41135a3da3282 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 10 Mar 2021 00:37:48 +0100 Subject: [PATCH] todo,todo-backend: switch to and implement offset/limit pagination Signed-off-by: Patrik Oldsberg --- .../src/service/TodoReaderService.ts | 72 ++++++------------- plugins/todo-backend/src/service/router.ts | 24 +++++-- plugins/todo-backend/src/service/types.ts | 10 ++- plugins/todo/dev/index.tsx | 7 +- plugins/todo/src/api/TodoClient.ts | 10 ++- plugins/todo/src/api/types.ts | 10 ++- .../todo/src/components/TodoList/TodoList.tsx | 23 ++++-- 7 files changed, 75 insertions(+), 81 deletions(-) diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index e646a29634..57010ee7e0 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -49,28 +49,40 @@ export class TodoReaderService implements TodoService { this.defaultPageSize = options.defaultPageSize ?? DEFAULT_DEFAULT_PAGE_SIZE; } - async listTodos({ - entity: entityName, - cursor, - }: ListTodosRequest): Promise { - if (!entityName) { + async listTodos(req: ListTodosRequest): Promise { + 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}`, - }; - } } diff --git a/plugins/todo-backend/src/service/router.ts b/plugins/todo-backend/src/service/router.ts index f5237d6c0a..12655cb208 100644 --- a/plugins/todo-backend/src/service/router.ts +++ b/plugins/todo-backend/src/service/router.ts @@ -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; +} diff --git a/plugins/todo-backend/src/service/types.ts b/plugins/todo-backend/src/service/types.ts index bf417f0895..ac8ecc16b5 100644 --- a/plugins/todo-backend/src/service/types.ts +++ b/plugins/todo-backend/src/service/types.ts @@ -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 { diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx index 08936f3657..25a6769cb5 100644 --- a/plugins/todo/dev/index.tsx +++ b/plugins/todo/dev/index.tsx @@ -55,11 +55,8 @@ const mockedApi = { }, ], totalCount: 15, - cursors: { - prev: 'prev', - self: 'self', - next: 'next', - }, + offset: 0, + limit: 10, }), }; diff --git a/plugins/todo/src/api/TodoClient.ts b/plugins/todo/src/api/TodoClient.ts index b8caee3ee9..44d80ba22e 100644 --- a/plugins/todo/src/api/TodoClient.ts +++ b/plugins/todo/src/api/TodoClient.ts @@ -34,7 +34,8 @@ export class TodoClient implements TodoApi { async listTodos({ entity, - cursor, + offset, + limit, }: TodoListOptions): Promise { 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}`, { diff --git a/plugins/todo/src/api/types.ts b/plugins/todo/src/api/types.ts index bc6a15620b..890bc50731 100644 --- a/plugins/todo/src/api/types.ts +++ b/plugins/todo/src/api/types.ts @@ -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 { diff --git a/plugins/todo/src/components/TodoList/TodoList.tsx b/plugins/todo/src/components/TodoList/TodoList.tsx index a27734c80b..df0413107a 100644 --- a/plugins/todo/src/components/TodoList/TodoList.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.tsx @@ -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[] = [ 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 ( );