@citrineos/base
Version:
The base module for OCPP v2.0.1 including all interfaces. This module is not intended to be used directly, but rather as a dependency for other modules.
342 lines • 16.3 kB
JavaScript
// Copyright (c) 2023 S44, LLC
// Copyright Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache 2.0
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import 'reflect-metadata';
import { Logger } from 'tslog';
import { HttpMethod, METADATA_DATA_ENDPOINTS, METADATA_MESSAGE_ENDPOINTS, } from '.';
import { ConfigStoreFactory, MessageConfirmationSchema, OCPPVersion, } from '../..';
import { OCPP2_0_1_Namespace } from '../../ocpp/persistence';
import { IMessageQuerystringSchema } from './MessageQuerystring';
import { AuthorizationSecurity } from './AuthorizationSecurity';
/**
* Abstract module api class implementation.
*/
export class AbstractModuleApi {
constructor(module, server, ocppVersion, logger) {
this.registerSchemaForOpts = (fastifyInstance, _opts) => {
if (_opts.schema['querystring']) {
_opts.schema['querystring'] = this.registerSchema(fastifyInstance, _opts.schema['querystring']);
}
if (_opts.schema['body']) {
_opts.schema['body'] = this.registerSchema(fastifyInstance, _opts.schema['body'], this._ocppVersion ? `${this._ocppVersion}-` : '');
}
if (_opts.schema['params']) {
_opts.schema['params'] = this.registerSchema(fastifyInstance, _opts.schema['params']);
}
if (_opts.schema['headers']) {
_opts.schema['headers'] = this.registerSchema(fastifyInstance, _opts.schema['headers']);
}
if (_opts.schema['response']) {
_opts.schema['response'] = {
200: this.registerSchema(fastifyInstance, _opts.schema['response'][200]),
};
}
};
this.registerSchema = (fastifyInstance, schema, schemaIdPrefix) => {
let id = schema['$id'];
if (!id) {
this._logger.error('Could not register schema because no ID', schema);
}
try {
const schemaCopy = this.removeUnknownKeys(schema);
if (id && schemaIdPrefix) {
id = schemaIdPrefix + id;
schemaCopy['$id'] = id;
this._logger.debug(`Update schema id: ${schemaCopy['$id']}`);
}
if (schemaCopy.required &&
Array.isArray(schemaCopy.required) &&
schemaCopy.required.length === 0) {
delete schemaCopy.required;
}
if (schema.definitions) {
Object.keys(schema.definitions).forEach((key) => {
const definition = schema.definitions[key];
if (!definition['$id']) {
definition['$id'] = key;
}
this.registerSchema(fastifyInstance, definition);
});
}
if (schemaCopy.properties) {
Object.keys(schemaCopy.properties).forEach((key) => {
const property = schemaCopy.properties[key];
if (property.$ref) {
property.$ref = property.$ref.replace('#/definitions/', '');
}
if (property.items && property.items.$ref) {
property.items.$ref = property.items.$ref.replace('#/definitions/', '');
}
});
}
fastifyInstance.addSchema(schemaCopy);
this._server.addSchema(schemaCopy);
return {
$ref: `${id}`,
};
}
catch (e) {
// ignore already declared
if (e.code === 'FST_ERR_SCH_ALREADY_PRESENT') {
return {
$ref: `${id}`,
};
}
else {
this._logger.error('Could not register schema', e, schema);
}
return null;
}
};
// TODO: for performance reasons can these unknown keys be removed directly from schemas?
this.removeUnknownKeys = (schema) => {
// Create a deep copy of the schema
const schemaCopy = structuredClone(schema); // Use structuredClone for a true deep copy
const cleanSchema = (obj) => {
if (typeof obj !== 'object' || obj === null)
return;
// Remove specific unknown keys
for (const unknownKey of ['comment', 'javaType', 'tsEnumNames']) {
if (unknownKey in obj) {
delete obj[unknownKey];
}
}
// Remove `additionalItems` if `items` is not an array
if ('items' in obj && !Array.isArray(obj.items) && 'additionalItems' in obj) {
delete obj.additionalItems;
}
// Remove `additionalProperties` if `type` is not "object"
if ('additionalProperties' in obj && obj.type !== 'object') {
delete obj.additionalProperties;
}
// Recursively process nested objects
for (const key in obj) {
if (typeof obj[key] === 'object') {
cleanSchema(obj[key]);
}
}
};
// Clean the copied schema
cleanSchema(schemaCopy);
return schemaCopy;
};
this._module = module;
this._server = server;
this._ocppVersion = ocppVersion;
this._logger = logger
? logger.getSubLogger({ name: this.constructor.name })
: new Logger({ name: this.constructor.name });
this._init(this._module);
}
/**
* Initializes the API for the given module.
*
* @param {T} module - The module to initialize the API for.
*/
_init(module) {
var _a;
(_a = Reflect.getMetadata(METADATA_MESSAGE_ENDPOINTS, this.constructor)) === null || _a === void 0 ? void 0 : _a.forEach((expose) => {
this._addMessageRoute.call(this, expose.action, expose.method, expose.bodySchema, expose.optionalQuerystrings);
});
const dataEndpointDefinitions = Reflect.getMetadata(METADATA_DATA_ENDPOINTS, this.constructor);
dataEndpointDefinitions === null || dataEndpointDefinitions === void 0 ? void 0 : dataEndpointDefinitions.forEach((expose) => {
this._addDataRoute.call(this, expose.namespace, expose.method, expose.httpMethod, expose.querySchema, expose.paramSchema, expose.headerSchema, expose.bodySchema, expose.responseSchema, expose.tags, expose.description, expose.security);
});
if (dataEndpointDefinitions && dataEndpointDefinitions.length > 0) {
this.registerSystemConfigRoutes(module);
}
}
/**
* Add a message route to the server.
*
* @param {CallAction} action - The action to be called.
* @param {Function} method - The method to be executed.
* @param {object} bodySchema - The schema for the route.
* @param {Record<string, any>} optionalQuerystrings - Optional querystrings for the route.
* @return {void}
*/
_addMessageRoute(action, method, bodySchema, optionalQuerystrings) {
var _a;
this._logger.debug(`Adding message route for ${action}`, this._toMessagePath(action));
/**
* Executes the handler function for the given request.
*
* @param {FastifyRequest<{ Body: OcppRequest, Querystring: IMessageQuerystring }>} request - The request object containing the body and querystring.
* @return {Promise<IMessageConfirmation>} The promise that resolves to the message confirmation.
*/
const _handler = (request) => __awaiter(this, void 0, void 0, function* () {
const _a = request.query, { identifier, tenantId, callbackUrl } = _a, extraQueries = __rest(_a, ["identifier", "tenantId", "callbackUrl"]);
const identifiers = Array.isArray(identifier) ? identifier : [identifier];
return method.call(this, identifiers, tenantId, request.body, callbackUrl, Object.keys(extraQueries).length > 0 ? extraQueries : undefined);
});
const mergedQuerySchema = Object.assign(Object.assign({}, IMessageQuerystringSchema), { properties: Object.assign(Object.assign({}, IMessageQuerystringSchema.properties), (optionalQuerystrings || {})) });
const _opts = {
method: HttpMethod.Post,
url: this._toMessagePath(action),
handler: _handler,
schema: {
body: bodySchema,
querystring: mergedQuerySchema,
response: {
200: {
$id: 'MessageConfirmationSchemaArray',
type: 'array',
items: MessageConfirmationSchema,
},
},
},
};
if ((_a = this._module.config.util.swagger) === null || _a === void 0 ? void 0 : _a.exposeMessage) {
this._server.register((fastifyInstance) => __awaiter(this, void 0, void 0, function* () {
this.registerSchemaForOpts(fastifyInstance, _opts);
fastifyInstance.route(_opts);
}));
}
else {
this._server.route(_opts);
}
}
/**
* Add a message route to the server.
*
* @param {OCPP2_0_1_Namespace | OCPP1_6_Namespace | Namespace} namespace - The entity type.
* @param {Function} method - The method to be executed.
* @param {HttpMethod} httpMethod - The HTTP method to be used.
* @param {object} querySchema - The schema for the querystring.
* @param {object} paramSchema - The schema for the parameters.
* @param {object} headerSchema - The schema for the headers.
* @param {object} bodySchema - The schema for the body.
* @param {object} responseSchema - The schema for the response.
* @param {string[]} tags - The tags for the route.
* @param {string} description - The description for the route.
* @param {object[]} security - The security for the route.
* @return {void}
*/
_addDataRoute(namespace, method, httpMethod, querySchema, paramSchema, headerSchema, bodySchema, responseSchema, tags, description, security) {
var _a;
this._logger.debug(`Adding data route for ${namespace}`, this._toDataPath(namespace), httpMethod);
const schema = {};
if (querySchema) {
schema['querystring'] = querySchema;
}
if (bodySchema) {
schema['body'] = bodySchema;
}
if (paramSchema) {
schema['params'] = paramSchema;
}
if (headerSchema) {
schema['headers'] = headerSchema;
}
if (responseSchema) {
schema['response'] = {
200: responseSchema,
};
}
if (tags) {
schema['tags'] = tags;
}
if (description) {
schema['description'] = description;
}
if (security) {
schema['security'] = security;
}
/**
* Handles the request and returns a Promise resolving to an object.
*
* @param {FastifyRequest<{ Body: object, Querystring: object }>} request - the request object
* @param {FastifyReply} reply - the reply object
* @return {Promise<any>} - a Promise resolving to an object
*/
const _handler = (request, reply) => __awaiter(this, void 0, void 0, function* () {
return method.call(this, request, reply).catch((err) => {
// TODO: figure out better error codes & messages
this._logger.error('Error in handling data route', err);
const statusCode = err.statusCode ? err.statusCode : 500;
reply.status(statusCode).send(err);
});
});
const _opts = {
method: httpMethod,
url: this._toDataPath(namespace),
schema: schema,
handler: _handler,
};
if (!!schema &&
!!schema.headers &&
!!schema.headers.properties &&
!!schema.headers.properties.Authorization) {
_opts['preHandler'] = this._server.auth([
this._server.authorization,
]);
if (!_opts['security']) {
_opts.schema['security'] = [AuthorizationSecurity];
}
else {
_opts.schema['security'].push(AuthorizationSecurity);
}
}
if ((_a = this._module.config.util.swagger) === null || _a === void 0 ? void 0 : _a.exposeData) {
this._server.register((fastifyInstance) => __awaiter(this, void 0, void 0, function* () {
this.registerSchemaForOpts(fastifyInstance, _opts);
fastifyInstance.route(_opts);
}));
}
else {
this._server.route(_opts);
}
}
registerSystemConfigRoutes(module) {
this._addDataRoute.call(this, OCPP2_0_1_Namespace.SystemConfig, () => new Promise((resolve) => resolve(module.config)), HttpMethod.Get);
this._addDataRoute.call(this, OCPP2_0_1_Namespace.SystemConfig, (request) => __awaiter(this, void 0, void 0, function* () {
yield ConfigStoreFactory.getInstance().saveConfig(request.body);
module.config = request.body;
}), HttpMethod.Put);
}
/**
* Convert a {@link CallAction} to a normed lowercase URL path.
*
* @param {CallAction} input - The {@link CallAction} to convert to a URL path.
* @param {string} prefix - The module name.
* @returns {string} - String representation of URL path.
*/
_toMessagePath(input, prefix) {
const endpointPrefix = prefix || '';
const endpointVersion = (this._ocppVersion ? this._ocppVersion : OCPPVersion.OCPP2_0_1).replace(/^ocpp/, '');
return `/ocpp/${endpointVersion}${!endpointPrefix.startsWith('/') ? '/' : ''}${endpointPrefix}${!endpointPrefix.endsWith('/') ? '/' : ''}${input.charAt(0).toLowerCase() + input.slice(1)}`;
}
/**
* Convert a namespace to a normed lowercase URL path.
*
* @param {OCPP2_0_1_Namespace | OCPP1_6_Namespace | Namespace} input - The {@link OCPP2_0_1_Namespace} or {@link OCPP1_6_Namespace} or {@link Namespace} to convert to a URL path.
* @param {string} prefix - The module name.
* @returns {string} - String representation of URL path.
*/
_toDataPath(input, prefix) {
const endpointPrefix = prefix || '';
return `/data${!endpointPrefix.startsWith('/') ? '/' : ''}${endpointPrefix}${!endpointPrefix.endsWith('/') ? '/' : ''}${input.charAt(0).toLowerCase() + input.slice(1)}`;
}
}
//# sourceMappingURL=AbstractModuleApi.js.map