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
+5 -4
View File
@@ -39,13 +39,14 @@ import { PluginEnvironment } from './types';
const DEFAULT_PORT = 7000;
const PORT = parseInt(process.env.PORT ?? '', 10) || DEFAULT_PORT;
const pluginEnvironment: PluginEnvironment = {
logger: getRootLogger().child({ type: 'plugin' }),
};
async function main() {
const database = await buildDatabase(getRootLogger());
console.log(await database.select().from('locations'));
const pluginEnvironment: PluginEnvironment = {
logger: getRootLogger().child({ type: 'plugin' }),
database,
};
const app = express();
+4 -14
View File
@@ -15,22 +15,12 @@
*/
import {
AggregatorInventory,
DatabaseInventory,
createRouter,
StaticInventory,
} from '@backstage/plugin-inventory-backend';
import type { PluginEnvironment } from '../types';
export default async function ({ logger }: PluginEnvironment) {
const inventory = new AggregatorInventory();
inventory.enlist(
new StaticInventory([
{ id: 'component1' },
{ id: 'component2' },
{ id: 'component3' },
{ id: 'component4' },
]),
);
import { PluginEnvironment } from '../types';
export default async function({ logger, database }: PluginEnvironment) {
const inventory = new DatabaseInventory(database);
return await createRouter({ inventory, logger });
}
+3 -1
View File
@@ -14,8 +14,10 @@
* limitations under the License.
*/
import type { Logger } from 'winston';
import Knex from 'knex';
import { Logger } from 'winston';
export type PluginEnvironment = {
logger: Logger;
database: Knex;
};
+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...');
+60
View File
@@ -956,6 +956,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.9.6":
version "7.9.6"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.4.0", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@@ -4542,6 +4549,11 @@
dependencies:
source-map "^0.6.1"
"@types/uuid@^7.0.3":
version "7.0.3"
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.3.tgz#45cd03e98e758f8581c79c535afbd4fc27ba7ac8"
integrity sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw==
"@types/webpack-dev-server@*", "@types/webpack-dev-server@^3.10.0":
version "3.10.1"
resolved "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.10.1.tgz#93b7133cc9dab1ca1b76659f5ef8b763ad54c28a"
@@ -4598,6 +4610,11 @@
dependencies:
"@types/yargs-parser" "*"
"@types/yup@^0.28.2":
version "0.28.2"
resolved "https://registry.npmjs.org/@types/yup/-/yup-0.28.2.tgz#ea6597d7bd48ecea94b81f8de7fd60fba07a4628"
integrity sha512-VvXPMsvUnkDIWHFIalJROljLSv9m9TaXKng5sLFUkmuybgY64AeBbZ5tOPZ9sjSpqhJFeOXIc22JSQDO6SCONg==
"@types/zen-observable@^0.8.0":
version "0.8.0"
resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
@@ -9907,6 +9924,11 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
fn-name@~3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/fn-name/-/fn-name-3.0.0.tgz#0596707f635929634d791f452309ab41558e3c5c"
integrity sha512-eNMNr5exLoavuAMhIUVsOKF79SWd/zG104ef6sxBTSw+cZc6BXdQXDvYcGvp0VbxVVSp1XDUNoz7mg1xMtSznA==
focus-lock@^0.6.6:
version "0.6.6"
resolved "https://registry.npmjs.org/focus-lock/-/focus-lock-0.6.6.tgz#98119a755a38cfdbeda0280eaa77e307eee850c7"
@@ -13910,6 +13932,11 @@ lockfile@^1.0.4:
dependencies:
signal-exit "^3.0.2"
lodash-es@^4.17.11:
version "4.17.15"
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -17287,6 +17314,11 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0,
object-assign "^4.1.1"
react-is "^16.8.1"
property-expr@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.2.tgz#fff2a43919135553a3bc2fdd94bdb841965b2330"
integrity sha512-bc/5ggaYZxNkFKj374aLbEDqVADdYaLcFo8XBkishUWbaAdjlphaBFns9TvRA2pUseVL/wMFmui9X3IdNDU37g==
property-information@^5.0.0:
version "5.4.0"
resolved "https://registry.npmjs.org/property-information/-/property-information-5.4.0.tgz#16e08f13f4e5c4a7be2e4ec431c01c4f8dba869a"
@@ -20186,6 +20218,11 @@ symbol.prototype.description@^1.0.0:
es-abstract "^1.17.0-next.1"
has-symbols "^1.0.1"
synchronous-promise@^2.0.10:
version "2.0.11"
resolved "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.11.tgz#e92022b0754e916f556d3ace1626d24a24214b7e"
integrity sha512-8/L5FOCjnlK0FoAfj+NqdCaImMKvEyOEzGmdfcezKp5K9HIukm4akx72endvM87eS/gU8kOxiMQflpC/CdkQAg==
table@^5.2.3:
version "5.4.6"
resolved "https://registry.npmjs.org/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@@ -20580,6 +20617,11 @@ toposort@^1.0.0:
resolved "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk=
toposort@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@@ -21220,6 +21262,11 @@ uuid@^7.0.3:
resolved "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
uuid@^8.0.0:
version "8.0.0"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"
integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==
v8-compile-cache@^2.0.3:
version "2.1.0"
resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"
@@ -22199,6 +22246,19 @@ yn@3.1.1:
resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
yup@^0.28.5:
version "0.28.5"
resolved "https://registry.npmjs.org/yup/-/yup-0.28.5.tgz#85cabb4000d3623ef69be81551190692e631a8a5"
integrity sha512-7JZcvpUGUxMKoaEtcoMEM8lCWRaueGNH/A3EhL/UWqfbFm3uloiI+x59Yq4nzhbbYWUTwAsCteaZOJ+VbqI1uw==
dependencies:
"@babel/runtime" "^7.9.6"
fn-name "~3.0.0"
lodash "^4.17.15"
lodash-es "^4.17.11"
property-expr "^2.0.2"
synchronous-promise "^2.0.10"
toposort "^2.0.2"
zen-observable@^0.8.15:
version "0.8.15"
resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"