UNPKG

signalk-server

Version:

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

510 lines 20.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.start = start; exports.deletePath = deletePath; exports.putPath = putPath; exports.registerActionHandler = registerActionHandler; exports.deRegisterActionHandler = deRegisterActionHandler; const lodash_1 = require("lodash"); const debug_1 = require("./debug"); const requestResponse_1 = require("./requestResponse"); const skConfig = __importStar(require("./config/config")); const path_metadata_1 = require("@signalk/path-metadata"); const unitpreferences_1 = require("./unitpreferences"); const debug = (0, debug_1.createDebug)('signalk-server:put'); const pathPrefix = '/signalk'; const versionPrefix = '/v1'; const apiPathPrefix = pathPrefix + versionPrefix + '/api/'; const actionHandlers = {}; let putMetaHandler; let deleteMetaHandler; let putNotificationHandler; // Drop a deleted metadata field from the live self leaf. Metadata is merged // onto value-bearing leaves in the full model, so deleting it from the // registry alone would leave the field stuck on the tree leaf. function removeLeafMetaField(app, metaPath, name) { const leaf = (0, lodash_1.get)(app.signalk.self, metaPath); if (leaf?.meta) { delete leaf.meta[name]; } } function start(app) { app.registerActionHandler = registerActionHandler; app.deRegisterActionHandler = deRegisterActionHandler; app.delete(apiPathPrefix + '*', function (req, res) { let path = String(req.path).replace(apiPathPrefix, ''); path = path.replace(/\/$/, '').replace(/\//g, '.'); const parts = path.length > 0 ? path.split('.') : []; if (parts.length < 3) { res.status(400).send('invalid path'); return; } const context = `${parts[0]}.${parts[1]}`; const skpath = parts.slice(2).join('.'); deletePath(app, context, skpath, req) .then((reply) => { res.status(reply.statusCode); res.json(reply); }) .catch((err) => { console.error(err); res.status(500).send(err.message); }); }); app.put(apiPathPrefix + '*', function (req, res) { let path = String(req.path).replace(apiPathPrefix, ''); const value = req.body; if (value.value === undefined) { res.status(400).send('input is missing a value'); return; } path = path.replace(/\/$/, '').replace(/\//g, '.'); const parts = path.length > 0 ? path.split('.') : []; if (parts.length < 3) { res.status(400).send('invalid path'); return; } const context = `${parts[0]}.${parts[1]}`; const skpath = parts.slice(2).join('.'); putPath(app, context, skpath, value, req) .then((reply) => { res.status(reply.statusCode); res.json(reply); }) .catch((err) => { console.error(err); res.status(500).send(err.message); }); }); putMetaHandler = (context, path, value, cb) => { const parts = path.split('.'); let metaPath = path; let metaValue = value; if (parts[parts.length - 1] !== 'meta') { const name = parts[parts.length - 1]; metaPath = parts.slice(0, parts.length - 2).join('.'); metaValue = { ...app.config.baseDeltaEditor.getMeta(context, metaPath), [name]: value }; } else { metaPath = parts.slice(0, parts.length - 1).join('.'); } // Validate displayUnits.category if present const displayUnits = metaValue.displayUnits; if (displayUnits?.category) { const schemaMeta = (0, path_metadata_1.getMetadata)('vessels.self.' + metaPath); // Allow override: use PUT's units if provided, otherwise use schema's units const pathSiUnit = metaValue.units || schemaMeta?.units; const validationError = (0, unitpreferences_1.validateCategoryAssignment)(pathSiUnit, displayUnits.category); if (validationError) { return { state: 'COMPLETED', statusCode: 400, message: validationError }; } } // set empty zones array explicitly to null for (const prop in metaValue) { if (Array.isArray(metaValue[prop]) && metaValue[prop].length === 0) { metaValue[prop] = null; } } const previousMeta = app.config.baseDeltaEditor.getMeta(context, metaPath); app.config.baseDeltaEditor.setMeta(context, metaPath, metaValue); // Remove fields that were deleted from the in-memory metadata registry // so they don't get re-injected via the spread below const full_meta = (0, path_metadata_1.getMetadata)('vessels.self.' + metaPath); if (previousMeta && full_meta) { for (const key of Object.keys(previousMeta)) { if (!(key in metaValue)) { delete full_meta[key]; } } } app.handleMessage('defaults', { context: 'vessels.self', updates: [ { meta: [ { path: metaPath, value: { ...full_meta, ...metaValue } } ] } ] }); if (app.config.hasOldDefaults) { let data; try { data = skConfig.readDefaultsFile(app); } catch (e) { if (e.code === 'ENOENT') { data = {}; } else { console.error(e); cb({ state: 'FAILURE', message: 'Unable to read defaults file' }); return; } } const pathWithContext = context + '.' + path; (0, lodash_1.set)(data, pathWithContext, value); skConfig.writeDefaultsFile(app, data, (err) => { if (err) { cb({ state: 'FAILURE', message: 'Unable to save to defaults file' }); } else { cb({ state: 'SUCCESS' }); } }); } else { skConfig .writeBaseDeltasFile(app) .then(() => { cb({ state: 'SUCCESS' }); }) .catch(() => { cb({ state: 'FAILURE', message: 'Unable to save to defaults file' }); }); } return { state: 'PENDING' }; }; deleteMetaHandler = (context, path, cb) => { const parts = path.split('.'); let metaPath = path; let full_meta; if (parts[parts.length - 1] !== 'meta') { const name = parts[parts.length - 1]; metaPath = parts.slice(0, parts.length - 2).join('.'); const metaValue = { ...app.config.baseDeltaEditor.getMeta(context, metaPath) }; if (typeof metaValue[name] === 'undefined') { return { state: 'COMPLETED', statusCode: 404 }; } delete metaValue[name]; full_meta = (0, path_metadata_1.getMetadata)('vessels.self.' + metaPath); delete full_meta[name]; removeLeafMetaField(app, metaPath, name); app.config.baseDeltaEditor.setMeta(context, metaPath, metaValue); if (Object.keys(metaValue).length === 0) { app.config.baseDeltaEditor.removeMeta(context, metaPath); } } else { metaPath = parts.slice(0, parts.length - 1).join('.'); full_meta = (0, path_metadata_1.getMetadata)('vessels.self.' + metaPath); const metaValue = app.config.baseDeltaEditor.getMeta(context, metaPath); if (!metaValue) { return { state: 'COMPLETED', statusCode: 404 }; } Object.keys(metaValue).forEach((key) => { delete full_meta[key]; removeLeafMetaField(app, metaPath, key); }); app.config.baseDeltaEditor.removeMeta(context, metaPath); } app.handleMessage('defaults', { context: 'vessels.self', updates: [ { meta: [ { path: metaPath, value: full_meta } ] } ] }); skConfig .writeBaseDeltasFile(app) .then(() => { cb({ state: 'COMPLETED', statusCode: 200 }); }) .catch(() => { cb({ state: 'COMPLETED', statusCode: 502, message: 'Unable to save to defaults file' }); }); return { state: 'PENDING' }; }; putNotificationHandler = (context, path, value) => { return putNotification(app, context, path, value); }; } function deletePath(app, contextParam, path, req, requestId, updateCb) { const context = contextParam || 'vessels.self'; debug('received delete %s %s', context, path); return new Promise((resolve, reject) => { (0, requestResponse_1.createRequest)(app, 'delete', { context: context, requestId: requestId ?? undefined, delete: { path: path } }, req && req.skPrincipal ? req.skPrincipal.identifier : undefined, undefined, updateCb) .then((request) => { if (req && app.securityStrategy.shouldAllowPut(req, context, null, path) === false) { (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', { statusCode: 403 }) .then(resolve) .catch(reject); return; } const parts = path.split('.'); let handler; if ((parts.length > 1 && parts[parts.length - 1] === 'meta') || (parts.length > 1 && parts[parts.length - 2] === 'meta')) { handler = deleteMetaHandler; } if (handler) { const actionResult = handler(context, path, (reply) => { debug('got result: %j', reply); (0, requestResponse_1.updateRequest)(request.requestId, reply.state, reply) .then(() => undefined) .catch((err) => { console.error(err); }); }); Promise.resolve(actionResult) .then((result) => { debug('got result: %j', result); (0, requestResponse_1.updateRequest)(request.requestId, result.state, result) .then((reply) => { if (reply.state === 'PENDING') { ; reply.action = { href: reply.href }; } resolve(reply); }) .catch(reject); }) .catch((err) => { (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', { statusCode: 500, message: err.message }) .then(resolve) .catch(reject); }); } else { (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', { statusCode: 405, message: `DELETE not supported for ${path}` }) .then(resolve) .catch(reject); } }) .catch(reject); }); } function putPath(app, contextParam, path, body, req, requestId, updateCb) { const context = contextParam || 'vessels.self'; debug('received put %s %s %j', context, path, body); return new Promise((resolve, reject) => { (0, requestResponse_1.createRequest)(app, 'put', { context: context, requestId: requestId ?? undefined, put: { path: path, value: body.value } }, req && req.skPrincipal ? req.skPrincipal.identifier : undefined, undefined, updateCb) .then((request) => { if (req && app.securityStrategy.shouldAllowPut(req, context, null, path) === false) { (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', { statusCode: 403 }) .then(resolve) .catch(reject); return; } let handler; const parts = path.split('.'); if ((parts.length > 1 && parts[parts.length - 1] === 'meta') || (parts.length > 1 && parts[parts.length - 2] === 'meta')) { handler = putMetaHandler; } else { const handlers = actionHandlers[context] ? actionHandlers[context][path] : null; if (handlers && Object.keys(handlers).length > 0) { if (body.source) { handler = handlers[body.source]; } else if (Object.keys(handlers).length === 1) { handler = Object.values(handlers)[0]; } else { (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', { statusCode: 400, message: 'there are multiple sources for the given path, but no source was specified in the request' }) .then(resolve) .catch(reject); return; } } if (!handler && parts[0] === 'notifications') { handler = putNotificationHandler; } } if (handler) { function fixReply(reply) { if (reply.state === 'FAILURE') { reply.state = 'COMPLETED'; reply.statusCode = 502; } else if (reply.state === 'SUCCESS') { reply.state = 'COMPLETED'; reply.statusCode = 200; } } const actionResult = handler(context, path, body.value, (reply) => { debug('got result: %j', reply); fixReply(reply); (0, requestResponse_1.updateRequest)(request.requestId, reply.state, reply) .then(() => undefined) .catch((err) => { console.error(err); }); }); Promise.resolve(actionResult) .then((result) => { debug('got result: %j', result); fixReply(result); (0, requestResponse_1.updateRequest)(request.requestId, result.state, result) .then((reply) => { if (reply.state === 'PENDING') { ; reply.action = { href: reply.href }; } resolve(reply); }) .catch(reject); }) .catch((err) => { (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', { statusCode: 500, message: err.message }) .then(resolve) .catch(reject); }); } else if (app.interfaces.ws && app.interfaces.ws.canHandlePut(path, body.source)) { app.interfaces.ws .handlePut(request.requestId, context, path, body.source, body.value) .then(resolve) .catch(reject); } else { (0, requestResponse_1.updateRequest)(request.requestId, 'COMPLETED', { statusCode: 405, message: `PUT not supported for ${path}` }) .then(resolve) .catch(reject); } }) .catch(reject); }); } function registerActionHandler(context, path, source, callback) { debug(`registered action handler for ${context} ${path} ${source}`); if (actionHandlers[context] === undefined) { actionHandlers[context] = {}; } if (actionHandlers[context][path] === undefined) { actionHandlers[context][path] = {}; } actionHandlers[context][path][source] = callback; return () => { deRegisterActionHandler(context, path, source, callback); }; } function deRegisterActionHandler(context, path, source, callback) { if (actionHandlers[context] && actionHandlers[context][path][source] === callback) { delete actionHandlers[context][path][source]; debug(`de-registered action handler for ${context} ${path} ${source}`); } } function putNotification(app, context, path, value) { const parts = path.split('.'); const notifPath = parts.slice(0, parts.length - 1).join('.'); const key = parts[parts.length - 1]; const existing = (0, lodash_1.get)(app.signalk.self, notifPath); if (existing === undefined || !existing.value) { return { state: 'COMPLETED', statusCode: 404 }; } if (key !== 'method' && key !== 'state') { return { state: 'COMPLETED', statusCode: 405 }; } existing.value[key] = value; existing.timestamp = new Date().toISOString(); const delta = { updates: [ { $source: existing.$source, values: [ { path: notifPath, value: existing.value } ] } ] }; app.handleMessage('server', delta); return { state: 'COMPLETED', statusCode: 200 }; } //# sourceMappingURL=put.js.map