Skip to content

Commit

Permalink
Merge pull request #294 from citrineos/fix/generate-directus-flows
Browse files Browse the repository at this point in the history
Fix: generate directus flows
  • Loading branch information
thanaParis authored Nov 18, 2024
2 parents 1c4174e + 4b7d2d2 commit 30a00c8
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 32 deletions.
73 changes: 57 additions & 16 deletions 00_Base/src/interfaces/api/AbstractModuleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,21 @@ 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';

/**
* Abstract module api class implementation.
*/
export abstract class AbstractModuleApi<T extends IModule>
implements IModuleApi {
implements IModuleApi
{
protected readonly _server: FastifyInstance;
protected readonly _module: T;
protected readonly _logger: Logger<ILogObj>;
Expand Down Expand Up @@ -117,7 +116,7 @@ export abstract class AbstractModuleApi<T extends IModule>
action: CallAction,
method: (...args: any[]) => any,
bodySchema: object,
optionalQuerystrings?: Record<string, any>
optionalQuerystrings?: Record<string, any>,
): void {
this._logger.debug(
`Adding message route for ${action}`,
Expand All @@ -136,7 +135,8 @@ export abstract class AbstractModuleApi<T extends IModule>
Querystring: Record<string, any>;
}>,
): Promise<IMessageConfirmation> => {
const { identifier, tenantId, callbackUrl, ...extraQueries } = request.query;
const { identifier, tenantId, callbackUrl, ...extraQueries } =
request.query;
return method.call(
this,
identifier,
Expand All @@ -145,7 +145,7 @@ export abstract class AbstractModuleApi<T extends IModule>
callbackUrl,
Object.keys(extraQueries).length > 0 ? extraQueries : undefined,
);
}
};

const mergedQuerySchema = {
...IMessageQuerystringSchema,
Expand All @@ -155,20 +155,26 @@ export abstract class AbstractModuleApi<T extends IModule>
},
};

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);
}
}

Expand Down Expand Up @@ -326,19 +332,54 @@ export abstract class AbstractModuleApi<T extends IModule>
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);
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: 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;
Expand Down
6 changes: 5 additions & 1 deletion 00_Base/src/interfaces/api/AsDataEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export const AsDataEndpoint = function (
METADATA_DATA_ENDPOINTS,
target.constructor,
) as Array<IDataEndpointDefinition>;
let tagList: string[] | undefined = undefined;
if (tags) {
tagList = Array.isArray(tags) ? tags : [tags];
}
dataEndpoints.push({
method: descriptor.value,
methodName: propertyKey,
Expand All @@ -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,
});
Expand Down
11 changes: 10 additions & 1 deletion 00_Base/src/ocpp/persistence/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -48,3 +48,12 @@ export function QuerySchema(
}
return schema;
}

export const MessageConfirmationSchema = QuerySchema(
'MessageConfirmationSchema',
[
['success', 'boolean'],
['payload', 'string'],
],
['success'],
);
2 changes: 1 addition & 1 deletion 01_Data/src/interfaces/queries/TlsCertificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { QuerySchema } from '@citrineos/base';
export const TlsCertificateSchema = QuerySchema(
'TlsCertificateSchema',
[
['certificateChain', 'array'],
['certificateChain', 'string[]'],
['privateKey', 'string'],
['rootCA', 'string'],
['subCAKey', 'string'],
Expand Down
13 changes: 10 additions & 3 deletions 02_Util/src/util/directus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
createOperation,
DirectusFlow,
DirectusOperation,
readAssetArrayBuffer,
readFlows,
rest,
RestClient,
staticToken,
updateFlow,
updateOperation,
readAssetArrayBuffer,
uploadFiles,
} from '@directus/sdk';
import { RouteOptions } from 'fastify';
Expand Down Expand Up @@ -74,6 +74,7 @@ export class DirectusUtil implements IFileAccess {

public addDirectusMessageApiFlowsFastifyRouteHook(
routeOptions: RouteOptions,
schemas: Record<string, unknown>,
) {
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') {
Expand All @@ -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,
);
}
}
}

Expand Down
43 changes: 43 additions & 0 deletions 02_Util/src/util/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OpenAPIV2.Document>;
openapiObject: Partial<OpenAPIV3.Document | OpenAPIV3_1.Document>;
}) {
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,
Expand Down Expand Up @@ -148,6 +190,7 @@ const registerFastifySwagger = (
},
},
},
transformObject: OcppTransformObject,
refResolver: {
buildLocalReference,
},
Expand Down
21 changes: 11 additions & 10 deletions Server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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');
});
Expand Down

0 comments on commit 30a00c8

Please sign in to comment.