UNPKG

signalk-server

Version:

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

723 lines (722 loc) 29.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ResourcesApi = exports.skUuid = exports.RESOURCES_API_PATH = void 0; /* eslint-disable @typescript-eslint/no-explicit-any */ const debug_1 = require("../../debug"); const debug = (0, debug_1.createDebug)('signalk-server:api:resources'); const server_api_1 = require("@signalk/server-api"); const uuid_1 = require("uuid"); const __1 = require("../"); const validate_1 = require("./validate"); const config_1 = require("../../config/config"); exports.RESOURCES_API_PATH = `/signalk/v2/api/resources`; const skUuid = () => `${(0, uuid_1.v4)()}`; exports.skUuid = skUuid; class ResourcesApi { resProvider = {}; app; settings; constructor(app) { this.app = app; this.initResourceRoutes(app); this.parseSettings(); } async start() { return new Promise(async (resolve) => { resolve(); }); } async parseSettings() { const defaultSettings = { defaultProviders: { routes: 'resources-provider', waypoints: 'resources-provider', regions: 'resources-provider', notes: 'resources-provider', charts: 'resources-provider' } }; if (!('resourcesApi' in this.app.config.settings)) { debug('***** Applying Default Settings ********'); this.app.config.settings['resourcesApi'] = defaultSettings; } else { const s = this.app.config.settings['resourcesApi']; Object.entries(defaultSettings.defaultProviders).forEach((k) => { if (!(k[0] in s.defaultProviders)) { s.defaultProviders[k[0]] = k[1]; } }); } this.settings = this.app.config.settings['resourcesApi']; debug('** Parsed Settings ***', this.app.config.settings); } saveSettings() { if (this.settings) { (0, config_1.writeSettingsFile)(this.app, this.app.config.settings, () => debug('***SETTINGS SAVED***')); } } register(pluginId, provider) { debug(`** Registering ${provider.type} provider => ${pluginId} `); if (!provider) { throw new Error(`Error registering provider ${pluginId}!`); } if (!provider.type) { throw new Error(`Invalid ResourceProvider.type value!`); } if (this.isResourceProvider(provider)) { if (!this.resProvider[provider.type]) { this.resProvider[provider.type] = new Map(); } this.resProvider[provider.type].set(pluginId, provider.methods); if (this.settings?.defaultProviders) { if (!(provider.type in this.settings.defaultProviders)) { this.settings.defaultProviders[provider.type] = pluginId; debug(`Added default provider for ${provider.type}`); this.saveSettings(); } } } else { throw new Error(`Error missing ResourceProvider.methods!`); } debug(`Type = ${provider.type}`, this.resProvider[provider.type]); } unRegister(pluginId) { if (!pluginId) { return; } debug(`** Un-registering ${pluginId} plugin as a resource provider....`); for (const resourceType in this.resProvider) { if (this.resProvider[resourceType].has(pluginId)) { debug(`** Un-registering ${pluginId} as ${resourceType} provider....`); this.resProvider[resourceType].delete(pluginId); // update default provider if (this.settings.defaultProviders[resourceType] && this.settings.defaultProviders[resourceType] === pluginId) { const p = this.checkForProvider(resourceType); if (p) { this.settings.defaultProviders[resourceType] = p; debug(`Assigned ${pluginId} as default provider for ${resourceType}.`); } else { delete this.settings.defaultProviders[resourceType]; debug(`Removed ${pluginId} as default provider for ${resourceType}.`); } } } } this.saveSettings(); debug(this.resProvider); } isResourceProvider(provider) { return !provider.methods.listResources || !provider.methods.getResource || !provider.methods.setResource || !provider.methods.deleteResource || typeof provider.methods.listResources !== 'function' || typeof provider.methods.getResource !== 'function' || typeof provider.methods.setResource !== 'function' || typeof provider.methods.deleteResource !== 'function' ? false : true; } async getResource(resType, resId, providerId) { debug(`** getResource(${resType}, ${resId})`); const provider = this.checkForProvider(resType, providerId); if (!provider) { return Promise.reject(new Error(`No provider for ${resType}`)); } return this.getFromAll(resType, resId); } async listResources(resType, params, providerId) { debug(`** listResources(${resType}, ${JSON.stringify(params)})`); const provider = this.checkForProvider(resType, providerId); debug(`** provider = ${provider}`); if (!provider) { return Promise.reject(new Error(`No provider for ${resType}`)); } return this.listFromAll(resType, params); } async setResource(resType, resId, data, providerId) { debug(`** setResource(${resType}, ${resId}, ${JSON.stringify(data)})`); if ((0, server_api_1.isSignalKResourceType)(resType)) { let isValidId; if (resType === 'charts') { isValidId = validate_1.validate.chartId(resId); } else { isValidId = validate_1.validate.uuid(resId); } if (!isValidId) { return Promise.reject(new Error(`Invalid resource id provided (${resId})`)); } validate_1.validate.resource(resType, resId, 'PUT', data); } else { if (!resId) { return Promise.reject(new Error(`No resource id provided!`)); } } const provider = await this.getProviderForWrite(resType, resId, providerId); if (provider) { this.resProvider[resType] ?.get(provider) ?.setResource(resId, data) .then((r) => { this.app.handleMessage(provider, this.buildDeltaMsg(resType, resId, data), server_api_1.SKVersion.v2); return r; }) .catch((e) => { debug(e); return Promise.reject(new Error(`Error writing ${resType} ${resId}`)); }); } else { return Promise.reject(new Error(`No provider for ${resType}`)); } } async deleteResource(resType, resId, providerId) { debug(`** deleteResource(${resType}, ${resId})`); let provider = undefined; if (providerId) { provider = this.checkForProvider(resType, providerId); } else { provider = await this.getProviderForResourceId(resType, resId); } if (provider) { this.resProvider[resType] ?.get(provider) ?.deleteResource(resId) .then((r) => { this.app.handleMessage(provider, this.buildDeltaMsg(resType, resId, null), server_api_1.SKVersion.v2); return r; }) .catch((e) => { debug(e); return Promise.reject(new Error(`Error deleting ${resType} ${resId}`)); }); } else { return Promise.reject(new Error(`No provider for ${resType}`)); } } /** Returns true if there is a registered provider for the resource type */ hasRegisteredProvider(resType) { const result = this.resProvider[resType] && this.resProvider[resType].size !== 0 ? true : false; debug(`hasRegisteredProvider(${resType}).result = ${result}`); return result; } /** Returns the provider id to use to write a resource entry */ async getProviderForWrite(resType, resId, providerId) { debug('***** getProviderForWrite()', resType, resId, providerId); let pv4resid; if (resId) { pv4resid = await this.getProviderForResourceId(resType, resId); } if (resId && pv4resid) { if (providerId && pv4resid !== providerId) { debug(`Detected provider for resource does not match supplied provider!`); } debug('***** Using provider ->', pv4resid); return pv4resid; } if (providerId) { debug(`***** Checking if provider ${providerId} is valid for ${resType}.`); const pv4restype = this.checkForProvider(resType, providerId); if (pv4restype) { debug('***** Using provider ->', pv4restype); return pv4restype; } else { debug(`***** ProviderId supplied is INVALID for ${resType}!`); return undefined; } } // use default provider for resType debug(`***** No providerId supplied...getting the default provider for ${resType}.`); if (this.settings.defaultProviders[resType]) { const pv = this.checkForProvider(resType, this.settings.defaultProviders[resType]); debug('***** Using default provider ->', pv); return pv; } else { return undefined; } } /** Validates providerId for a given resourceType */ checkForProvider(resType, providerId) { debug(`** checkForProvider(${resType}, ${providerId})`); let result = undefined; if (!this.resProvider[resType]) { debug(`${resType} not found!`); return result; } if (providerId) { result = this.resProvider[resType].has(providerId) ? providerId : undefined; } else { result = this.resProvider[resType].keys().next().value; } debug(`** checkForProvider().result = ${result}`); return result; } /** Retrieve matching resources from ALL providers */ async listFromAll(resType, params) { debug(`listFromAll(${resType}, ${JSON.stringify(params)})`); const result = {}; if (!this.resProvider[resType]) { return result; } const req = []; this.resProvider[resType].forEach((v) => { req.push(v.listResources(params)); }); const resp = await Promise.allSettled(req); resp.forEach((r) => { if (r.status === 'fulfilled') { Object.assign(result, r.value); } }); return result; } /** Query ALL providers for supplied resource id */ async getFromAll(resType, resId, property) { debug(`getFromAll(${resType}, ${resId})`); const result = {}; if (!this.resProvider[resType]) { return result; } const req = []; this.resProvider[resType].forEach((id) => { req.push(id.getResource(resId, property)); }); const resp = await Promise.allSettled(req); resp.forEach((r) => { if (r.status === 'fulfilled') { Object.assign(result, r.value); } }); return result; } /** Return providerId for supplied resource id */ async getProviderForResourceId(resType, resId, fallbackToDefault) { debug(`getProviderForResourceId(${resType}, ${resId}, ${fallbackToDefault})`); let result = undefined; if (!this.resProvider[resType]) { return result; } const req = []; const idList = []; this.resProvider[resType].forEach((v, k) => { idList.push(k); req.push(v.getResource(resId)); }); const resp = await Promise.allSettled(req); let idx = 0; resp.forEach((r) => { if (r.status === 'fulfilled') { result = !result ? idList[idx] : result; } idx++; }); if (!result && fallbackToDefault) { result = this.resProvider[resType].keys().next().value; } debug(`getProviderForResourceId().result = ${result}`); return result; } /** Return array of provider ids for supplied resource type */ getProvidersForResourceType(resType) { const result = this.resProvider[resType] ? Array.from(this.resProvider[resType].keys()) : []; debug(`getProvidersForResourceType().result = ${result}`); return result; } initResourceRoutes(server) { const updateAllowed = (req) => { return server.securityStrategy.shouldAllowPut(req, 'vessels.self', null, 'resources'); }; // list all serviced paths under resources server.get(`${exports.RESOURCES_API_PATH}`, (req, res) => { res.json(this.getResourcePaths()); }); // Providers: Return list of providers server.get(`${exports.RESOURCES_API_PATH}/:resourceType/_providers`, async (req, res) => { debug(`** ${req.method} ${req.path}`); res.json(this.getProvidersForResourceType(req.params.resourceType)); }); // Providers: Return the default provider for the supplied resource type server.get(`${exports.RESOURCES_API_PATH}/:resourceType/_providers/_default`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!this.settings.defaultProviders[req.params.resourceType]) { res.status(404).json({ state: 'FAILED', statusCode: 404, message: `Resource type not found! (${req.params.resourceType})` }); } else { res.json(this.settings.defaultProviders[req.params.resourceType]); } }); // Providers: Set the default write provider for a resource type server.post(`${exports.RESOURCES_API_PATH}/:resourceType/_providers/_default/:providerId`, async (req, res) => { debug(`** ${req.method} ${req.path}`); if (!updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } if (!this.hasRegisteredProvider(req.params.resourceType)) { res.status(400).json({ state: 'FAILED', statusCode: 400, message: `Invalid resource type (${req.params.resourceType}) supplied!` }); return; } if (!this.checkForProvider(req.params.resourceType, req.params.providerId)) { res.status(400).json({ state: 'FAILED', statusCode: 400, message: `Resource provider not found for ${req.params.resourceType}!` }); return; } this.settings.defaultProviders[req.params.resourceType] = req.params.providerId; this.saveSettings(); res.status(201).json({ state: 'COMPLETED', statusCode: 201, message: `${req.params.providerId}` }); }); // facilitate retrieval of a specific resource server.get(`${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`, async (req, res, next) => { debug(`** GET ${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`); if (!this.hasRegisteredProvider(req.params.resourceType)) { next(); return; } try { if (req.query.provider) { const provider = this.checkForProvider(req.params.resourceType, req.query.provider ? req.query.provider : undefined); if (!provider) { debug('** No provider found... calling next()...'); next(); return; } const retVal = await this.resProvider[req.params.resourceType] ?.get(provider) ?.getResource(req.params.resourceId); res.json(retVal); } else { const retVal = await this.getFromAll(req.params.resourceType, req.params.resourceId); res.json(retVal); } } catch (_err) { res.status(404).json({ state: 'FAILED', statusCode: 404, message: `Resource not found! (${req.params.resourceId})` }); } }); // facilitate retrieval of a specific resource property server.get(`${exports.RESOURCES_API_PATH}/:resourceType/:resourceId/*`, async (req, res, next) => { debug(`** GET ${exports.RESOURCES_API_PATH}/:resourceType/:resourceId/*`); if (req.path.match(`/charts/(\\w*\\W*)+/[0-9]*/[0-9]*/[0-9]*`)) { debug('*** CHART TILE request -> next()'); next(); return; } if (!this.hasRegisteredProvider(req.params.resourceType)) { next(); return; } try { const property = req.params['0'] ? req.params['0'].split('/').join('.') : undefined; if (req.query.provider) { const provider = this.checkForProvider(req.params.resourceType, req.query.provider ? req.query.provider : undefined); if (!provider) { debug('** No provider found... calling next()...'); next(); return; } const retVal = await this.resProvider[req.params.resourceType] ?.get(provider) ?.getResource(req.params.resourceId, property); res.json(retVal); } else { const retVal = await this.getFromAll(req.params.resourceType, req.params.resourceId, property); res.json(retVal); } } catch (_err) { res.status(404).json({ state: 'FAILED', statusCode: 404, message: `Resource not found! (${req.params.resourceId})` }); } }); // facilitate retrieval of a collection of resource entries server.get(`${exports.RESOURCES_API_PATH}/:resourceType`, async (req, res, next) => { debug(`** GET ${exports.RESOURCES_API_PATH}/:resourceType`); if (!this.hasRegisteredProvider(req.params.resourceType)) { next(); return; } const parsedQuery = Object.entries(req.query).reduce((acc, [name, value]) => { try { acc[name] = JSON.parse(value); return acc; } catch (_error) { acc[name] = value; return acc; } }, {}); if ((0, server_api_1.isSignalKResourceType)(req.params.resourceType)) { try { validate_1.validate.query(req.params.resourceType, undefined, req.method, parsedQuery); } catch (e) { res.status(400).json({ state: 'FAILED', statusCode: 400, message: e.message }); return; } } try { if (req.query.provider) { const provider = this.checkForProvider(req.params.resourceType, req.query.provider ? req.query.provider : undefined); if (!provider) { debug('** No provider found... calling next()...'); next(); return; } const retVal = await this.resProvider[req.params.resourceType] ?.get(provider) ?.listResources(parsedQuery); res.json(retVal); } else { const retVal = await this.listFromAll(req.params.resourceType, parsedQuery); res.json(retVal); } } catch (err) { console.error(err); res.status(404).json({ state: 'FAILED', statusCode: 404, message: `Error retrieving resources!` }); } }); // facilitate creation of new resource entry of supplied type server.post(`${exports.RESOURCES_API_PATH}/:resourceType`, async (req, res, next) => { debug(`** POST ${req.path}`); if (!this.hasRegisteredProvider(req.params.resourceType)) { next(); return; } const provider = await this.getProviderForWrite(req.params.resourceType, '', req.query.provider ? req.query.provider : undefined); if (!provider) { debug('** No provider found... calling next()...'); next(); return; } if (!updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } if ((0, server_api_1.isSignalKResourceType)(req.params.resourceType)) { try { validate_1.validate.resource(req.params.resourceType, undefined, req.method, req.body); } catch (e) { res.status(400).json({ state: 'FAILED', statusCode: 400, message: e.message }); return; } } let id; if (req.params.resourceType === 'charts') { id = req.body.identifier ?? (0, exports.skUuid)(); } else { id = (0, exports.skUuid)(); } try { await this.resProvider[req.params.resourceType] ?.get(provider) ?.setResource(id, req.body); server.handleMessage(provider, this.buildDeltaMsg(req.params.resourceType, id, req.body), server_api_1.SKVersion.v2); res.status(201).json({ state: 'COMPLETED', statusCode: 201, id }); } catch (_err) { res.status(400).json({ state: 'FAILED', statusCode: 400, message: `Error saving ${req.params.resourceType} resource (${id})!` }); } }); // facilitate creation / update of resource entry at supplied id server.put(`${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`, async (req, res, next) => { debug(`** PUT ${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`); if (!this.hasRegisteredProvider(req.params.resourceType)) { next(); return; } if (!updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } if ((0, server_api_1.isSignalKResourceType)(req.params.resourceType)) { let isValidId; if (req.params.resourceType === 'charts') { isValidId = validate_1.validate.chartId(req.params.resourceId); } else { isValidId = validate_1.validate.uuid(req.params.resourceId); } if (!isValidId) { res.status(400).json({ state: 'FAILED', statusCode: 400, message: `Invalid resource id provided (${req.params.resourceId})` }); return; } debug(req.body); try { validate_1.validate.resource(req.params.resourceType, req.params.resourceId, req.method, req.body); } catch (e) { res.status(400).json({ state: 'FAILED', statusCode: 400, message: e.message }); return; } } try { const provider = await this.getProviderForWrite(req.params.resourceType, req.params.resourceId, req.query.provider ? req.query.provider : undefined); if (!provider) { debug('** No provider found... calling next()...'); next(); return; } await this.resProvider[req.params.resourceType] ?.get(provider) ?.setResource(req.params.resourceId, req.body); server.handleMessage(provider, this.buildDeltaMsg(req.params.resourceType, req.params.resourceId, req.body), server_api_1.SKVersion.v2); res.status(200).json({ state: 'COMPLETED', statusCode: 200, message: req.params.resourceId }); } catch (_err) { res.status(404).json({ state: 'FAILED', statusCode: 404, message: `Error saving ${req.params.resourceType} resource (${req.params.resourceId})!` }); } }); // facilitate deletion of specific of resource entry at supplied id server.delete(`${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`, async (req, res, next) => { debug(`** DELETE ${exports.RESOURCES_API_PATH}/:resourceType/:resourceId`); if (!this.hasRegisteredProvider(req.params.resourceType)) { next(); return; } if (!updateAllowed(req)) { res.status(403).json(__1.Responses.unauthorised); return; } try { let provider = undefined; if (req.query.provider) { provider = this.checkForProvider(req.params.resourceType, req.query.provider ? req.query.provider : undefined); } else { provider = await this.getProviderForResourceId(req.params.resourceType, req.params.resourceId); } if (!provider) { debug('** No provider found... calling next()...'); next(); return; } await this.resProvider[req.params.resourceType] ?.get(provider) ?.deleteResource(req.params.resourceId); server.handleMessage(provider, this.buildDeltaMsg(req.params.resourceType, req.params.resourceId, null), server_api_1.SKVersion.v2); res.status(200).json({ state: 'COMPLETED', statusCode: 200, message: req.params.resourceId }); } catch (_err) { res.status(400).json({ state: 'FAILED', statusCode: 400, message: `Error deleting resource (${req.params.resourceId})!` }); } }); } getResourcePaths() { const resPaths = {}; for (const i in this.resProvider) { if (this.resProvider.hasOwnProperty(i)) { resPaths[i] = { description: `Path containing ${i.slice(-1) === 's' ? i.slice(0, i.length - 1) : i} resources` }; } } return resPaths; } buildDeltaMsg(resType, resid, resValue) { return { updates: [ { values: [ { path: `resources.${resType}.${resid}`, value: resValue } ] } ] }; } } exports.ResourcesApi = ResourcesApi;