diff --git a/plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js b/plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js index 755a645894..bbaf4820cd 100644 --- a/plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js +++ b/plugins/catalog-backend/migrationsv2/20210302150147_refresh_state.js @@ -160,7 +160,7 @@ exports.up = async function up(knex) { 'Flattened key-values from the entities, used for quick filtering', ); table - .uuid('entity_id') + .text('entity_id') .references('entity_id') .inTable('refresh_state') .onDelete('CASCADE') diff --git a/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts new file mode 100644 index 0000000000..be20179e9a --- /dev/null +++ b/plugins/catalog-backend/src/next/DefaultLocationStore.test.ts @@ -0,0 +1,146 @@ +/* + * Copyright 2021 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 { DatabaseManager } from './database/DatabaseManager'; +import { DefaultLocationStore } from './DefaultLocationStore'; +import { v4 } from 'uuid'; + +describe('Default Location Store', () => { + 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 }; + }; + + it('should do a full sync with the locations on connect', async () => { + const { connection } = await createLocationStore(); + + expect(connection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [], + }); + }); + + 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', + type: 'url', + }); + + const listLocations = await store.listLocations(); + + expect(listLocations).toHaveLength(1); + expect(listLocations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + target: + 'https://github.com/backstage/demo/blob/master/catalog-info.yml', + type: 'url', + }), + ]), + ); + }); + }); + + describe('createLocation', () => { + it('throws when the location already exists', async () => { + const { store } = await createLocationStore(); + const spec = { + target: + 'https://github.com/backstage/demo/blob/master/catalog-info.yml', + type: 'url', + }; + await store.createLocation(spec); + + await expect(() => store.createLocation(spec)).rejects.toThrow( + new RegExp(`Location ${spec.type}:${spec.target} already exists`), + ); + }); + + 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', + type: 'url', + }); + + expect(connection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + removed: [], + added: expect.arrayContaining([ + expect.objectContaining({ + spec: { + target: + 'https://github.com/backstage/demo/blob/master/catalog-info.yml', + type: 'url', + }, + }), + ]), + }); + }); + }); + + describe('deleteLocation', () => { + it('throws if the location does not exist', async () => { + const { store } = await createLocationStore(); + + const id = v4(); + + await expect(() => store.deleteLocation(id)).rejects.toThrow( + new RegExp(`Found no location with ID ${id}`), + ); + }); + + it('calls apply mutation when adding a new location', async () => { + const { store, connection } = await createLocationStore(); + + const location = await store.createLocation({ + target: + 'https://github.com/backstage/demo/blob/master/catalog-info.yml', + type: 'url', + }); + + await store.deleteLocation(location.id); + + expect(connection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [], + removed: expect.arrayContaining([ + expect.objectContaining({ + spec: { + target: + 'https://github.com/backstage/demo/blob/master/catalog-info.yml', + type: 'url', + }, + }), + ]), + }); + }); + }); +}); diff --git a/plugins/catalog-backend/src/next/DefaultLocationStore.ts b/plugins/catalog-backend/src/next/DefaultLocationStore.ts index a56e5d1585..28db3790b6 100644 --- a/plugins/catalog-backend/src/next/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/next/DefaultLocationStore.ts @@ -34,10 +34,8 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { return 'DefaultLocationStore'; } - createLocation(spec: LocationSpec): Promise { + async createLocation(spec: LocationSpec): Promise { return this.db.transaction(async tx => { - // TODO: id should really be type and target combined and not a uuid. - // Attempt to find a previous location matching the spec const previousLocations = await this.listLocations(); const previousLocation = previousLocations.some( @@ -50,6 +48,7 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { ); } + // TODO: id should really be type and target combined and not a uuid. const location = await this.db.addLocation(tx, { id: uuidv4(), type: spec.type, @@ -68,11 +67,17 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { async listLocations(): Promise { const dbLocations = await this.db.locations(); - return dbLocations.map(item => ({ - id: item.id, - target: item.target, - type: item.type, - })); + 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, + })) + ); } getLocation(id: string): Promise { @@ -86,9 +91,6 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { return this.db.transaction(async tx => { const location = await this.db.location(id); - if (!location) { - throw new ConflictError(`No location found with id: ${id}`); - } await this.db.removeLocation(tx, id); await this.connection.applyMutation({ type: 'delta', @@ -108,7 +110,7 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { async connect(connection: EntityProviderConnection): Promise { this._connection = connection; - const locations = await this.db.locations(); + const locations = await this.listLocations(); const entities = locations.map(location => { return locationSpecToLocationEntity(location); });