Merge pull request #29259 from drodil/default_notification_settings

feat: allow defining default notification settings via config
This commit is contained in:
Patrik Oldsberg
2025-07-15 11:11:39 +02:00
committed by GitHub
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
@@ -29,6 +29,22 @@ export interface Config {
*/
throttleInterval?: HumanDuration | string;
/**
* Default settings for user specific notification settings
*/
defaultSettings?: {
channels?: {
id: string;
origins?: {
id: string;
enabled: boolean;
topics?: {
id: string;
enabled: boolean;
}[];
}[];
}[];
};
/*
* Time to keep the notifications in the database, defaults to 365 days.
* Can be disabled by setting to false.
*/
@@ -53,7 +53,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({
@@ -314,6 +331,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',
@@ -338,11 +364,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',
@@ -369,7 +406,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 () => {
@@ -570,9 +608,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',
@@ -588,9 +647,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' }],
},
]),
},
],
});
@@ -97,6 +97,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'] });
@@ -104,61 +106,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 });