signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
176 lines (175 loc) • 6.41 kB
JavaScript
;
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 };
};