chore: refactor service and backend to use REST
Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
@@ -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';
|
||||
@@ -20,6 +20,5 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './database';
|
||||
export * from './service';
|
||||
export * from './lib';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user