Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add Feature discovery API endpoint and plugin interface #1630

Merged
merged 27 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a892979
Features API
panaaj Sep 20, 2023
efa4d23
Create interface for plugins.
panaaj Sep 22, 2023
aa679e1
chore: format
panaaj Sep 22, 2023
9135032
Add getFeatures and registerFeature definitions
panaaj Sep 23, 2023
be80e2a
Only add api feature to apiList once
panaaj Sep 26, 2023
6f93c91
Merge branch 'master' into features_api
panaaj Sep 27, 2023
072fbd2
API feature registration moved to API constructor.
panaaj Mar 22, 2024
713f306
Add option to show only enabled plugins.
panaaj Mar 29, 2024
42de518
Remove registration method.
panaaj Mar 30, 2024
0a813eb
chore: formatting
panaaj Mar 30, 2024
82fbe6e
typings and API name list
panaaj Mar 31, 2024
e222331
align with discovery OpenApi
panaaj Apr 2, 2024
5bb8a50
addressed review comments
panaaj Apr 9, 2024
c39e4cc
SignalKApiId is type, not const
tkurki Apr 16, 2024
17b0f67
move FeatureInfo to server-api, use SignalKApiId
tkurki Apr 16, 2024
645d0a0
remove duplicated type: use FeatureInfo
tkurki Apr 16, 2024
79fc9c8
refactor: rename
tkurki Apr 16, 2024
f2ce485
refactor: remove unused code
tkurki Apr 16, 2024
36a71af
replace shared module const with return value
tkurki Apr 16, 2024
f0732d1
add history apis from the specification
tkurki Apr 16, 2024
9ad53f3
Fix apis not being returned in response.
panaaj Apr 17, 2024
273d2c5
chore: format
panaaj Apr 17, 2024
25e3c49
Update docs
panaaj Apr 18, 2024
df19419
Merge branch 'master' into features_api
panaaj Apr 18, 2024
b236aa5
fix duplicates
panaaj Apr 19, 2024
ed1ef16
update doc
panaaj Apr 19, 2024
458ee4c
Do not return apis when enabled=false.
panaaj Apr 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions docs/src/develop/plugins/server_plugin_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,66 @@

# Server API for plugins

SignalK server provides an interface to allow plugins to access / update the full data model, operations and send / receive deltas (updates).
SignalK server provides an interface to allow plugins to:
- Discover Features.
- Access / update the full data model
- send / receive deltas (updates)
- Interact with APIs
- Expose HTTP endpoints

These functions are available via the `app` passed to the plugin when it is invoked.

These functions are available via the `app` object passed to the plugin when it is invoked.

---

### Discover Features

#### `app.getFeatures(enabed)`

Returns an object detailing the available APIs and Plugins.

The `enabled` parameter is optional and has the following values:
- `undefined` (not provided): list all features
- `true`: list only enabled features
- `false`: list only disabled features

_Example:_
```javascript
let features = app.getFeatures();

{
"apis": [
"resources","course"
],
"plugins": [
{
"id": "anchoralarm",
"name": "Anchor Alarm",
"version": "1.13.0",
"enabled": true
},
{
"id": "autopilot",
"name": "Autopilot Control",
"version": "1.4.0",
"enabled": false
},
{
"id": "sk-to-nmea2000",
"name": "Signal K to NMEA 2000",
"version": "2.17.0",
"enabled": false
},
{
"id": "udp-nmea-sender",
"name": "UDP NMEA0183 Sender",
"version": "2.0.0",
"enabled": false
}
]
}
```

### Accessing the Data Model

