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