feat: allow defining default notification settings via config

Signed-off-by: Hellgren Heikki <heikki.hellgren@op.fi>
This commit is contained in:
Hellgren Heikki
2025-03-17 15:50:34 +02:00
parent 58cb0452a9
commit 4401dfbf80
4 changed files with 205 additions and 23 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-notifications-backend': patch
---
Allow defining default notification settings via configuration
+16
View File
@@ -28,5 +28,21 @@ export interface Config {
* Throttle duration between notification sending, defaults to 50ms
*/
throttleInterval?: HumanDuration | string;
/**
* Default settings for user specific notification settings
*/
defaultSettings?: {
channels?: {
id: string;
origins?: {
id: string;
enabled: boolean;
topics?: {
id: string;
enabled: boolean;
}[];
}[];
}[];
};
};
}
@@ -51,7 +51,24 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => {
const auth = mockServices.auth();
const config = mockServices.rootConfig({
data: { app: { baseUrl: 'http://localhost' } },
data: {
app: { baseUrl: 'http://localhost' },
notifications: {
defaultSettings: {
channels: [
{
id: 'Web',
origins: [
{
id: 'external:test-service2',
enabled: false,
},
],
},
],
},
},
},
});
const catalog = catalogServiceMock({
@@ -309,6 +326,15 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => {
it('should not send to user entity if origin is disabled in settings', async () => {
const client = await database.getClient();
// Insert a notification with a origin
await client('notification').insert({
id: uuid(),
user: 'user:default/mock',
origin: 'external:test-service',
title: 'Test notification',
created: new Date(),
severity: 'normal',
});
await client('user_settings').insert({
settings_key_hash: 'hash',
user: 'user:default/mock',
@@ -333,11 +359,22 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => {
const notifications = await client('notification')
.where('user', 'user:default/mock')
.select();
expect(notifications).toHaveLength(0);
// This should not create a new notification since the origin is disabled
expect(notifications).toHaveLength(1);
});
it('should not send to user entity if topic is disabled in settings', async () => {
const client = await database.getClient();
// Insert a notification with a topic
await client('notification').insert({
id: uuid(),
user: 'user:default/mock',
origin: 'external:test-service',
topic: 'test-topic',
title: 'Test notification',
created: new Date(),
severity: 'normal',
});
await client('user_settings').insert({
settings_key_hash: 'hash',
user: 'user:default/mock',
@@ -364,7 +401,8 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => {
const notifications = await client('notification')
.where('user', 'user:default/mock')
.select();
expect(notifications).toHaveLength(0);
// This should not create a new notification since the topic is disabled
expect(notifications).toHaveLength(1);
});
it('should send to user entity if origin is enabled, but topic is disabled in settings', async () => {
@@ -565,9 +603,30 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => {
jest.resetAllMocks();
const client = await database.getClient();
await client('user_settings').del();
await client('notification').del();
await client('notification').insert({
id: uuid(),
user: 'user:default/mock',
origin: 'external:test-service',
topic: 'test-topic',
title: 'Test notification',
created: new Date(),
severity: 'normal',
});
await client('notification').insert({
id: uuid(),
user: 'user:default/mock',
origin: 'external:test-service2',
title: 'Test notification',
topic: 'test-topic2',
created: new Date(),
severity: 'normal',
});
});
it('should return user settings', async () => {
it('should return origin settings correctly', async () => {
const client = await database.getClient();
await client('user_settings').insert({
settings_key_hash: 'hash',
@@ -583,9 +642,76 @@ describe.each(databases.eachSupportedId())('createRouter (%s)', databaseId => {
channels: [
{
id: 'Web',
origins: [
{ enabled: false, id: 'external:test-service', topics: [] },
],
origins: expect.arrayContaining([
{
enabled: false,
id: 'external:test-service',
topics: [{ enabled: false, id: 'test-topic' }],
},
{
enabled: false,
id: 'external:test-service2',
topics: [{ enabled: false, id: 'test-topic2' }],
},
]),
},
],
});
});
it('should return topic settings correctly', async () => {
const client = await database.getClient();
await client('user_settings').insert({
settings_key_hash: 'hash',
user: 'user:default/mock',
channel: 'Web',
origin: 'external:test-service',
topic: 'test-topic',
enabled: false,
});
const response = await request(app).get('/settings');
expect(response.status).toEqual(200);
expect(response.body).toEqual({
channels: [
{
id: 'Web',
origins: expect.arrayContaining([
{
enabled: true,
id: 'external:test-service',
topics: [{ enabled: false, id: 'test-topic' }],
},
{
enabled: false,
id: 'external:test-service2',
topics: [{ enabled: false, id: 'test-topic2' }],
},
]),
},
],
});
});
it('should return default user settings from config', async () => {
const response = await request(app).get('/settings');
expect(response.status).toEqual(200);
expect(response.body).toEqual({
channels: [
{
id: 'Web',
origins: expect.arrayContaining([
{
enabled: true,
id: 'external:test-service',
topics: [{ enabled: true, id: 'test-topic' }],
},
{
enabled: false,
id: 'external:test-service2',
topics: [{ enabled: false, id: 'test-topic2' }],
},
]),
},
],
});
@@ -99,6 +99,8 @@ export async function createRouter(
limit: concurrencyLimit,
interval: throttleInterval,
});
const defaultNotificationSettings: NotificationSettings | undefined =
config.getOptional<NotificationSettings>('notifications.defaultSettings');
const getUser = async (req: Request<unknown>) => {
const credentials = await httpAuth.credentials(req, { allow: ['user'] });
@@ -106,61 +108,94 @@ export async function createRouter(
return info.userEntityRef;
};
const getNotificationChannels = () => {
return [WEB_NOTIFICATION_CHANNEL, ...processors.map(p => p.getName())];
};
const getTopicSettings = (
topic: any,
existingOrigin: OriginSetting | undefined,
defaultOriginSettings: OriginSetting | undefined,
defaultEnabled: boolean,
) => {
const existingTopic = existingOrigin?.topics?.find(
t => t.id === topic.topic,
t => t.id.toLowerCase() === topic.topic.toLowerCase(),
);
const defaultTopicSettings = defaultOriginSettings?.topics?.find(
t => t.id.toLowerCase() === topic.topic.toLowerCase(),
);
return {
id: topic.topic,
enabled: existingTopic ? existingTopic.enabled : defaultEnabled,
enabled: existingTopic
? existingTopic.enabled
: defaultTopicSettings?.enabled ?? defaultEnabled,
};
};
const getOriginSettings = (
originId: string,
existingChannel: ChannelSetting | undefined,
defaultChannelSettings: ChannelSetting | undefined,
topics: { origin: string; topic: string }[],
) => {
const existingOrigin = existingChannel?.origins.find(
o => o.id === originId,
const existingOrigin = existingChannel?.origins?.find(
o => o.id.toLowerCase() === originId.toLowerCase(),
);
const defaultEnabled = existingOrigin ? existingOrigin.enabled : true;
const defaultOriginSettings = defaultChannelSettings?.origins?.find(
c => c.id.toLowerCase() === originId.toLowerCase(),
);
const defaultEnabled = existingOrigin
? existingOrigin.enabled
: defaultOriginSettings?.enabled ?? true;
return {
id: originId,
enabled: defaultEnabled,
topics: topics
.filter(t => t.origin === originId)
.map(t => getTopicSettings(t, existingOrigin, defaultEnabled)),
.map(t =>
getTopicSettings(
t,
existingOrigin,
defaultOriginSettings,
defaultEnabled,
),
),
};
};
const getNotificationChannels = () => {
return [WEB_NOTIFICATION_CHANNEL, ...processors.map(p => p.getName())];
};
const getChannelSettings = (
channelId: string,
settings: NotificationSettings,
origins: string[],
topics: { origin: string; topic: string }[],
) => {
const existingChannel = settings.channels.find(c => c.id === channelId);
if (existingChannel) {
return existingChannel;
}
const existingChannel = settings.channels.find(
c => c.id.toLowerCase() === channelId.toLowerCase(),
);
const defaultChannelSettings = defaultNotificationSettings?.channels?.find(
c => c.id.toLowerCase() === channelId.toLowerCase(),
);
return {
id: channelId,
origins: origins.map(originId =>
getOriginSettings(originId, existingChannel, topics),
getOriginSettings(
originId,
existingChannel,
defaultChannelSettings,
topics,
),
),
};
};
const getNotificationSettings = async (user: string) => {
const getNotificationSettings = async (
user: string,
): Promise<NotificationSettings> => {
const { origins } = await store.getUserNotificationOrigins({ user });
const { topics } = await store.getUserNotificationTopics({ user });
const settings = await store.getNotificationSettings({ user });