todo-backend: full TodoReaderService implementation
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -33,7 +33,6 @@ export default async function createPlugin({
|
||||
});
|
||||
const catalogClient = new CatalogClient({ discoveryApi: discovery });
|
||||
const todoService = new TodoReaderService({
|
||||
logger,
|
||||
todoReader,
|
||||
catalogClient,
|
||||
});
|
||||
|
||||
@@ -32,9 +32,9 @@ export class TodoScmReader implements TodoReader {
|
||||
this.reader = options.reader;
|
||||
}
|
||||
|
||||
async readTodos(_options: ReadTodosOptions): Promise<ReadTodosResult> {
|
||||
async readTodos({ url }: ReadTodosOptions): Promise<ReadTodosResult> {
|
||||
return {
|
||||
items: [{ text: 'My mock todo' }],
|
||||
items: [{ text: 'My mock todo', viewUrl: url }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,40 +14,135 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { InputError, NotFoundError } from '@backstage/backend-common';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
LOCATION_ANNOTATION,
|
||||
SOURCE_LOCATION_ANNOTATION,
|
||||
serializeEntityRef,
|
||||
Entity,
|
||||
parseLocationReference,
|
||||
} from '@backstage/catalog-model';
|
||||
import { TodoReader } from '../lib';
|
||||
import { ListTodosRequest, ListTodosResponse, TodoService } from './types';
|
||||
|
||||
const DEFAULT_DEFAULT_PAGE_SIZE = 10;
|
||||
const DEFAULT_MAX_PAGE_SIZE = 50;
|
||||
|
||||
type Options = {
|
||||
logger: Logger;
|
||||
todoReader: TodoReader;
|
||||
catalogClient: CatalogClient;
|
||||
maxPageSize?: number;
|
||||
defaultPageSize?: number;
|
||||
};
|
||||
|
||||
export class TodoReaderService implements TodoService {
|
||||
private readonly logger: Logger;
|
||||
private readonly todoReader: TodoReader;
|
||||
private readonly catalogClient: CatalogClient;
|
||||
private readonly maxPageSize: number;
|
||||
private readonly defaultPageSize: number;
|
||||
|
||||
constructor(options: Options) {
|
||||
this.logger = options.logger;
|
||||
this.todoReader = options.todoReader;
|
||||
this.catalogClient = options.catalogClient;
|
||||
this.maxPageSize = options.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE;
|
||||
this.defaultPageSize = options.defaultPageSize ?? DEFAULT_DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
|
||||
async listTodos(_req: ListTodosRequest): Promise<ListTodosResponse> {
|
||||
const todos = await this.todoReader.readTodos({
|
||||
url: 'https://github.com/backstage/backstage',
|
||||
});
|
||||
async listTodos({
|
||||
entity: entityName,
|
||||
cursor,
|
||||
}: ListTodosRequest): Promise<ListTodosResponse> {
|
||||
if (!entityName) {
|
||||
throw new InputError('entity filter is required to list todos');
|
||||
}
|
||||
const entity = await this.catalogClient.getEntityByName(entityName);
|
||||
if (!entity) {
|
||||
throw new NotFoundError(
|
||||
`Entity not found, ${serializeEntityRef(entityName)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const url = this.getEntitySourceUrl(entity);
|
||||
const todos = await this.todoReader.readTodos({ url });
|
||||
|
||||
const { offset, limit } = this.parseCursor(cursor);
|
||||
return {
|
||||
items: todos.items.slice(0, 10),
|
||||
items: todos.items.slice(offset, offset + limit),
|
||||
totalCount: todos.items.length,
|
||||
cursors: {
|
||||
prev: 'prev',
|
||||
self: 'self',
|
||||
next: 'next',
|
||||
},
|
||||
cursors: this.calculateCursors(offset, limit, todos.items.length),
|
||||
};
|
||||
}
|
||||
|
||||
private getEntitySourceUrl(entity: Entity) {
|
||||
const sourceLocation =
|
||||
entity.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION];
|
||||
if (sourceLocation) {
|
||||
const parsed = parseLocationReference(sourceLocation);
|
||||
if (parsed.type !== 'url') {
|
||||
throw new InputError(
|
||||
`Invalid entity source location type for ${serializeEntityRef(
|
||||
entity,
|
||||
)}, got ${parsed.type}`,
|
||||
);
|
||||
}
|
||||
return parsed.target;
|
||||
}
|
||||
|
||||
const location = entity.metadata.annotations?.[LOCATION_ANNOTATION];
|
||||
if (location) {
|
||||
const parsed = parseLocationReference(location);
|
||||
if (parsed.type !== 'url') {
|
||||
throw new InputError(
|
||||
`Invalid entity source location type for ${serializeEntityRef(
|
||||
entity,
|
||||
)}, got ${parsed.type}`,
|
||||
);
|
||||
}
|
||||
return parsed.target;
|
||||
}
|
||||
throw new InputError(
|
||||
`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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user