1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

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.
This commit is contained in:
Nuno Góis 2024-07-23 10:09:19 +01:00 committed by GitHub
parent 9ff393b3d7
commit 1d6dc9b195
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 187 additions and 16 deletions

View File

@ -20,9 +20,7 @@ fs.readdir(directoryPath, (err, files) => {
const script = path.basename(__filename);
const message = `/**
* Auto-generated file by ${script}. Do not edit.
* To run it manually execute \`node ${script}\` from ${path.basename(
__dirname,
)}
* To run it manually execute \`yarn schema:update\` or \`node ${path.basename(__dirname)}/${script}\`
*/\n`;
fs.writeFileSync(indexPath, `${message}${exports}\n${message}`, (err) => {
if (err) {

View File

@ -63,7 +63,8 @@
"clean": "del-cli --force dist",
"preversion": "./scripts/check-release.sh",
"heroku-postbuild": "cd frontend && yarn && yarn build",
"prepack": "./scripts/prepack.sh"
"prepack": "./scripts/prepack.sh",
"schema:update": "node ./.husky/update-openapi-spec-list.js"
},
"jest-junit": {
"suiteName": "Unleash Unit Tests",

View File

@ -1,4 +1,5 @@
import { CRUDStore, type CrudStoreConfig } from '../../db/crud/crud-store';
import type { Row } from '../../db/crud/row-type';
import type { Db } from '../../db/db';
import type { IntegrationEventSchema } from '../../openapi/spec/integration-event-schema';
@ -11,7 +12,10 @@ export type IntegrationEventState = IntegrationEventWriteModel['state'];
export class IntegrationEventsStore extends CRUDStore<
IntegrationEventSchema,
IntegrationEventWriteModel
IntegrationEventWriteModel,
Row<IntegrationEventSchema>,
Row<IntegrationEventWriteModel>,
string
> {
constructor(db: Db, config: CrudStoreConfig) {
super('integration_events', db, config);

View File

@ -1,7 +1,7 @@
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import {
type IUnleashTest,
setupAppWithAuth,
setupAppWithCustomConfig,
} from '../../../test/e2e/helpers/test-helper';
import getLogger from '../../../test/fixtures/no-logger';
import { TEST_AUDIT_USER } from '../../types';
@ -37,7 +37,7 @@ const EVENT_FAILED: IntegrationEventWriteModel = {
beforeAll(async () => {
db = await dbInit('integration_events', getLogger);
app = await setupAppWithAuth(
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
@ -192,3 +192,16 @@ test('clean up events, keeping the last 100 events', async () => {
expect(events).toHaveLength(100);
});
test('return events from the API', async () => {
await integrationEventsService.registerEvent(getTestEventSuccess());
await integrationEventsService.registerEvent(getTestEventFailed());
const { body } = await app.request.get(
`/api/admin/addons/${integrationId}/events`,
);
expect(body.integrationEvents).toHaveLength(2);
expect(body.integrationEvents[0].state).toBe('failed');
expect(body.integrationEvents[1].state).toBe('success');
});

View File

@ -0,0 +1,28 @@
import type { FromQueryParams } from '../util/from-query-params';
export const basePaginationParameters = [
{
name: 'limit',
schema: {
type: 'string',
example: '50',
},
description:
'The number of results to return in a page. By default it is set to 50.',
in: 'query',
},
{
name: 'offset',
schema: {
type: 'string',
example: '50',
},
description:
'The number of results to skip when returning a page. By default it is set to 0.',
in: 'query',
},
] as const;
export type BasePaginationParameters = Partial<
FromQueryParams<typeof basePaginationParameters>
>;

View File

@ -1,6 +1,6 @@
/**
* Auto-generated file by update-openapi-spec-list.js. Do not edit.
* To run it manually execute `node update-openapi-spec-list.js` from .husky
* To run it manually execute `yarn schema:update` or `node .husky/update-openapi-spec-list.js`
*/
export * from './addon-create-update-schema';
export * from './addon-parameter-schema';
@ -112,6 +112,7 @@ export * from './inactive-user-schema';
export * from './inactive-users-schema';
export * from './instance-admin-stats-schema';
export * from './integration-event-schema';
export * from './integration-events-schema';
export * from './legal-value-schema';
export * from './login-schema';
export * from './maintenance-schema';
@ -210,5 +211,5 @@ export * from './variants-schema';
export * from './version-schema';
/**
* Auto-generated file by update-openapi-spec-list.js. Do not edit.
* To run it manually execute `node update-openapi-spec-list.js` from .husky
* To run it manually execute `yarn schema:update` or `node .husky/update-openapi-spec-list.js`
*/

View File

@ -19,11 +19,11 @@ export const integrationEventSchema = {
additionalProperties: false,
properties: {
id: {
type: 'integer',
type: 'string',
pattern: '^[0-9]+$', // BigInt
description:
"The integration event's ID. Integration event IDs are incrementing integers. In other words, a more recently created integration event will always have a higher ID than an older one.",
minimum: 1,
example: 7,
"The integration event's ID. Integration event IDs are incrementing integers. In other words, a more recently created integration event will always have a higher ID than an older one. This ID is represented as a string since it is a BigInt.",
example: '7',
},
integrationId: {
type: 'integer',

View File

@ -0,0 +1,34 @@
import type { FromSchema } from 'json-schema-to-ts';
import { eventSchema } from './event-schema';
import { tagSchema } from './tag-schema';
import { variantSchema } from './variant-schema';
import { integrationEventSchema } from './integration-event-schema';
export const integrationEventsSchema = {
$id: '#/components/schemas/integrationEventsSchema',
description: 'A response model with a list of integration events.',
type: 'object',
additionalProperties: false,
required: ['integrationEvents'],
properties: {
integrationEvents: {
type: 'array',
description: 'A list of integration events.',
items: {
$ref: integrationEventSchema.$id,
},
},
},
components: {
schemas: {
integrationEventSchema,
eventSchema,
tagSchema,
variantSchema,
},
},
} as const;
export type IntegrationEventsSchema = FromSchema<
typeof integrationEventsSchema
>;

View File

@ -1,10 +1,15 @@
import type { Request, Response } from 'express';
import Controller from '../controller';
import type { IUnleashConfig, IUnleashServices } from '../../types';
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,
@ -25,8 +30,21 @@ import {
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'>;
type AddonServices = Pick<
IUnleashServices,
'addonService' | 'openApiService' | 'integrationEventsService'
>;
const PATH = '/';
@ -37,14 +55,24 @@ class AddonController extends Controller {
private openApiService: OpenApiService;
private integrationEventsService: IntegrationEventsService;
private flagResolver: IFlagResolver;
constructor(
config: IUnleashConfig,
{ addonService, openApiService }: AddonServices,
{
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',
@ -149,6 +177,28 @@ Note: passing \`null\` as a value for the description property will set it to an
}),
],
});
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> {
@ -216,6 +266,48 @@ Note: passing \`null\` as a value for the description property will set it to an
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;