UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

176 lines (175 loc) 6.41 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HistoryApiHttpRegistry = void 0; const history_1 = require("@signalk/server-api/history"); const polyfill_1 = require("@js-temporal/polyfill"); const debug_1 = require("../../debug"); const debug = (0, debug_1.createDebug)('signalk-server:api:history'); class HistoryApiHttpRegistry { app; provider; providerPluginId; proxy; constructor(app) { this.app = app; this.proxy = { getValues: (query) => { return this.provider.getValues(query); }, getContexts: (query) => { return this.provider.getContexts(query); }, getPaths: (query) => { return this.provider.getPaths(query); } }; app.getHistoryApi = () => { return this.provider ? Promise.resolve(this.proxy) : Promise.reject('No history api provider configured'); }; } registerHistoryApiProvider(pluginId, provider) { if (!(0, history_1.isHistoryApi)(provider)) { throw new Error('Invalid history api provider'); } debug(`Registering history api provider ${pluginId}`); this.providerPluginId = pluginId; this.provider = provider; } unregisterHistoryApiProvider(pluginId) { if (this.providerPluginId !== pluginId) { throw new Error('No history api provider registered for pluginId ' + pluginId); } debug(`Unregistering history api provider ${pluginId}`); this.provider = undefined; this.providerPluginId = undefined; } start() { this.app.get('/signalk/v2/history/values', (req, res) => respondWith(this.provider, () => { return this.provider?.getValues(parseValuesQuery(req.query)); }, req, res)); this.app.get('/signalk/v2/history/contexts', (req, res) => respondWith(this.provider, () => { const { timeRangeParams, errors } = parseTimeRangeParams(req.query); if (errors.length > 0) { throw new Error(`Validation errors: ${errors.join(', ')}`); } debug(JSON.stringify(timeRangeParams, null, 2)); return this.provider?.getContexts(timeRangeParams); }, req, res)); this.app.get('/signalk/v2/history/paths', (req, res) => respondWith(this.provider, () => { const { timeRangeParams, errors } = parseTimeRangeParams(req.query); if (errors.length > 0) { throw new Error(`Validation errors: ${errors.join(', ')}`); } debug(JSON.stringify(timeRangeParams, null, 2)); return this.provider?.getPaths(timeRangeParams); }, req, res)); } } exports.HistoryApiHttpRegistry = HistoryApiHttpRegistry; async function respondWith(provider, handler, req, res) { if (!provider) { return res.status(501).json({ error: 'No history api provider configured' }); } try { res.json(await handler()); } catch (error) { res.status(400).json({ error: error instanceof Error ? error.message : 'Invalid request' }); } } const parseValuesQuery = (query) => { const { timeRangeParams, errors } = parseTimeRangeParams(query); const context = query.context; const resolution = getMaybeNumber(query.resolution); const paths = query.paths; if (!paths) { errors.push('paths parameter is required and must be a string'); } if (errors.length > 0) { throw new Error(`Validation errors: ${errors.join(', ')}`); } const pathExpressions = (query.paths || '') .replace(/[^0-9a-z.,:]/gi, '') .split(','); const pathSpecs = pathExpressions.map(splitPathExpression); const parsed = { ...timeRangeParams, context, resolution, pathSpecs }; debug(JSON.stringify(parsed, null, 2)); return parsed; }; const getMaybeNumber = (value) => { if (typeof value === 'string') return Number(value); if (typeof value === 'number') return value; return undefined; }; const splitPathExpression = (pathExpression) => { const parts = pathExpression.split(':'); let aggregateMethod = (parts[1] || 'average'); if (parts[0] === 'navigation.position') { aggregateMethod = 'first'; } return { path: parts[0], aggregate: aggregateMethod }; }; const parseTimeRangeParams = (query) => { const errors = []; const fromStr = query.from; let from; if (fromStr) { try { from = polyfill_1.Temporal.Instant.from(fromStr); } catch (error) { errors.push(`from parameter must be a valid ISO 8601 timestamp: ${error instanceof Error ? error.message : 'Invalid format'}`); } } const durationStr = query.duration; const durationNum = getMaybeNumber(query.duration); let duration; if (durationStr) { try { duration = polyfill_1.Temporal.Duration.from(durationStr); } catch (error) { errors.push(`duration parameter must be a valid ISO 8601 duration string: ${error instanceof Error ? error.message : 'Invalid format'}`); } } else if (durationNum !== undefined) { duration = polyfill_1.Temporal.Duration.from({ milliseconds: durationNum }); } if (!from && !duration) { errors.push('Either from or duration parameter is required at minimum'); } const toStr = query.to; let to; if (toStr) { try { to = polyfill_1.Temporal.Instant.from(toStr); } catch (error) { errors.push(`to parameter must be a valid ISO 8601 timestamp: ${error instanceof Error ? error.message : 'Invalid format'}`); } } if (from && to && duration) { errors.push('Cannot specify all of from, to, and duration together; choose either from+to or from+duration or to+duration'); } if (from && to && polyfill_1.Temporal.Instant.compare(from, to) >= 0) { errors.push('from parameter must be before to parameter'); } if (errors.length > 0) { throw new Error(`Validation errors: ${errors.join(', ')}`); } return { timeRangeParams: { from, to, duration }, errors }; };