diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts index e1bd8ec386..d1cd6e7c5e 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts +++ b/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts @@ -15,7 +15,10 @@ */ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; -import { ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model'; +import { + ANNOTATION_ORIGIN_LOCATION, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { v4 as uuid } from 'uuid'; import { applyDatabaseMigrations } from '../database/migrations'; import { @@ -25,6 +28,7 @@ import { DbSearchRow, } from '../database/tables'; import { DefaultLocationStore } from './DefaultLocationStore'; +import { locationSpecToLocationEntity } from '../util/conversion'; import { CatalogScmEventsServiceSubscriber } from '@backstage/plugin-catalog-node/alpha'; import waitFor from 'wait-for-expect'; @@ -154,6 +158,48 @@ describe('DefaultLocationStore', () => { }); }, ); + + it.each(databases.eachSupportedId())( + 'updates refresh_state when onConflict is refresh, %p', + async databaseId => { + const { store, knex } = await createLocationStore(databaseId); + const spec = { + type: 'url', + target: + 'https://github.com/backstage/demo/blob/master/catalog-info.yml', + }; + + // Create the location initially + await store.createLocation(spec); + + // Seed a refresh_state row for the corresponding Location entity + const entity = locationSpecToLocationEntity({ location: spec }); + const entityRef = stringifyEntityRef(entity); + const entityId = uuid(); + const oldDate = new Date('2020-01-01T00:00:00Z'); + await knex('refresh_state').insert({ + entity_id: entityId, + entity_ref: entityRef, + unprocessed_entity: '{}', + errors: '[]', + next_update_at: oldDate, + last_discovery_at: oldDate, + result_hash: 'old-hash', + }); + + // Re-register the same location with onConflict: 'refresh' + await store.createLocation(spec, { onConflict: 'refresh' }); + + // Verify that the refresh_state row was updated + const [row] = await knex('refresh_state').where({ + entity_ref: entityRef, + }); + expect(row.result_hash).toBe(''); + expect(new Date(row.next_update_at).getTime()).toBeGreaterThan( + oldDate.getTime(), + ); + }, + ); }); describe('deleteLocation', () => { diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts index ab9ffdefbb..8619d13459 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts @@ -105,23 +105,25 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { return inner; }); + // Always upsert the entity, even if the location already existed, to + // recover from cases where the entity was inadvertently deleted. + const entity = locationSpecToLocationEntity({ location }); + await this.connection.applyMutation({ + type: 'delta', + added: [{ entity, locationKey: getEntityLocationRef(entity) }], + removed: [], + }); + if (existed) { - const entityRef = stringifyEntityRef( - locationSpecToLocationEntity({ location }), - ); + // This is the "onConflict refresh" case, where a re-registration safely + // tries to recover from a bad state. + const entityRef = stringifyEntityRef(entity); await this.db('refresh_state') .where({ entity_ref: entityRef }) .update({ next_update_at: this.db.fn.now(), result_hash: '', }); - } else { - const entity = locationSpecToLocationEntity({ location }); - await this.connection.applyMutation({ - type: 'delta', - added: [{ entity, locationKey: getEntityLocationRef(entity) }], - removed: [], - }); } return location;