Provide a migration from the old storage location and format to the new one
Signed-off-by: Dominik Henneke <dominik.henneke@sda-se.com>
This commit is contained in:
@@ -6,7 +6,5 @@
|
||||
Introduce a new `StarredEntitiesApi` that is used in the `useStarredEntities` hook.
|
||||
The `@backstage/plugin-catalog` installs a default implementation that is backed by the `StorageApi`, but one can also override the `starredEntitiesApiRef`.
|
||||
|
||||
**BREAKING** All previously stored entities get lost with this update, because:
|
||||
|
||||
1. The storage format has been migrated from a custom string to an entity reference.
|
||||
2. The storage key in the local storage has been changed.
|
||||
This change also updates the storage format from a custom string to an entity reference and moves the location in the local storage.
|
||||
A migration will convert the previously starred entities to the location on the first load of Backstage.
|
||||
|
||||
@@ -18,6 +18,9 @@ import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { StorageApi } from '@backstage/core-plugin-api';
|
||||
import { MockStorageApi } from '@backstage/test-utils';
|
||||
import { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi';
|
||||
import { performMigrationToTheNewBucket } from './migration';
|
||||
|
||||
jest.mock('./migration');
|
||||
|
||||
describe('DefaultStarredEntitiesApi', () => {
|
||||
let mockStorage: StorageApi;
|
||||
@@ -32,6 +35,8 @@ describe('DefaultStarredEntitiesApi', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(performMigrationToTheNewBucket as jest.Mock).mockResolvedValue(undefined);
|
||||
|
||||
mockStorage = MockStorageApi.create();
|
||||
starredEntitiesApi = new DefaultStarredEntitiesApi({
|
||||
storageApi: mockStorage,
|
||||
@@ -42,6 +47,12 @@ describe('DefaultStarredEntitiesApi', () => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should call migration', () => {
|
||||
expect(performMigrationToTheNewBucket).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleStarred', () => {
|
||||
it('should star unstarred entity', async () => {
|
||||
expect(starredEntitiesApi.isStarred(mockEntityRef)).toBe(false);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { Observable, StorageApi } from '@backstage/core-plugin-api';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
import { performMigrationToTheNewBucket } from './migration';
|
||||
import { StarredEntitiesApi } from './StarredEntitiesApi';
|
||||
|
||||
/**
|
||||
@@ -28,6 +29,9 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi {
|
||||
private starredEntities: Set<string>;
|
||||
|
||||
constructor(opts: { storageApi: StorageApi }) {
|
||||
// no need to await. The updated content will be caught by the observe$
|
||||
performMigrationToTheNewBucket(opts).then();
|
||||
|
||||
this.settingsStore = opts.storageApi.forBucket('starredEntities');
|
||||
|
||||
this.starredEntities = new Set(
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* 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 { StorageApi } from '@backstage/core-plugin-api';
|
||||
import { MockStorageApi } from '@backstage/test-utils';
|
||||
import { performMigrationToTheNewBucket } from './migration';
|
||||
|
||||
describe('performMigrationToTheNewBucket', () => {
|
||||
let mockStorage: StorageApi;
|
||||
|
||||
beforeEach(() => {
|
||||
mockStorage = MockStorageApi.create();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should migrate', async () => {
|
||||
const oldBucket = mockStorage.forBucket('settings');
|
||||
const newBucket = mockStorage.forBucket('starredEntities');
|
||||
|
||||
// fill NEW bucket
|
||||
await newBucket.set('entityRefs', ['component:default/c']);
|
||||
|
||||
// fill OLD bucket
|
||||
await oldBucket.set('starredEntities', [
|
||||
'entity:Component:default:a',
|
||||
'entity:template:custom:b',
|
||||
]);
|
||||
expect(oldBucket.get('starredEntities')).not.toBeUndefined();
|
||||
|
||||
await performMigrationToTheNewBucket({ storageApi: mockStorage });
|
||||
|
||||
// read NEW bucket
|
||||
expect(await newBucket.get('entityRefs')).toEqual([
|
||||
'component:default/c',
|
||||
'component:default/a',
|
||||
'template:custom/b',
|
||||
]);
|
||||
|
||||
// OLD bucket should be removed
|
||||
expect(oldBucket.get('starredEntities')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should ignore invalid entries', async () => {
|
||||
const oldBucket = mockStorage.forBucket('settings');
|
||||
const newBucket = mockStorage.forBucket('starredEntities');
|
||||
|
||||
// fill OLD bucket
|
||||
await oldBucket.set('starredEntities', [
|
||||
'entity:Component:default:a',
|
||||
1,
|
||||
'entity:Component:a',
|
||||
'invalid',
|
||||
]);
|
||||
expect(oldBucket.get('starredEntities')).not.toBeUndefined();
|
||||
|
||||
await performMigrationToTheNewBucket({ storageApi: mockStorage });
|
||||
|
||||
// read NEW bucket
|
||||
expect(await newBucket.get('entityRefs')).toEqual(['component:default/a']);
|
||||
|
||||
// OLD bucket should be removed
|
||||
expect(oldBucket.get('starredEntities')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should skip migration without old starred entities', async () => {
|
||||
const newBucket = mockStorage.forBucket('starredEntities');
|
||||
|
||||
// fill NEW bucket
|
||||
const expectedEntries = ['component:default/a'];
|
||||
await newBucket.set('entityRefs', expectedEntries);
|
||||
|
||||
await performMigrationToTheNewBucket({ storageApi: mockStorage });
|
||||
|
||||
// read NEW bucket
|
||||
expect(newBucket.get('entityRefs')).toBe(expectedEntries);
|
||||
});
|
||||
|
||||
it('should skip migration with non-array old starred entities', async () => {
|
||||
const oldBucket = mockStorage.forBucket('settings');
|
||||
const newBucket = mockStorage.forBucket('starredEntities');
|
||||
|
||||
// fill OLD bucket with invalid content
|
||||
await oldBucket.set('starredEntities', 'invalid');
|
||||
|
||||
// fill NEW bucket
|
||||
const expectedEntries = ['component:default/a'];
|
||||
await newBucket.set('entityRefs', expectedEntries);
|
||||
|
||||
await performMigrationToTheNewBucket({ storageApi: mockStorage });
|
||||
|
||||
// read NEW bucket
|
||||
expect(newBucket.get('entityRefs')).toBe(expectedEntries);
|
||||
|
||||
// OLD bucket should be unchanged
|
||||
expect(oldBucket.get('starredEntities')).toBe('invalid');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* 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 { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { StorageApi } from '@backstage/core-plugin-api';
|
||||
import { isArray, isString } from 'lodash';
|
||||
|
||||
/**
|
||||
* Migrate the starred entities from the old format (entity:<kind>:<namespace>:<name>) from the
|
||||
* old storage location (/settings/starredEntities) to entity references in the new location
|
||||
* (/starredEntities/entityRefs).
|
||||
*
|
||||
* This will only be executed once since the old location is cleared.
|
||||
*
|
||||
* @param storageApi - the StorageApi to migrate
|
||||
*/
|
||||
export async function performMigrationToTheNewBucket({
|
||||
storageApi,
|
||||
}: {
|
||||
storageApi: StorageApi;
|
||||
}) {
|
||||
const source = storageApi.forBucket('settings');
|
||||
const target = storageApi.forBucket('starredEntities');
|
||||
|
||||
const oldStarredEntities = source.get('starredEntities');
|
||||
|
||||
if (!isArray(oldStarredEntities)) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEntities = new Set(target.get('entityRefs') ?? []);
|
||||
|
||||
oldStarredEntities
|
||||
.filter(isString)
|
||||
// extract the old format 'entity:<kind>:<namespace>:<name>'
|
||||
.map(old => old.split(':'))
|
||||
// check if the format is valid
|
||||
.filter(split => split.length === 4 && split[0] === 'entity')
|
||||
// convert to entity references
|
||||
.map(([_, kind, namespace, name]) =>
|
||||
stringifyEntityRef({ kind, namespace, name }),
|
||||
)
|
||||
.forEach(e => targetEntities.add(e));
|
||||
|
||||
await target.set('entityRefs', Array.from(targetEntities));
|
||||
|
||||
await source.remove('starredEntities');
|
||||
}
|
||||
Reference in New Issue
Block a user