diff --git a/SERVERPLUGINS.md b/SERVERPLUGINS.md index 3c419003d..bf443430a 100644 --- a/SERVERPLUGINS.md +++ b/SERVERPLUGINS.md @@ -729,6 +729,49 @@ app.registerDeltaInputHandler((delta, next) => { }) ``` +### `app.notify(path, value, pluginId)` + +Notifications API interface method for raising, updating and clearing notifications. + + - `path`: Signal K path of the notification + + - `value`: A valid `Notification` object or `null` if clearing a notification. + + - `pluginId` The plugin identifier. + + +To raise or update a for a specified path, call the method with a valid `Notification` object as the `value`. + +- returns: `string` value containing the `id` of the new / updated notification. + +_Example:_ +```javascript +const alarmId = app.notify( + 'myalarm', + { + message: 'My cutom alarm text', + state: 'alert' + }, + 'myAlarmPlugin' +) + +// alarmId = "ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a" +``` + +To clear (cancel) a notification call the method with `null` as the `value`. + +- returns: `void`. + +_Example: Clear notification_ +```javascript +const alarmId = app.notify( + 'myalarm', + null, + 'myAlarmPlugin' +) +``` + + ### `app.registerResourceProvider(resourceProvider)` See [`RESOURCE_PROVIDER_PLUGINS`](./RESOURCE_PROVIDER_PLUGINS.md) for details. diff --git a/WORKING_WITH_NOTIFICATIONS_API.md b/WORKING_WITH_NOTIFICATIONS_API.md new file mode 100644 index 000000000..33627910d --- /dev/null +++ b/WORKING_WITH_NOTIFICATIONS_API.md @@ -0,0 +1,356 @@ +# Working with the Notifications API + + +## Overview + +The SignalK Notifications API provides the ability to raise and action notifications / alarms using `HTTP` requests. + +The API provides endpoints at the following path `/signalk/v2/api/notifications`. + +**See the [OpenAPI documentation](https://demo.signalk.io/admin/openapi/) in the Signal K server Admin UI (under Documentation) for details.** + +--- + +## Operation + +The Notifications API manages the raising, actioning and clearing of notifications. + +It does this by providing: +1. HTTP endpoints for interactive use +1. An Interface for use by plugins and connection handlers. + +In this way, notifications triggered by both stream data and client interactions are consistently represented in the Signal K data model and that actions (e.g. acknowledge, silence, etc) and their resulting status is preseved and available to all connected devices. + +Additionally, the Notifications API applies a unique `id` to each notification which can be used in as an alternative to the `path` and `$source` to identify a notification entry. + + +## Using the API Plugin Interface +--- + +The Notifications API exposes the `notify()` method for use by plugins for raising, updating and clearing notifications. + +**`app.notify(path, value, sourceId)`** + + - `path`: Signal K path of the notification + + - `value`: A valid `Notification` object or `null` if clearing a notification. + + - `sourceId` The source identifier associated with the notification. + + +To raise (create) a new notification or update and existing notification call the method with a valid `Notification` object as the `value`. + +- returns: `string` value containing the `id` of the new / updated notification. + +_Example: Raise notification_ +```javascript +const alarmId = app.notify( + 'myalarm', + { + message: 'My alarm text', + state: 'alert' + }, + 'myAlarmPlugin' +) + +// alarmId = "ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a" +``` + +To clear (cancel) a notification call the method with `null` as the `value`. + +- returns: `void`. + +_Example: Clear notification_ +```javascript +const alarmId = app.notify( + 'myalarm', + null, + 'myAlarmPlugin' +) +``` + +## Using HTTP Endpoints +--- + +### Raising a Notification + +To create (or raise) a notification you submit a HTTP `PUT` request to the specified `path` under `/signalk/v2/api/notifications`. + +_Example: Raise notification_ +```typescript +HTTP PUT 'http://hostname:3000/signalk/v2/api/notifications/myalarm' { + "message": "My alarm text.", + "state": "alert" +} +``` + +You can also provide additional data values associated with the alarm. + +_Example: Raise notification with temperature values_ +```typescript +HTTP PUT 'http://hostname:3000/signalk/v2/api/notifications/myalarm' { + "message": "My alarm text.", + "state": "alert", + "data": { + "temperature": { + "outside": 293.5, + "inside": 289.7 + } + } +} +``` + +If the action is successful, a response containing the `id` of the notification is generated. + +_Example response:_ +```JSON +{ + "state": "COMPLETED", + "statusCode": 201, + "id": "ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a" +} +``` + +This `id` can be used to perform actions on the notification. + + +### Updating notification content +--- + +To update the information contained in a notification, you need to replace it by submitting another `HTTP PUT` request containing a the new values. + +You can either use the notification `path` or `id` to update it. + +_Example: Update notification by path_ +```typescript +HTTP PUT 'http://hostname:3000/signalk/v2/api/notifications/myalarm' { + "message": "New alarm text.", + "state": "warning" +} +``` + +_Example: Update notification by id_ +```typescript +HTTP PUT 'http://hostname:3000/signalk/v2/api/notifications/ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a' { + "message": "New alarm text.", + "state": "warning" +} +``` + +### Clear a notification +--- + +To clear or cancel a notification submit a `HTTP DELETE` request to either the notification `path` or `id`. + +_Example: Clear notification by path_ +```typescript +HTTP DELETE 'http://hostname:3000/signalk/v2/api/notifications/myalarm' +``` + +_Example: Clear notification by id_ +```typescript +HTTP DELETE 'http://hostname:3000/signalk/v2/api/notifications/ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a' +``` + +Additionally, you can clear a notification with a specific `$source` by providing the source value as a query parameter. + +_Example: Clear notification by path created by `zone-watch`_ +```typescript +HTTP DELETE 'http://hostname:3000/signalk/v2/api/notifications/enteredZone?source=zone-watch' +``` + +### Acknowledge a notification +--- + +To acknowledge a notification, submit a `HTTP PUT` request to `http://hostname:3000/signalk/v2/api/notifications/ack/`. + +This adds the **`actions`** property to the notification which holds a list of actions taken on the notification. "ACK" will be added to the list of actions when a notificationis acknowledged. + +``` +{ + ... + "actions": ["ACK"], + ... +} +``` + +_Example: Acknowledge notification using a path_ +```typescript +HTTP PUT 'http://hostname:3000/signalk/v2/api/notifications/ack/myalarm' +``` + +_Example: Acknowledge notification using an id_ +```typescript +HTTP PUT 'http://hostname:3000/signalk/v2/api/notifications/ack/ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a' +``` + +_Acknowledged notification response._ +```JSON +{ + "message": "Man Overboard!", + "method": [ + "sound", + "visual" + ], + "actions": ["ACK"], + "state": "emergency", + "id": "96171e52-38de-45d9-aa32-30633553f58d", + "data": { + "position": { + "longitude": -166.18340908333334, + "latitude": 60.03309133333333 + } + } +} +``` + +### Standard Alarms +--- + +Standard alarms, such as Man Overboard, can be raised submitting a HTTP `POST` request to the specified `alarm path`. + +These alarms will be raised by the server with pre-defined content. + + +_Example: Raise Man Overboard Alarm_ +```typescript +HTTP POST 'http://hostname:3000/signalk/v2/api/notifications/mob' +``` + +_Notification content._ +```JSON +{ + "message": "Man Overboard!", + "method": [ + "sound", + "visual" + ], + "state": "emergency", + "id": "96171e52-38de-45d9-aa32-30633553f58d", + "data": { + "position": { + "longitude": -166.18340908333334, + "latitude": 60.03309133333333 + } + } +} +``` + +## View / List notifications +--- + +### View a specified notification +To view a specific notification submit a `HTTP GET` request to either the notification `path` or `id`. + +_Example: Retrieve notification by path_ +```typescript +HTTP GET 'http://hostname:3000/signalk/v2/api/notifications/myalarm' +``` + +_Example: Retrieve notification by id_ +```typescript +HTTP GET 'http://hostname:3000/signalk/v2/api/notifications/ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a' +``` + +_Response: Includes `path` attribute associated with the notification._ +```JSON +{ + "meta": {}, + "value": { + "message": "My test alarm!", + "method": ["sound", "visual"], + "state": "alert", + "id": "d3f1be57-2672-4c4d-8dc1-0978dea7a8d6", + "data": { + "position": { + "lat": 12, + "lon": 148 + } + } + }, + "$source": "notificationsApi", + "timestamp": "2023-06-08T07:52:52.459Z", + "path": "notifications.myalarm" + } +``` + +_Example: Retrieve notification by path with the specified $source_ +```typescript +HTTP GET 'http://hostname:3000/signalk/v2/api/notifications/myalarm?source=zone-watch' +``` + +### View a list notifications +A list of notifications generated using the Notifications API and be retrieved by submitting a `HTTP GET` request to `http://hostname:3000/signalk/v2/api/notifications`. + +By default the list of notification objects will be keyed by their `path`. + +_Example: Notification list keyed by path (default)_ +```JSON +{ + "notifications.myalarm": { + "value": { + "message": "My test alarm!", + "method": ["sound", "visual"], + "state": "alert", + "id": "d3f1be57-2672-4c4d-8dc1-0978dea7a8d6", + }, + "$source": "notificationsApi", + "timestamp": "2023-06-08T07:52:52.459Z" + }, + "notifications.mob": { + "value": { + "message": "Man Overboard!", + "method": ["sound", "visual"], + "state": "emergency", + "id": "ff105ae9-43d5-4039-abaf-afeefb03566e", + "data": { + "position": "No vessel position data." + } + }, + "$source": "notificationsApi", + "timestamp": "2023-06-08T07:52:54.124Z" + } +} +``` + +To view a list of notifications keyed by their identifier, add `key=id` to the request. + +```typescript +HTTP GET 'http://hostname:3000/signalk/v2/api/notifications?key=id` +``` + +```JSON +{ + "d3f1be57-2672-4c4d-8dc1-0978dea7a8d6": { + "value": { + "message": "My test alarm!", + "method": ["sound", "visual"], + "state": "alert", + "id": "d3f1be57-2672-4c4d-8dc1-0978dea7a8d6", + "data": { + "position": { + "lat": 12, + "lon": 148 + } + } + }, + "$source": "notificationsApi", + "timestamp": "2023-06-08T07:52:52.459Z", + "path": "notifications.myalarm" + }, + "ff105ae9-43d5-4039-abaf-afeefb03566e": { + "value": { + "message": "Man Overboard!", + "method": ["sound", "visual"], + "state": "emergency", + "id": "ff105ae9-43d5-4039-abaf-afeefb03566e", + "data": { + "position": "No vessel position data." + } + }, + "$source": "notificationsApi", + "timestamp": "2023-06-08T07:52:54.124Z", + "path": "notifications.mob" + } +} +``` diff --git a/packages/server-api/src/deltas.ts b/packages/server-api/src/deltas.ts index 2e60078bd..8d38ea291 100644 --- a/packages/server-api/src/deltas.ts +++ b/packages/server-api/src/deltas.ts @@ -63,6 +63,8 @@ export interface Notification { state: ALARM_STATE method: ALARM_METHOD[] message: string + data?: { [key: string]: object | number | string | null } + id?: string } // MetaMessage diff --git a/packages/server-api/src/index.ts b/packages/server-api/src/index.ts index a73c08c04..21cf23ac4 100644 --- a/packages/server-api/src/index.ts +++ b/packages/server-api/src/index.ts @@ -22,6 +22,7 @@ export enum SKVersion { export type Brand = K & { __brand: T } export * from './deltas' +import { Notification } from './deltas' export * from './coursetypes' export * from './resourcetypes' export * from './resourcesapi' @@ -128,6 +129,7 @@ export interface ServerAPI extends PluginServerApp { source: string // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => Promise + notify: (path: string, value: Notification, source: string) => void //TSTODO convert queryRequest to ts // eslint-disable-next-line @typescript-eslint/no-explicit-any queryRequest: (requestId: string) => Promise diff --git a/src/api/index.ts b/src/api/index.ts index 62f40f0a5..656795113 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -3,11 +3,12 @@ import { SignalKMessageHub, WithConfig } from '../app' import { WithSecurityStrategy } from '../security' import { CourseApi } from './course' import { ResourcesApi } from './resources' +import { NotificationsApi } from './notifications' export interface ApiResponse { state: 'FAILED' | 'COMPLETED' | 'PENDING' statusCode: number - message: string + message?: string requestId?: string href?: string token?: string @@ -45,5 +46,12 @@ export const startApis = ( const courseApi = new CourseApi(app, resourcesApi) // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(app as any).courseApi = courseApi - Promise.all([resourcesApi.start(), courseApi.start()]) + const notificationsApi = new NotificationsApi(app) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(app as any).notificationsApi = notificationsApi + Promise.all([ + resourcesApi.start(), + courseApi.start(), + notificationsApi.start() + ]) } diff --git a/src/api/notifications/index.ts b/src/api/notifications/index.ts new file mode 100644 index 000000000..59c8e44b4 --- /dev/null +++ b/src/api/notifications/index.ts @@ -0,0 +1,578 @@ +/* + API for working with Notifications / Alarms. +*/ + +import { createDebug } from '../../debug' +const debug = createDebug('signalk-server:api:notifications') + +import { IRouter, Request, Response } from 'express' +import _ from 'lodash' +import { v4 as uuidv4 } from 'uuid' + +import { SignalKMessageHub, WithConfig } from '../../app' +import { WithSecurityStrategy } from '../../security' + +import { + ALARM_METHOD, + ALARM_STATE, + Notification, + SKVersion +} from '@signalk/server-api' + +import { buildSchemaSync } from 'api-schema-builder' +import notificationsApiDoc from './openApi.json' + +const NOTI_API_SCHEMA = buildSchemaSync(notificationsApiDoc) + +const SIGNALK_API_PATH = `/signalk/v2/api` +const NOTI_API_PATH = `${SIGNALK_API_PATH}/notifications` +const $SRC = 'notificationsApi' + +interface NotificationsApplication + extends IRouter, + WithConfig, + WithSecurityStrategy, + SignalKMessageHub {} + +export class NotificationsApi { + private idToPathMap: Map + + constructor(private server: NotificationsApplication) { + this.idToPathMap = new Map() + } + + async start() { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + this.initApiEndpoints() + resolve() + }) + } + + /** public interface methods */ + notify(path: string, value: Notification | null, source: string) { + debug(`** Interface:put(${path}, value, ${source})`) + if (!path || !source) { + throw new Error('Path and source values must be specified!') + } + if (path.split('.')[0] !== 'notifications') { + throw new Error('Invalid notifications path!') + } + + try { + if (!value) { + this.clearNotificationAtPath(path, source) + } else { + return this.setNotificationAtPath(path, value, source) + } + } catch (e) { + debug((e as Error).message) + throw e + } + } + + private updateAllowed(request: Request): boolean { + return this.server.securityStrategy.shouldAllowPut( + request, + 'vessels.self', + null, + 'notifications' + ) + } + + private initApiEndpoints() { + debug(`** Initialise ${NOTI_API_PATH} path handlers **`) + + // Raise man overboard alarm + this.server.post(`${NOTI_API_PATH}/mob`, (req: Request, res: Response) => { + debug(`** POST ${NOTI_API_PATH}/mob`) + + const notiPath = `notifications.mob` + const pos = this.getSelfPath('navigation.position') + try { + const notiValue: Notification = { + message: 'Man Overboard!', + method: [ALARM_METHOD.sound, ALARM_METHOD.visual], + state: ALARM_STATE.emergency, + id: uuidv4(), + data: { + position: pos ? pos.value : 'No vessel position data.' + } + } + this.updateModel(notiPath, notiValue, $SRC) + + res.status(201).json({ + state: 'COMPLETED', + statusCode: 201, + id: notiValue.id + }) + } catch (e) { + res.status(400).json({ + state: 'FAILED', + statusCode: 400, + message: (e as Error).message + }) + } + }) + + // Acknowledge notification + this.server.put(`${NOTI_API_PATH}/ack/*`, (req: Request, res: Response) => { + debug(`** PUT ${NOTI_API_PATH}/ack/${req.params[0]}`) + debug(`** params ${JSON.stringify(req.query)}`) + const source = (req.query.source as string) ?? $SRC + + try { + const id = this.pathIsUuid(req.params[0]) + let noti + if (id) { + debug(`** id detected: Fetch Notification with id = ${id}`) + noti = this.getNotificationById(id) + if (!noti) { + res.status(400).json({ + state: 'FAILED', + statusCode: 404, + message: `Notification with id = ${id} NOT found!` + }) + return + } + } else { + const notiPath = `notifications.` + req.params[0].split('/').join('.') + noti = this.getNotificationByPath(notiPath, source) + if (noti) { + res.status(200).json(noti) + } else { + res.status(400).json({ + state: 'FAILED', + statusCode: 404, + message: `Notification ${notiPath} NOT found!` + }) + return + } + } + if (noti.value.actions && Array.isArray(noti.value.actions)) { + if (!noti.value.actions.includes('ACK')) { + noti.value.actions.push('ACK') + } + } else { + noti.value.actions = ['ACK'] + } + } catch (e) { + res.status(400).json({ + state: 'FAILED', + statusCode: 400, + message: (e as Error).message + }) + } + }) + + // Create / update notification + this.server.put(`${NOTI_API_PATH}/*`, (req: Request, res: Response) => { + debug(`** PUT ${NOTI_API_PATH}/${req.params[0]}`) + debug(JSON.stringify(req.body)) + + /*if (!this.updateAllowed(req)) { + res.status(403).json(Responses.unauthorised) + return + }*/ + if (!req.params[0]) { + res.status(400).json({ + state: 'FAILED', + statusCode: 400, + message: 'No path provided!' + }) + } + + try { + /*const endpoint = + NOTI_API_SCHEMA[`${NOTI_API_PATH}/:standardAlarm`].put + if (!endpoint.body.validate(req.body)) { + res.status(400).json(endpoint.body.errors) + return + }*/ + let id = this.pathIsUuid(req.params[0]) + let notiPath: string + if (id) { + notiPath = this.idToPathMap.get(id) as string + debug(`** id supplied: PUT(${id}) ---> mapped to path ${notiPath}`) + if (!notiPath) { + res.status(400).json({ + state: 'FAILED', + statusCode: 400, + message: `Supplied id is not mapped to a notification path!` + }) + return + } + } else { + notiPath = `notifications.` + req.params[0].split('/').join('.') + debug(`** path supplied: ${notiPath}`) + } + + const notiValue: Notification = { + message: req.body.message ?? '', + method: this.getNotificationMethod(), + state: req.body.state ?? ALARM_STATE.alert + } + if (req.body.data) { + notiValue.data = req.body.data + } + + id = this.setNotificationAtPath(notiPath, notiValue, $SRC) + + res.status(201).json({ + state: 'COMPLETED', + statusCode: 201, + id: id + }) + } catch (e) { + res.status(400).json({ + state: 'FAILED', + statusCode: 400, + message: (e as Error).message + }) + } + }) + + // Clear notification + this.server.delete(`${NOTI_API_PATH}/*`, (req: Request, res: Response) => { + debug(`** DELETE ${NOTI_API_PATH}/${req.params[0]}`) + debug(`** params ${JSON.stringify(req.query)}`) + /* + if (!this.updateAllowed(req)) { + res.status(403).json(Responses.unauthorised) + return + } + */ + const source = (req.query.source as string) ?? $SRC + debug(`** source = ${source}`) + try { + const id = this.pathIsUuid(req.params[0]) + if (id) { + debug(`** id supplied: ${id}`) + this.clearNotificationWithId(id) + res.status(200).json({ + state: 'COMPLETED', + statusCode: 200 + }) + } else { + const notiPath = `notifications.` + req.params[0].split('/').join('.') + debug(`** path supplied: Clear ${notiPath} from $source= ${source}`) + this.clearNotificationAtPath(notiPath, source) + res.status(200).json({ + state: 'COMPLETED', + statusCode: 200 + }) + } + } catch (e) { + res.status(400).json({ + state: 'FAILED', + statusCode: 400, + message: (e as Error).message + }) + } + }) + + // List notifications keyed by either path or id + this.server.get(`${NOTI_API_PATH}`, (req: Request, res: Response) => { + debug(`** GET ${NOTI_API_PATH}`) + debug(`** params ${JSON.stringify(req.query)}`) + const keyById = req.query.key === 'id' ? true : false + try { + const notiList: { [key: string]: Notification } = {} + this.idToPathMap.forEach((path, id) => { + const noti = this.getNotificationById(id, keyById) + if (noti) { + const key = keyById ? id : path + notiList[key] = noti + } + }) + res.status(200).json(notiList) + } catch (e) { + res.status(400).json({ + state: 'FAILED', + statusCode: 400, + message: (e as Error).message + }) + } + }) + + // Return notification + this.server.get(`${NOTI_API_PATH}/*`, (req: Request, res: Response) => { + debug(`** GET ${NOTI_API_PATH}/*`) + debug(`** params ${JSON.stringify(req.query)}`) + const source = req.query.source as string + + try { + const id = this.pathIsUuid(req.params[0]) + if (id) { + debug(`** id detected: getNotificationById(${id})`) + const noti = this.getNotificationById(id, true) + if (noti) { + res.status(200).json(noti) + } else { + res.status(400).json({ + state: 'FAILED', + statusCode: 404, + message: `Notification with id = ${id} NOT found!` + }) + } + } else { + const notiPath = `notifications.` + req.params[0].split('/').join('.') + let noti + if (source) { + debug(`** filtering results by source: ${source}`) + noti = this.getNotificationByPath(notiPath, source) + } else { + noti = this.getSelfPath(notiPath) + } + if (noti) { + res.status(200).json(noti) + } else { + res.status(400).json({ + state: 'FAILED', + statusCode: 404, + message: `Notification ${notiPath} NOT found!` + }) + } + } + } catch (e) { + res.status(400).json({ + state: 'FAILED', + statusCode: 400, + message: (e as Error).message + }) + } + }) + } + + /** Clear Notification with provided id + * @param id: UUID of notification to clear + */ + private clearNotificationWithId(id: string) { + if (!this.idToPathMap.has(id)) { + throw new Error(`Notification with id = ${id} NOT found!`) + } + const path = this.idToPathMap.get(id) + // Get $source of notification + const noti = this.getSelfPath(path as string) + const source = this.sourceOfId(noti, id) ?? $SRC + this.updateModel(path as string, null, source) + this.idToPathMap.delete(id) + } + + /** Clear Notification at `path` raised by the specified $source + * @param path: signal k path in dot notation + * @param source: $source value to use. + */ + private clearNotificationAtPath(path: string, source: string) { + debug(`** path supplied: Clear ${path} from $source= ${source}`) + // Get notification value for the supplied source + const noti = this.getSelfPath(path) + const notiValue = this.valueWithSource(noti, source) + if (!notiValue) { + throw new Error( + `No notification found at ${path} that is from ${source}!` + ) + } + // Check notification for an id, if present then delete from map + if (notiValue.id && this.idToPathMap.has(notiValue.id)) { + debug(`** id detected..removing from map: ${notiValue.id}`) + this.idToPathMap.delete(notiValue.id) + } + this.updateModel(path, null, source) + } + + /** Set Notification value and $source at supplied path. + * @param path: signal k path in dot notation + * @param value: value to assign to path + * @param source: source identifier + * @returns id assigned to notification + */ + private setNotificationAtPath( + path: string, + value: Notification, + source: string + ): string { + debug(`** Set Notification at ${path} with $source= ${source}`) + // get id from existing value or generate id + const noti = this.getSelfPath(path) + const nv = noti ? this.valueWithSource(noti, source) : null + value.id = nv && nv.id ? noti.value.id : uuidv4() + debug(`** id = ${value.id}`) + + this.updateModel(path, value, source) + + return value.id as string + } + + /** TODO *** Get the Notification method for the supplied Notification type */ + private getNotificationMethod = (type?: string) => { + if (!type) { + return [ALARM_METHOD.sound, ALARM_METHOD.visual] + } else { + // return method for supplied type from settings + return [ALARM_METHOD.sound, ALARM_METHOD.visual] + } + } + + /** Maintain id mapping and send delta. + * @param path: signal k path in dot notation + * @param value: value to assign to path + * @param source: source identifier + */ + private updateModel = ( + path: string, + value: Notification | null, + source: string + ) => { + debug(`****** Sending ${path} Notification: ******`) + debug(`value: `, JSON.stringify(value)) + debug(`source: `, source ?? 'self (default)') + + if (value && value.id) { + debug(`ADDING to idToPathMap(${value.id})`) + this.idToPathMap.set(value.id, path) + } + + this.server.handleMessage( + source, + { + updates: [ + { + values: [ + { + path: path, + value: value + } + ] + } + ] + }, + SKVersion.v1 + ) + } + + /** Checks if path is a UUID + * @param path: UUID or signal k path in / notation + * @returns UUID value (or empty string + * */ + private pathIsUuid(path: string): string { + const testId = (id: string): boolean => { + const uuid = RegExp( + '^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$' + ) + return uuid.test(id) + } + const p = path.indexOf('/') !== -1 ? path.split('/') : path.split('.') + if (p.length === 1 && testId(p[0])) { + return p[0] + } else { + return '' + } + } + + /** Get Signal K object from `self` at supplied path. + * @param path: signal k path in dot notation + * @returns signal k object + */ + private getSelfPath(path: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return _.get((this.server.signalk as any).self, path) + } + + /** Get Notification object with the supplied id. + * Note: values attribute (if present) is omitted from the returned object! + * @param id: notification id value to match. + @param incPath: If true includes a path attribute containing the signal k path. + @returns signal k object or null + */ + private getNotificationById(id: string, incPath?: boolean) { + if (this.idToPathMap.has(id)) { + const path = this.idToPathMap.get(id) + debug(`getNotificationById(${id}) => ${path}`) + const n = this.getSelfPath(path as string) + if (n['$source'] !== $SRC) { + const v = this.valueWithSource(n, $SRC) + if (!v) { + return null + } + n.value = v + n['$source'] !== $SRC + } + delete n.values + + const noti = Object.assign({}, n, incPath ? { path: path } : {}) + debug(`**NOTIFICATION with id = ${id}`, JSON.stringify(noti)) + return noti + } else { + debug(`idToPathMap(${id}) => NOT FOUND`) + return null + } + } + + /** Get Notification object at specified path with the value from the supplied $source. + * Note: values attribute (if present) is omitted from the returned object! + * @param path: signal k path in dot notation. + @param source: source identifier of the value to return + @returns signal k object or null + */ + private getNotificationByPath(path: string, source: string = $SRC) { + const n = this.getSelfPath(path as string) + if (n['$source'] !== source) { + const v = this.valueWithSource(n, source) + if (!v) { + console.log(`*** Couldn't find $source = ${source}`) + return null + } + n.value = v + n['$source'] = source + } + delete n.values + const noti = Object.assign({}, n) + debug(`**NOTIFICATION at ${path} from ${source}`, JSON.stringify(noti)) + return noti + } + + // returns $source value of supplied SK object with the specified id attribute value + /** Get the $source of the notification with the supplied id (including when multiple values are present). + * @param o: signal k object + * @param id: notification id + * @returns astring containing the value of $source | undefined + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private sourceOfId(o: any, id: string) { + let src + if (o.values) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Object.entries(o.values).forEach((e: Array) => { + if (e[1].value && e[1].value.id && e[1].value.id === id) { + src = e[0] + } + }) + } else { + if (o.value && o.value.id && o.value.id === id) { + src = o['$source'] + } + } + debug(`** sourceWithId(${id}) = ${src}`) + return src + } + + /** Get the value (including when multiple values are present) with the provided $source. + * @param o: signal k object + * @param source: $source identifier of desired value. + * @returns Notification | null + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private valueWithSource(o: any, source: string) { + let v + if (o.values && o.values[source]) { + v = Object.assign({}, o.values[source].value) + } else { + if (o['$source'] === source) { + v = Object.assign({}, o.value) + } + } + debug(`** valueWithSource(${source}) = ${JSON.stringify(v)}`) + return v + } +} diff --git a/src/api/notifications/openApi.json b/src/api/notifications/openApi.json index a28e33d58..344377992 100644 --- a/src/api/notifications/openApi.json +++ b/src/api/notifications/openApi.json @@ -7,7 +7,8 @@ "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - } + }, + "description": "API for raising and actioning Notifications / Alarms. Notifications raised using this API are assigned an id which can be used for subsequent actions." }, "externalDocs": { "url": "http://signalk.org/specification/", @@ -15,91 +16,169 @@ }, "servers": [ { - "url": "/signalk/v1/api/vessels/self/notifications" + "url": "/signalk/v2/api/notifications" } ], "tags": [ { "name": "notifications", - "description": "Root path" + "description": "General actions." + }, + { + "name": "via path", + "description": "Action notifications via specified path." }, { - "name": "special", - "description": "Special Alarms" + "name": "via id", + "description": "Action notifications via supplied id." }, { - "name": "course", - "description": "Course notifications" + "name": "alarms", + "description": "Standard, pre-defined alarms." } ], "components": { "schemas": { - "AlarmState": { + "UuidDef": { "type": "string", - "description": "Value describing the current state of the alarm.", - "example": "alert", - "enum": ["normal", "nominal", "alert", "warning", "alarm", "emergency"] + "pattern": "[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$", + "example": "ac3a3b2d-07e8-4f25-92bc-98e7c92f7f1a" }, - "AlarmMethod": { + "PathDef": { + "type": "string", + "description": "Notification path.", + "example": "notifications.mob" + }, + "MethodDef": { "type": "array", "minimum": 0, "maximum": 2, "uniqueItems": true, - "description": "Methods to use to raise the alarm.", + "description": "How the alarm is actioned.", "example": ["sound"], "items": { "type": "string", "enum": ["visual", "sound"] } }, - "Alarm": { + "SeverityDef": { + "type": "string", + "description": "Severity of the alarm.", + "example": "alert", + "enum": ["normal", "nominal", "alert", "warning", "alarm", "emergency"] + }, + "MessageDef": { + "type": "string", + "description": "Notification message to display.", + "example": "My message!" + }, + "TypeDef": { + "type": "string", + "description": "Type of notification.", + "example": "OverVoltage" + }, + "DataDef": { + "type": "object", + "additionalProperties": true, + "description": "Data values associated with this notification." + }, + "RequestModel": { "type": "object", - "required": ["state", "method", "message"], "properties": { "state": { - "$ref": "#/components/schemas/AlarmState" - }, - "method": { - "$ref": "#/components/schemas/AlarmMethod" + "$ref": "#/components/schemas/SeverityDef" }, "message": { - "type": "string" + "$ref": "#/components/schemas/MessageDef" + }, + "data": { + "$ref": "#/components/schemas/DataDef" + }, + "type": { + "$ref": "#/components/schemas/TypeDef" } } }, - "Notification": { + "ResponseModel": { + "description": "Notification information", "type": "object", - "required": ["value"], + "required": ["timestamp", "$source"], "properties": { + "timestamp": { + "type": "string" + }, + "$source": { + "type": "string" + }, "value": { - "$ref": "#/components/schemas/Alarm" + "type": "object", + "required": ["method", "state", "message"], + "properties": { + "method": { + "$ref": "#/components/schemas/MethodDef" + }, + "state": { + "$ref": "#/components/schemas/SeverityDef" + }, + "message": { + "$ref": "#/components/schemas/MessageDef" + }, + "data": { + "$ref": "#/components/schemas/DataDef" + }, + "id": { + "$ref": "#/components/schemas/UuidDef" + }, + "type": { + "$ref": "#/components/schemas/TypeDef" + } + } } } - }, - "Notifications": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/Alarm" - } } }, "responses": { - "ListResponse": { - "description": "Collection of notifications", + "200Ok": { + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Notifications" + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": ["COMPLETED"] + }, + "statusCode": { + "type": "number", + "enum": [200] + } + }, + "required": ["state", "statusCode"] } } } }, - "200Ok": { - "description": "OK", + "201ActionResponse": { + "description": "Action response - success.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Notification" + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": ["COMPLETED"] + }, + "statusCode": { + "type": "number", + "enum": [201] + }, + "id": { + "$ref": "#/components/schemas/UuidDef" + } + }, + "required": ["id", "statusCode", "state"] } } } @@ -128,30 +207,105 @@ } } } + }, + "NotificationResponse": { + "description": "Notification information response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseModel" + } + } + } + } + }, + "parameters": { + "Source": { + "name": "source", + "description": "The source that raised the notification at the defined path.", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "cookieAuth": { + "type": "apiKey", + "in": "cookie", + "name": "JAUTHENTICATION" } } }, + "security": [{ "cookieAuth": [] }, { "bearerAuth": [] }], "paths": { "/": { "get": { "tags": ["notifications"], - "summary": "Notifications endpoint.", - "description": "Root path for notifications.", - "responses": { - "200": { - "$ref": "#/components/responses/ListResponse" + "summary": "Filtered notification list keyed by id.", + "description": "Retrieve list of notifications filtered by the specified parameter.", + "parameters": [ + { + "name": "type", + "description": "Filter results by notification type.", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/TypeDef" + } + }, + { + "name": "severity", + "description": "Filter results by alarm severity.", + "in": "query", + "required": false, + "schema": { + "$ref": "#/components/schemas/SeverityDef" + } }, + { + "name": "key", + "description": "List results by provided key value.", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": ["path", "id"] + } + } + ], + "responses": { "default": { - "$ref": "#/components/responses/ErrorResponse" + "description": "An object containing notifications, keyed by their id.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "allOf": [ + { + "$ref": "#/components/schemas/ResponseModel" + } + ] + } + } + } + } } } } }, "/mob": { - "get": { - "tags": ["special"], + "post": { + "tags": ["alarms"], "summary": "Man overboard alarm.", - "description": "Alarm indicating person(s) overboard.", + "description": "Raise a Man overboard alarm with system generated message including vessel position.", "responses": { "200": { "$ref": "#/components/responses/200Ok" @@ -162,56 +316,42 @@ } } }, - "/fire": { - "get": { - "tags": ["special"], - "summary": "Fire onboard alarm.", - "description": "Alarm indicating there is a fire onboard.", - "responses": { - "200": { - "$ref": "#/components/responses/200Ok" - }, - "default": { - "$ref": "#/components/responses/ErrorResponse" + "/{id}": { + "parameters": [ + { + "name": "id", + "description": "Notification identifier.", + "in": "path", + "required": true, + "schema": { + "$ref": "#/components/schemas/UuidDef" } } - } - }, - "/sinking": { + ], "get": { - "tags": ["special"], - "summary": "Sinking vessel alarm.", - "description": "Alarm indicating vessel is sinking.", + "tags": ["via id"], + "summary": "Retrieve notification information.", + "description": "Retrieve information for notification with supplied id.", "responses": { - "200": { - "$ref": "#/components/responses/200Ok" - }, "default": { - "$ref": "#/components/responses/ErrorResponse" + "$ref": "#/components/responses/NotificationResponse" } } - } - }, - "/flooding": { - "get": { - "tags": ["special"], - "summary": "Floodingalarm.", - "description": "Alarm indicating that veseel is taking on water.", - "responses": { - "200": { - "$ref": "#/components/responses/200Ok" - }, - "default": { - "$ref": "#/components/responses/ErrorResponse" + }, + "put": { + "tags": ["via id"], + "summary": "Action a notification.", + "description": "Raise or action the notification with the specified id.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestModel" + } + } } - } - } - }, - "/collision": { - "get": { - "tags": ["special"], - "summary": "Collision alarm.", - "description": "Alarm indicating vessel has been involved in a collision.", + }, "responses": { "200": { "$ref": "#/components/responses/200Ok" @@ -220,13 +360,11 @@ "$ref": "#/components/responses/ErrorResponse" } } - } - }, - "/grounding": { - "get": { - "tags": ["special"], - "summary": "Grounding alarm.", - "description": "Alarm indicating vessel has run aground.", + }, + "delete": { + "tags": ["via id"], + "summary": "Clear notification.", + "description": "Clear the notification with the supplied id.", "responses": { "200": { "$ref": "#/components/responses/200Ok" @@ -237,86 +375,54 @@ } } }, - "/listing": { + "/*": { "get": { - "tags": ["special"], - "summary": "Listing alarm.", - "description": "Alarm indicating vessel is listing beyond acceptable parameters.", - "responses": { - "200": { - "$ref": "#/components/responses/200Ok" - }, - "default": { - "$ref": "#/components/responses/ErrorResponse" + "tags": ["via path"], + "summary": "Retrieve notification information.", + "description": "Retrieve information for notification with supplied id.", + "parameters": [ + { + "$ref": "#/components/parameters/Source" } - } - } - }, - "/adrift": { - "get": { - "tags": ["special"], - "summary": "Adrift alarm.", - "description": "Alarm indicating that the vessel is set adrift.", + ], "responses": { - "200": { - "$ref": "#/components/responses/200Ok" - }, "default": { - "$ref": "#/components/responses/ErrorResponse" + "$ref": "#/components/responses/NotificationResponse" } } - } - }, - "/piracy": { - "get": { - "tags": ["special"], - "summary": "Piracy alarm.", - "description": "Alarm indicating pirates have been encountered / boarded.", - "responses": { - "200": { - "$ref": "#/components/responses/200Ok" - }, - "default": { - "$ref": "#/components/responses/ErrorResponse" + }, + "put": { + "tags": ["via path"], + "summary": "Action a notification.", + "description": "Raise or action a notification at the specified path.", + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestModel" + } + } } - } - } - }, - "/abandon": { - "get": { - "tags": ["special"], - "summary": "Abandon alarm.", - "description": "Alarm indicating vessel has been abandoned.", + }, "responses": { - "200": { - "$ref": "#/components/responses/200Ok" + "201": { + "$ref": "#/components/responses/201ActionResponse" }, "default": { "$ref": "#/components/responses/ErrorResponse" } } - } - }, - "/navigation/course/arrivalCircleEntered": { - "get": { - "tags": ["course"], - "summary": "Arrival circle entered.", - "description": "Set when arrival circle around destination point has been entered.", - "responses": { - "200": { - "$ref": "#/components/responses/200Ok" - }, - "default": { - "$ref": "#/components/responses/ErrorResponse" + }, + "delete": { + "tags": ["via path"], + "summary": "Clear notification.", + "description": "Clear the specified notification.", + "parameters": [ + { + "$ref": "#/components/parameters/Source" } - } - } - }, - "/navigation/course/perpendicularPassed": { - "get": { - "tags": ["course"], - "summary": "Perpendicular passed.", - "description": "Set when line perpendicular to destination point has been passed by the vessel.", + ], "responses": { "200": { "$ref": "#/components/responses/200Ok" diff --git a/src/api/notifications/openApi.ts b/src/api/notifications/openApi.ts index ffc4c0f49..3de54479f 100644 --- a/src/api/notifications/openApi.ts +++ b/src/api/notifications/openApi.ts @@ -3,6 +3,6 @@ import notificationsApiDoc from './openApi.json' export const notificationsApiRecord = { name: 'notifications', - path: '/signalk/v1/api/vessels/self/notifications', + path: '/signalk/v2/api/notifications', apiDoc: notificationsApiDoc as unknown as OpenApiDescription } diff --git a/src/api/notificationsv1/openApi.json b/src/api/notificationsv1/openApi.json new file mode 100644 index 000000000..a28e33d58 --- /dev/null +++ b/src/api/notificationsv1/openApi.json @@ -0,0 +1,331 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "2.0.0", + "title": "Signal K Notifications API", + "termsOfService": "http://signalk.org/terms/", + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "externalDocs": { + "url": "http://signalk.org/specification/", + "description": "Signal K specification." + }, + "servers": [ + { + "url": "/signalk/v1/api/vessels/self/notifications" + } + ], + "tags": [ + { + "name": "notifications", + "description": "Root path" + }, + { + "name": "special", + "description": "Special Alarms" + }, + { + "name": "course", + "description": "Course notifications" + } + ], + "components": { + "schemas": { + "AlarmState": { + "type": "string", + "description": "Value describing the current state of the alarm.", + "example": "alert", + "enum": ["normal", "nominal", "alert", "warning", "alarm", "emergency"] + }, + "AlarmMethod": { + "type": "array", + "minimum": 0, + "maximum": 2, + "uniqueItems": true, + "description": "Methods to use to raise the alarm.", + "example": ["sound"], + "items": { + "type": "string", + "enum": ["visual", "sound"] + } + }, + "Alarm": { + "type": "object", + "required": ["state", "method", "message"], + "properties": { + "state": { + "$ref": "#/components/schemas/AlarmState" + }, + "method": { + "$ref": "#/components/schemas/AlarmMethod" + }, + "message": { + "type": "string" + } + } + }, + "Notification": { + "type": "object", + "required": ["value"], + "properties": { + "value": { + "$ref": "#/components/schemas/Alarm" + } + } + }, + "Notifications": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Alarm" + } + } + }, + "responses": { + "ListResponse": { + "description": "Collection of notifications", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Notifications" + } + } + } + }, + "200Ok": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Notification" + } + } + } + }, + "ErrorResponse": { + "description": "Failed operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Request error response", + "properties": { + "state": { + "type": "string", + "enum": ["FAILED"] + }, + "statusCode": { + "type": "number", + "enum": [404] + }, + "message": { + "type": "string" + } + }, + "required": ["state", "statusCode", "message"] + } + } + } + } + } + }, + "paths": { + "/": { + "get": { + "tags": ["notifications"], + "summary": "Notifications endpoint.", + "description": "Root path for notifications.", + "responses": { + "200": { + "$ref": "#/components/responses/ListResponse" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/mob": { + "get": { + "tags": ["special"], + "summary": "Man overboard alarm.", + "description": "Alarm indicating person(s) overboard.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/fire": { + "get": { + "tags": ["special"], + "summary": "Fire onboard alarm.", + "description": "Alarm indicating there is a fire onboard.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/sinking": { + "get": { + "tags": ["special"], + "summary": "Sinking vessel alarm.", + "description": "Alarm indicating vessel is sinking.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/flooding": { + "get": { + "tags": ["special"], + "summary": "Floodingalarm.", + "description": "Alarm indicating that veseel is taking on water.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/collision": { + "get": { + "tags": ["special"], + "summary": "Collision alarm.", + "description": "Alarm indicating vessel has been involved in a collision.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/grounding": { + "get": { + "tags": ["special"], + "summary": "Grounding alarm.", + "description": "Alarm indicating vessel has run aground.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/listing": { + "get": { + "tags": ["special"], + "summary": "Listing alarm.", + "description": "Alarm indicating vessel is listing beyond acceptable parameters.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/adrift": { + "get": { + "tags": ["special"], + "summary": "Adrift alarm.", + "description": "Alarm indicating that the vessel is set adrift.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/piracy": { + "get": { + "tags": ["special"], + "summary": "Piracy alarm.", + "description": "Alarm indicating pirates have been encountered / boarded.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/abandon": { + "get": { + "tags": ["special"], + "summary": "Abandon alarm.", + "description": "Alarm indicating vessel has been abandoned.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/navigation/course/arrivalCircleEntered": { + "get": { + "tags": ["course"], + "summary": "Arrival circle entered.", + "description": "Set when arrival circle around destination point has been entered.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + }, + "/navigation/course/perpendicularPassed": { + "get": { + "tags": ["course"], + "summary": "Perpendicular passed.", + "description": "Set when line perpendicular to destination point has been passed by the vessel.", + "responses": { + "200": { + "$ref": "#/components/responses/200Ok" + }, + "default": { + "$ref": "#/components/responses/ErrorResponse" + } + } + } + } + } +} diff --git a/src/api/notificationsv1/openApi.ts b/src/api/notificationsv1/openApi.ts new file mode 100644 index 000000000..ffc4c0f49 --- /dev/null +++ b/src/api/notificationsv1/openApi.ts @@ -0,0 +1,8 @@ +import { OpenApiDescription } from '../swagger' +import notificationsApiDoc from './openApi.json' + +export const notificationsApiRecord = { + name: 'notifications', + path: '/signalk/v1/api/vessels/self/notifications', + apiDoc: notificationsApiDoc as unknown as OpenApiDescription +} diff --git a/src/api/swagger.ts b/src/api/swagger.ts index 028b8719e..233c90b43 100644 --- a/src/api/swagger.ts +++ b/src/api/swagger.ts @@ -24,12 +24,12 @@ interface ApiRecords { } const apiDocs = [ - discoveryApiRecord, appsApiRecord, - securityApiRecord, courseApiRecord, + discoveryApiRecord, notificationsApiRecord, - resourcesApiRecord + resourcesApiRecord, + securityApiRecord ].reduce((acc, apiRecord: OpenApiRecord) => { acc[apiRecord.name] = apiRecord return acc diff --git a/src/interfaces/plugins.ts b/src/interfaces/plugins.ts index ec245f58b..ff264b6fd 100644 --- a/src/interfaces/plugins.ts +++ b/src/interfaces/plugins.ts @@ -32,6 +32,7 @@ import fs from 'fs' import _ from 'lodash' import path from 'path' import { ResourcesApi } from '../api/resources' +import { NotificationsApi } from '../api/notifications' import { CourseApi } from '../api/course' import { SERVERROUTESPREFIX } from '../constants' import { createDebug } from '../debug' @@ -522,6 +523,12 @@ module.exports = (theApp: any) => { resourcesApi.register(plugin.id, provider) } + const notificationsApi: NotificationsApi = app.notificationsApi + _.omit(appCopy, 'notificationsApi') // don't expose the actual notifications api manager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + appCopy.notify = (path: string, value: any, source: string) => { + notificationsApi.notify(path, value, source) + const courseApi: CourseApi = app.courseApi _.omit(appCopy, 'courseApi') // don't expose the actual course api manager appCopy.getCourse = () => {