todo,todo-backend: switch to and implement offset/limit pagination
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -55,11 +55,8 @@ const mockedApi = {
|
||||
},
|
||||
],
|
||||
totalCount: 15,
|
||||
cursors: {
|
||||
prev: 'prev',
|
||||
self: 'self',
|
||||
next: 'next',
|
||||
},
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -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}`, {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user