diff --git a/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts index f9bb97477c..88806d0ace 100644 --- a/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts +++ b/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts @@ -17,16 +17,17 @@ import { v4 as uuid } from 'uuid'; import { DatabaseManager } from './database/DatabaseManager'; import { DefaultLocationStore } from './DefaultLocationStore'; -/* eslint-disable */ -xdescribe('DefaultLocationStore', () => { - const createLocationStore = async () => { - const db = await DatabaseManager.createTestDatabase(); - const connection = { applyMutation: jest.fn() }; - const store = new DefaultLocationStore(db); - await store.connect(connection); - return { store, connection }; - }; +const createLocationStore = async () => { + const knex = await DatabaseManager.createTestDatabaseConnection(); + await DatabaseManager.createDatabase(knex); + const connection = { applyMutation: jest.fn() }; + const store = new DefaultLocationStore(knex); + await store.connect(connection); + return { store, connection }; +}; + +describe('DefaultLocationStore', () => { it('should do a full sync with the locations on connect', async () => { const { connection } = await createLocationStore(); @@ -39,13 +40,11 @@ xdescribe('DefaultLocationStore', () => { describe('listLocations', () => { it('lists empty locations when there is no locations', async () => { const { store } = await createLocationStore(); - expect(await store.listLocations()).toEqual([]); }); it('lists locations that are added to the db', async () => { const { store } = await createLocationStore(); - await store.createLocation({ target: 'https://github.com/backstage/demo/blob/master/catalog-info.yml', @@ -53,7 +52,6 @@ xdescribe('DefaultLocationStore', () => { }); const listLocations = await store.listLocations(); - expect(listLocations).toHaveLength(1); expect(listLocations).toEqual( expect.arrayContaining([ @@ -76,7 +74,6 @@ xdescribe('DefaultLocationStore', () => { type: 'url', }; await store.createLocation(spec); - await expect(() => store.createLocation(spec)).rejects.toThrow( new RegExp(`Location ${spec.type}:${spec.target} already exists`), ); @@ -84,7 +81,6 @@ xdescribe('DefaultLocationStore', () => { it('calls apply mutation when adding a new location', async () => { const { store, connection } = await createLocationStore(); - await store.createLocation({ target: 'https://github.com/backstage/demo/blob/master/catalog-info.yml', @@ -110,9 +106,7 @@ xdescribe('DefaultLocationStore', () => { describe('deleteLocation', () => { it('throws if the location does not exist', async () => { const { store } = await createLocationStore(); - const id = uuid(); - await expect(() => store.deleteLocation(id)).rejects.toThrow( new RegExp(`Found no location with ID ${id}`), ); diff --git a/plugins/catalog-backend/src/next/DefaultLocationStore.ts b/plugins/catalog-backend/src/next/DefaultLocationStore.ts index f6267df6a5..fe1eb30467 100644 --- a/plugins/catalog-backend/src/next/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/next/DefaultLocationStore.ts @@ -15,7 +15,6 @@ */ import { LocationSpec, Location } from '@backstage/catalog-model'; -import { Database } from '../database'; import { LocationStore, EntityProvider, @@ -23,80 +22,96 @@ import { } from './types'; import { v4 as uuid } from 'uuid'; import { locationSpecToLocationEntity } from './util'; -import { ConflictError } from '@backstage/errors'; +import { ConflictError, NotFoundError } from '@backstage/errors'; +import { Knex } from 'knex'; + +type DbLocationsRow = { + id: string; + type: string; + target: string; +}; export class DefaultLocationStore implements LocationStore, EntityProvider { private _connection: EntityProviderConnection | undefined; - constructor(private readonly db: Database) {} + constructor(private readonly db: Knex) {} getProviderName(): string { return 'DefaultLocationStore'; } async createLocation(spec: LocationSpec): Promise { - return this.db.transaction(async tx => { + const location = await this.db.transaction(async tx => { // Attempt to find a previous location matching the spec - const previousLocations = await this.listLocations(); + const previousLocations = await this.locations(tx); + // TODO: when location id's are a compilation of spec target we can remove this full + // lookup of locations first and just grab the by that instead. const previousLocation = previousLocations.some( l => spec.type === l.type && spec.target === l.target, ); - if (previousLocation) { throw new ConflictError( `Location ${spec.type}:${spec.target} already exists`, ); } - // TODO: id should really be type and target combined and not a uuid. - const location = await this.db.addLocation(tx, { + const inner: DbLocationsRow = { id: uuid(), type: spec.type, target: spec.target, - }); + }; - await this.connection.applyMutation({ - type: 'delta', - added: [locationSpecToLocationEntity(location)], - removed: [], - }); + await tx('locations').insert(inner); - return location; + return inner; }); + + await this.connection.applyMutation({ + type: 'delta', + added: [locationSpecToLocationEntity(location)], + removed: [], + }); + + return location; } async listLocations(): Promise { - const dbLocations = await this.db.locations(); - return ( - dbLocations - // TODO(blam): We should create a mutation to remove this location for everyone - // eventually when it's all done and dusted - .filter(({ type }) => type !== 'bootstrap') - .map(item => ({ - id: item.id, - target: item.target, - type: item.type, - })) - ); + return await this.locations(); } - getLocation(id: string): Promise { - return this.db.location(id); + async getLocation(id: string): Promise { + const items = await this.db('locations') + .where({ id }) + .select(); + + if (!items.length) { + throw new NotFoundError(`Found no location with ID ${id}`); + } + return items[0]; } - deleteLocation(id: string): Promise { + async deleteLocation(id: string): Promise { if (!this.connection) { throw new Error('location store is not initialized'); } - return this.db.transaction(async tx => { - const location = await this.db.location(id); - await this.db.removeLocation(tx, id); - await this.connection.applyMutation({ - type: 'delta', - added: [], - removed: [locationSpecToLocationEntity(location)], - }); + const deleted = await this.db.transaction(async tx => { + const [location] = await tx('locations') + .where({ id }) + .select(); + + if (!location) { + throw new NotFoundError(`Found no location with ID ${id}`); + } + + await tx('locations').where({ id }).del(); + return location; + }); + + await this.connection.applyMutation({ + type: 'delta', + added: [], + removed: [locationSpecToLocationEntity(deleted)], }); } @@ -110,13 +125,31 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { async connect(connection: EntityProviderConnection): Promise { this._connection = connection; - const locations = await this.listLocations(); + + const locations = await this.locations(); + const entities = locations.map(location => { return locationSpecToLocationEntity(location); }); + await this.connection.applyMutation({ type: 'full', entities, }); } + + private async locations(dbOrTx: Knex.Transaction | Knex = this.db) { + const locations = await dbOrTx('locations').select(); + return ( + locations + // TODO(blam): We should create a mutation to remove this location for everyone + // eventually when it's all done and dusted + .filter(({ type }) => type !== 'bootstrap') + .map(item => ({ + id: item.id, + target: item.target, + type: item.type, + })) + ); + } } diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index 0158cc9fc9..92f3822055 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -261,7 +261,7 @@ export class NextCatalogBuilder { }); const entitiesCatalog = new NextEntitiesCatalog(dbClient); - const locationStore = new DefaultLocationStore(db); + const locationStore = new DefaultLocationStore(dbClient); const stitcher = new Stitcher(dbClient, logger); const configLocationProvider = new ConfigLocationEntityProvider(config); const processingEngine = new DefaultCatalogProcessingEngine(