Individual entity fetches

This commit is contained in:
Fredrik Adelöw
2020-05-26 15:50:31 +02:00
parent 642de90255
commit bfeaf43c71
9 changed files with 180 additions and 44 deletions
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { NotFoundError } from '@backstage/backend-common';
import { Database } from '../database';
import { DescriptorEnvelope } from '../ingestion/types';
import { EntitiesCatalog, EntityFilters } from './types';
@@ -29,17 +28,33 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog {
return items.map(i => i.entity);
}
async entity(
async entityByUid(uid: string): Promise<DescriptorEnvelope | undefined> {
const matches = await this.database.transaction(tx =>
this.database.entities(tx, [{ key: 'uid', values: [uid] }]),
);
return matches.length ? matches[0].entity : undefined;
}
async entityByName(
kind: string,
name: string,
namespace: string | undefined,
): Promise<DescriptorEnvelope | undefined> {
const item = await this.database.transaction(tx =>
this.database.entity(tx, kind, name, namespace),
const matches = await this.database.transaction(tx =>
this.database.entities(tx, [
{ key: 'kind', values: [kind] },
{ key: 'name', values: [name] },
{
key: 'namespace',
values:
!namespace || namespace === 'default'
? [null, 'default']
: [namespace],
},
]),
);
if (!item) {
throw new NotFoundError('Entity cannot be found');
}
return item.entity;
return matches.length ? matches[0].entity : undefined;
}
}
@@ -35,7 +35,7 @@ export class DatabaseLocationsCatalog implements LocationsCatalog {
}
async location(id: string): Promise<Location> {
const item = await this.location(id);
const item = await this.database.location(id);
return item;
}
}
@@ -30,7 +30,15 @@ export class StaticEntitiesCatalog implements EntitiesCatalog {
return lodash.cloneDeep(this._entities);
}
async entity(
async entityByUid(uid: string): Promise<DescriptorEnvelope | undefined> {
const item = this._entities.find(e => uid === e.metadata?.uid);
if (!item) {
throw new NotFoundError('Entity cannot be found');
}
return lodash.cloneDeep(item);
}
async entityByName(
kind: string,
name: string,
namespace: string | undefined,
+3 -2
View File
@@ -29,10 +29,11 @@ export type EntityFilters = EntityFilter[];
export type EntitiesCatalog = {
entities(filters?: EntityFilters): Promise<DescriptorEnvelope[]>;
entity(
entityByUid(uid: string): Promise<DescriptorEnvelope | undefined>;
entityByName(
kind: string,
name: string,
namespace: string | undefined,
name: string,
): Promise<DescriptorEnvelope | undefined>;
};
@@ -21,6 +21,7 @@ import {
} from '@backstage/backend-common';
import Knex from 'knex';
import path from 'path';
import { DescriptorEnvelope } from '../ingestion';
import { Database } from './Database';
import {
AddDatabaseLocation,
@@ -28,7 +29,6 @@ import {
DbEntityResponse,
DbLocationsRow,
} from './types';
import { DescriptorEnvelope } from '../ingestion';
describe('Database', () => {
let database: Knex;
@@ -264,6 +264,7 @@ describe('Database', () => {
{ locationId: undefined, entity: expect.objectContaining(e2) },
]);
});
it('can get all specific entities for matching filters (naive case)', async () => {
const catalog = new Database(database, getVoidLogger());
const entities: DescriptorEnvelope[] = [
@@ -276,7 +276,7 @@ export class Database {
? oldRow.generation
: oldRow.generation + 1;
const newEntity = lodash.cloneDeep(request.entity);
newEntity.metadata = Object.assign({}, request.entity.metadata, {
newEntity.metadata = Object.assign({}, newEntity.metadata, {
uid: oldRow.id,
etag: newEtag,
generation: newGeneration,
@@ -357,9 +357,7 @@ export class Database {
async addLocation(location: AddDatabaseLocation): Promise<DbLocationsRow> {
return await this.database.transaction<DbLocationsRow>(async tx => {
const existingLocation = await tx<DbLocationsRow>('locations')
.where({
target: location.target,
})
.where({ target: location.target })
.select();
if (existingLocation?.[0]) {
@@ -15,6 +15,7 @@
*/
import { getVoidLogger } from '@backstage/backend-common';
import Knex from 'knex';
import {
ComponentDescriptor,
DescriptorParser,
@@ -24,7 +25,6 @@ import {
import { Database } from './Database';
import { DatabaseManager } from './DatabaseManager';
import { DatabaseLocationUpdateLogStatus, DbLocationsRow } from './types';
import Knex from 'knex';
describe('DatabaseManager', () => {
describe('refreshLocations', () => {
@@ -14,11 +14,7 @@
* limitations under the License.
*/
import {
errorHandler,
getVoidLogger,
InputError,
} from '@backstage/backend-common';
import { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { EntitiesCatalog, Location, LocationsCatalog } from '../catalog';
@@ -27,7 +23,8 @@ import { createRouter } from './router';
class MockEntitiesCatalog implements EntitiesCatalog {
entities = jest.fn();
entity = jest.fn();
entityByUid = jest.fn();
entityByName = jest.fn();
}
class MockLocationsCatalog implements LocationsCatalog {
@@ -77,6 +74,89 @@ describe('createRouter', () => {
});
});
describe('entityByUid', () => {
it('can fetch entity by uid', async () => {
const entity: DescriptorEnvelope = {
apiVersion: 'a',
kind: 'b',
metadata: {
name: 'c',
},
};
const catalog = new MockEntitiesCatalog();
catalog.entityByUid.mockResolvedValue(entity);
const router = await createRouter({
entitiesCatalog: catalog,
logger: getVoidLogger(),
});
const app = express().use(router);
const response = await request(app).get('/entities/by-uid/zzz');
expect(response.status).toEqual(200);
expect(response.body).toEqual(expect.objectContaining(entity));
});
it('responds with a 404 for missing entities', async () => {
const catalog = new MockEntitiesCatalog();
catalog.entityByUid.mockResolvedValue(undefined);
const router = await createRouter({
entitiesCatalog: catalog,
logger: getVoidLogger(),
});
const app = express().use(router);
const response = await request(app).get('/entities/by-uid/zzz');
expect(response.status).toEqual(404);
expect(response.text).toMatch(/uid/);
});
});
describe('entityByName', () => {
it('can fetch entity by name', async () => {
const entity: DescriptorEnvelope = {
apiVersion: 'a',
kind: 'b',
metadata: {
name: 'c',
namespace: 'd',
},
};
const catalog = new MockEntitiesCatalog();
catalog.entityByName.mockResolvedValue(entity);
const router = await createRouter({
entitiesCatalog: catalog,
logger: getVoidLogger(),
});
const app = express().use(router);
const response = await request(app).get('/entities/by-name/b/d/c');
expect(response.status).toEqual(200);
expect(response.body).toEqual(expect.objectContaining(entity));
});
it('responds with a 404 for missing entities', async () => {
const catalog = new MockEntitiesCatalog();
catalog.entityByName.mockResolvedValue(undefined);
const router = await createRouter({
entitiesCatalog: catalog,
logger: getVoidLogger(),
});
const app = express().use(router);
const response = await request(app).get('/entities/by-name//b/d/c');
expect(response.status).toEqual(404);
expect(response.text).toMatch(/name/);
});
});
describe('locations', () => {
it('happy path: lists locations', async () => {
const locations: Location[] = [{ id: 'a', type: 'b', target: 'c' }];
+53 -20
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { errorHandler } from '@backstage/backend-common';
import { errorHandler, InputError } from '@backstage/backend-common';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
@@ -41,26 +41,36 @@ export async function createRouter(
router.use(express.json());
if (entitiesCatalog) {
router.get('/entities', async (req, res) => {
const filters: EntityFilters = [];
for (const [key, valueOrValues] of Object.entries(req.query)) {
const values = Array.isArray(valueOrValues)
? valueOrValues
: [valueOrValues];
if (values.some(v => typeof v !== 'string')) {
res.status(400).send('Complex query parameters are not supported');
return;
router
.get('/entities', async (req, res) => {
const filters = translateQueryToEntityFilters(req);
const entities = await entitiesCatalog.entities(filters);
res.status(200).send(entities);
})
.get('/entities/by-uid/:uid', async (req, res) => {
const { uid } = req.params;
const entity = await entitiesCatalog.entityByUid(uid);
if (!entity) {
res.status(404).send(`No entity with uid ${uid}`);
}
filters.push({
key,
values: values.map(v => v || null) as string[],
});
}
const entities = await entitiesCatalog.entities(filters);
res.status(200).send(entities);
});
res.status(200).send(entity);
})
.get('/entities/by-name/:kind/:namespace/:name', async (req, res) => {
const { kind, namespace, name } = req.params;
const entity = await entitiesCatalog.entityByName(
kind,
name,
namespace,
);
if (!entity) {
res
.status(404)
.send(
`No entity with kind ${kind} namespace ${namespace} name ${name}`,
);
}
res.status(200).send(entity);
});
}
if (locationsCatalog) {
@@ -89,3 +99,26 @@ export async function createRouter(
router.use(errorHandler());
return router;
}
function translateQueryToEntityFilters(
request: express.Request,
): EntityFilters {
const filters: EntityFilters = [];
for (const [key, valueOrValues] of Object.entries(request.query)) {
const values = Array.isArray(valueOrValues)
? valueOrValues
: [valueOrValues];
if (values.some(v => typeof v !== 'string')) {
throw new InputError('Complex query parameters are not supported');
}
filters.push({
key,
values: values.map(v => v || null) as string[],
});
}
return filters;
}