feat: location update results
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user