From dce98a92f71dbf2733128d27962be7cadfeb7b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 14 Jan 2022 15:06:29 +0100 Subject: [PATCH 1/2] Clear parent entity hashes on deletion, to get healing behaviors back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/green-candles-remember.md | 5 ++ .../src/service/NextEntitiesCatalog.test.ts | 67 +++++++++++++++++++ .../src/service/NextEntitiesCatalog.ts | 23 +++++++ 3 files changed, 95 insertions(+) create mode 100644 .changeset/green-candles-remember.md diff --git a/.changeset/green-candles-remember.md b/.changeset/green-candles-remember.md new file mode 100644 index 0000000000..2edec229e4 --- /dev/null +++ b/.changeset/green-candles-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Now when entities are deleted, the parent entity state is updated such that it will "heal" accidental deletes on the next refresh round. diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts index d0f284c11c..86fabd8955 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts @@ -72,6 +72,8 @@ describe('NextEntitiesCatalog', () => { target_entity_ref: stringifyEntityRef(entity), }); } + + return id; } async function addEntityToSearch(knex: Knex, entity: Entity) { @@ -468,4 +470,69 @@ describe('NextEntitiesCatalog', () => { }, ); }); + + describe('removeEntityByUid', () => { + it.each(databases.eachSupportedId())( + 'also clears parent hashes', + async databaseId => { + const { knex } = await createDatabase(databaseId); + + const grandparent: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'grandparent' }, + spec: {}, + }; + const parent1: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'parent1' }, + spec: {}, + }; + const parent2: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'parent2' }, + spec: {}, + }; + const root: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'root' }, + spec: {}, + }; + const unrelated: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'unrelated' }, + spec: {}, + }; + + await addEntity(knex, grandparent, [{ source: 's' }]); + await addEntity(knex, parent1, [{ entity: grandparent }]); + await addEntity(knex, parent2, [{ entity: grandparent }]); + const uid = await addEntity(knex, root, [ + { entity: parent1 }, + { entity: parent2 }, + ]); + await addEntity(knex, unrelated, []); + await knex('refresh_state').update({ result_hash: 'not-changed' }); + + const catalog = new NextEntitiesCatalog(knex); + await catalog.removeEntityByUid(uid); + + await expect( + knex + .from('refresh_state') + .select('entity_ref', 'result_hash') + .orderBy('entity_ref'), + ).resolves.toEqual([ + { entity_ref: 'k:default/grandparent', result_hash: 'not-changed' }, + { entity_ref: 'k:default/parent1', result_hash: 'child-was-deleted' }, + { entity_ref: 'k:default/parent2', result_hash: 'child-was-deleted' }, + { entity_ref: 'k:default/unrelated', result_hash: 'not-changed' }, + ]); + }, + ); + }); }); diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts index f9b3abf190..28b53cf76d 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts @@ -204,6 +204,29 @@ export class NextEntitiesCatalog implements EntitiesCatalog { } async removeEntityByUid(uid: string): Promise { + // Clear the hashed state of the immediate parents of the deleted entity. + // This makes sure that when they get reprocessed, their output is written + // down again. The reason for wanting to do this, is that if the user + // deletes entities that ARE still emitted by the parent, the parent + // processing will still generate the same output hash as always, which + // means it'll never try to write down the children again (it assumes that + // they already exist). This makes the database never "heal" from accidental + // deletes. + await this.database('refresh_state') + .update({ + result_hash: 'child-was-deleted', + }) + .whereIn('entity_ref', function parents(builder) { + return builder + .from('refresh_state') + .innerJoin('refresh_state_references', { + 'refresh_state_references.target_entity_ref': + 'refresh_state.entity_ref', + }) + .where('refresh_state.entity_id', '=', uid) + .select('refresh_state_references.source_entity_ref'); + }); + await this.database('refresh_state') .where('entity_id', uid) .delete(); From 653d1fe263b10d34a6383f1c5fdad79001b530d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 14 Jan 2022 16:47:27 +0100 Subject: [PATCH 2/2] better formulation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/catalog-backend/src/service/NextEntitiesCatalog.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts index 28b53cf76d..d27a3be95a 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts @@ -210,8 +210,8 @@ export class NextEntitiesCatalog implements EntitiesCatalog { // deletes entities that ARE still emitted by the parent, the parent // processing will still generate the same output hash as always, which // means it'll never try to write down the children again (it assumes that - // they already exist). This makes the database never "heal" from accidental - // deletes. + // they already exist). This means that without the code below, the database + // never "heals" from accidental deletes. await this.database('refresh_state') .update({ result_hash: 'child-was-deleted',