Stop special-casing component, call things 'entity' everywhere

This commit is contained in:
Fredrik Adelöw
2020-05-21 23:11:28 +02:00
parent be3f8c3b52
commit e1c5c6cd58
20 changed files with 147 additions and 141 deletions
+6 -6
View File
@@ -15,13 +15,13 @@
*/
import {
DatabaseItemsCatalog,
createRouter,
DatabaseEntitiesCatalog,
DatabaseLocationsCatalog,
DatabaseManager,
createRouter,
runPeriodically,
LocationReaders,
DescriptorParsers,
LocationReaders,
runPeriodically,
} from '@backstage/plugin-catalog-backend';
import { PluginEnvironment } from '../types';
@@ -35,8 +35,8 @@ export default async function ({ logger, database }: PluginEnvironment) {
10000,
);
const itemsCatalog = new DatabaseItemsCatalog(db);
const entitiesCatalog = new DatabaseEntitiesCatalog(db);
const locationsCatalog = new DatabaseLocationsCatalog(db);
return await createRouter({ itemsCatalog, locationsCatalog, logger });
return await createRouter({ entitiesCatalog, locationsCatalog, logger });
}
@@ -15,18 +15,18 @@
*/
import { Database } from '../database';
import { Component, ItemsCatalog } from './types';
import { EntitiesCatalog, Entity } from './types';
export class DatabaseItemsCatalog implements ItemsCatalog {
export class DatabaseEntitiesCatalog implements EntitiesCatalog {
constructor(private readonly database: Database) {}
async components(): Promise<Component[]> {
const items = await this.database.components();
async entities(): Promise<Entity[]> {
const items = await this.database.entities();
return items;
}
async component(name: string): Promise<Component> {
const item = await this.database.component(name);
async entity(name: string): Promise<Entity> {
const item = await this.database.entity(name);
return item;
}
}
@@ -15,23 +15,23 @@
*/
import { NotFoundError } from '@backstage/backend-common';
import { Component, ItemsCatalog } from './types';
import { EntitiesCatalog, Entity } from './types';
export class StaticItemsCatalog implements ItemsCatalog {
private _components: Component[];
export class StaticEntitiesCatalog implements EntitiesCatalog {
private _entities: Entity[];
constructor(components: Component[]) {
this._components = components;
constructor(entities: Entity[]) {
this._entities = entities;
}
async components(): Promise<Component[]> {
return this._components.slice();
async entities(): Promise<Entity[]> {
return this._entities.slice();
}
async component(name: string): Promise<Component> {
const item = this._components.find(i => i.name === name);
async entity(name: string): Promise<Entity> {
const item = this._entities.find(e => e.name === name);
if (!item) {
throw new NotFoundError(`Found no component with name ${name}`);
throw new NotFoundError(`Found no entity with name ${name}`);
}
return item;
}
+2 -2
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
export * from './DatabaseItemsCatalog';
export * from './DatabaseEntitiesCatalog';
export * from './DatabaseLocationsCatalog';
export * from './StaticItemsCatalog';
export * from './StaticEntitiesCatalog';
export * from './types';
+4 -4
View File
@@ -20,15 +20,15 @@ import * as yup from 'yup';
// Items
//
export type Component = {
export type Entity = {
id: string;
locationId?: string;
name: string;
};
export type ItemsCatalog = {
components(): Promise<Component[]>;
component(id: string): Promise<Component>;
export type EntitiesCatalog = {
entities(): Promise<Entity[]>;
entity(id: string): Promise<Entity>;
};
//
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import { NotFoundError } from '@backstage/backend-common';
import Knex from 'knex';
import { v4 as uuidv4 } from 'uuid';
import { NotFoundError } from '@backstage/backend-common';
import {
AddDatabaseComponent,
AddDatabaseEntity,
AddDatabaseLocation,
DatabaseComponent,
DatabaseEntity,
DatabaseLocation,
DatabaseLocationUpdateLogEvent,
DatabaseLocationUpdateLogStatus,
@@ -29,34 +29,34 @@ import {
export class Database {
constructor(private readonly database: Knex) {}
async addOrUpdateComponent(component: AddDatabaseComponent): Promise<void> {
async addOrUpdateEntity(entity: AddDatabaseEntity): Promise<void> {
await this.database.transaction(async tx => {
// TODO(freben): Currently, several locations can compete for the same component
// TODO(freben): Currently, several locations can compete for the same entity
// TODO(freben): If locationId is unset in the input, it won't be overwritten - should we instead replace with null?
const count = await tx<DatabaseComponent>('components')
.where({ name: component.name })
.update({ ...component });
const count = await tx<DatabaseEntity>('entities')
.where({ name: entity.name })
.update({ ...entity });
if (!count) {
await tx<DatabaseComponent>('components').insert({
...component,
await tx<DatabaseEntity>('entities').insert({
...entity,
id: uuidv4(),
});
}
});
}
async components(): Promise<DatabaseComponent[]> {
return await this.database<DatabaseComponent>('components')
async entities(): Promise<DatabaseEntity[]> {
return await this.database<DatabaseEntity>('entities')
.orderBy('name')
.select();
}
async component(name: string): Promise<DatabaseComponent> {
const items = await this.database<DatabaseComponent>('components')
async entity(name: string): Promise<DatabaseEntity> {
const items = await this.database<DatabaseEntity>('entities')
.where({ name })
.select();
if (!items.length) {
throw new NotFoundError(`Found no component with name ${name}`);
throw new NotFoundError(`Found no entity with name ${name}`);
}
return items[0];
}
@@ -114,7 +114,7 @@ export class Database {
async addLocationUpdateLogEvent(
locationId: string,
status: DatabaseLocationUpdateLogStatus,
componentName?: string,
entityName?: string,
message?: string,
): Promise<void> {
return this.database<DatabaseLocationUpdateLogEvent>(
@@ -123,7 +123,7 @@ export class Database {
id: uuidv4(),
status: status,
location_id: locationId,
component_name: componentName,
entity_name: entityName,
message,
});
}
@@ -34,7 +34,7 @@ describe('DatabaseManager', () => {
describe('refreshLocations', () => {
it('works with no locations added', async () => {
const db = ({
addOrUpdateComponent: jest.fn(),
addOrUpdateEntity: jest.fn(),
locations: jest.fn().mockResolvedValue([]),
} as unknown) as Database;
const reader: LocationReader = {
@@ -53,7 +53,7 @@ describe('DatabaseManager', () => {
it('can update a single location', async () => {
const db = ({
addOrUpdateComponent: jest.fn(),
addOrUpdateEntity: jest.fn(),
locations: jest.fn(() =>
Promise.resolve([
{
@@ -76,9 +76,7 @@ describe('DatabaseManager', () => {
read: jest.fn(() => Promise.resolve([{ type: 'data', data: desc }])),
};
const parser: DescriptorParser = {
parse: jest.fn(() =>
Promise.resolve({ kind: 'Component', component: desc }),
),
parse: jest.fn(() => Promise.resolve(desc)),
};
await expect(
@@ -86,8 +84,8 @@ describe('DatabaseManager', () => {
).resolves.toBeUndefined();
expect(reader.read).toHaveBeenCalledTimes(1);
expect(reader.read).toHaveBeenNthCalledWith(1, 'some', 'thing');
expect(db.addOrUpdateComponent).toHaveBeenCalledTimes(1);
expect(db.addOrUpdateComponent).toHaveBeenNthCalledWith(
expect(db.addOrUpdateEntity).toHaveBeenCalledTimes(1);
expect(db.addOrUpdateEntity).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ locationId: '123', name: 'c1' }),
);
@@ -95,7 +93,7 @@ describe('DatabaseManager', () => {
it('logs successful updates', async () => {
const db = ({
addOrUpdateComponent: jest.fn(),
addOrUpdateEntity: jest.fn(),
locations: jest.fn(() =>
Promise.resolve([
{
@@ -118,9 +116,7 @@ describe('DatabaseManager', () => {
read: jest.fn(() => Promise.resolve([{ type: 'data', data: desc }])),
};
const parser: DescriptorParser = {
parse: jest.fn(() =>
Promise.resolve({ kind: 'Component', component: desc }),
),
parse: jest.fn(() => Promise.resolve(desc)),
};
await expect(
@@ -144,7 +140,7 @@ describe('DatabaseManager', () => {
it('logs unsuccessful updates when parser fails', async () => {
const db = ({
addOrUpdateComponent: jest.fn(),
addOrUpdateEntity: jest.fn(),
locations: jest.fn(() =>
Promise.resolve([
{
@@ -194,7 +190,7 @@ describe('DatabaseManager', () => {
it('logs unsuccessful updates when reader fails', async () => {
const db = ({
addOrUpdateComponent: jest.fn(),
addOrUpdateEntity: jest.fn(),
locations: jest.fn(() =>
Promise.resolve([
{
@@ -19,7 +19,7 @@ import path from 'path';
import { Logger } from 'winston';
import { DescriptorParser, LocationReader, ParserError } from '../ingestion';
import { Database } from './Database';
import { AddDatabaseComponent, DatabaseLocationUpdateLogStatus } from './types';
import { AddDatabaseEntity, DatabaseLocationUpdateLogStatus } from './types';
export class DatabaseManager {
public static async createDatabase(database: Knex): Promise<Database> {
@@ -33,12 +33,12 @@ export class DatabaseManager {
private static async logUpdateSuccess(
database: Database,
locationId: string,
componentName?: string,
entityName?: string,
) {
return database.addLocationUpdateLogEvent(
locationId,
DatabaseLocationUpdateLogStatus.SUCCESS,
componentName,
entityName,
);
}
@@ -46,12 +46,12 @@ export class DatabaseManager {
database: Database,
locationId: string,
error?: Error,
componentName?: string,
entityName?: string,
) {
return database.addLocationUpdateLogEvent(
locationId,
DatabaseLocationUpdateLogStatus.FAIL,
componentName,
entityName,
error?.message,
);
}
@@ -76,32 +76,28 @@ export class DatabaseManager {
logger.debug(readerItem.error);
continue;
}
let parserOutput;
try {
parserOutput = await parser.parse(readerItem.data);
if (parserOutput.kind === 'Component') {
const component = parserOutput.component;
const dbc: AddDatabaseComponent = {
locationId: location.id,
name: component.metadata.name,
};
await database.addOrUpdateComponent(dbc);
await DatabaseManager.logUpdateSuccess(
database,
location.id,
component.metadata.name,
);
}
const entity = await parser.parse(readerItem.data);
const dbc: AddDatabaseEntity = {
locationId: location.id,
name: entity.metadata!.name!,
};
await database.addOrUpdateEntity(dbc);
await DatabaseManager.logUpdateSuccess(
database,
location.id,
entity.metadata!.name,
);
} catch (error) {
let componentName;
let entityName;
if (error instanceof ParserError) {
componentName = error.componentName;
entityName = error.entityName;
}
await DatabaseManager.logUpdateFailure(
database,
location.id,
error,
componentName,
entityName,
);
}
}
@@ -29,23 +29,23 @@ export async function up(knex: Knex): Promise<any> {
.notNullable()
.comment('The actual target of the location');
})
.createTable('components', table => {
table.comment('All components currently stored in the catalog');
table.uuid('id').primary().comment('Auto-generated ID of the component');
.createTable('entities', table => {
table.comment('All entities currently stored in the catalog');
table.uuid('id').primary().comment('Auto-generated ID of the entity');
table
.uuid('locationId')
.references('id')
.inTable('locations')
.nullable()
.comment('The location that originated the component');
.comment('The location that originated the entity');
table
.string('name')
.unique()
.notNullable()
.comment('The external name of the component, as used in references');
.comment('The external name of the entity, as used in references');
});
}
export async function down(knex: Knex): Promise<any> {
return knex.schema.dropTable('components').dropTable('locations');
return knex.schema.dropTable('entities').dropTable('locations');
}
@@ -27,7 +27,7 @@ export async function up(knex: Knex): Promise<any> {
.inTable('locations')
.onUpdate('CASCADE')
.onDelete('CASCADE');
table.string('component_name').notNullable();
table.string('entity_name').notNullable();
});
}
@@ -16,18 +16,18 @@
import * as yup from 'yup';
export type DatabaseComponent = {
export type DatabaseEntity = {
id: string;
locationId?: string;
name: string;
};
export type AddDatabaseComponent = {
export type AddDatabaseEntity = {
locationId?: string;
name: string;
};
export const addDatabaseComponentSchema: yup.Schema<AddDatabaseComponent> = yup
export const addDatabaseEntitySchema: yup.Schema<AddDatabaseEntity> = yup
.object({
locationId: yup.string().optional(),
name: yup.string().required(),
@@ -61,7 +61,7 @@ export type DatabaseLocationUpdateLogEvent = {
id: string;
status: DatabaseLocationUpdateLogStatus;
location_id: string;
component_name: string;
entity_name: string;
created_at?: string;
message?: string;
};
@@ -14,11 +14,14 @@
* limitations under the License.
*/
import { DescriptorEnvelopeParser } from './descriptors/DescriptorEnvelopeParser';
import { ComponentDescriptorV1beta1Parser } from './descriptors/ComponentDescriptorV1beta1Parser';
import { KindParser } from './descriptors/types';
import { DescriptorParser, ParserError, ParserOutput } from './types';
import { makeValidator } from '../validation';
import { ComponentDescriptorV1beta1Parser } from './descriptors/ComponentDescriptorV1beta1Parser';
import {
DescriptorEnvelope,
DescriptorEnvelopeParser,
} from './descriptors/DescriptorEnvelopeParser';
import { KindParser } from './descriptors/types';
import { DescriptorParser, ParserError } from './types';
export class DescriptorParsers implements DescriptorParser {
static create(): DescriptorParser {
@@ -33,7 +36,7 @@ export class DescriptorParsers implements DescriptorParser {
private readonly kindParsers: KindParser[],
) {}
async parse(descriptor: object): Promise<ParserOutput> {
async parse(descriptor: object): Promise<DescriptorEnvelope> {
const envelope = await this.envelopeParser.parse(descriptor);
for (const parser of this.kindParsers) {
const parsed = await parser.tryParse(envelope);
@@ -15,7 +15,7 @@
*/
import * as yup from 'yup';
import { ParserError, ParserOutput } from '../types';
import { ParserError } from '../types';
import { DescriptorEnvelope } from './DescriptorEnvelopeParser';
import { KindParser } from './types';
@@ -48,7 +48,7 @@ export class ComponentDescriptorV1beta1Parser implements KindParser {
async tryParse(
envelope: DescriptorEnvelope,
): Promise<ParserOutput | undefined> {
): Promise<DescriptorEnvelope | undefined> {
if (
envelope.apiVersion !== 'backstage.io/v1beta1' ||
envelope.kind !== 'Component'
@@ -57,10 +57,7 @@ export class ComponentDescriptorV1beta1Parser implements KindParser {
}
try {
return {
kind: 'Component',
component: await this.schema.validate(envelope, { strict: true }),
};
return await this.schema.validate(envelope, { strict: true });
} catch (e) {
throw new ParserError(
`Malformed component, ${e}`,
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { ParserOutput } from '../types';
import { DescriptorEnvelope } from './DescriptorEnvelopeParser';
export type KindParser = {
@@ -30,5 +29,7 @@ export type KindParser = {
* @throws An Error if the type was handled and found to not be properly
* formatted
*/
tryParse(envelope: DescriptorEnvelope): Promise<ParserOutput | undefined>;
tryParse(
envelope: DescriptorEnvelope,
): Promise<DescriptorEnvelope | undefined>;
};
@@ -15,14 +15,10 @@
*/
import { ComponentDescriptorV1beta1 } from './descriptors/ComponentDescriptorV1beta1Parser';
import { DescriptorEnvelope } from './descriptors/DescriptorEnvelopeParser';
export type ComponentDescriptor = ComponentDescriptorV1beta1;
export type ParserOutput = {
kind: 'Component';
component: ComponentDescriptor;
};
export type DescriptorParser = {
/**
* Parses and validates a single raw descriptor.
@@ -31,15 +27,15 @@ export type DescriptorParser = {
* @returns A structure describing the parsed and validated descriptor
* @throws An Error if the descriptor was malformed
*/
parse(descriptor: object): Promise<ParserOutput>;
parse(descriptor: object): Promise<DescriptorEnvelope>;
};
export class ParserError extends Error {
constructor(message?: string, private _componentName?: string | undefined) {
constructor(message?: string, private _entityName?: string | undefined) {
super(message);
}
get componentName() {
return this._componentName;
get entityName() {
return this._entityName;
}
}
+16 -17
View File
@@ -17,11 +17,15 @@
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { addLocationSchema, ItemsCatalog, LocationsCatalog } from '../catalog';
import {
addLocationSchema,
EntitiesCatalog,
LocationsCatalog,
} from '../catalog';
import { validateRequestBody } from './util';
export interface RouterOptions {
itemsCatalog?: ItemsCatalog;
entitiesCatalog?: EntitiesCatalog;
locationsCatalog?: LocationsCatalog;
logger: Logger;
}
@@ -29,21 +33,20 @@ export interface RouterOptions {
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { itemsCatalog, locationsCatalog } = options;
const logger = options.logger.child({ plugin: 'catalog' });
const { entitiesCatalog, locationsCatalog } = options;
const router = Router();
if (itemsCatalog) {
// Components
if (entitiesCatalog) {
// Entities
router
.get('/components', async (_req, res) => {
const components = await itemsCatalog.components();
res.status(200).send(components);
.get('/entities', async (_req, res) => {
const entities = await entitiesCatalog.entities();
res.status(200).send(entities);
})
.get('/components/:id', async (req, res) => {
.get('/entities/:id', async (req, res) => {
const { id } = req.params;
const component = await itemsCatalog.component(id);
res.status(200).send(component);
const entity = await entitiesCatalog.entity(id);
res.status(200).send(entity);
});
}
@@ -71,9 +74,5 @@ export async function createRouter(
});
}
const app = express();
app.set('logger', logger);
app.use('/', router);
return app;
return router;
}
@@ -24,12 +24,12 @@ import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import { Logger } from 'winston';
import { ItemsCatalog, LocationsCatalog } from '../catalog';
import { EntitiesCatalog, LocationsCatalog } from '../catalog';
import { createRouter } from './router';
export interface ApplicationOptions {
enableCors: boolean;
itemsCatalog: ItemsCatalog;
entitiesCatalog: EntitiesCatalog;
locationsCatalog?: LocationsCatalog;
logger: Logger;
}
@@ -37,7 +37,7 @@ export interface ApplicationOptions {
export async function createStandaloneApplication(
options: ApplicationOptions,
): Promise<express.Application> {
const { enableCors, itemsCatalog, locationsCatalog, logger } = options;
const { enableCors, entitiesCatalog, locationsCatalog, logger } = options;
const app = express();
app.use(helmet());
@@ -47,7 +47,10 @@ export async function createStandaloneApplication(
app.use(compression());
app.use(express.json());
app.use(requestLoggingHandler());
app.use('/', await createRouter({ itemsCatalog, locationsCatalog, logger }));
app.use(
'/',
await createRouter({ entitiesCatalog, locationsCatalog, logger }),
);
app.use(notFoundHandler());
app.use(errorHandler());
@@ -16,7 +16,7 @@
import { Server } from 'http';
import { Logger } from 'winston';
import { StaticItemsCatalog } from '../catalog';
import { StaticEntitiesCatalog } from '../catalog';
import { createStandaloneApplication } from './standaloneApplication';
export interface ServerOptions {
@@ -30,15 +30,15 @@ export async function startStandaloneServer(
): Promise<Server> {
const logger = options.logger.child({ service: 'catalog-backend' });
const itemsCatalog = new StaticItemsCatalog([
{ id: '1', name: 'component1' },
{ id: '2', name: 'component2' },
const entitiesCatalog = new StaticEntitiesCatalog([
{ id: '1', name: 'entity1' },
{ id: '2', name: 'entity2' },
]);
logger.debug('Creating application...');
const app = await createStandaloneApplication({
enableCors: options.enableCors,
itemsCatalog,
entitiesCatalog,
logger,
});
@@ -14,9 +14,9 @@
* limitations under the License.
*/
import { Validators } from './types';
import { CommonValidatorFunctions } from './CommonValidatorFunctions';
import { KubernetesValidatorFunctions } from './KubernetesValidatorFunctions';
import { Validators } from './types';
const defaultValidators: Validators = {
isValidApiVersion: KubernetesValidatorFunctions.isValidApiVersion,
+15
View File
@@ -0,0 +1,15 @@
{
"include": ["src"],
"compilerOptions": {
"outDir": "dist",
"incremental": true,
"sourceMap": true,
"declaration": true,
"strict": true,
"target": "es2019",
"module": "commonjs",
"esModuleInterop": true,
"lib": ["es2019"],
"types": ["node", "jest"]
}
}