feat: initial notifications support

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2023-12-15 14:05:35 +02:00
parent 778ea9e152
commit f24a0c1f6a
59 changed files with 1964 additions and 7 deletions
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+5
View File
@@ -0,0 +1,5 @@
# @backstage/plugin-notifications-node
Welcome to the Node.js library package for the notifications plugin!
_This plugin was created through the Backstage CLI_
+70
View File
@@ -0,0 +1,70 @@
## API Report File for "@backstage/plugin-notifications-node"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
import { NotificationStatus } from '@backstage/plugin-notifications-common';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
// @public (undocumented)
export class DatabaseNotificationsStore implements NotificationsStore {
// (undocumented)
static create({
database,
skipMigrations,
}: {
database: PluginDatabaseManager;
skipMigrations?: boolean;
}): Promise<DatabaseNotificationsStore>;
// (undocumented)
getNotifications(options: NotificationGetOptions): Promise<any[]>;
// (undocumented)
getStatus(options: NotificationGetOptions): Promise<{
unread: number;
read: number;
}>;
// (undocumented)
saveNotification(notification: Notification_2): Promise<void>;
}
// @public (undocumented)
export type NotificationGetOptions = {
user_ref: string;
};
// @public (undocumented)
export class NotificationService {
// (undocumented)
static create({
database,
discovery,
}: NotificationServiceOptions): NotificationService;
// (undocumented)
getStore(): Promise<NotificationsStore>;
// (undocumented)
send(
entityRef: string | string[],
title: string,
description: string,
link: string,
): Promise<Notification_2[]>;
}
// @public (undocumented)
export type NotificationServiceOptions = {
database: PluginDatabaseManager;
discovery: PluginEndpointDiscovery;
};
// @public (undocumented)
export interface NotificationsStore {
// (undocumented)
getNotifications(options: NotificationGetOptions): Promise<Notification_2[]>;
// (undocumented)
getStatus(options: NotificationGetOptions): Promise<NotificationStatus>;
// (undocumented)
saveNotification(notification: Notification_2): Promise<void>;
}
```
@@ -0,0 +1,34 @@
/*
* 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.
*/
exports.up = async function up(knex) {
await knex.schema.createTable('notifications', table => {
table.uuid('id').primary();
table.string('userRef').notNullable();
table.string('title').notNullable();
table.text('description').notNullable();
table.text('link').notNullable();
table.datetime('created').notNullable();
table.datetime('read').nullable();
});
};
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.dropTable('notifications');
};
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@backstage/plugin-notifications-node",
"description": "Node.js library for the notifications plugin",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "node-library"
},
"scripts": {
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
],
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/plugin-notifications-common": "workspace:^",
"knex": "^3.0.0",
"uuid": "^8.0.0"
}
}
@@ -0,0 +1,97 @@
/*
* 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,
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;
};
async getNotifications(options: NotificationGetOptions) {
const { user_ref } = options;
const notificationQuery = this.db('notifications').where(
'userRef',
user_ref,
);
const notifications = await notificationQuery.select('*');
return notifications;
}
async saveNotification(notification: Notification) {
await this.db.insert(notification).into('notifications');
}
async getStatus(options: NotificationGetOptions) {
const { user_ref } = options;
const notificationQuery = this.db('notifications').where(
'userRef',
user_ref,
);
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),
};
}
}
@@ -0,0 +1,36 @@
/*
* 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,
} from '@backstage/plugin-notifications-common';
/** @public */
export type NotificationGetOptions = {
user_ref: string;
};
/** @public */
export interface NotificationsStore {
getNotifications(options: NotificationGetOptions): Promise<Notification[]>;
saveNotification(notification: Notification): Promise<void>;
getStatus(options: NotificationGetOptions): Promise<NotificationStatus>;
// TODO: Mark as read/unread by notification id(s)
}
@@ -0,0 +1,17 @@
/*
* 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';
+24
View File
@@ -0,0 +1,24 @@
/*
* 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.
*/
/**
* Node.js library for the notifications plugin.
*
* @packageDocumentation
*/
export * from './database';
export * from './service';
@@ -0,0 +1,136 @@
/*
* 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 { 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';
/** @public */
export type NotificationServiceOptions = {
database: PluginDatabaseManager;
discovery: PluginEndpointDiscovery;
};
/** @public */
export class NotificationService {
private store: NotificationsStore | null = null;
private constructor(
private readonly database: PluginDatabaseManager,
private readonly catalog: CatalogApi,
) {}
static create({
database,
discovery,
}: NotificationServiceOptions): NotificationService {
const catalogClient = new CatalogClient({
discoveryApi: discovery,
});
return new NotificationService(database, catalogClient);
}
async send(
entityRef: string | string[],
title: string,
description: string,
link: string,
): Promise<Notification[]> {
const users = await this.getUsersForEntityRef(entityRef);
const notifications = [];
const store = await this.getStore();
for (const user of users) {
const notification = {
id: uuid(),
userRef: user,
title,
description,
link,
created: new Date(),
};
await store.saveNotification(notification);
notifications.push(notification);
}
// TODO: Signal service
// TODO: Other senders
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;
}
}
@@ -0,0 +1,16 @@
/*
* 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 './NotificationService';
@@ -0,0 +1,16 @@
/*
* 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 {};