#### `app.getPath(path)`
Expand Down
49 changes: 49 additions & 0 deletions docs/src/develop/webapps.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,55 @@ GET /signalk/v1/applicationData/user/my-application
[ "1.0", "1.1"]
```

## Discovering Server Features

To assist in tailoring a WebApps UI, it can "discover" the features supported by the server by sending a request to `/signalk/v2/features`.

The response wil contain an object detailing the available APIs and Plugins.

You can use the `enabled` parameter to specify to only return enabled or disabled features.

To list only enabled features:
`/signalk/v2/features?enable=1`

To list only disabled features:
`/signalk/v2/features?enable=0`

_Example response:_
```JSON
{
"apis": [
"resources","course"
],
"plugins": [
{
"id": "anchoralarm",
"name": "Anchor Alarm",
"version": "1.13.0",
"enabled": true
},
{
"id": "autopilot",
"name": "Autopilot Control",
"version": "1.4.0",
"enabled": false
},
{
"id": "sk-to-nmea2000",
"name": "Signal K to NMEA 2000",
"version": "2.17.0",
"enabled": false
},
{
"id": "udp-nmea-sender",
"name": "UDP NMEA0183 Sender",
"version": "2.0.0",
"enabled": false
}
]
}
```

## Embedded Components and Admin UI / Server interfaces

Embedded components are implemented using [Webpack Federated Modules](https://webpack.js.org/concepts/module-federation/) and [React Code Splitting](https://reactjs.org/docs/code-splitting.html).
Expand Down
23 changes: 23 additions & 0 deletions packages/server-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ import { PointDestination, RouteDestination, CourseInfo } from './coursetypes'

export * from './autopilotapi'

export type SignalKApiId =
| 'resources'
| 'course'
| 'history'
| 'autopilot'
| 'anchor'
| 'logbook'
| 'historyplayback' //https://signalk.org/specification/1.7.0/doc/streaming_api.html#history-playback
| 'historysnapshot' //https://signalk.org/specification/1.7.0/doc/rest_api.html#history-snapshot-retrieval

export {
PropertyValue,
PropertyValues,
Expand Down Expand Up @@ -113,6 +123,16 @@ export interface Metadata {
description?: string
}

export interface FeatureInfo {
apis: SignalKApiId[]
plugins: Array<{
id: string
name: string
version: string
enabled: boolean
}>
}

export interface ServerAPI extends PluginServerApp {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getSelfPath: (path: string) => any
Expand Down Expand Up @@ -172,6 +192,9 @@ export interface ServerAPI extends PluginServerApp {
) => void
}) => void
getSerialPorts: () => Promise<Ports>

getFeatures: () => FeatureInfo

getCourse: () => Promise<CourseInfo>
clearDestination: () => Promise<void>
setDestination: (
Expand Down
40 changes: 40 additions & 0 deletions src/api/discovery/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createDebug } from '../../debug'
const debug = createDebug('signalk-server:api:features')

import { IRouter, Request, Response } from 'express'

import { WithFeatures } from '../../app'

const FEATURES_API_PATH = `/signalk/v2/features`

interface FeaturesApplication extends IRouter, WithFeatures {}

export class FeaturesApi {
constructor(private app: FeaturesApplication) {}

async start() {
return new Promise<void>((resolve) => {
this.initApiRoutes()
resolve()
})
}

private initApiRoutes() {
debug(`** Initialise ${FEATURES_API_PATH} path handlers **`)
// return Feature information
this.app.get(
`${FEATURES_API_PATH}`,
async (req: Request, res: Response) => {
debug(`** GET ${req.path}`)

const enabled = ['true', '1'].includes(req.query.enabled as string)
? true
: ['false', '0'].includes(req.query.enabled as string)
? false
: undefined

res.json(await this.app.getFeatures(enabled))
}
)
}
}
143 changes: 139 additions & 4 deletions src/api/discovery/openApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,19 @@
},
"servers": [
{
"url": "/"
"url": "/signalk"
}
],
"tags": [
{
"name": "server",
"description": "Signal K Server."
},
{
"name": "features",
"description": "Signal K Server features."
}
],
"tags": [],
"components": {
"schemas": {
"DiscoveryData": {
Expand Down Expand Up @@ -70,6 +79,46 @@
}
}
}
},
"PluginMetaData": {
"type": "object",
"required": ["id", "name", "version"],
"description": "Plugin metadata.",
"properties": {
"id": {
"type": "string",
"description": "Plugin ID."
},
"name": {
"type": "string",
"description": "Plugin name."
},
"version": {
"type": "string",
"description": "Plugin verison."
}
}
},
"FeaturesModel": {
"type": "object",
"required": ["apis", "plugins"],
"description": "Features response",
"properties": {
"apis": {
"type": "array",
"description": "Implemented APIs.",
"items": {
"type": "string"
}
},
"plugins": {
"type": "array",
"description": "Installed Plugins.",
"items": {
"$ref": "#/components/schemas/PluginMetaData"
}
}
}
}
},
"responses": {
Expand All @@ -82,21 +131,107 @@
}
}
}
},
"200Ok": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"state": {
"type": "string",
"enum": ["COMPLETED"]
},
"statusCode": {
"type": "number",
"enum": [200]
}
},
"required": ["state", "statusCode"]
}
}
}
},
"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"]
}
}
}
},
"FeaturesResponse": {
"description": "Server features response.",
"content": {
"application/json": {
"schema": {
"description": "Features response.",
"$ref": "#/components/schemas/FeaturesModel"
}
}
}
}
}
},

"paths": {
"/signalk": {
"/": {
"get": {
"tags": [],
"tags": ["server"],
"summary": "Retrieve server version and service endpoints.",
"description": "Returns data about server's endpoints and versions.",
"responses": {
"200": {
"$ref": "#/components/responses/DiscoveryResponse"
}
}
}
},
"/v2/features": {
"get": {
"tags": ["features"],
"parameters": [
{
"name": "enabled",
"in": "query",
"description": "Limit results to enabled features.",
"required": false,
"explode": false,
"schema": {
"type": "string",
"enum": ["enabled", "1", "false", "0"]
}
}
],
"summary": "Retrieve available server features.",
"description": "Returns object detailing the available server features.",
"responses": {
"200": {
"$ref": "#/components/responses/FeaturesResponse"
},
"default": {
"$ref": "#/components/responses/ErrorResponse"
}
}
}
}
}
}
Loading