feat: location update results

This commit is contained in:
Ivan Shmidt
2020-05-26 16:40:41 +02:00
parent fdb2241ad0
commit 9ca8f8355f
8 changed files with 181 additions and 21 deletions
@@ -14,9 +14,15 @@
* limitations under the License.
*/
import { Database } from '../database';
import { AddLocation, Location, LocationsCatalog } from './types';
import { LocationReader } from '../ingestion';
import { Database, DatabaseLocationUpdateLogEvent } from '../database';
import {
AddLocation,
LocationEnvelope,
Location,
LocationsCatalog,
} from './types';
export class DatabaseLocationsCatalog implements LocationsCatalog {
constructor(
@@ -42,13 +48,36 @@ export class DatabaseLocationsCatalog implements LocationsCatalog {
await this.database.removeLocation(id);
}
async locations(): Promise<Location[]> {
async locations(): Promise<LocationEnvelope[]> {
const items = await this.database.locations();
return items;
return items.map(({ message, status, timestamp, ...data }) => ({
lastUpdate: {
message,
status,
timestamp,
},
data,
}));
}
async location(id: string): Promise<Location> {
const item = await this.location(id);
return item;
async locationHistory(id: string): Promise<DatabaseLocationUpdateLogEvent[]> {
return this.database.locationHistory(id);
}
async location(id: string): Promise<LocationEnvelope> {
const {
message,
status,
timestamp,
...data
} = await this.database.location(id);
return {
lastUpdate: {
message,
status,
timestamp,
},
data,
};
}
}
+15 -2
View File
@@ -16,6 +16,7 @@
import * as yup from 'yup';
import { DescriptorEnvelope } from '../ingestion';
import { DatabaseLocationUpdateLogEvent } from '../database';
//
// Items
@@ -34,12 +35,23 @@ export type EntitiesCatalog = {
// Locations
//
export type status = {
timestamp: DatabaseLocationUpdateLogEvent['created_at'] | null;
status: DatabaseLocationUpdateLogEvent['status'] | null;
message: DatabaseLocationUpdateLogEvent['message'] | null;
};
export type Location = {
id: string;
type: string;
target: string;
};
export type LocationEnvelope = {
data: Location;
lastUpdate: status;
};
export type AddLocation = {
type: string;
target: string;
@@ -55,6 +67,7 @@ export const addLocationSchema: yup.Schema<AddLocation> = yup
export type LocationsCatalog = {
addLocation(location: AddLocation): Promise<Location>;
removeLocation(id: string): Promise<void>;
locations(): Promise<Location[]>;
location(id: string): Promise<Location>;
locations(): Promise<LocationEnvelope[]>;
location(id: string): Promise<LocationEnvelope>;
locationHistory(id: string): Promise<DatabaseLocationUpdateLogEvent[]>;
};
@@ -27,6 +27,8 @@ import {
DbEntityRequest,
DbEntityResponse,
DbLocationsRow,
DbLocationsRowWithStatus,
DatabaseLocationUpdateLogStatus,
} from './types';
describe('Database', () => {
@@ -73,7 +75,10 @@ describe('Database', () => {
name: 'c',
namespace: 'd',
labels: { e: 'f' },
annotations: { g: 'h' },
annotations: {
g: 'h',
'backstage.io/managed-by-location': undefined,
},
},
spec: { i: 'j' },
},
@@ -83,10 +88,13 @@ describe('Database', () => {
it('manages locations', async () => {
const db = new Database(database, getVoidLogger());
const input: AddDatabaseLocation = { type: 'a', target: 'b' };
const output: DbLocationsRow = {
const output: DbLocationsRowWithStatus = {
id: expect.anything(),
type: 'a',
target: 'b',
message: null,
status: null,
timestamp: null,
};
await db.addLocation(input);
@@ -117,7 +125,7 @@ describe('Database', () => {
// Output is the same
expect(output2).toEqual(output1);
// Locations contain only one record
expect(locations).toEqual([output1]);
expect(locations[0]).toMatchObject(output1);
});
describe('addEntity', () => {
@@ -149,6 +157,45 @@ describe('Database', () => {
});
});
describe('locationHistory', () => {
it('outputs the history correctly', async () => {
const catalog = new Database(database, getVoidLogger());
const location: AddDatabaseLocation = { type: 'a', target: 'b' };
const { id: locationId } = await catalog.addLocation(location);
await catalog.addLocationUpdateLogEvent(
locationId,
DatabaseLocationUpdateLogStatus.SUCCESS,
);
await catalog.addLocationUpdateLogEvent(
locationId,
DatabaseLocationUpdateLogStatus.FAIL,
undefined,
'Something went wrong',
);
const result = await catalog.locationHistory(locationId);
expect(result).toEqual([
{
created_at: expect.anything(),
entity_name: null,
id: expect.anything(),
location_id: locationId,
message: null,
status: DatabaseLocationUpdateLogStatus.SUCCESS,
},
{
created_at: expect.anything(),
entity_name: null,
id: expect.anything(),
location_id: locationId,
message: 'Something went wrong',
status: DatabaseLocationUpdateLogStatus.FAIL,
},
]);
});
});
describe('updateEntity', () => {
it('can read and no-op-update an entity', async () => {
const catalog = new Database(database, getVoidLogger());
@@ -34,6 +34,7 @@ import {
DbEntityRequest,
DbEntityResponse,
DbLocationsRow,
DbLocationsRowWithStatus,
} from './types';
function getStrippedMetadata(metadata: EntityMeta): EntityMeta {
@@ -190,6 +191,10 @@ export class Database {
uid: generateUid(),
etag: generateEtag(),
generation: 1,
annotations: {
...(newEntity.metadata?.annotations ?? {}),
'backstage.io/managed-by-location': request.locationId,
},
});
const newRow = toEntityRow(request.locationId, newEntity);
@@ -367,18 +372,65 @@ export class Database {
}
}
async location(id: string): Promise<DbLocationsRow> {
const items = await this.database<DbLocationsRow>('locations')
.where({ id })
.select();
async location(id: string): Promise<DbLocationsRowWithStatus> {
const items = await this.database<DbLocationsRowWithStatus>('locations')
.where('locations.id', id)
.leftJoin(
'location_update_log',
'locations.id',
'location_update_log.location_id',
)
.orderBy('location_update_log.created_at', 'desc')
.select({
status: 'location_update_log.status',
timestamp: 'location_update_log.created_at',
message: 'location_update_log.message',
id: 'locations.id',
type: 'locations.type',
target: 'locations.target',
});
if (!items.length) {
throw new NotFoundError(`Found no location with ID ${id}`);
}
return items[0];
}
async locations(): Promise<DbLocationsRow[]> {
return this.database<DbLocationsRow>('locations').select();
async locations(): Promise<DbLocationsRowWithStatus[]> {
const query = this.database
.select({
status: 'location_update_log.status',
timestamp: 'location_update_log.created_at',
message: 'location_update_log.message',
id: 'locations.id',
type: 'locations.type',
target: 'locations.target',
})
.from('locations')
.leftJoin(
'location_update_log',
'locations.id',
'location_update_log.location_id',
)
.orderBy('location_update_log.created_at', 'desc');
const result = await this.database<DbLocationsRowWithStatus>(query)
.select()
.groupBy('id');
return result;
}
async locationHistory(id: string): Promise<DatabaseLocationUpdateLogEvent[]> {
const result = await this.database<DatabaseLocationUpdateLogEvent>(
'location_update_log',
)
.where('location_id', id)
.orderBy('created_at', 'desc')
.limit(10)
.select();
return result;
}
async addLocationUpdateLogEvent(
@@ -391,7 +443,7 @@ export class Database {
'location_update_log',
).insert({
id: uuidv4(),
status: status,
status,
location_id: locationId,
entity_name: entityName,
message,
@@ -23,7 +23,11 @@ import {
} from '../ingestion';
import { Database } from './Database';
import { DatabaseManager } from './DatabaseManager';
import { DatabaseLocationUpdateLogStatus, DbLocationsRow } from './types';
import {
DatabaseLocationUpdateLogStatus,
DbLocationsRow,
DbLocationsRowWithStatus,
} from './types';
import Knex from 'knex';
describe('DatabaseManager', () => {
@@ -47,10 +51,13 @@ describe('DatabaseManager', () => {
});
it('can update a single location', async () => {
const location: DbLocationsRow = {
const location: DbLocationsRowWithStatus = {
id: '123',
type: 'some',
target: 'thing',
message: '',
status: DatabaseLocationUpdateLogStatus.SUCCESS,
timestamp: new Date(314159265).toISOString(),
};
const desc: ComponentDescriptor = {
apiVersion: 'backstage.io/v1beta1',
@@ -58,6 +65,7 @@ describe('DatabaseManager', () => {
metadata: { name: 'c1' },
spec: { type: 'service' },
};
const tx = (undefined as unknown) as Knex.Transaction<any, any>;
const db = ({
@@ -52,6 +52,12 @@ export type DbLocationsRow = {
target: string;
};
export type DbLocationsRowWithStatus = DbLocationsRow & {
status: DatabaseLocationUpdateLogEvent['status'] | null;
timestamp: DatabaseLocationUpdateLogEvent['created_at'] | null;
message: DatabaseLocationUpdateLogEvent['message'] | null;
};
export type AddDatabaseLocation = {
type: string;
target: string;
@@ -79,7 +79,7 @@ export type EntityMeta = {
* Key/value pairs of non-identifying auxiliary information attached to the
* entity.
*/
annotations?: Record<string, string>;
annotations?: Record<string, string | undefined>;
};
/**
@@ -56,6 +56,11 @@ export async function createRouter(
const output = await locationsCatalog.locations();
res.status(200).send(output);
})
.get('/locations/:id/history', async (req, res) => {
const { id } = req.params;
const output = await locationsCatalog.locationHistory(id);
res.status(200).send(output);
})
.get('/locations/:id', async (req, res) => {
const { id } = req.params;
const output = await locationsCatalog.location(id);