chore: refactor service and backend to use REST

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-01-12 15:39:03 +02:00
parent 13346970b4
commit 1a38100a8e
27 changed files with 407 additions and 359 deletions
@@ -1,128 +0,0 @@
/*
* 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 {
PluginDatabaseManager,
resolvePackagePath,
} from '@backstage/backend-common';
import {
NotificationGetOptions,
NotificationModifyOptions,
NotificationsStore,
} from './NotificationsStore';
import { Notification } from '@backstage/plugin-notifications-common';
import { Knex } from 'knex';
const migrationsDir = resolvePackagePath(
'@backstage/plugin-notifications-node',
'migrations',
);
/** @public */
export class DatabaseNotificationsStore implements NotificationsStore {
private constructor(private readonly db: Knex) {}
static async create({
database,
skipMigrations,
}: {
database: PluginDatabaseManager;
skipMigrations?: boolean;
}): Promise<DatabaseNotificationsStore> {
const client = await database.getClient();
if (!database.migrations?.skip && !skipMigrations) {
await client.migrate.latest({
directory: migrationsDir,
});
}
return new DatabaseNotificationsStore(client);
}
private mapToInteger = (val: string | number | undefined): number => {
return typeof val === 'string' ? Number.parseInt(val, 10) : val ?? 0;
};
private getNotificationsBaseQuery = (
options: NotificationGetOptions | NotificationModifyOptions,
) => {
const { user_ref, type } = options;
const query = this.db('notifications').where('userRef', user_ref);
if (type === 'unread') {
query.whereNull('read');
} else if (type === 'read') {
query.whereNotNull('read');
} else if (type === 'saved') {
query.where('saved', true);
}
if ('ids' in options && options.ids) {
query.whereIn('id', options.ids);
}
return query;
};
async getNotifications(options: NotificationGetOptions) {
const notificationQuery = this.getNotificationsBaseQuery(options);
const notifications = await notificationQuery.select('*');
return notifications;
}
async saveNotification(notification: Notification) {
await this.db.insert(notification).into('notifications');
}
async getStatus(options: NotificationGetOptions) {
const notificationQuery = this.getNotificationsBaseQuery(options);
const unreadQuery = await notificationQuery
.clone()
.whereNull('read')
.count('id as UNREAD')
.first();
const readQuery = await notificationQuery
.clone()
.whereNotNull('read')
.count('id as READ')
.first();
return {
unread: this.mapToInteger((unreadQuery as any)?.UNREAD),
read: this.mapToInteger((readQuery as any)?.READ),
};
}
async markRead(options: NotificationModifyOptions): Promise<void> {
const notificationQuery = this.getNotificationsBaseQuery(options);
await notificationQuery.update({ read: new Date() });
}
async markUnread(options: NotificationModifyOptions): Promise<void> {
const notificationQuery = this.getNotificationsBaseQuery(options);
await notificationQuery.update({ read: null });
}
async markSaved(options: NotificationModifyOptions): Promise<void> {
const notificationQuery = this.getNotificationsBaseQuery(options);
await notificationQuery.update({ saved: true });
}
async markUnsaved(options: NotificationModifyOptions): Promise<void> {
const notificationQuery = this.getNotificationsBaseQuery(options);
await notificationQuery.update({ saved: false });
}
}
@@ -1,49 +0,0 @@
/*
* 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 {
Notification,
NotificationStatus,
NotificationType,
} from '@backstage/plugin-notifications-common';
/** @public */
export type NotificationGetOptions = {
user_ref: string;
type?: NotificationType;
};
/** @public */
export type NotificationModifyOptions = {
ids: string[];
} & NotificationGetOptions;
/** @public */
export interface NotificationsStore {
getNotifications(options: NotificationGetOptions): Promise<Notification[]>;
saveNotification(notification: Notification): Promise<void>;
getStatus(options: NotificationGetOptions): Promise<NotificationStatus>;
markRead(options: NotificationModifyOptions): Promise<void>;
markUnread(options: NotificationModifyOptions): Promise<void>;
markSaved(options: NotificationModifyOptions): Promise<void>;
markUnsaved(options: NotificationModifyOptions): Promise<void>;
}
@@ -1,17 +0,0 @@
/*
* 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.
*/
export * from './DatabaseNotificationsStore';
export * from './NotificationsStore';
-1
View File
@@ -20,6 +20,5 @@
* @packageDocumentation
*/
export * from './database';
export * from './service';
export * from './lib';
+10 -4
View File
@@ -18,7 +18,8 @@ import {
createServiceFactory,
createServiceRef,
} from '@backstage/backend-plugin-api';
import { NotificationService } from './service';
import { DefaultNotificationService } from './service';
import { NotificationService } from './service/NotificationService';
/** @public */
export const notificationService = createServiceRef<NotificationService>({
@@ -28,12 +29,17 @@ export const notificationService = createServiceRef<NotificationService>({
createServiceFactory({
service,
deps: {
logger: coreServices.logger,
discovery: coreServices.discovery,
database: coreServices.database,
tokenManager: coreServices.tokenManager,
// TODO: Signals
},
factory({ discovery, database }) {
return NotificationService.create({ discovery, database });
factory({ logger, discovery, tokenManager }) {
return DefaultNotificationService.create({
logger,
discovery,
tokenManager,
});
},
}),
});
@@ -0,0 +1,73 @@
/*
* 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 { Notification } from '@backstage/plugin-notifications-common';
import {
PluginEndpointDiscovery,
TokenManager,
} from '@backstage/backend-common';
import { NotificationService } from './NotificationService';
import { LoggerService } from '@backstage/backend-plugin-api';
/** @public */
export type NotificationServiceOptions = {
logger: LoggerService;
discovery: PluginEndpointDiscovery;
tokenManager: TokenManager;
};
/** @public */
export type NotificationSendOptions = {
entityRef: string | string[];
title: string;
description: string;
link: string;
};
/** @public */
export class DefaultNotificationService implements NotificationService {
private constructor(
private readonly logger: LoggerService,
private readonly discovery: PluginEndpointDiscovery,
private readonly tokenManager: TokenManager,
) {}
static create({
logger,
tokenManager,
discovery,
}: NotificationServiceOptions): DefaultNotificationService {
return new DefaultNotificationService(logger, discovery, tokenManager);
}
async send(options: NotificationSendOptions): Promise<Notification[]> {
try {
const baseUrl = await this.discovery.getBaseUrl('notifications');
const { token } = await this.tokenManager.getToken();
const response = await fetch(`${baseUrl}/notifications`, {
method: 'POST',
body: JSON.stringify(options),
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
return await response.json();
} catch (error) {
this.logger.error(`Failed to send notifications: ${error}`);
return [];
}
}
}
@@ -1,34 +0,0 @@
/*
* 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 { Notification } from '@backstage/plugin-notifications-common';
/** @public */
export type NotificationProcessor = {
/**
* Decorate notification before sending it
*
* @param notification - The notification to decorate
* @returns The same notification or a modified version of it
*/
decorate?(notification: Notification): Promise<Notification>;
/**
* Send notification using this processor.
*
* @param notification - The notification to send
*/
send?(notification: Notification): Promise<void>;
};
@@ -1,5 +1,5 @@
/*
* Copyright 2023 The Backstage Authors
* Copyright 2024 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.
@@ -13,163 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Notification,
NotificationIcon,
} from '@backstage/plugin-notifications-common';
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { NotificationsStore } from '../database/NotificationsStore';
import { v4 as uuid } from 'uuid';
import {
Entity,
isGroupEntity,
isUserEntity,
RELATION_HAS_MEMBER,
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
PluginDatabaseManager,
PluginEndpointDiscovery,
} from '@backstage/backend-common';
import { DatabaseNotificationsStore } from '../database';
import { NotificationProcessor } from './NotificationProcessor';
import { NotificationSendOptions } from './DefaultNotificationService';
import { Notification } from '@backstage/plugin-notifications-common';
/** @public */
export type NotificationServiceOptions = {
database: PluginDatabaseManager;
discovery: PluginEndpointDiscovery;
processors?: NotificationProcessor[];
export type NotificationService = {
send(options: NotificationSendOptions): Promise<Notification[]>;
};
/** @public */
export type NotificationSendOptions = {
entityRef: string | string[];
title: string;
description: string;
link: string;
image?: string;
icon?: NotificationIcon;
};
/** @public */
export class NotificationService {
private store: NotificationsStore | null = null;
private readonly processors: NotificationProcessor[];
private constructor(
private readonly database: PluginDatabaseManager,
private readonly catalog: CatalogApi,
processors?: NotificationProcessor[],
) {
this.processors = processors ?? [];
}
static create({
database,
discovery,
processors,
}: NotificationServiceOptions): NotificationService {
const catalogClient = new CatalogClient({
discoveryApi: discovery,
});
return new NotificationService(database, catalogClient, processors);
}
addProcessor(processor: NotificationProcessor) {
this.processors.push(processor);
return this;
}
async send(options: NotificationSendOptions): Promise<Notification[]> {
const { entityRef, title, description, link, icon, image } = options;
const notifications = [];
let users = [];
try {
users = await this.getUsersForEntityRef(entityRef);
} catch (e) {
return [];
}
const store = await this.getStore();
const baseNotification = {
id: uuid(),
title,
description,
link,
created: new Date(),
icon,
image,
saved: false,
};
for (const user of users) {
let notification: Notification = { ...baseNotification, userRef: user };
for (const processor of this.processors) {
notification = processor.decorate
? await processor.decorate(notification)
: notification;
}
await store.saveNotification(notification);
for (const processor of this.processors) {
if (processor.send) {
processor.send(notification);
}
}
notifications.push(notification);
// TODO: Signal service
}
return notifications;
}
async getStore(): Promise<NotificationsStore> {
if (!this.store) {
this.store = await DatabaseNotificationsStore.create({
database: this.database,
});
}
return this.store;
}
private async getUsersForEntityRef(
entityRef: string | string[],
): Promise<string[]> {
const refs = Array.isArray(entityRef) ? entityRef : [entityRef];
const entities = await this.catalog.getEntitiesByRefs({ entityRefs: refs });
const mapEntity = async (entity: Entity | undefined): Promise<string[]> => {
if (!entity) {
return [];
}
if (isUserEntity(entity)) {
return [stringifyEntityRef(entity)];
} else if (isGroupEntity(entity) && entity.relations) {
return entity.relations
.filter(
relation =>
relation.type === RELATION_HAS_MEMBER && relation.targetRef,
)
.map(r => r.targetRef);
} else if (!isGroupEntity(entity) && entity.spec?.owner) {
const owner = await this.catalog.getEntityByRef(
entity.spec.owner as string,
);
if (owner) {
return mapEntity(owner);
}
}
return [];
};
const users: string[] = [];
for (const entity of entities.items) {
const u = await mapEntity(entity);
users.push(...u);
}
return users;
}
}
@@ -13,5 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './NotificationService';
export * from './NotificationProcessor';
export * from './DefaultNotificationService';
export type { NotificationService } from './NotificationService';