Add multiget to usersettings to batch get()s in one call and cache the result for a short period

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
This commit is contained in:
Gustaf Räntilä
2025-09-05 14:11:36 +02:00
parent 3581e7d2c9
commit 104ca74ec2
13 changed files with 917 additions and 133 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-user-settings-backend': minor
'@backstage/plugin-user-settings-common': minor
'@backstage/plugin-user-settings': minor
---
User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance
@@ -55,6 +55,7 @@
"@backstage/plugin-signals-node": "workspace:^",
"@backstage/plugin-user-settings-common": "workspace:^",
"@backstage/types": "workspace:^",
"already": "^2.2.1",
"express": "^4.22.0",
"express-promise-router": "^4.1.0",
"knex": "^3.0.0"
@@ -14,12 +14,17 @@
* limitations under the License.
*/
import { InputError } from '@backstage/errors';
import { InputError, serializeError } from '@backstage/errors';
import { map } from 'already';
import express, { Request } from 'express';
import Router from 'express-promise-router';
import { UserSettingsStore } from '../database/UserSettingsStore';
import { SignalsService } from '@backstage/plugin-signals-node';
import { UserSettingsSignal } from '@backstage/plugin-user-settings-common';
import {
MultiUserSetting,
UserSettingsSignal,
parseDataLoaderKey,
} from '@backstage/plugin-user-settings-common';
import { HttpAuthService } from '@backstage/backend-plugin-api';
export async function createRouter(options: {
@@ -40,6 +45,59 @@ export async function createRouter(options: {
return credentials.principal.userEntityRef;
};
// get multiple values
router.get('/multi', async (req, res) => {
const userEntityRef = await getUserEntityRef(req);
const bucketsAndKeys: ReturnType<typeof parseDataLoaderKey>[] = [];
const items = req.query.items;
if (typeof items === 'string') {
bucketsAndKeys.push(parseDataLoaderKey(items));
} else if (Array.isArray(items)) {
bucketsAndKeys.push(
...items.map(item => {
if (typeof item !== 'string') {
throw new InputError(
'Expected query param "items" to be an array of strings',
);
}
return parseDataLoaderKey(item);
}),
);
} else {
throw new InputError('Expected query param "items" to be an array');
}
const userSettings = await map(
bucketsAndKeys,
{ concurrency: 10 },
async ({ bucket, key }): Promise<MultiUserSetting> => {
try {
const setting = await options.userSettingsStore.get({
userEntityRef,
bucket,
key,
});
return setting;
} catch (e) {
if (e instanceof Error) {
const serialized = serializeError(e);
return { bucket, key, error: serialized };
}
return {
bucket,
key,
error: { name: 'Error', message: 'Unknown error' },
};
}
},
);
res.json(userSettings);
});
// get a single value
router.get('/buckets/:bucket/keys/:key(*)', async (req, res) => {
const userEntityRef = await getUserEntityRef(req);
@@ -37,6 +37,10 @@
"postpack": "backstage-cli package postpack",
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/errors": "workspace:^",
"@backstage/types": "workspace:^"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
}
@@ -3,6 +3,40 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import type { JsonValue } from '@backstage/types';
import type { SerializedError } from '@backstage/errors';
// @public (undocumented)
export function isMultiUserSettingError(
setting: MultiUserSetting,
): setting is MultiUserSettingError;
// @public
export type MultiUserSetting = MultiUserSettingError | MultiUserSettingSuccess;
// @public
export type MultiUserSettingError = {
bucket: string;
key: string;
error: SerializedError;
};
// @public
export type MultiUserSettingSuccess = {
bucket: string;
key: string;
value: JsonValue;
};
// @public (undocumented)
export function parseDataLoaderKey(bucketAndKey: string): {
bucket: string;
key: string;
};
// @public (undocumented)
export function stringifyDataLoaderKey(bucket: string, key: string): string;
// @public (undocumented)
export type UserSettingsSignal = {
type: 'key-changed' | 'key-deleted';
@@ -15,3 +15,6 @@
*/
export * from './types';
export { isMultiUserSettingError } from './types';
export { stringifyDataLoaderKey, parseDataLoaderKey } from './keys';
+26
View File
@@ -0,0 +1,26 @@
/*
* Copyright 2025 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.
*/
/** @public */
export function stringifyDataLoaderKey(bucket: string, key: string) {
return `${encodeURIComponent(bucket)}/${encodeURIComponent(key)}`;
}
/** @public */
export function parseDataLoaderKey(bucketAndKey: string) {
const [bucket, key] = bucketAndKey.split('/');
return { bucket: decodeURIComponent(bucket), key: decodeURIComponent(key) };
}
+39
View File
@@ -14,8 +14,47 @@
* limitations under the License.
*/
import type { SerializedError } from '@backstage/errors';
import type { JsonValue } from '@backstage/types';
/** @public */
export type UserSettingsSignal = {
type: 'key-changed' | 'key-deleted';
key: string;
};
/**
* A failed fetch of a user setting in a bucket
*
* @public
*/
export type MultiUserSettingError = {
bucket: string;
key: string;
error: SerializedError;
};
/**
* A successful value of a user setting in a bucket
*
* @public
*/
export type MultiUserSettingSuccess = {
bucket: string;
key: string;
value: JsonValue;
};
/**
* A single setting in a bucket, or an error, used result from the /multi endpoint
*
* @public
*/
export type MultiUserSetting = MultiUserSettingError | MultiUserSettingSuccess;
/** @public */
export function isMultiUserSettingError(
setting: MultiUserSetting,
): setting is MultiUserSettingError {
return 'error' in setting;
}
+2
View File
@@ -69,6 +69,7 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"dataloader": "^2.0.0",
"react-use": "^17.2.4",
"zen-observable": "^0.10.0"
},
@@ -82,6 +83,7 @@
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/react": "^18.0.0",
"already": "^2.2.1",
"msw": "^1.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
@@ -0,0 +1,56 @@
/*
* Copyright 2025 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.
*/
/**
* A Map<K, V> with TTL (time-to-live) support, for the Dataloader of user settings.
*/
export class CacheMap<K, V> extends Map<K, V> {
#ttlMs: number;
#timestamps: Map<K, number> = new Map();
constructor(ttlMs: number) {
super();
this.#ttlMs = ttlMs;
}
set(key: K, value: V) {
const result = super.set(key, value);
this.#timestamps.set(key, Date.now());
return result;
}
get(key: K) {
if (!this.has(key)) {
return undefined;
}
const timestamp = this.#timestamps.get(key)!;
if (Date.now() - timestamp > this.#ttlMs) {
this.delete(key);
return undefined;
}
return super.get(key);
}
delete(key: K) {
this.#timestamps.delete(key);
return super.delete(key);
}
clear() {
this.#timestamps.clear();
return super.clear();
}
}
@@ -25,6 +25,9 @@ import {
mockApis,
registerMswTestHooks,
} from '@backstage/test-utils';
import { NotFoundError } from '@backstage/errors';
import { parseDataLoaderKey } from '@backstage/plugin-user-settings-common';
import { defer } from 'already';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { UserSettingsStorage } from './UserSettingsStorage';
@@ -41,14 +44,12 @@ describe('Persistent Storage API', () => {
const mockIdentityApi = mockApis.identity({ token: 'a-token' });
const mockIdentityApiFallback = mockApis.identity();
const createPersistentStorage = (
args?: Partial<{
fetchApi: FetchApi;
discoveryApi: DiscoveryApi;
errorApi: ErrorApi;
namespace?: string;
}>,
): StorageApi => {
const createPersistentStorage = (args: {
fetchApi?: FetchApi;
discoveryApi?: DiscoveryApi;
errorApi?: ErrorApi;
namespace: string;
}): StorageApi => {
return UserSettingsStorage.create({
errorApi: mockErrorApi,
fetchApi: new MockFetchApi(),
@@ -58,14 +59,12 @@ describe('Persistent Storage API', () => {
});
};
const createPersistentStorageFallback = (
args?: Partial<{
fetchApi: FetchApi;
discoveryApi: DiscoveryApi;
errorApi: ErrorApi;
namespace?: string;
}>,
): StorageApi => {
const createPersistentStorageFallback = (args: {
fetchApi?: FetchApi;
discoveryApi?: DiscoveryApi;
errorApi?: ErrorApi;
namespace: string;
}): StorageApi => {
return UserSettingsStorage.create({
errorApi: mockErrorApi,
fetchApi: new MockFetchApi(),
@@ -75,21 +74,17 @@ describe('Persistent Storage API', () => {
});
};
afterEach(() => {
afterEach(async () => {
// Wait for server callbacks to settle before clearing the handlers.
// DataLoader delay is 10ms, so this should be plenty.
await new Promise(resolve => setTimeout(resolve, 100));
jest.clearAllMocks();
server.resetHandlers();
});
it('should return undefined for values which are unset', async () => {
const storage = createPersistentStorage();
server.use(
rest.get(
`${mockBaseUrl}/buckets/:bucket/keys/:key`,
async (_req, res, ctx) => {
return res(ctx.json({ value: 'a' }));
},
),
);
const storage = createPersistentStorage({ namespace: 'undefined' });
expect(storage.snapshot('myfakekey').value).toBeUndefined();
expect(storage.snapshot('myfakekey')).toEqual({
@@ -100,7 +95,7 @@ describe('Persistent Storage API', () => {
});
it('should allow setting of a simple data structure', async () => {
const storage = createPersistentStorage();
const storage = createPersistentStorage({ namespace: 'simple' });
const dummyValue = 'a';
server.use(
@@ -121,7 +116,7 @@ describe('Persistent Storage API', () => {
});
it('should allow setting of a complex data structure', async () => {
const storage = createPersistentStorage();
const storage = createPersistentStorage({ namespace: 'complex' });
const dummyValue = {
some: 'nice data',
with: { nested: 'values', nice: true },
@@ -144,7 +139,9 @@ describe('Persistent Storage API', () => {
});
it('should fallback set when user not logged in', async () => {
const storage = createPersistentStorageFallback();
const storage = createPersistentStorageFallback({
namespace: 'not-logged-in',
});
const selectedKeyNextHandler = jest.fn();
const dummyValue = 'my-value';
@@ -171,12 +168,14 @@ describe('Persistent Storage API', () => {
});
it('should subscribe to key changes when setting a new value', async () => {
const storage = createPersistentStorage();
const storage = createPersistentStorage({ namespace: 'key-change-set' });
const wrongKeyNextHandler = jest.fn();
const selectedKeyNextHandler = jest.fn();
const mockData = { hello: 'im a great new value' };
const serverCall = defer(undefined);
server.use(
rest.put(
`${mockBaseUrl}/buckets/:bucket/keys/:key`,
@@ -188,6 +187,10 @@ describe('Persistent Storage API', () => {
return res(ctx.json(data));
},
),
rest.get(`${mockBaseUrl}/multi`, async (_req, res, ctx) => {
serverCall.resolve();
return res(ctx.json([]));
}),
);
await new Promise<void>(resolve => {
@@ -212,14 +215,18 @@ describe('Persistent Storage API', () => {
presence: 'present',
value: mockData,
});
await serverCall.promise;
});
it('should subscribe to key changes when deleting a value', async () => {
const storage = createPersistentStorage();
const storage = createPersistentStorage({ namespace: 'key-change-delete' });
const wrongKeyNextHandler = jest.fn();
const selectedKeyNextHandler = jest.fn();
const serverCall = defer(undefined);
server.use(
rest.delete(
`${mockBaseUrl}/buckets/:bucket/keys/:key`,
@@ -227,6 +234,10 @@ describe('Persistent Storage API', () => {
return res(ctx.status(204));
},
),
rest.get(`${mockBaseUrl}/multi`, async (_req, res, ctx) => {
serverCall.resolve();
return res(ctx.json([]));
}),
);
await new Promise<void>(resolve => {
@@ -251,10 +262,12 @@ describe('Persistent Storage API', () => {
presence: 'absent',
value: undefined,
});
await serverCall.promise;
});
it('should not clash with other namespaces when creating buckets', async () => {
const rootStorage = createPersistentStorage();
const rootStorage = createPersistentStorage({ namespace: 'clash' });
const selectedKeyNextHandler = jest.fn();
server.use(
@@ -264,23 +277,22 @@ describe('Persistent Storage API', () => {
const { bucket, key } = req.params;
const { value } = await req.json();
expect(bucket).toEqual('default.profile.something.deep');
expect(bucket).toEqual('clash.profile.something.deep');
expect(key).toEqual('test2');
return res(ctx.json({ value }));
},
),
rest.get(
`${mockBaseUrl}/buckets/:bucket/keys/:key`,
async (req, res, ctx) => {
const { bucket, key } = req.params;
rest.get(`${mockBaseUrl}/multi`, async (req, res, ctx) => {
const { bucket, key } = parseDataLoaderKey(
req.url.searchParams.get('items') as string,
);
expect(bucket).toEqual('default.profile/something');
expect(key).toEqual('deep/test2');
expect(bucket).toEqual('clash.profile/something');
expect(key).toEqual('deep/test2');
return res(ctx.status(404));
},
),
return res(ctx.status(404));
}),
);
// when getting key test2 it will translate to default.profile.something.deep/test2
@@ -321,15 +333,14 @@ describe('Persistent Storage API', () => {
});
server.use(
rest.get(
`${mockBaseUrl}/buckets/:bucket/keys/:key`,
async (req, res, ctx) => {
const { bucket, key } = req.params;
expect(bucket).toEqual('Test.Mock.Thing');
expect(key).toEqual('key');
return res(ctx.text('{ invalid: json string }'));
},
),
rest.get(`${mockBaseUrl}/multi`, async (req, res, ctx) => {
const { bucket, key } = parseDataLoaderKey(
req.url.searchParams.get('items') as string,
);
expect(bucket).toEqual('Test.Mock.Thing');
expect(key).toEqual('key');
return res(ctx.text('{ invalid: json string }'));
}),
);
await new Promise<void>(resolve => {
@@ -353,17 +364,14 @@ describe('Persistent Storage API', () => {
});
it('should freeze the snapshot value', async () => {
const storage = createPersistentStorage();
const storage = createPersistentStorage({ namespace: 'freeze' });
const selectedKeyNextHandler = jest.fn();
const data = { foo: 'bar', baz: [{ foo: 'bar' }] };
server.use(
rest.get(
`${mockBaseUrl}/buckets/:bucket/keys/:key`,
async (_req, res, ctx) => {
return res(ctx.json({ value: data }));
},
),
rest.get(`${mockBaseUrl}/multi`, async (_req, res, ctx) => {
return res(ctx.json([{ bucket: 'freeze', key: 'key', value: data }]));
}),
);
await new Promise<void>(resolve => {
@@ -396,4 +404,112 @@ describe('Persistent Storage API', () => {
snapshot.value.baz.push({ foo: 'buzz' });
}).toThrow(/Cannot add property 1, object is not extensible/);
});
it('should batch multiple calls into one', async () => {
const storage = createPersistentStorage({ namespace: 'multiget' });
const selectedKeyNextHandler = jest.fn();
const selectedKeyNextHandlerCached = jest.fn();
const data1 = { foo: 'bar1', baz: [{ foo: 'bar1' }] };
const data2 = { foo: 'bar2', baz: [{ foo: 'bar2' }] };
let serverCalls = 0;
server.use(
rest.get(`${mockBaseUrl}/multi`, async (req, res, ctx) => {
++serverCalls;
const result = req.url.searchParams
.getAll('items')
.map(item => parseDataLoaderKey(item))
.map(({ key }) => {
if (key === 'key1') {
return { bucket: 'multiget', key, value: data1 };
} else if (key === 'key2') {
return { bucket: 'multiget', key, value: data2 };
}
return { bucket: 'multiget', key, error: new NotFoundError() };
});
return res(ctx.json(result));
}),
);
await Promise.all([
new Promise<void>(resolve => {
storage.observe$('key1').subscribe({
next: snapshot => {
selectedKeyNextHandler(snapshot);
if (snapshot.presence === 'present') {
resolve();
}
},
});
storage.snapshot('key1');
}),
new Promise<void>(resolve => {
storage.observe$('missing-key').subscribe({
next: snapshot => {
selectedKeyNextHandler(snapshot);
if (snapshot.presence === 'absent') {
resolve();
}
},
});
storage.snapshot('missing-key');
}),
new Promise<void>(resolve => {
storage.observe$('key2').subscribe({
next: snapshot => {
selectedKeyNextHandler(snapshot);
if (snapshot.presence === 'present') {
resolve();
}
},
});
storage.snapshot('key2');
}),
]);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'key1',
presence: 'present',
value: data1,
});
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'key2',
presence: 'present',
value: data2,
});
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'missing-key',
presence: 'absent',
});
expect(mockErrorApi.post).not.toHaveBeenCalled();
expect(serverCalls).toBe(1);
// Get key1 again, should use cached value
await new Promise<void>(resolve => {
storage.observe$('key1').subscribe({
next: snapshot => {
selectedKeyNextHandlerCached(snapshot);
if (snapshot.presence === 'present') {
resolve();
}
},
});
storage.snapshot('key1');
});
expect(selectedKeyNextHandlerCached).toHaveBeenCalledWith({
key: 'key1',
presence: 'present',
value: data1,
});
expect(serverCalls).toBe(1);
});
});
@@ -23,11 +23,19 @@ import {
StorageApi,
StorageValueSnapshot,
} from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import { deserializeError, ResponseError } from '@backstage/errors';
import { JsonValue, Observable } from '@backstage/types';
import { SignalApi, SignalSubscriber } from '@backstage/plugin-signals-react';
import ObservableImpl from 'zen-observable';
import { UserSettingsSignal } from '@backstage/plugin-user-settings-common';
import {
isMultiUserSettingError,
MultiUserSetting,
parseDataLoaderKey,
stringifyDataLoaderKey,
UserSettingsSignal,
} from '@backstage/plugin-user-settings-common';
import DataLoader from 'dataloader';
import { CacheMap } from './CacheMap';
const JSON_HEADERS = {
'Content-Type': 'application/json; charset=utf-8',
@@ -36,6 +44,9 @@ const JSON_HEADERS = {
const buckets = new Map<string, UserSettingsStorage>();
const DATALOADER_CACHE_TTL_MS = 2 * 1000; // 2 seconds cache
const DATALOADER_WINDOW_MS = 10; // 10 ms
/**
* An implementation of the storage API, that uses the user-settings backend to
* persist the data in the DB.
@@ -59,6 +70,7 @@ export class UserSettingsStorage implements StorageApi {
private readonly identityApi: IdentityApi;
private readonly fallback: WebStorage;
private readonly signalApi?: SignalApi;
private readonly userSettingsLoader: DataLoader<string, any>;
private constructor(
namespace: string,
@@ -68,6 +80,7 @@ export class UserSettingsStorage implements StorageApi {
identityApi: IdentityApi,
fallback: WebStorage,
signalApi?: SignalApi,
userSettingsLoader?: DataLoader<string, any>,
) {
this.namespace = namespace;
this.fetchApi = fetchApi;
@@ -76,6 +89,27 @@ export class UserSettingsStorage implements StorageApi {
this.identityApi = identityApi;
this.fallback = fallback;
this.signalApi = signalApi;
this.userSettingsLoader =
userSettingsLoader ??
new DataLoader<string, any>(
async bucketAndKeyList => this.getMulti(bucketAndKeyList),
{
name: 'UserSettingsStorage.userSettingsLoader',
cacheMap: new CacheMap<string, Promise<unknown>>(
DATALOADER_CACHE_TTL_MS,
),
maxBatchSize: 100,
batchScheduleFn: cb => setTimeout(cb, DATALOADER_WINDOW_MS),
},
);
}
private stringifyDataLoaderKey(key: string) {
return stringifyDataLoaderKey(this.namespace, key);
}
private clearCacheKey(key: string) {
this.userSettingsLoader.clear(this.stringifyDataLoaderKey(key));
}
static create(options: {
@@ -114,6 +148,8 @@ export class UserSettingsStorage implements StorageApi {
this.errorApi,
this.identityApi,
this.fallback,
this.signalApi,
this.userSettingsLoader,
),
);
}
@@ -132,6 +168,8 @@ export class UserSettingsStorage implements StorageApi {
throw await ResponseError.fromResponse(response);
}
this.clearCacheKey(key);
this.notifyChanges({ key, presence: 'absent' });
}
@@ -139,9 +177,12 @@ export class UserSettingsStorage implements StorageApi {
if (!(await this.isSignedIn())) {
await this.fallback.set(key, data);
this.notifyChanges({ key, presence: 'present', value: data });
this.clearCacheKey(key);
return;
}
this.clearCacheKey(key);
const fetchUrl = await this.getFetchUrl(key);
const response = await this.fetchApi.fetch(fetchUrl, {
@@ -156,6 +197,12 @@ export class UserSettingsStorage implements StorageApi {
const { value } = await response.json();
this.userSettingsLoader.prime(this.stringifyDataLoaderKey(key), {
key,
presence: 'present',
value,
});
this.notifyChanges({ key, value, presence: 'present' });
}
@@ -171,7 +218,9 @@ export class UserSettingsStorage implements StorageApi {
const updateSnapshot = () => {
Promise.resolve()
.then(() => this.get(key))
.then(() =>
this.userSettingsLoader.load(this.stringifyDataLoaderKey(key)),
)
.then(snapshot => subscriber.next(snapshot))
.catch(error => this.errorApi.post(error));
};
@@ -206,19 +255,30 @@ export class UserSettingsStorage implements StorageApi {
return { key, presence: 'unknown' };
}
private async get<T extends JsonValue>(
key: string,
): Promise<StorageValueSnapshot<T>> {
private async getMulti(
bucketAndKeyList: readonly string[],
): Promise<StorageValueSnapshot<JsonValue>[]> {
if (bucketAndKeyList.length === 0) return [];
if (!(await this.isSignedIn())) {
// This explicitly uses WebStorage, which we know is synchronous and doesn't return presence: unknown
return this.fallback.snapshot(key);
return bucketAndKeyList.map(bucketAndKey =>
this.fallback.snapshot(parseDataLoaderKey(bucketAndKey).key),
);
}
const fetchUrl = await this.getFetchUrl(key);
const response = await this.fetchApi.fetch(fetchUrl);
const baseUrl = await this.discoveryApi.getBaseUrl('user-settings');
const url = new URL(`${baseUrl}/multi`);
for (const bucketAndKey of bucketAndKeyList) {
url.searchParams.append('items', bucketAndKey);
}
const response = await this.fetchApi.fetch(url);
if (response.status === 404) {
return { key, presence: 'absent' };
return bucketAndKeyList.map(bucketAndKey => ({
key: parseDataLoaderKey(bucketAndKey).key,
presence: 'absent',
}));
}
if (!response.ok) {
@@ -226,18 +286,37 @@ export class UserSettingsStorage implements StorageApi {
}
try {
const { value: rawValue } = await response.json();
const value = JSON.parse(JSON.stringify(rawValue), (_key, val) => {
if (typeof val === 'object' && val !== null) {
Object.freeze(val);
}
return val;
});
const values = await response.json();
return { key, presence: 'present', value };
return (values as MultiUserSetting[]).map(
(setting): StorageValueSnapshot<JsonValue> => {
if (isMultiUserSettingError(setting)) {
if (setting.error.name === 'NotFoundError') {
return {
key: setting.key,
presence: 'absent',
};
}
throw deserializeError(setting.error);
}
return {
key: setting.key,
presence: 'present',
value: JSON.parse(JSON.stringify(setting.value), (_key, val) => {
if (typeof val === 'object' && val !== null) {
Object.freeze(val);
}
return val;
}),
};
},
);
} catch {
// If the value is not valid JSON, we return an unknown presence. This should never happen
return { key, presence: 'absent' };
return bucketAndKeyList.map(bucketAndKey => ({
key: parseDataLoaderKey(bucketAndKey).key,
presence: 'absent',
}));
}
}
+412 -53
View File
@@ -2291,7 +2291,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.8.3":
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.8.3":
version: 7.27.1
resolution: "@babel/code-frame@npm:7.27.1"
dependencies:
@@ -2440,6 +2440,13 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-plugin-utils@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-plugin-utils@npm:7.25.9"
checksum: 10/e347d87728b1ab10b6976d46403941c8f9008c045ea6d99997a7ffca7b852dc34b6171380f7b17edf94410e0857ff26f3a53d8618f11d73744db86e8ca9b8c64
languageName: node
linkType: hard
"@babel/helper-replace-supers@npm:^7.25.0":
version: 7.25.0
resolution: "@babel/helper-replace-supers@npm:7.25.0"
@@ -2615,7 +2622,18 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-syntax-jsx@npm:^7.24.7, @babel/plugin-syntax-jsx@npm:^7.27.1":
"@babel/plugin-syntax-jsx@npm:^7.24.7":
version: 7.24.7
resolution: "@babel/plugin-syntax-jsx@npm:7.24.7"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.24.7"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/a93516ae5b34868ab892a95315027d4e5e38e8bd1cfca6158f2974b0901cbb32bbe64ea10ad5b25f919ddc40c6d8113c4823372909c9c9922170c12b0b1acecb
languageName: node
linkType: hard
"@babel/plugin-syntax-jsx@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-syntax-jsx@npm:7.27.1"
dependencies:
@@ -2714,7 +2732,18 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-syntax-typescript@npm:^7.24.7, @babel/plugin-syntax-typescript@npm:^7.27.1":
"@babel/plugin-syntax-typescript@npm:^7.24.7":
version: 7.25.9
resolution: "@babel/plugin-syntax-typescript@npm:7.25.9"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.25.9"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/0e9821e8ba7d660c36c919654e4144a70546942ae184e85b8102f2322451eae102cbfadbcadd52ce077a2b44b400ee52394c616feab7b5b9f791b910e933fd33
languageName: node
linkType: hard
"@babel/plugin-syntax-typescript@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-syntax-typescript@npm:7.27.1"
dependencies:
@@ -2899,7 +2928,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5":
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.27.1, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/types@npm:7.28.4"
dependencies:
"@babel/helper-string-parser": "npm:^7.27.1"
"@babel/helper-validator-identifier": "npm:^7.27.1"
checksum: 10/db50bf257aafa5d845ad16dae0587f57d596e4be4cbb233ea539976a4c461f9fbcc0bf3d37adae3f8ce5dcb4001462aa608f3558161258b585f6ce6ce21a2e45
languageName: node
linkType: hard
"@babel/types@npm:^7.27.3, @babel/types@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/types@npm:7.28.5"
dependencies:
@@ -7784,6 +7823,7 @@ __metadata:
"@backstage/types": "workspace:^"
"@types/express": "npm:^4.17.6"
"@types/supertest": "npm:^2.0.8"
already: "npm:^2.2.1"
express: "npm:^4.22.0"
express-promise-router: "npm:^4.1.0"
knex: "npm:^3.0.0"
@@ -7796,6 +7836,8 @@ __metadata:
resolution: "@backstage/plugin-user-settings-common@workspace:plugins/user-settings-common"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/types": "workspace:^"
languageName: unknown
linkType: soft
@@ -7826,6 +7868,8 @@ __metadata:
"@testing-library/react": "npm:^16.0.0"
"@testing-library/user-event": "npm:^14.0.0"
"@types/react": "npm:^18.0.0"
already: "npm:^2.2.1"
dataloader: "npm:^2.0.0"
msw: "npm:^1.0.0"
react: "npm:^18.0.2"
react-dom: "npm:^18.0.2"
@@ -10308,6 +10352,15 @@ __metadata:
languageName: node
linkType: hard
"@jest/expect-utils@npm:^29.7.0":
version: 29.7.0
resolution: "@jest/expect-utils@npm:29.7.0"
dependencies:
jest-get-type: "npm:^29.6.3"
checksum: 10/ef8d379778ef574a17bde2801a6f4469f8022a46a5f9e385191dc73bb1fc318996beaed4513fbd7055c2847227a1bed2469977821866534593a6e52a281499ee
languageName: node
linkType: hard
"@jest/expect@npm:30.2.0":
version: 30.2.0
resolution: "@jest/expect@npm:30.2.0"
@@ -13956,13 +14009,20 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/semantic-conventions@npm:^1.24.0, @opentelemetry/semantic-conventions@npm:^1.27.0, @opentelemetry/semantic-conventions@npm:^1.29.0, @opentelemetry/semantic-conventions@npm:^1.30.0, @opentelemetry/semantic-conventions@npm:^1.33.0, @opentelemetry/semantic-conventions@npm:^1.33.1, @opentelemetry/semantic-conventions@npm:^1.34.0, @opentelemetry/semantic-conventions@npm:^1.36.0, @opentelemetry/semantic-conventions@npm:^1.37.0":
"@opentelemetry/semantic-conventions@npm:^1.24.0, @opentelemetry/semantic-conventions@npm:^1.33.0, @opentelemetry/semantic-conventions@npm:^1.36.0, @opentelemetry/semantic-conventions@npm:^1.37.0":
version: 1.38.0
resolution: "@opentelemetry/semantic-conventions@npm:1.38.0"
checksum: 10/9d549f4896e900f644d5e70dd7142505daff88ed83c1cb7bcd976ac55e9496d4ddd686bb2815dd68655c739950514394c3b73ff51e53b2e4ff2d54a7f6d22521
languageName: node
linkType: hard
"@opentelemetry/semantic-conventions@npm:^1.27.0, @opentelemetry/semantic-conventions@npm:^1.29.0, @opentelemetry/semantic-conventions@npm:^1.30.0, @opentelemetry/semantic-conventions@npm:^1.33.1, @opentelemetry/semantic-conventions@npm:^1.34.0":
version: 1.34.0
resolution: "@opentelemetry/semantic-conventions@npm:1.34.0"
checksum: 10/1892b4cc69c9e00456c809604a980e32696563e96463ff5f9d07e72d5aca73836a7378090509f28f54445ac6e072d2343a888c9d64d9ce287198e899082ff7aa
languageName: node
linkType: hard
"@opentelemetry/semantic-conventions@npm:~1.30.0":
version: 1.30.0
resolution: "@opentelemetry/semantic-conventions@npm:1.30.0"
@@ -17624,7 +17684,7 @@ __metadata:
languageName: node
linkType: hard
"@sinonjs/fake-timers@npm:^13.0.0, @sinonjs/fake-timers@npm:^13.0.1":
"@sinonjs/fake-timers@npm:^13.0.0":
version: 13.0.5
resolution: "@sinonjs/fake-timers@npm:13.0.5"
dependencies:
@@ -17633,6 +17693,15 @@ __metadata:
languageName: node
linkType: hard
"@sinonjs/fake-timers@npm:^13.0.1":
version: 13.0.2
resolution: "@sinonjs/fake-timers@npm:13.0.2"
dependencies:
"@sinonjs/commons": "npm:^3.0.1"
checksum: 10/77cca5c548e2529931908c48ac375f162ee901bc52110197b4c470b2535c6c571f9ecd4fa12157f4d2ae174c5391f03940fb563a681a691fb44204a0ef3ded35
languageName: node
linkType: hard
"@sinonjs/samsam@npm:^8.0.0":
version: 8.0.0
resolution: "@sinonjs/samsam@npm:8.0.0"
@@ -17720,7 +17789,14 @@ __metadata:
languageName: node
linkType: hard
"@slack/types@npm:^2.11.0, @slack/types@npm:^2.13.0, @slack/types@npm:^2.14.0, @slack/types@npm:^2.18.0":
"@slack/types@npm:^2.11.0, @slack/types@npm:^2.13.0, @slack/types@npm:^2.14.0":
version: 2.14.0
resolution: "@slack/types@npm:2.14.0"
checksum: 10/fa24a113b88e087f899078504c2ba50ab9795f7c2dd1a2d95b28217a3af20e554494f9cc3b8c8ce173120990d98e19400c95369f9067cecfcc46c08b59d2a46f
languageName: node
linkType: hard
"@slack/types@npm:^2.18.0":
version: 2.19.0
resolution: "@slack/types@npm:2.19.0"
checksum: 10/d5fbc227147b32a964072cb46e2c328b896507a374194532ddf594e18e4ccc2afe9f31f2b92d14ace43ef856b22979523e565616be2c87a2dc6c575d4f919fd0
@@ -18378,7 +18454,7 @@ __metadata:
languageName: node
linkType: hard
"@smithy/signature-v4@npm:^4.1.0, @smithy/signature-v4@npm:^4.1.1":
"@smithy/signature-v4@npm:^4.1.0":
version: 4.2.4
resolution: "@smithy/signature-v4@npm:4.2.4"
dependencies:
@@ -18394,6 +18470,22 @@ __metadata:
languageName: node
linkType: hard
"@smithy/signature-v4@npm:^4.1.1":
version: 4.1.1
resolution: "@smithy/signature-v4@npm:4.1.1"
dependencies:
"@smithy/is-array-buffer": "npm:^3.0.0"
"@smithy/protocol-http": "npm:^4.1.1"
"@smithy/types": "npm:^3.4.0"
"@smithy/util-hex-encoding": "npm:^3.0.0"
"@smithy/util-middleware": "npm:^3.0.4"
"@smithy/util-uri-escape": "npm:^3.0.0"
"@smithy/util-utf8": "npm:^3.0.0"
tslib: "npm:^2.6.2"
checksum: 10/f77d8b6cb384a0b9a0925ce6a693fbbcbf13c6f9ddf12cb9e1b39fb06452d001b17164d5e2618ea103edb427f2f1225d057827e2815e629b582115b2d533194f
languageName: node
linkType: hard
"@smithy/signature-v4@npm:^5.3.5":
version: 5.3.5
resolution: "@smithy/signature-v4@npm:5.3.5"
@@ -18689,7 +18781,7 @@ __metadata:
languageName: node
linkType: hard
"@smithy/util-middleware@npm:^3.0.11, @smithy/util-middleware@npm:^3.0.4":
"@smithy/util-middleware@npm:^3.0.11":
version: 3.0.11
resolution: "@smithy/util-middleware@npm:3.0.11"
dependencies:
@@ -18699,6 +18791,16 @@ __metadata:
languageName: node
linkType: hard
"@smithy/util-middleware@npm:^3.0.4":
version: 3.0.4
resolution: "@smithy/util-middleware@npm:3.0.4"
dependencies:
"@smithy/types": "npm:^3.4.0"
tslib: "npm:^2.6.2"
checksum: 10/d358c8e5ab462b749b32421135f1660302de8b2a942e5cf92b63b1489e87d0fc57aa4cc578fee46a7192472946b06a898d98b1f01c8cf4b9fccc4ad489ffde5a
languageName: node
linkType: hard
"@smithy/util-middleware@npm:^4.2.5":
version: 4.2.5
resolution: "@smithy/util-middleware@npm:4.2.5"
@@ -21004,14 +21106,14 @@ __metadata:
linkType: hard
"@types/express-serve-static-core@npm:*":
version: 5.1.1
resolution: "@types/express-serve-static-core@npm:5.1.1"
version: 4.19.6
resolution: "@types/express-serve-static-core@npm:4.19.6"
dependencies:
"@types/node": "npm:*"
"@types/qs": "npm:*"
"@types/range-parser": "npm:*"
"@types/send": "npm:*"
checksum: 10/7f3d8cf7e68764c9f3e8f6a12825b69ccf5287347fc1c20b29803d4f08a4abc1153ae11d7258852c61aad50f62ef72d4c1b9c97092b0a90462c3dddec2f6026c
checksum: 10/a2e00b6c5993f0dd63ada2239be81076fe0220314b9e9fde586e8946c9c09ce60f9a2dd0d74410ee2b5fd10af8c3e755a32bb3abf134533e2158142488995455
languageName: node
linkType: hard
@@ -21036,7 +21138,19 @@ __metadata:
languageName: node
linkType: hard
"@types/express@npm:*, @types/express@npm:^4.16.1, @types/express@npm:^4.17.21, @types/express@npm:^4.17.25, @types/express@npm:^4.17.6":
"@types/express@npm:*, @types/express@npm:^4.16.1, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6":
version: 4.17.23
resolution: "@types/express@npm:4.17.23"
dependencies:
"@types/body-parser": "npm:*"
"@types/express-serve-static-core": "npm:^4.17.33"
"@types/qs": "npm:*"
"@types/serve-static": "npm:*"
checksum: 10/cf4d540bbd90801cdc79a46107b8873404698a7fd0c3e8dd42989d52d3bd7f5b8768672e54c20835e41e27349c319bb47a404ad14c0f8db0e9d055ba1cb8a05b
languageName: node
linkType: hard
"@types/express@npm:^4.17.25":
version: 4.17.25
resolution: "@types/express@npm:4.17.25"
dependencies:
@@ -21398,14 +21512,7 @@ __metadata:
languageName: node
linkType: hard
"@types/luxon@npm:^3.0.0":
version: 3.7.1
resolution: "@types/luxon@npm:3.7.1"
checksum: 10/c7bc164c278393ea0be938f986c74b4cddfab9013b1aff4495b016f771ded1d5b7b7b4825b2c7f0b8799edce19c5f531c28ff434ab3dedf994ac2d99a20fd4c4
languageName: node
linkType: hard
"@types/luxon@npm:~3.4.0":
"@types/luxon@npm:^3.0.0, @types/luxon@npm:~3.4.0":
version: 3.4.2
resolution: "@types/luxon@npm:3.4.2"
checksum: 10/fd89566e3026559f2bc4ddcc1e70a2c16161905ed50be9473ec0cfbbbe919165041408c4f6e06c4bcf095445535052e2c099087c76b1b38e368127e618fc968d
@@ -21530,7 +21637,7 @@ __metadata:
languageName: node
linkType: hard
"@types/node-fetch@npm:^2.6.13, @types/node-fetch@npm:^2.6.4":
"@types/node-fetch@npm:^2.6.13":
version: 2.6.13
resolution: "@types/node-fetch@npm:2.6.13"
dependencies:
@@ -21540,6 +21647,16 @@ __metadata:
languageName: node
linkType: hard
"@types/node-fetch@npm:^2.6.4":
version: 2.6.12
resolution: "@types/node-fetch@npm:2.6.12"
dependencies:
"@types/node": "npm:*"
form-data: "npm:^4.0.0"
checksum: 10/8107c479da83a3114fcbfa882eba95ee5175cccb5e4dd53f737a96f2559ae6262f662176b8457c1656de09ec393cc7b20a266c077e4bfb21e929976e1cf4d0f9
languageName: node
linkType: hard
"@types/node-forge@npm:^1.3.0":
version: 1.3.14
resolution: "@types/node-forge@npm:1.3.14"
@@ -22058,6 +22175,17 @@ __metadata:
languageName: node
linkType: hard
"@types/serve-static@npm:*":
version: 1.15.7
resolution: "@types/serve-static@npm:1.15.7"
dependencies:
"@types/http-errors": "npm:*"
"@types/node": "npm:*"
"@types/send": "npm:*"
checksum: 10/c5a7171d5647f9fbd096ed1a26105759f3153ccf683824d99fee4c7eb9cde2953509621c56a070dd9fb1159e799e86d300cbe4e42245ebc5b0c1767e8ca94a67
languageName: node
linkType: hard
"@types/serve-static@npm:^1, @types/serve-static@npm:^1.15.5":
version: 1.15.10
resolution: "@types/serve-static@npm:1.15.10"
@@ -22136,6 +22264,13 @@ __metadata:
languageName: node
linkType: hard
"@types/stack-utils@npm:^2.0.0":
version: 2.0.0
resolution: "@types/stack-utils@npm:2.0.0"
checksum: 10/b3fbae25b073116977ecb5c67d22f14567b51a7792403b0bf46e5de8f29bde3bd4ec1626afb22065495ca7f1c699c8bd66720050c94b8f8f9bcefbee79d161fd
languageName: node
linkType: hard
"@types/stack-utils@npm:^2.0.3":
version: 2.0.3
resolution: "@types/stack-utils@npm:2.0.3"
@@ -22371,7 +22506,7 @@ __metadata:
languageName: node
linkType: hard
"@types/yargs@npm:^17.0.33, @types/yargs@npm:^17.0.8":
"@types/yargs@npm:^17.0.33":
version: 17.0.35
resolution: "@types/yargs@npm:17.0.35"
dependencies:
@@ -22380,6 +22515,15 @@ __metadata:
languageName: node
linkType: hard
"@types/yargs@npm:^17.0.8":
version: 17.0.12
resolution: "@types/yargs@npm:17.0.12"
dependencies:
"@types/yargs-parser": "npm:*"
checksum: 10/ffbbfad0c75cc058e0518f202e3651b9fb60c7c1325240cc72ac0a022da746a759ba3c6e0099152076fed5012fb694eb4685e4520c04dc8399c22bb81221bff5
languageName: node
linkType: hard
"@types/yarnpkg__lockfile@npm:^1.1.4":
version: 1.1.9
resolution: "@types/yarnpkg__lockfile@npm:1.1.9"
@@ -22447,6 +22591,19 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.35.0":
version: 8.35.0
resolution: "@typescript-eslint/project-service@npm:8.35.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.35.0"
"@typescript-eslint/types": "npm:^8.35.0"
debug: "npm:^4.3.4"
peerDependencies:
typescript: ">=4.8.4 <5.9.0"
checksum: 10/a9419da92231aa27f75078fcffab1d02398b50fdb7d5399775a414ba02570682b4b60cdfafb544a021b0dc2372f029c4195f5ae17c50deb11c25661b2ac18a74
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/project-service@npm:8.48.1"
@@ -22503,6 +22660,15 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.35.0, @typescript-eslint/tsconfig-utils@npm:^8.35.0":
version: 8.35.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.35.0"
peerDependencies:
typescript: ">=4.8.4 <5.9.0"
checksum: 10/4160928313ccbe8b169a009b9c1220826c7df7aab427f960c31f3b838931bc7a121ebee8040118481e4528e2e3cf1b26da047c6ac1d802ecff2ef7206026ea6b
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/tsconfig-utils@npm:8.48.1"
@@ -22544,6 +22710,13 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.35.0, @typescript-eslint/types@npm:^8.35.0":
version: 8.35.0
resolution: "@typescript-eslint/types@npm:8.35.0"
checksum: 10/34b5e6da2c59ea84cd528608fff0cc14b102fd23f5517dfee4ef38c9372861d80b5bf92445c9679674f0a4f8dc4ded5066c1bca2bc5569c47515f94568984f35
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/types@npm:8.48.1"
@@ -22596,7 +22769,7 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.54.0, @typescript-eslint/typescript-estree@npm:^8.23.0":
"@typescript-eslint/typescript-estree@npm:8.54.0":
version: 8.54.0
resolution: "@typescript-eslint/typescript-estree@npm:8.54.0"
dependencies:
@@ -22615,6 +22788,26 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:^8.23.0":
version: 8.35.0
resolution: "@typescript-eslint/typescript-estree@npm:8.35.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.35.0"
"@typescript-eslint/tsconfig-utils": "npm:8.35.0"
"@typescript-eslint/types": "npm:8.35.0"
"@typescript-eslint/visitor-keys": "npm:8.35.0"
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3"
minimatch: "npm:^9.0.4"
semver: "npm:^7.6.0"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
typescript: ">=4.8.4 <5.9.0"
checksum: 10/4dff7c5a8853c8f4e30d35565c62d3ad5bf8445309bd465d94e9bca725853012bb9f58896a04207c30e10b6669511caac8c0f080ed781c93a3db81d5808195aa
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/utils@npm:8.48.1"
@@ -22669,6 +22862,16 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.35.0":
version: 8.35.0
resolution: "@typescript-eslint/visitor-keys@npm:8.35.0"
dependencies:
"@typescript-eslint/types": "npm:8.35.0"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10/c0acb13aac3a2be5e82844f7d2e86137347efdd04661dbf9fa69ef04a19dd2f1eb2f1eb6bfbfbaada78a46884308d2c0e0b5d0d1a094c84f2dfb670b67ac2b3b
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.48.1":
version: 8.48.1
resolution: "@typescript-eslint/visitor-keys@npm:8.48.1"
@@ -24494,6 +24697,13 @@ __metadata:
languageName: node
linkType: hard
"already@npm:^2.2.1":
version: 2.2.1
resolution: "already@npm:2.2.1"
checksum: 10/1c55b50667c3dbe9d40716454d4a870f5758143061a0e39c0a7077eab2c6dbec116edf081796afb6f441462096bf68ef72a4daad074843a0d970527f29037ffe
languageName: node
linkType: hard
"anser@npm:^2.1.1":
version: 2.3.3
resolution: "anser@npm:2.3.3"
@@ -25287,7 +25497,18 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.0.0, axios@npm:^1.11.0, axios@npm:^1.12.2, axios@npm:^1.13.0, axios@npm:^1.7.4":
"axios@npm:^1.0.0, axios@npm:^1.7.4":
version: 1.12.2
resolution: "axios@npm:1.12.2"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.4"
proxy-from-env: "npm:^1.1.0"
checksum: 10/886a79770594eaad76493fecf90344b567bd956240609b5dcd09bd0afe8d3e6f1ad6d3257a93a483b6192b409d4b673d9515a34619e3e3ed1b2c0ec2a83b20ba
languageName: node
linkType: hard
"axios@npm:^1.11.0, axios@npm:^1.12.2, axios@npm:^1.13.0":
version: 1.13.4
resolution: "axios@npm:1.13.4"
dependencies:
@@ -28498,15 +28719,15 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.3":
version: 4.4.3
resolution: "debug@npm:4.4.3"
"debug@npm:4, debug@npm:^4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0":
version: 4.4.0
resolution: "debug@npm:4.4.0"
dependencies:
ms: "npm:^2.1.3"
peerDependenciesMeta:
supports-color:
optional: true
checksum: 10/9ada3434ea2993800bd9a1e320bd4aa7af69659fb51cca685d390949434bc0a8873c21ed7c9b852af6f2455a55c6d050aa3937d52b3c69f796dab666f762acad
checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367
languageName: node
linkType: hard
@@ -28531,6 +28752,18 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:^4.3.7, debug@npm:^4.4.3":
version: 4.4.3
resolution: "debug@npm:4.4.3"
dependencies:
ms: "npm:^2.1.3"
peerDependenciesMeta:
supports-color:
optional: true
checksum: 10/9ada3434ea2993800bd9a1e320bd4aa7af69659fb51cca685d390949434bc0a8873c21ed7c9b852af6f2455a55c6d050aa3937d52b3c69f796dab666f762acad
languageName: node
linkType: hard
"debuglog@npm:^1.0.1":
version: 1.0.1
resolution: "debuglog@npm:1.0.1"
@@ -30674,13 +30907,20 @@ __metadata:
languageName: node
linkType: hard
"eventsource-parser@npm:^3.0.0, eventsource-parser@npm:^3.0.1":
"eventsource-parser@npm:^3.0.0":
version: 3.0.6
resolution: "eventsource-parser@npm:3.0.6"
checksum: 10/febf7058b9c2168ecbb33e92711a1646e06bd1568f60b6eb6a01a8bf9f8fcd29cc8320d57247059cacf657a296280159f21306d2e3ff33309a9552b2ef889387
languageName: node
linkType: hard
"eventsource-parser@npm:^3.0.1":
version: 3.0.1
resolution: "eventsource-parser@npm:3.0.1"
checksum: 10/2730c54c3cb47d55d2967f2ece843f9fc95d8a11c2fef6fece8d17d9080193cbe3cd9ac7b04a325977f63cbf8c1664fdd0512dec1aec601666a5c5bd8564b61f
languageName: node
linkType: hard
"eventsource@npm:^3.0.2":
version: 3.0.7
resolution: "eventsource@npm:3.0.7"
@@ -30989,7 +31229,7 @@ __metadata:
languageName: node
linkType: hard
"expect@npm:30.2.0, expect@npm:>28.1.3, expect@npm:^30.0.0":
"expect@npm:30.2.0, expect@npm:^30.0.0":
version: 30.2.0
resolution: "expect@npm:30.2.0"
dependencies:
@@ -31003,6 +31243,19 @@ __metadata:
languageName: node
linkType: hard
"expect@npm:>28.1.3":
version: 29.7.0
resolution: "expect@npm:29.7.0"
dependencies:
"@jest/expect-utils": "npm:^29.7.0"
jest-get-type: "npm:^29.6.3"
jest-matcher-utils: "npm:^29.7.0"
jest-message-util: "npm:^29.7.0"
jest-util: "npm:^29.7.0"
checksum: 10/63f97bc51f56a491950fb525f9ad94f1916e8a014947f8d8445d3847a665b5471b768522d659f5e865db20b6c2033d2ac10f35fcbd881a4d26407a4f6f18451a
languageName: node
linkType: hard
"exponential-backoff@npm:^3.1.1":
version: 3.1.1
resolution: "exponential-backoff@npm:3.1.1"
@@ -32031,7 +32284,7 @@ __metadata:
languageName: node
linkType: hard
"fs-extra@npm:11.3.3, fs-extra@npm:^11.0.0, fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0, fs-extra@npm:~11.3.0":
"fs-extra@npm:11.3.3, fs-extra@npm:~11.3.0":
version: 11.3.3
resolution: "fs-extra@npm:11.3.3"
dependencies:
@@ -32065,6 +32318,17 @@ __metadata:
languageName: node
linkType: hard
"fs-extra@npm:^11.0.0, fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0":
version: 11.3.1
resolution: "fs-extra@npm:11.3.1"
dependencies:
graceful-fs: "npm:^4.2.0"
jsonfile: "npm:^6.0.1"
universalify: "npm:^2.0.0"
checksum: 10/2b893213411b1da11f9b061ccb0bcff4d6dd66fe90aa8f5b1616219a5e7ca659da869f454ebd8e94aa21c58342730fb43a2e5c98b5c6c5124f0c54a4633f64b0
languageName: node
linkType: hard
"fs-extra@npm:^7.0.1":
version: 7.0.1
resolution: "fs-extra@npm:7.0.1"
@@ -32342,7 +32606,25 @@ __metadata:
languageName: node
linkType: hard
"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0":
"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0":
version: 1.3.0
resolution: "get-intrinsic@npm:1.3.0"
dependencies:
call-bind-apply-helpers: "npm:^1.0.2"
es-define-property: "npm:^1.0.1"
es-errors: "npm:^1.3.0"
es-object-atoms: "npm:^1.1.1"
function-bind: "npm:^1.1.2"
get-proto: "npm:^1.0.1"
gopd: "npm:^1.2.0"
has-symbols: "npm:^1.1.0"
hasown: "npm:^2.0.2"
math-intrinsics: "npm:^1.1.0"
checksum: 10/6e9dd920ff054147b6f44cb98104330e87caafae051b6d37b13384a45ba15e71af33c3baeac7cb630a0aaa23142718dcf25b45cfdd86c184c5dcb4e56d953a10
languageName: node
linkType: hard
"get-intrinsic@npm:^1.2.2":
version: 1.3.1
resolution: "get-intrinsic@npm:1.3.1"
dependencies:
@@ -35549,7 +35831,7 @@ __metadata:
languageName: node
linkType: hard
"jest-diff@npm:^29.2.0":
"jest-diff@npm:^29.2.0, jest-diff@npm:^29.7.0":
version: 29.7.0
resolution: "jest-diff@npm:29.7.0"
dependencies:
@@ -35649,6 +35931,18 @@ __metadata:
languageName: node
linkType: hard
"jest-matcher-utils@npm:^29.7.0":
version: 29.7.0
resolution: "jest-matcher-utils@npm:29.7.0"
dependencies:
chalk: "npm:^4.0.0"
jest-diff: "npm:^29.7.0"
jest-get-type: "npm:^29.6.3"
pretty-format: "npm:^29.7.0"
checksum: 10/981904a494299cf1e3baed352f8a3bd8b50a8c13a662c509b6a53c31461f94ea3bfeffa9d5efcfeb248e384e318c87de7e3baa6af0f79674e987482aa189af40
languageName: node
linkType: hard
"jest-message-util@npm:30.2.0":
version: 30.2.0
resolution: "jest-message-util@npm:30.2.0"
@@ -35666,6 +35960,23 @@ __metadata:
languageName: node
linkType: hard
"jest-message-util@npm:^29.7.0":
version: 29.7.0
resolution: "jest-message-util@npm:29.7.0"
dependencies:
"@babel/code-frame": "npm:^7.12.13"
"@jest/types": "npm:^29.6.3"
"@types/stack-utils": "npm:^2.0.0"
chalk: "npm:^4.0.0"
graceful-fs: "npm:^4.2.9"
micromatch: "npm:^4.0.4"
pretty-format: "npm:^29.7.0"
slash: "npm:^3.0.0"
stack-utils: "npm:^2.0.3"
checksum: 10/31d53c6ed22095d86bab9d14c0fa70c4a92c749ea6ceece82cf30c22c9c0e26407acdfbdb0231435dc85a98d6d65ca0d9cbcd25cd1abb377fe945e843fb770b9
languageName: node
linkType: hard
"jest-mock@npm:30.2.0":
version: 30.2.0
resolution: "jest-mock@npm:30.2.0"
@@ -37865,14 +38176,7 @@ __metadata:
languageName: node
linkType: hard
"luxon@npm:^3.0.0, luxon@npm:^3.2.1, luxon@npm:^3.4.3, luxon@npm:^3.5.0":
version: 3.7.2
resolution: "luxon@npm:3.7.2"
checksum: 10/b24cd205ed306ce7415991687897dcc4027921ae413c9116590bc33a95f93b86ce52cf74ba72b4f5c5ab1c10090517f54ac8edfb127c049e0bf55b90dc2260be
languageName: node
linkType: hard
"luxon@npm:~3.5.0":
"luxon@npm:^3.0.0, luxon@npm:^3.2.1, luxon@npm:^3.4.3, luxon@npm:^3.5.0, luxon@npm:~3.5.0":
version: 3.5.0
resolution: "luxon@npm:3.5.0"
checksum: 10/48f86e6c1c96815139f8559456a3354a276ba79bcef0ae0d4f2172f7652f3ba2be2237b0e103b8ea0b79b47715354ac9fac04eb1db3485dcc72d5110491dd47f
@@ -38870,7 +39174,7 @@ __metadata:
languageName: node
linkType: hard
"micromatch@npm:^4.0.2, micromatch@npm:^4.0.5, micromatch@npm:^4.0.7, micromatch@npm:^4.0.8":
"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5, micromatch@npm:^4.0.7, micromatch@npm:^4.0.8":
version: 4.0.8
resolution: "micromatch@npm:4.0.8"
dependencies:
@@ -41921,13 +42225,20 @@ __metadata:
languageName: node
linkType: hard
"pg-connection-string@npm:^2.10.1, pg-connection-string@npm:^2.3.0, pg-connection-string@npm:^2.5.0":
"pg-connection-string@npm:^2.10.1":
version: 2.10.1
resolution: "pg-connection-string@npm:2.10.1"
checksum: 10/8785cfac30f64a48f47031c24d7c5c53241fabb336d5be8f30ca06af0e16aea8d9647b645b4b44ed931717a47a0afb95ddb57e60b6af103767a4d8606dec22dc
languageName: node
linkType: hard
"pg-connection-string@npm:^2.3.0, pg-connection-string@npm:^2.5.0":
version: 2.7.0
resolution: "pg-connection-string@npm:2.7.0"
checksum: 10/68015a8874b7ca5dad456445e4114af3d2602bac2fdb8069315ecad0ff9660ec93259b9af7186606529ac4f6f72a06831e6f20897a689b16cc7fda7ca0e247fd
languageName: node
linkType: hard
"pg-format@npm:^1.0.4":
version: 1.0.4
resolution: "pg-format@npm:1.0.4"
@@ -41951,7 +42262,14 @@ __metadata:
languageName: node
linkType: hard
"pg-protocol@npm:*, pg-protocol@npm:^1.11.0":
"pg-protocol@npm:*":
version: 1.8.0
resolution: "pg-protocol@npm:1.8.0"
checksum: 10/52f67d8161ae4afb1dbf96f6ad12a2ecf478dbb0b80baa239047cd562dee378961fd446f0a1cfc1fd323e052fbb3df47e886c5d9d86a2803a000d36682b29094
languageName: node
linkType: hard
"pg-protocol@npm:^1.11.0":
version: 1.11.0
resolution: "pg-protocol@npm:1.11.0"
checksum: 10/a70b1b4a3fc5b1be80dfdd65c829a149b8bd9df7488f9c47e0b51c9413aec5eb6da0a9ae9812891d74cd9f2ee90c0e391984a41b64603e7375fcbb9e07070b08
@@ -46294,13 +46612,20 @@ __metadata:
languageName: node
linkType: hard
"shell-quote@npm:1.8.3, shell-quote@npm:^1.6.1, shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1":
"shell-quote@npm:1.8.3":
version: 1.8.3
resolution: "shell-quote@npm:1.8.3"
checksum: 10/5473e354637c2bd698911224129c9a8961697486cff1fb221f234d71c153fc377674029b0223d1d3c953a68d451d79366abfe53d1a0b46ee1f28eb9ade928f4c
languageName: node
linkType: hard
"shell-quote@npm:^1.6.1, shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1":
version: 1.8.1
resolution: "shell-quote@npm:1.8.1"
checksum: 10/af19ab5a1ec30cb4b2f91fd6df49a7442d5c4825a2e269b3712eded10eedd7f9efeaab96d57829880733fc55bcdd8e9b1d8589b4befb06667c731d08145e274d
languageName: node
linkType: hard
"shelljs@npm:^0.9.2":
version: 0.9.2
resolution: "shelljs@npm:0.9.2"
@@ -46965,7 +47290,7 @@ __metadata:
languageName: node
linkType: hard
"stack-utils@npm:^2.0.2, stack-utils@npm:^2.0.6":
"stack-utils@npm:^2.0.2, stack-utils@npm:^2.0.3, stack-utils@npm:^2.0.6":
version: 2.0.6
resolution: "stack-utils@npm:2.0.6"
dependencies:
@@ -47904,7 +48229,19 @@ __metadata:
languageName: node
linkType: hard
"tar-fs@npm:^2.0.0, tar-fs@npm:^2.1.4":
"tar-fs@npm:^2.0.0":
version: 2.1.2
resolution: "tar-fs@npm:2.1.2"
dependencies:
chownr: "npm:^1.1.1"
mkdirp-classic: "npm:^0.5.2"
pump: "npm:^3.0.0"
tar-stream: "npm:^2.1.4"
checksum: 10/623f7e8e58a43578ba7368002c3cc7e321f6d170053ac0691d95172dbc7daf5dcf4347eb061277627340870ce6cfda89f5a5d633cc274c41ae6d69f54a2374e7
languageName: node
linkType: hard
"tar-fs@npm:^2.1.4":
version: 2.1.4
resolution: "tar-fs@npm:2.1.4"
dependencies:
@@ -49330,13 +49667,20 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^7.16.0, undici@npm:^7.2.3":
"undici@npm:^7.16.0":
version: 7.19.2
resolution: "undici@npm:7.19.2"
checksum: 10/26a01804402e5f09d02a5d09607e42096828698dddbcf32132dd313af4fda99f3c04ed84d884e28528f945a5b77d87df5c01ec6c1bdc918fd28878cb35f1cb4a
languageName: node
linkType: hard
"undici@npm:^7.2.3":
version: 7.9.0
resolution: "undici@npm:7.9.0"
checksum: 10/fa92d3d9106612be566e79942174e00426c2e1f9a688fb86c8d1809c84a032c02fc57bd601d2051a45d94bbd312c50221644b58c8186c4f0d64f8ecc7f18b806
languageName: node
linkType: hard
"uni-global@npm:^1.0.0":
version: 1.0.0
resolution: "uni-global@npm:1.0.0"
@@ -50875,9 +51219,9 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:*, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.18.2, ws@npm:^8.18.3, ws@npm:^8.8.0":
version: 8.19.0
resolution: "ws@npm:8.19.0"
"ws@npm:*, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.8.0":
version: 8.18.3
resolution: "ws@npm:8.18.3"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ">=5.0.2"
@@ -50886,7 +51230,7 @@ __metadata:
optional: true
utf-8-validate:
optional: true
checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b
checksum: 10/725964438d752f0ab0de582cd48d6eeada58d1511c3f613485b5598a83680bedac6187c765b0fe082e2d8cc4341fc57707c813ae780feee82d0c5efe6a4c61b6
languageName: node
linkType: hard
@@ -50920,6 +51264,21 @@ __metadata:
languageName: node
linkType: hard
"ws@npm:^8.18.2, ws@npm:^8.18.3":
version: 8.19.0
resolution: "ws@npm:8.19.0"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ">=5.0.2"
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b
languageName: node
linkType: hard
"wsl-utils@npm:^0.1.0":
version: 0.1.0
resolution: "wsl-utils@npm:0.1.0"