UNPKG

@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.

346 lines 16.5 kB
"use strict"; // 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; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AbstractModuleApi = void 0; require("reflect-metadata"); const tslog_1 = require("tslog"); const _1 = require("."); const __1 = require("../.."); const persistence_1 = require("../../ocpp/persistence"); const MessageQuerystring_1 = require("./MessageQuerystring"); const AuthorizationSecurity_1 = require("./AuthorizationSecurity"); /** * Abstract module api class implementation. */ 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 tslog_1.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(_1.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(_1.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({}, MessageQuerystring_1.IMessageQuerystringSchema), { properties: Object.assign(Object.assign({}, MessageQuerystring_1.IMessageQuerystringSchema.properties), (optionalQuerystrings || {})) }); const _opts = { method: _1.HttpMethod.Post, url: this._toMessagePath(action), handler: _handler, schema: { body: bodySchema, querystring: mergedQuerySchema, response: { 200: { $id: 'MessageConfirmationSchemaArray', type: 'array', items: __1.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_1.AuthorizationSecurity]; } else { _opts.schema['security'].push(AuthorizationSecurity_1.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, persistence_1.OCPP2_0_1_Namespace.SystemConfig, () => new Promise((resolve) => resolve(module.config)), _1.HttpMethod.Get); this._addDataRoute.call(this, persistence_1.OCPP2_0_1_Namespace.SystemConfig, (request) => __awaiter(this, void 0, void 0, function* () { yield __1.ConfigStoreFactory.getInstance().saveConfig(request.body); module.config = request.body; }), _1.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 : __1.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)}`; } } exports.AbstractModuleApi = AbstractModuleApi; //# sourceMappingURL=AbstractModuleApi.js.map