From 687b0601ff3fa2125feeab438449017cd344cec7 Mon Sep 17 00:00:00 2001 From: Elliot Sabitov <> Date: Thu, 14 Nov 2024 15:37:17 -0500 Subject: [PATCH 1/2] fix: generate directus flows failing because full route object schemas were no longer available after registering schemas and using $refs fix: adjusting to register schemas globally so that they can be available when generating directus flows feat: added MessageConfirmationSchema and setting default message endpoint responses to be MesageConfirmationSchema fix: bug where if shema was already registered it would not properly return the $ref for the schema resulting in missing definition in OpenAPI spec --- .../src/interfaces/api/AbstractModuleApi.ts | 43 ++++++++++++------- 00_Base/src/ocpp/persistence/index.ts | 11 ++++- 02_Util/src/util/directus.ts | 13 ++++-- Server/src/index.ts | 21 ++++----- 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/00_Base/src/interfaces/api/AbstractModuleApi.ts b/00_Base/src/interfaces/api/AbstractModuleApi.ts index 361ac846..48c211c3 100644 --- a/00_Base/src/interfaces/api/AbstractModuleApi.ts +++ b/00_Base/src/interfaces/api/AbstractModuleApi.ts @@ -13,14 +13,12 @@ import { METADATA_DATA_ENDPOINTS, METADATA_MESSAGE_ENDPOINTS, } from '.'; -import { OcppRequest, SystemConfig } from '../..'; +import { MessageConfirmationSchema, OcppRequest, SystemConfig } from '../..'; import { Namespace } from '../../ocpp/persistence'; import { CallAction } from '../../ocpp/rpc/message'; import { IMessageConfirmation } from '../messages'; import { IModule } from '../modules'; -import { - IMessageQuerystringSchema, -} from './MessageQuerystring'; +import { IMessageQuerystringSchema } from './MessageQuerystring'; import { IModuleApi } from './ModuleApi'; import { AuthorizationSecurity } from './AuthorizationSecurity'; @@ -28,7 +26,8 @@ import { AuthorizationSecurity } from './AuthorizationSecurity'; * Abstract module api class implementation. */ export abstract class AbstractModuleApi - implements IModuleApi { + implements IModuleApi +{ protected readonly _server: FastifyInstance; protected readonly _module: T; protected readonly _logger: Logger; @@ -117,7 +116,7 @@ export abstract class AbstractModuleApi action: CallAction, method: (...args: any[]) => any, bodySchema: object, - optionalQuerystrings?: Record + optionalQuerystrings?: Record, ): void { this._logger.debug( `Adding message route for ${action}`, @@ -136,7 +135,8 @@ export abstract class AbstractModuleApi Querystring: Record; }>, ): Promise => { - const { identifier, tenantId, callbackUrl, ...extraQueries } = request.query; + const { identifier, tenantId, callbackUrl, ...extraQueries } = + request.query; return method.call( this, identifier, @@ -145,7 +145,7 @@ export abstract class AbstractModuleApi callbackUrl, Object.keys(extraQueries).length > 0 ? extraQueries : undefined, ); - } + }; const mergedQuerySchema = { ...IMessageQuerystringSchema, @@ -155,20 +155,26 @@ export abstract class AbstractModuleApi }, }; - const _opts = { + const _opts: any = { + method: HttpMethod.Post, + url: this._toMessagePath(action), + handler: _handler, schema: { body: bodySchema, querystring: mergedQuerySchema, + response: { + 200: MessageConfirmationSchema, + }, } as const, }; if (this._module.config.util.swagger?.exposeMessage) { this._server.register(async (fastifyInstance) => { this.registerSchemaForOpts(fastifyInstance, _opts); - fastifyInstance.post(this._toMessagePath(action), _opts, _handler); + fastifyInstance.route(_opts); }); } else { - this._server.post(this._toMessagePath(action), _opts, _handler); + this._server.route(_opts); } } @@ -326,19 +332,24 @@ export abstract class AbstractModuleApi fastifyInstance: FastifyInstance, schema: any, ): object | null => { + const id = schema['$id']; + if (!id) { + this._logger.error('Could not register schema because no ID', schema); + } try { - const id = schema['$id']; - if (!id) { - this._logger.error('Could not register schema because no ID', schema); - } const schemaCopy = this.removeUnknownKeys(schema); fastifyInstance.addSchema(schemaCopy); + this._server.addSchema(schemaCopy); return { $ref: `${id}`, }; } catch (e: any) { // ignore already declared - if (e.code !== 'FST_ERR_SCH_ALREADY_PRESENT') { + if (e.code === 'FST_ERR_SCH_ALREADY_PRESENT') { + return { + $ref: `${id}`, + }; + } else { this._logger.error('Could not register schema', e, schema); } return null; diff --git a/00_Base/src/ocpp/persistence/index.ts b/00_Base/src/ocpp/persistence/index.ts index ea354ff2..10633fd6 100644 --- a/00_Base/src/ocpp/persistence/index.ts +++ b/00_Base/src/ocpp/persistence/index.ts @@ -15,7 +15,7 @@ export { default as UpdateChargingStationPasswordSchema } from './schemas/Update * Utility function for creating querystring schemas for fastify route definitions * @param properties An array of key-type pairs. Types ending in '[]' will be treated as arrays of that type. * @param required An array of required keys. - * @returns + * @returns */ export function QuerySchema( name: string, @@ -48,3 +48,12 @@ export function QuerySchema( } return schema; } + +export const MessageConfirmationSchema = QuerySchema( + 'MessageConfirmationSchema', + [ + ['success', 'boolean'], + ['payload', 'string'], + ], + ['success'], +); diff --git a/02_Util/src/util/directus.ts b/02_Util/src/util/directus.ts index f81303cd..8bc5cacf 100644 --- a/02_Util/src/util/directus.ts +++ b/02_Util/src/util/directus.ts @@ -11,13 +11,13 @@ import { createOperation, DirectusFlow, DirectusOperation, + readAssetArrayBuffer, readFlows, rest, RestClient, staticToken, updateFlow, updateOperation, - readAssetArrayBuffer, uploadFiles, } from '@directus/sdk'; import { RouteOptions } from 'fastify'; @@ -74,6 +74,7 @@ export class DirectusUtil implements IFileAccess { public addDirectusMessageApiFlowsFastifyRouteHook( routeOptions: RouteOptions, + schemas: Record, ) { const messagePath = routeOptions.url; // 'Url' here means the route specified when the endpoint was added to the fastify server, such as '/ocpp/configuration/reset' if (messagePath.split('/')[1] === 'ocpp') { @@ -85,8 +86,14 @@ export class DirectusUtil implements IFileAccess { lowercaseAction.charAt(0).toUpperCase() + lowercaseAction.slice(1); // _addMessageRoute in AbstractModuleApi adds the bodySchema specified in the @MessageEndpoint decorator to the fastify route schema // These body schemas are the ones generated directly from the specification using the json-schema-processor in 00_Base - const bodySchema = routeOptions.schema?.body as object; - this.addDirectusFlowForAction(action, messagePath, bodySchema); + const bodySchema: any = routeOptions.schema?.body; + if (bodySchema && bodySchema.$ref && schemas[bodySchema.$ref]) { + this.addDirectusFlowForAction( + action, + messagePath, + schemas[bodySchema.$ref] as object, + ); + } } } diff --git a/Server/src/index.ts b/Server/src/index.ts index 131fb978..c89bc6db 100644 --- a/Server/src/index.ts +++ b/Server/src/index.ts @@ -21,23 +21,23 @@ import { import { MonitoringModule, MonitoringModuleApi } from '@citrineos/monitoring'; import { Authenticator, - CertificateAuthorityService, BasicAuthenticationFilter, + CertificateAuthorityService, ConnectedStationFilter, DirectusUtil, IdGenerator, initSwagger, MemoryCache, + NetworkProfileFilter, RabbitMqReceiver, RabbitMqSender, RedisCache, UnknownStationFilter, WebsocketNetworkConnection, - NetworkProfileFilter, } from '@citrineos/util'; import { type JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts'; import addFormats from 'ajv-formats'; -import fastify, { type FastifyInstance } from 'fastify'; +import fastify, { type FastifyInstance, RouteOptions } from 'fastify'; import { type ILogObj, Logger } from 'tslog'; import { systemConfig } from './config'; import { @@ -161,15 +161,16 @@ export class CitrineOSServer { this.initSwagger(); // Add Directus Message API flow creation if enabled - let directusUtil; + let directusUtil: DirectusUtil | undefined = undefined; if (this._config.util.directus?.generateFlows) { directusUtil = new DirectusUtil(this._config, this._logger); - this._server.addHook( - 'onRoute', - directusUtil.addDirectusMessageApiFlowsFastifyRouteHook.bind( - directusUtil, - ), - ); + this._server.addHook('onRoute', (routeOptions: RouteOptions) => { + directusUtil!.addDirectusMessageApiFlowsFastifyRouteHook( + routeOptions, + this._server.getSchemas(), + ); + }); + this._server.addHook('onReady', async () => { this._logger?.info('Directus actions initialization finished'); }); From 4b7d2d2f8063e418d6f39bed3f02c48c629606a7 Mon Sep 17 00:00:00 2001 From: Elliot Sabitov <> Date: Mon, 18 Nov 2024 13:21:20 -0500 Subject: [PATCH 2/2] fix: TlsCertificates schema to properly set certificateChain as string array fix: AsDataEndpoint to properly ignore `undefined` tags instead of setting `[undefined] as the tag list which resulted in incorrect null tags fix: registerSchema in AbstractModuleApi to remove empty required lists, recursively register nested schema definitions (which fastify does not do by default), fix property.$refs and property.items.$refs to not use '#/definitions/' since fastify simply expects the unique id fix: bringing back a smaller version of OcppTransformObject which now only sets the default tags (if not passed in) based on path key --- .../src/interfaces/api/AbstractModuleApi.ts | 30 +++++++++++++ 00_Base/src/interfaces/api/AsDataEndpoint.ts | 6 ++- .../src/interfaces/queries/TlsCertificate.ts | 2 +- 02_Util/src/util/swagger.ts | 43 +++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/00_Base/src/interfaces/api/AbstractModuleApi.ts b/00_Base/src/interfaces/api/AbstractModuleApi.ts index 48c211c3..3ab5ab78 100644 --- a/00_Base/src/interfaces/api/AbstractModuleApi.ts +++ b/00_Base/src/interfaces/api/AbstractModuleApi.ts @@ -338,6 +338,36 @@ export abstract class AbstractModuleApi } try { const schemaCopy = this.removeUnknownKeys(schema); + 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 { diff --git a/00_Base/src/interfaces/api/AsDataEndpoint.ts b/00_Base/src/interfaces/api/AsDataEndpoint.ts index 5e406e5a..1e8d3091 100644 --- a/00_Base/src/interfaces/api/AsDataEndpoint.ts +++ b/00_Base/src/interfaces/api/AsDataEndpoint.ts @@ -43,6 +43,10 @@ export const AsDataEndpoint = function ( METADATA_DATA_ENDPOINTS, target.constructor, ) as Array; + let tagList: string[] | undefined = undefined; + if (tags) { + tagList = Array.isArray(tags) ? tags : [tags]; + } dataEndpoints.push({ method: descriptor.value, methodName: propertyKey, @@ -53,7 +57,7 @@ export const AsDataEndpoint = function ( paramSchema: paramSchema, headerSchema: headerSchema, responseSchema: responseSchema, - tags: (Array.isArray(tags) ? tags : [tags]) as string[], + tags: tagList, description: description, security: security, }); diff --git a/01_Data/src/interfaces/queries/TlsCertificate.ts b/01_Data/src/interfaces/queries/TlsCertificate.ts index 45642feb..c3437629 100644 --- a/01_Data/src/interfaces/queries/TlsCertificate.ts +++ b/01_Data/src/interfaces/queries/TlsCertificate.ts @@ -7,7 +7,7 @@ import { QuerySchema } from '@citrineos/base'; export const TlsCertificateSchema = QuerySchema( 'TlsCertificateSchema', [ - ['certificateChain', 'array'], + ['certificateChain', 'string[]'], ['privateKey', 'string'], ['rootCA', 'string'], ['subCAKey', 'string'], diff --git a/02_Util/src/util/swagger.ts b/02_Util/src/util/swagger.ts index a80e9f83..e5e69512 100644 --- a/02_Util/src/util/swagger.ts +++ b/02_Util/src/util/swagger.ts @@ -16,6 +16,48 @@ import { } from '@citrineos/base'; import * as FastifyAuth from '@fastify/auth'; import * as packageJson from '../../package.json'; +import { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; + +/** + * This transformation is used to set default tags + * + * @param {object} swaggerObject - The original Swagger object to be transformed. + * @param {object} openapiObject - The original OpenAPI object to be transformed. + * @return {object} The transformed OpenAPI object. + */ +function OcppTransformObject({ + swaggerObject, + openapiObject, +}: { + swaggerObject: Partial; + openapiObject: Partial; +}) { + console.log('OcppTransformObject: Transforming OpenAPI object...'); + if (openapiObject.paths && openapiObject.components) { + for (const pathKey in openapiObject.paths) { + const path: OpenAPIV3.PathsObject = openapiObject.paths[ + pathKey + ] as OpenAPIV3.PathsObject; + if (path) { + for (const methodKey in path) { + const method: OpenAPIV3.OperationObject = path[ + methodKey + ] as OpenAPIV3.OperationObject; + if (method) { + // Set tags based on path key if tags were not passed in + if (!method.tags) { + method.tags = pathKey + .split('/') + .slice(2, -1) + .map((tag) => tag.charAt(0).toUpperCase() + tag.slice(1)); + } + } + } + } + } + } + return openapiObject; +} const registerSwaggerUi = ( systemConfig: SystemConfig, @@ -148,6 +190,7 @@ const registerFastifySwagger = ( }, }, }, + transformObject: OcppTransformObject, refResolver: { buildLocalReference, },