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 {
|
||||
|
||||
Reference in New Issue
Block a user