1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-11-10 01:19:53 +01:00
unleash.unleash/src/lib/routes/admin-api/addon.ts
Nuno Góis 1d6dc9b195
chore: integration events API (#7639)
https://linear.app/unleash/issue/2-2439/create-new-integration-events-endpoint

https://linear.app/unleash/issue/2-2436/create-new-integration-event-openapi-schemas

This adds a new `/events` endpoint to the Addons API, allowing us to
fetch integration events for a specific integration configuration id.


![image](https://github.com/user-attachments/assets/e95b669e-e498-40c0-9d66-55be30a24c13)

Also includes:
- `IntegrationEventsSchema`: New schema to represent the response object
of the list of integration events;
- `yarn schema:update`: New `package.json` script to update the OpenAPI
spec file;
- `BasePaginationParameters`: This is copied from Enterprise. After
merging this we should be able to refactor Enterprise to use this one
instead of the one it has, so we don't repeat ourselves;

We're also now correctly representing the BIGSERIAL as BigInt (string +
pattern) in our OpenAPI schema. Otherwise our validation would complain,
since we're saying it's a number in the schema but in fact returning a
string.
2024-07-23 10:09:19 +01:00

314 lines
10 KiB
TypeScript

import type { Request, Response } from 'express';
import Controller from '../controller';
import type {
IFlagResolver,
IUnleashConfig,
IUnleashServices,
} from '../../types';
import type { Logger } from '../../logger';
import type AddonService from '../../services/addon-service';
import {
ADMIN,
CREATE_ADDON,
DELETE_ADDON,
NONE,
UPDATE_ADDON,
} from '../../types/permissions';
import type { IAuthRequest } from '../unleash-types';
import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema';
import type { OpenApiService } from '../../services/openapi-service';
import { type AddonSchema, addonSchema } from '../../openapi/spec/addon-schema';
import { serializeDates } from '../../types/serialize-dates';
import {
type AddonsSchema,
addonsSchema,
} from '../../openapi/spec/addons-schema';
import {
emptyResponse,
getStandardResponses,
} from '../../openapi/util/standard-responses';
import type { AddonCreateUpdateSchema } from '../../openapi/spec/addon-create-update-schema';
import {
type BasePaginationParameters,
basePaginationParameters,
} from '../../openapi/spec/base-pagination-parameters';
import {
type IntegrationEventsSchema,
integrationEventsSchema,
} from '../../openapi/spec/integration-events-schema';
import { BadDataError, NotFoundError } from '../../error';
import type { IntegrationEventsService } from '../../services';
type AddonServices = Pick<
IUnleashServices,
'addonService' | 'openApiService' | 'integrationEventsService'
>;
const PATH = '/';
class AddonController extends Controller {
private logger: Logger;
private addonService: AddonService;
private openApiService: OpenApiService;
private integrationEventsService: IntegrationEventsService;
private flagResolver: IFlagResolver;
constructor(
config: IUnleashConfig,
{
addonService,
openApiService,
integrationEventsService,
}: AddonServices,
) {
super(config);
this.logger = config.getLogger('/admin-api/addon.ts');
this.addonService = addonService;
this.openApiService = openApiService;
this.integrationEventsService = integrationEventsService;
this.flagResolver = config.flagResolver;
this.route({
method: 'get',
path: '',
permission: NONE,
handler: this.getAddons,
middleware: [
openApiService.validPath({
summary: 'Get all addons and providers',
description:
'Retrieve all addons and providers that are defined on this Unleash instance.',
tags: ['Addons'],
operationId: 'getAddons',
responses: {
...getStandardResponses(401),
200: createResponseSchema('addonsSchema'),
},
}),
],
});
this.route({
method: 'post',
path: '',
handler: this.createAddon,
permission: CREATE_ADDON,
middleware: [
openApiService.validPath({
summary: 'Create a new addon',
description:
'Create an addon instance. The addon must use one of the providers available on this Unleash instance.',
tags: ['Addons'],
operationId: 'createAddon',
requestBody: createRequestSchema('addonCreateUpdateSchema'),
responses: {
200: createResponseSchema('addonSchema'),
...getStandardResponses(400, 401, 403, 413, 415),
},
}),
],
});
this.route({
method: 'get',
path: `${PATH}:id`,
handler: this.getAddon,
permission: NONE,
middleware: [
openApiService.validPath({
summary: 'Get a specific addon',
description:
'Retrieve information about the addon whose ID matches the ID in the request URL.',
tags: ['Addons'],
operationId: 'getAddon',
responses: {
200: createResponseSchema('addonSchema'),
...getStandardResponses(401),
},
}),
],
});
this.route({
method: 'put',
path: `${PATH}:id`,
handler: this.updateAddon,
permission: UPDATE_ADDON,
middleware: [
openApiService.validPath({
summary: 'Update an addon',
description: `Update the addon with a specific ID. Any fields in the update object will be updated. Properties that are not included in the update object will not be affected. To empty a property, pass \`null\` as that property's value.
Note: passing \`null\` as a value for the description property will set it to an empty string.`,
tags: ['Addons'],
operationId: 'updateAddon',
requestBody: createRequestSchema('addonCreateUpdateSchema'),
responses: {
200: createResponseSchema('addonSchema'),
...getStandardResponses(400, 401, 403, 404, 413, 415),
},
}),
],
});
this.route({
method: 'delete',
path: `${PATH}:id`,
handler: this.deleteAddon,
acceptAnyContentType: true,
permission: DELETE_ADDON,
middleware: [
openApiService.validPath({
summary: 'Delete an addon',
description:
'Delete the addon specified by the ID in the request path.',
tags: ['Addons'],
operationId: 'deleteAddon',
responses: {
200: emptyResponse,
...getStandardResponses(401, 403, 404),
},
}),
],
});
this.route({
method: 'get',
path: `${PATH}:id/events`,
handler: this.getIntegrationEvents,
permission: ADMIN,
middleware: [
openApiService.validPath({
tags: ['Unstable'],
operationId: 'getIntegrationEvents',
summary:
'Get integration events for a specific integration configuration.',
description:
'Returns a list of integration events belonging to a specific integration configuration, identified by its id.',
parameters: [...basePaginationParameters],
responses: {
...getStandardResponses(401, 403, 404),
200: createResponseSchema(integrationEventsSchema.$id),
},
}),
],
});
}
async getAddons(req: Request, res: Response<AddonsSchema>): Promise<void> {
const addons = await this.addonService.getAddons();
const providers = this.addonService.getProviderDefinitions();
this.openApiService.respondWithValidation(200, res, addonsSchema.$id, {
addons: serializeDates(addons),
providers: serializeDates(providers),
});
}
async getAddon(
req: Request<{ id: number }, any, any, any>,
res: Response<AddonSchema>,
): Promise<void> {
const { id } = req.params;
const addon = await this.addonService.getAddon(id);
this.openApiService.respondWithValidation(
200,
res,
addonSchema.$id,
serializeDates(addon),
);
}
async updateAddon(
req: IAuthRequest<{ id: number }, any, AddonCreateUpdateSchema, any>,
res: Response<AddonSchema>,
): Promise<void> {
const { id } = req.params;
const data = req.body;
const addon = await this.addonService.updateAddon(id, data, req.audit);
this.openApiService.respondWithValidation(
200,
res,
addonSchema.$id,
serializeDates(addon),
);
}
async createAddon(
req: IAuthRequest<AddonCreateUpdateSchema, any, any, any>,
res: Response<AddonSchema>,
): Promise<void> {
const data = req.body;
const addon = await this.addonService.createAddon(data, req.audit);
this.openApiService.respondWithValidation(
201,
res,
addonSchema.$id,
serializeDates(addon),
);
}
async deleteAddon(
req: IAuthRequest<{ id: number }, any, any, any>,
res: Response<void>,
): Promise<void> {
const { id } = req.params;
await this.addonService.removeAddon(id, req.audit);
res.status(200).end();
}
async getIntegrationEvents(
req: IAuthRequest<
{ id: number },
unknown,
unknown,
BasePaginationParameters
>,
res: Response<IntegrationEventsSchema>,
): Promise<void> {
if (!this.flagResolver.isEnabled('integrationEvents')) {
throw new NotFoundError('This feature is not enabled');
}
const { id } = req.params;
if (Number.isNaN(Number(id))) {
throw new BadDataError('Invalid integration configuration id');
}
const { limit = '50', offset = '0' } = req.query;
const normalizedLimit =
Number(limit) > 0 && Number(limit) <= 100 ? Number(limit) : 50;
const normalizedOffset = Number(offset) > 0 ? Number(offset) : 0;
const integrationEvents =
await this.integrationEventsService.getPaginatedEvents(
id,
normalizedLimit,
normalizedOffset,
);
this.openApiService.respondWithValidation(
200,
res,
integrationEventsSchema.$id,
{
integrationEvents: serializeDates(integrationEvents),
},
);
}
}
export default AddonController;
module.exports = AddonController;