Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: generate directus flows #294

Merged
merged 2 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading