Implement location management
This commit is contained in:
@@ -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...');
|
||||
|
||||
Reference in New Issue
Block a user