backend-dev-utils: add DevDataStore

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: blam <ben@blam.sh>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-01-31 12:08:55 +01:00
parent 9d9cdea117
commit a70806c3b0
4 changed files with 242 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
## API Report File for "@backstage/backend-dev-utils"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public
export class DevDataStore {
static get(): DevDataStore | undefined;
load<T>(key: string): Promise<{
loaded: boolean;
data: T;
}>;
save<T>(
key: string,
data: T,
): Promise<{
saved: boolean;
}>;
}
```
@@ -0,0 +1,121 @@
/*
* Copyright 2023 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 { Serializable, spawn } from 'child_process';
import { DevDataStore } from './DevDataStore';
import { BackstageIpcClient } from './ipcClient';
function applyIpcTransform(value: Serializable): Promise<Serializable> {
const child = spawn(
'node',
[
'--eval',
`
const listener = msg => {
process.send(msg);
process.removeListener('message', listener);
}
process.addListener('message', listener);
`,
],
{
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
serialization: 'advanced',
},
);
return new Promise(resolve => {
child.addListener('message', resolve);
child.send(value);
});
}
class MockIpcClient implements Pick<BackstageIpcClient, 'request'> {
#data = new Map<string, Serializable>();
async request(method: string, body: any): Promise<any> {
if (method === 'DevDataStore.save') {
this.#data.set(body.key, body.data);
return { saved: true };
} else if (method === 'DevDataStore.load') {
if (!this.#data.has(body.key)) {
return { loaded: false, data: undefined };
}
const data = this.#data.get(body.key)!;
return { loaded: true, data: await applyIpcTransform(data) };
}
throw new Error('Unknown message');
}
}
describe('DevDataStore', () => {
it('should save and load data', async () => {
const store = DevDataStore.forTest(new MockIpcClient());
await expect(store.save('test', { foo: 'bar' })).resolves.toEqual({
saved: true,
});
await expect(store.load('test')).resolves.toEqual({
loaded: true,
data: { foo: 'bar' },
});
});
it('should save and load buffers', async () => {
const store = DevDataStore.forTest(new MockIpcClient());
await expect(
store.save('test', Buffer.from('abc', 'ascii')),
).resolves.toEqual({
saved: true,
});
await expect(store.load('test')).resolves.toEqual({
loaded: true,
data: Buffer.from('abc', 'ascii'),
});
});
it('should save and load array buffers', async () => {
const store = DevDataStore.forTest(new MockIpcClient());
await expect(
store.save('test', new Uint8Array(Buffer.from('abc', 'ascii'))),
).resolves.toEqual({
saved: true,
});
await expect(store.load('test')).resolves.toEqual({
loaded: true,
data: new Uint8Array(Buffer.from('abc', 'ascii')),
});
});
it('should save and load buffers nested in objects', async () => {
const store = DevDataStore.forTest(new MockIpcClient());
await expect(
store.save('test', {
x: Buffer.from('x', 'ascii'),
y: [Buffer.from('y', 'ascii')],
}),
).resolves.toEqual({
saved: true,
});
await expect(store.load('test')).resolves.toEqual({
loaded: true,
data: { x: Buffer.from('x', 'ascii'), y: [Buffer.from('y', 'ascii')] },
});
});
});
@@ -0,0 +1,99 @@
/*
* Copyright 2023 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 { BackstageIpcClient, ipcClient } from './ipcClient';
interface SaveRequest {
key: string;
data: unknown;
}
interface SaveResponse {
saved: boolean;
}
interface LoadRequest {
key: string;
}
interface LoadResponse {
loaded: boolean;
data: unknown;
}
/**
* A data store that can be used to store temporary data during development.
*
* @public
*/
export class DevDataStore {
static #instance?: DevDataStore;
/**
* Tries to acquire a DevDataStore instance. This will only succeed when the backend
* process is being run through the `@backstage/cli` in development mode.
*
* @returns A DevDataStore instance, or undefined if not available.
*/
static get(): DevDataStore | undefined {
if (ipcClient) {
if (!this.#instance) {
this.#instance = new DevDataStore(ipcClient);
}
return this.#instance;
}
return undefined;
}
/** @internal */
static forTest(client: Pick<BackstageIpcClient, 'request'>): DevDataStore {
return new DevDataStore(client as BackstageIpcClient);
}
#client: BackstageIpcClient;
private constructor(client: BackstageIpcClient) {
this.#client = client;
}
/**
* Save data to the data store.
*
* @param key - The key used to identify the data.
* @param data - The data to save. The data will be serialized using advanced IPC serialization.
* @returns A promise that resolves to a result object that indicates whether the data was saved.
*/
async save<T>(key: string, data: T): Promise<{ saved: boolean }> {
return this.#client.request<SaveRequest, SaveResponse>(
'DevDataStore.save',
{ key, data },
);
}
/**
* Loads data from the data store.
*
* @param key - The key used to identify the data.
* @returns A promise that resolves to a result object that indicates whether the data was loaded, as well as the data.
*/
async load<T>(key: string): Promise<{ loaded: boolean; data: T }> {
const result = await this.#client.request<LoadRequest, LoadResponse>(
'DevDataStore.load',
{ key },
);
return result as { loaded: boolean; data: T };
}
}
+2
View File
@@ -19,3 +19,5 @@
*
* @packageDocumentation
*/
export { DevDataStore } from './DevDataStore';