Skip to content

Commit

Permalink
Features API
Browse files Browse the repository at this point in the history
Implement features API endpoint.
  • Loading branch information
panaaj committed Sep 20, 2023
1 parent d8882fc commit a892979
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 4 deletions.
67 changes: 67 additions & 0 deletions src/api/features/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* 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`
const FEATURES_API_PATH = `${SIGNALK_API_PATH}/features`

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

interface FeatureInfo {
apis: string[]
plugins: string[]
}

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) => {
this.initApiRoutes()
resolve()
})
}

// Exposed to plugins
public getFeatures() {
this.features.plugins = (this.server as any).plugins.map(
(plugin: any) => {
return {
id: plugin.id,
name: plugin.name,
version: plugin.version
}
}
)
this.features.apis = this.server.apis.slice()
return this.features
}

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.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
}
14 changes: 11 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,11 +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
app.apis.push('resources')
const courseApi = new CourseApi(app, resourcesApi)
Promise.all([resourcesApi.start(), courseApi.start()])
app.apis.push('course')
const featuresApi = new FeaturesApi(app)
Promise.all([resourcesApi.start(), courseApi.start(), featuresApi.start()])
}
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
4 changes: 4 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ export interface SelfIdentity {
selfId: string
selfContext: string
}

export interface WithFeatures {
apis: string[]
}
11 changes: 10 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,8 @@ class Server {

app.providerStatus = {}

app.apis = []

// 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

0 comments on commit a892979

Please sign in to comment.