mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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.  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:
		
							parent
							
								
									9ff393b3d7
								
							
						
					
					
						commit
						1d6dc9b195
					
				@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								src/lib/openapi/spec/base-pagination-parameters.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/lib/openapi/spec/base-pagination-parameters.ts
									
									
									
									
									
										Normal 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>
 | 
			
		||||
>;
 | 
			
		||||
@ -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`
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										34
									
								
								src/lib/openapi/spec/integration-events-schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/lib/openapi/spec/integration-events-schema.ts
									
									
									
									
									
										Normal 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
 | 
			
		||||
>;
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user