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 7 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
11 changes: 11 additions & 0 deletions packages/server-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ export interface ServerAPI extends PluginServerApp {
) => void
}) => void
getSerialPorts: () => Promise<Ports>

getFeatures: () => {
apis: string[]
panaaj marked this conversation as resolved.
Show resolved Hide resolved
plugins: {
id: string
name: string
version: string
}
}
registerFeature: (type: string, id: string) => void
tkurki marked this conversation as resolved.
Show resolved Hide resolved

getCourse: () => Promise<CourseInfo>
clearDestination: () => Promise<void>
setDestination: (
Expand Down
3 changes: 3 additions & 0 deletions src/api/course/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const { Location, RoutePoint, VesselPosition } = COURSE_POINT_TYPES
import { isValidCoordinate } from 'geolib'
import { Responses } from '../'
import { Store } from '../../serverstate/store'
import { WithFeatures } from '../../app'

import { buildSchemaSync } from 'api-schema-builder'
import courseOpenApi from './openApi.json'
Expand All @@ -46,6 +47,7 @@ interface CourseApplication
extends IRouter,
WithConfig,
WithSecurityStrategy,
WithFeatures,
SignalKMessageHub {}

export class CourseApi {
Expand All @@ -65,6 +67,7 @@ export class CourseApi {
private resourcesApi: ResourcesApi
) {
this.store = new Store(server, 'course')
server.registerFeature('api', 'course')
}

async start() {
Expand Down
52 changes: 52 additions & 0 deletions src/api/features/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createDebug } from '../../debug'
const debug = createDebug('signalk-server:api:features')

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

import { SignalKMessageHub, WithConfig, WithFeatures } from '../../app'
import { WithSecurityStrategy } from '../../security'

const SIGNALK_API_PATH = `/signalk/v2/api`
panaaj marked this conversation as resolved.
Show resolved Hide resolved
const FEATURES_API_PATH = `${SIGNALK_API_PATH}/features`

interface FeaturesApplication
extends IRouter,
WithConfig,
WithFeatures,
WithSecurityStrategy,
SignalKMessageHub {}

interface FeatureInfo {
apis: string[]
plugins: string[]
panaaj marked this conversation as resolved.
Show resolved Hide resolved
}

export class FeaturesApi {
private features: FeatureInfo = {
apis: [],
plugins: []
}

constructor(private server: FeaturesApplication) {}

async start() {
// eslint-disable-next-line no-async-promise-executor
return new Promise<void>(async (resolve) => {
panaaj marked this conversation as resolved.
Show resolved Hide resolved
this.initApiRoutes()
resolve()
})
}

private initApiRoutes() {
debug(`** Initialise ${FEATURES_API_PATH} path handlers **`)
// return Feature information
this.server.get(
`${FEATURES_API_PATH}`,
async (req: Request, res: Response) => {
debug(`** GET ${FEATURES_API_PATH}`)
res.json(this.server.getFeatures())
}
)
}
}
142 changes: 142 additions & 0 deletions src/api/features/openApi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
{
"openapi": "3.0.0",
"info": {
"version": "2.0.0",
"title": "Signal K Features 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."
},
"tags": [
{
"name": "features",
"description": "Signal K Server features."
}
],
"components": {
"schemas": {
"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": {
"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": {
"/": {
"get": {
"tags": ["features"],
"summary": "Retrieve available server features.",
"description": "Returns object detailing the available server features.",
"responses": {
"200": {
"$ref": "#/components/responses/FeaturesResponse"
},
"default": {
"$ref": "#/components/responses/ErrorResponse"
}
}
}
}
}
}
8 changes: 8 additions & 0 deletions src/api/features/openApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { OpenApiDescription } from '../swagger'
import featuresApiDoc from './openApi.json'

export const featuresApiRecord = {
name: 'features',
path: '/signalk/v2/api/features',
apiDoc: featuresApiDoc as unknown as OpenApiDescription
}
12 changes: 9 additions & 3 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { IRouter } from 'express'
import { SignalKMessageHub, WithConfig } from '../app'
import { SignalKMessageHub, WithConfig, WithFeatures } from '../app'
import { WithSecurityStrategy } from '../security'
import { CourseApi } from './course'
import { FeaturesApi } from './features'
import { ResourcesApi } from './resources'

export interface ApiResponse {
Expand Down Expand Up @@ -37,13 +38,18 @@ export const Responses = {
}

export const startApis = (
app: SignalKMessageHub & WithSecurityStrategy & IRouter & WithConfig
app: SignalKMessageHub &
WithSecurityStrategy &
IRouter &
WithConfig &
WithFeatures
) => {
const resourcesApi = new ResourcesApi(app)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(app as any).resourcesApi = resourcesApi
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 featuresApi = new FeaturesApi(app)
Promise.all([resourcesApi.start(), courseApi.start(), featuresApi.start()])
}
4 changes: 3 additions & 1 deletion src/api/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { WithSecurityStrategy } from '../../security'

import { Responses } from '../'
import { validate } from './validate'
import { SignalKMessageHub } from '../../app'
import { SignalKMessageHub, WithFeatures } from '../../app'

export const RESOURCES_API_PATH = `/signalk/v2/api/resources`

Expand All @@ -27,13 +27,15 @@ export const skUuid = () => `${uuidv4()}`
interface ResourceApplication
extends IRouter,
WithSecurityStrategy,
WithFeatures,
SignalKMessageHub {}

export class ResourcesApi {
private resProvider: { [key: string]: Map<string, ResourceProviderMethods> } =
{}

constructor(app: ResourceApplication) {
app.registerFeature('api', 'resources')
this.initResourceRoutes(app)
}

Expand Down
2 changes: 2 additions & 0 deletions src/api/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IRouter, NextFunction, Request, Response } from 'express'
import swaggerUi from 'swagger-ui-express'
import { SERVERROUTESPREFIX } from '../constants'
import { courseApiRecord } from './course/openApi'
import { featuresApiRecord } from './features/openApi'
import { notificationsApiRecord } from './notifications/openApi'
import { resourcesApiRecord } from './resources/openApi'
import { securityApiRecord } from './security/openApi'
Expand All @@ -28,6 +29,7 @@ const apiDocs = [
appsApiRecord,
securityApiRecord,
courseApiRecord,
featuresApiRecord,
notificationsApiRecord,
resourcesApiRecord
].reduce<ApiRecords>((acc, apiRecord: OpenApiRecord) => {
Expand Down
14 changes: 14 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,17 @@ export interface SelfIdentity {
selfId: string
selfContext: string
}

panaaj marked this conversation as resolved.
Show resolved Hide resolved
interface FeaturesCollection {
apis: string[]
plugins: {
id: string
name: string
version: string
}
}

export interface WithFeatures {
getFeatures: () => FeaturesCollection
registerFeature: (type: string, id: string) => void
}
35 changes: 34 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ import https from 'https'
import _ from 'lodash'
import path from 'path'
import { startApis } from './api'
import { SelfIdentity, ServerApp, SignalKMessageHub, WithConfig } from './app'
import {
SelfIdentity,
ServerApp,
SignalKMessageHub,
WithConfig,
WithFeatures
} from './app'
import { ConfigApp, load, sendBaseDeltas } from './config/config'
import { createDebug } from './debug'
import DeltaCache from './deltacache'
Expand Down Expand Up @@ -72,6 +78,7 @@ class Server {
app: ServerApp &
SelfIdentity &
WithConfig &
WithFeatures &
SignalKMessageHub &
PluginManager &
WithSecurityStrategy &
Expand Down Expand Up @@ -116,6 +123,32 @@ class Server {

app.providerStatus = {}

// feature registration
app.apiList = []

app.registerFeature = (type: string, id: string) => {
if (type === 'api' && !app.apiList.includes(id)) {
panaaj marked this conversation as resolved.
Show resolved Hide resolved
app.apiList.push(id)
}
}

// feature detection
app.getFeatures = () => {
const features = {
apis: [],
plugins: []
}
features.plugins = app.plugins.map((plugin: any) => {
panaaj marked this conversation as resolved.
Show resolved Hide resolved
return {
id: plugin.id,
name: plugin.name,
version: plugin.version
}
})
features.apis = app.apiList.slice()
return features
}

// create first temporary pluginManager to get typechecks, as
// app is any and not typechecked
// TODO separate app.plugins and app.pluginsMap from app
Expand Down
Loading