Implement location management

This commit is contained in:
Fredrik Adelöw
2020-05-11 16:51:54 +02:00
parent 2955884047
commit d682714249
12 changed files with 257 additions and 83 deletions
+6 -1
View File
@@ -18,11 +18,16 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"helmet": "^3.22.0",
"knex": "^0.21.1",
"morgan": "^1.10.0",
"winston": "^3.2.1"
"uuid": "^8.0.0",
"winston": "^3.2.1",
"yup": "^0.28.5"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.4",
"@types/uuid": "^7.0.3",
"@types/yup": "^0.28.2",
"jest-fetch-mock": "^3.0.3",
"tsc-watch": "^4.2.3"
},
@@ -1,47 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NotFoundError } from '@backstage/backend-common';
import { Component, Inventory } from './types';
export class AggregatorInventory implements Inventory {
#inventories: Inventory[] = [];
async components(): Promise<Component[]> {
const lists = await Promise.all(
this.#inventories.map((i) => i.components()),
);
return lists.flat();
}
component(id: string): Promise<Component> {
return new Promise((resolve, reject) => {
const promises = this.#inventories.map((i) =>
i.component(id).then(resolve, () => {
// For now, just swallow errors in individual inventories;
// should handle partial errors better
}),
);
Promise.all(promises).then(() =>
reject(new NotFoundError(`Found no component with ID ${id}`)),
);
});
}
enlist(inventory: Inventory) {
this.#inventories.push(inventory);
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NotFoundError } from '@backstage/backend-common';
import Knex from 'knex';
import { v4 as uuidv4 } from 'uuid';
import { AddLocationRequest, Component, Inventory, Location } from './types';
export class DatabaseInventory implements Inventory {
constructor(private readonly database: Knex) {}
async components(): Promise<Component[]> {
throw new Error('Not supported');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async component(id: string): Promise<Component> {
throw new Error('Not supported');
}
async addLocation(location: AddLocationRequest): Promise<Location> {
const id = uuidv4();
const { type, target } = location;
await this.database('locations').insert({ id, type, target });
return await this.location(id);
}
async removeLocation(id: string): Promise<void> {
const result = await this.database('locations')
.where({ id })
.del();
if (!result) {
throw new NotFoundError(`Found no location with ID ${id}`);
}
}
async location(id: string): Promise<Location> {
const items = await this.database('locations')
.where({ id })
.select();
if (!items.length) {
throw new NotFoundError(`Found no location with ID ${id}`);
}
return items[0];
}
async locations(): Promise<Location[]> {
return await this.database('locations').select();
}
}
@@ -15,24 +15,49 @@
*/
import { NotFoundError } from '@backstage/backend-common';
import { Component, Inventory } from './types';
import { v4 as uuidv4 } from 'uuid';
import { AddLocationRequest, Component, Inventory, Location } from './types';
export class StaticInventory implements Inventory {
#components: Component[];
private _components: Component[];
private _locations: Location[];
constructor(components: Component[]) {
this.#components = components;
constructor(components: Component[], locations: Location[]) {
this._components = components;
this._locations = locations;
}
async components(): Promise<Component[]> {
return this.#components.slice();
return this._components.slice();
}
async component(id: string): Promise<Component> {
const item = this.#components.find((i) => i.id === id);
const item = this._components.find(i => i.id === id);
if (!item) {
throw new NotFoundError(`Found no component with ID ${id}`);
}
return item;
}
async addLocation(location: AddLocationRequest): Promise<Location> {
const l = { id: uuidv4(), type: location.type, target: location.target };
this._locations.push(l);
return l;
}
async removeLocation(id: string): Promise<void> {
this._locations = this._locations.filter(l => l.id !== id);
}
async location(id: string): Promise<Location> {
const location = this._locations.find(l => l.id === id);
if (!location) {
throw new NotFoundError(`Found no location with ID ${id}`);
}
return location;
}
async locations(): Promise<Location[]> {
return this._locations;
}
}
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export * from './AggregatorInventory';
export * from './DatabaseInventory';
export * from './StaticInventory';
export * from './types';
@@ -14,11 +14,35 @@
* limitations under the License.
*/
import * as yup from 'yup';
export type Component = {
id: string;
};
export type Location = {
id: string;
type: string;
target: string;
};
export type AddLocationRequest = {
type: string;
target: string;
};
export const addLocationRequestShape: yup.Schema<AddLocationRequest> = yup
.object({
type: yup.string().required(),
target: yup.string().required(),
})
.noUnknown();
export type Inventory = {
components(): Promise<Component[]>;
component(id: string): Promise<Component>;
addLocation(location: AddLocationRequest): Promise<Location>;
removeLocation(id: string): Promise<void>;
location(id: string): Promise<Location>;
locations(): Promise<Location[]>;
};
@@ -14,23 +14,51 @@
* limitations under the License.
*/
import express from 'express';
import { Logger } from 'winston';
import { InputError } from '@backstage/backend-common';
import express, { Request } from 'express';
import Router from 'express-promise-router';
import { Inventory } from '../inventory';
import { Logger } from 'winston';
import yup from 'yup';
import { addLocationRequestShape, Inventory } from '../inventory';
export interface RouterOptions {
inventory: Inventory;
logger: Logger;
}
async function validateRequestBody<T>(
req: Request,
schema: yup.Schema<T>,
): Promise<T> {
const contentType = req.header('content-type');
if (!contentType) {
throw new InputError('Content-Type missing');
} else if (!contentType.match(/^application\/json($|;)/)) {
throw new InputError('Illegal Content-Type');
}
const body = req.body;
if (!body) {
throw new InputError('Missing request body');
}
try {
await schema.validate(body, { strict: true });
} catch (e) {
throw new InputError(`Malformed request: ${e}`);
}
return body as T;
}
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const inventory = options.inventory;
const logger = options.logger.child({ plugin: 'inventory' });
const router = Router();
// Components
router
.get('/components', async (req, res) => {
const components = await inventory.components();
@@ -42,6 +70,28 @@ export async function createRouter(
res.status(200).send(component);
});
// Locations
router
.post('/locations', async (req, res) => {
const input = await validateRequestBody(req, addLocationRequestShape);
const output = await inventory.addLocation(input);
res.status(201).send(output);
})
.get('/locations', async (req, res) => {
const output = await inventory.locations();
res.status(200).send(output);
})
.get('/locations/:id', async (req, res) => {
const { id } = req.params;
const output = await inventory.location(id);
res.status(200).send(output);
})
.delete('/locations/:id', async (req, res) => {
const { id } = req.params;
await inventory.removeLocation(id);
res.status(200).send();
});
const app = express();
app.set('logger', logger);
app.use('/', router);
@@ -16,7 +16,7 @@
import { Server } from 'http';
import { Logger } from 'winston';
import { AggregatorInventory, StaticInventory } from '../inventory';
import { StaticInventory } from '../inventory';
import { createStandaloneApplication } from './standaloneApplication';
export interface ServerOptions {
@@ -30,14 +30,14 @@ export async function startStandaloneServer(
): Promise<Server> {
const logger = options.logger.child({ service: 'inventory-backend' });
const inventory = new AggregatorInventory();
inventory.enlist(
new StaticInventory([
const inventory = new StaticInventory(
[
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]),
],
[],
);
logger.debug('Creating application...');