todo-backend: full TodoReaderService implementation

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-03-09 22:18:04 +01:00
parent c829e1e983
commit 76e6336312
3 changed files with 111 additions and 17 deletions
-1
View File
@@ -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}`,
};
}
}