From 741ba2294328f8583fbd270510dfbf5cc3031278 Mon Sep 17 00:00:00 2001 From: Uddish Verma Date: Wed, 7 Oct 2026 12:34:47 +0000 Subject: [PATCH] refactor(API): Migrate the log streaming destination writes to the decorator pattern (no-changelog) (#40397) Co-authored-by: Claude Opus 5.5 --- packages/@n8n/api-types/src/dto/index.ts | 8 +- .../log-streaming/log-streaming-public.dto.ts | 67 ++- .../log-streaming-public.openapi.ts | 4 + packages/cli/src/public-api/types.ts | 3 - .../v1/controllers/log-streaming.mapper.ts | 24 +- .../log-streaming.public.controller.ts | 92 ++++- .../log-streaming/log-streaming.handler.ts | 75 +--- .../log-streaming/log-streaming.mapper.ts | 17 - .../spec/paths/destinations.id.yml | 44 -- .../log-streaming/spec/paths/destinations.yml | 35 -- ...reateLogStreamingDestination.generated.yml | 385 ++++++++++++++++++ .../getLogStreamingDestination.generated.yml | 355 +--------------- ...pdateLogStreamingDestination.generated.yml | 379 +++++++++++++++++ .../v1/openapi.decorator-routes.generated.yml | 4 + packages/cli/src/public-api/v1/openapi.yml | 2 - ...treamingDestinationPublicDto.generated.yml | 354 ++++++++++++++++ .../integration/public-api/discover.test.ts | 3 +- .../public-api/log-streaming.test.ts | 52 ++- 18 files changed, 1337 insertions(+), 566 deletions(-) delete mode 100644 packages/cli/src/public-api/v1/handlers/log-streaming/log-streaming.mapper.ts delete mode 100644 packages/cli/src/public-api/v1/handlers/log-streaming/spec/paths/destinations.yml create mode 100644 packages/cli/src/public-api/v1/handlers/settings/spec/paths/createLogStreamingDestination.generated.yml create mode 100644 packages/cli/src/public-api/v1/handlers/settings/spec/paths/updateLogStreamingDestination.generated.yml create mode 100644 packages/cli/src/public-api/v1/shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml diff --git a/packages/@n8n/api-types/src/dto/index.ts b/packages/@n8n/api-types/src/dto/index.ts index ec013c1978c..6489ca92fae 100644 --- a/packages/@n8n/api-types/src/dto/index.ts +++ b/packages/@n8n/api-types/src/dto/index.ts @@ -339,10 +339,12 @@ export { LogStreamingDestinationListPublicDto, LogStreamingDestinationPublicDto, LogStreamingEventTypesPublicDto, - PublicCreateDestinationDto, + CreateLogStreamingDestinationPublicDto, + UpdateLogStreamingDestinationPublicDto, type LogStreamingDestinationPublic, - type PublicCreateDestination, - type PublicDestinationType, + type CreateLogStreamingDestinationPublic, + type UpdateLogStreamingDestinationPublic, + type LogStreamingDestinationPublicType, } from './log-streaming/log-streaming-public.dto'; export { PaginationDto, MAX_ITEMS_PER_PAGE } from './pagination/pagination.dto'; diff --git a/packages/@n8n/api-types/src/dto/log-streaming/log-streaming-public.dto.ts b/packages/@n8n/api-types/src/dto/log-streaming/log-streaming-public.dto.ts index c5f5da1deec..fcca3fdace6 100644 --- a/packages/@n8n/api-types/src/dto/log-streaming/log-streaming-public.dto.ts +++ b/packages/@n8n/api-types/src/dto/log-streaming/log-streaming-public.dto.ts @@ -15,6 +15,7 @@ import { logStreamingDestinationCommonFieldDocs, logStreamingDestinationDocs, logStreamingDestinationListFieldDocs, + logStreamingDestinationRequestReadOnlyFieldDocs, logStreamingEventTypesFieldDocs, logStreamingParameterFieldDocs, logStreamingParameterListDocs, @@ -23,6 +24,7 @@ import { logStreamingWebhookFieldDocs, logStreamingWebhookOptionsFieldDocs, } from './log-streaming-public.openapi'; +import { readOnlyPublicSchema } from '../../schemas/read-only-public.schema'; import { Z } from '../../zod-class'; const logStreamingEventTypesPublicSchema = z @@ -166,29 +168,34 @@ const publicSentrySchema = z.object({ dsn: sentryShape.dsn.openapi(sentryDocs.dsn), }); -export const PublicCreateDestinationDto = z.discriminatedUnion('type', [ - publicWebhookSchema, - publicSyslogSchema, - publicSentrySchema, -]); - -export type PublicCreateDestination = z.infer; - -export type PublicDestinationType = PublicCreateDestination['type']; - const idSchema = z.string().openapi(commonDocs.id); -export const logStreamingDestinationPublicSchema = z - .discriminatedUnion('type', [ - publicWebhookSchema.extend({ id: idSchema }), - publicSyslogSchema.extend({ id: idSchema }), - publicSentrySchema.extend({ id: idSchema }), - ]) +const readOnlyIdSchema = readOnlyPublicSchema(logStreamingDestinationRequestReadOnlyFieldDocs.id); + +const destinationUnion = (id: Id) => + z + .discriminatedUnion('type', [ + publicWebhookSchema.extend({ id }), + publicSyslogSchema.extend({ id }), + publicSentrySchema.extend({ id }), + ]) + .openapi(logStreamingDestinationDocs); + +export const logStreamingDestinationPublicSchema = destinationUnion(idSchema); +const createLogStreamingDestinationPublicSchema = destinationUnion(readOnlyIdSchema); +const updateLogStreamingDestinationPublicSchema = z + .discriminatedUnion('type', [publicWebhookSchema, publicSyslogSchema, publicSentrySchema]) .openapi(logStreamingDestinationDocs); export type LogStreamingDestinationPublic = z.infer; +export type CreateLogStreamingDestinationPublic = z.infer< + typeof createLogStreamingDestinationPublicSchema +>; +export type UpdateLogStreamingDestinationPublic = z.infer< + typeof updateLogStreamingDestinationPublicSchema +>; +export type LogStreamingDestinationPublicType = LogStreamingDestinationPublic['type']; -/** A discriminated union has no object shape, so this cannot extend `Z.class`. */ export class LogStreamingDestinationPublicDto { static schema = logStreamingDestinationPublicSchema; @@ -201,6 +208,32 @@ export class LogStreamingDestinationPublicDto { } } +export type CreateLogStreamingDestinationPublicDto = CreateLogStreamingDestinationPublic; +export const CreateLogStreamingDestinationPublicDto = class { + static schema = createLogStreamingDestinationPublicSchema; + + static safeParse(data: unknown) { + return createLogStreamingDestinationPublicSchema.safeParse(data); + } + + static parse(data: unknown): CreateLogStreamingDestinationPublic { + return createLogStreamingDestinationPublicSchema.parse(data); + } +}; + +export type UpdateLogStreamingDestinationPublicDto = UpdateLogStreamingDestinationPublic; +export const UpdateLogStreamingDestinationPublicDto = class { + static schema = updateLogStreamingDestinationPublicSchema; + + static safeParse(data: unknown) { + return updateLogStreamingDestinationPublicSchema.safeParse(data); + } + + static parse(data: unknown): UpdateLogStreamingDestinationPublic { + return updateLogStreamingDestinationPublicSchema.parse(data); + } +}; + const logStreamingDestinationListPublicSchema = z .object({ data: z diff --git a/packages/@n8n/api-types/src/dto/log-streaming/log-streaming-public.openapi.ts b/packages/@n8n/api-types/src/dto/log-streaming/log-streaming-public.openapi.ts index 32daf10fef2..6fcb421a41f 100644 --- a/packages/@n8n/api-types/src/dto/log-streaming/log-streaming-public.openapi.ts +++ b/packages/@n8n/api-types/src/dto/log-streaming/log-streaming-public.openapi.ts @@ -39,6 +39,10 @@ export const logStreamingDestinationCommonFieldDocs = { }, } as const satisfies Record; +export const logStreamingDestinationRequestReadOnlyFieldDocs = { + id: { type: 'string', readOnly: true, ...logStreamingDestinationCommonFieldDocs.id }, +} as const satisfies Record; + export const logStreamingCircuitBreakerFieldDocs = { maxFailures: { description: 'Maximum failures within the sliding window before the breaker opens.', diff --git a/packages/cli/src/public-api/types.ts b/packages/cli/src/public-api/types.ts index 4a69f6f1998..bd4be33d12d 100644 --- a/packages/cli/src/public-api/types.ts +++ b/packages/cli/src/public-api/types.ts @@ -1,5 +1,4 @@ import type { - PublicCreateDestination, UpdateOtelSettingsDto, TestOtelTraceDto, UpdateSamlConfigurationDto, @@ -115,8 +114,6 @@ export declare namespace CommunityPackageRequest { } export declare namespace LogStreamingRequest { - type CreateDestination = AuthenticatedRequest<{}, {}, PublicCreateDestination>; - type UpdateDestination = AuthenticatedRequest<{ id: string }, {}, PublicCreateDestination>; type TestDestination = AuthenticatedRequest<{ id: string }>; type DeleteDestination = AuthenticatedRequest<{ id: string }>; } diff --git a/packages/cli/src/public-api/v1/controllers/log-streaming.mapper.ts b/packages/cli/src/public-api/v1/controllers/log-streaming.mapper.ts index 66ada5a32ac..0397032d9d7 100644 --- a/packages/cli/src/public-api/v1/controllers/log-streaming.mapper.ts +++ b/packages/cli/src/public-api/v1/controllers/log-streaming.mapper.ts @@ -1,16 +1,36 @@ -import { LogStreamingDestinationPublicDto, type PublicDestinationType } from '@n8n/api-types'; +import { + LogStreamingDestinationPublicDto, + type UpdateLogStreamingDestinationPublic, + type LogStreamingDestinationPublicType, +} from '@n8n/api-types'; import type { MessageEventBusDestinationOptions } from 'n8n-workflow'; import { MessageEventBusDestinationTypeNames } from 'n8n-workflow'; const INTERNAL_TO_PUBLIC: Partial< - Record + Record > = { [MessageEventBusDestinationTypeNames.webhook]: 'webhook', [MessageEventBusDestinationTypeNames.syslog]: 'syslog', [MessageEventBusDestinationTypeNames.sentry]: 'sentry', }; +const PUBLIC_TO_INTERNAL: Record< + LogStreamingDestinationPublicType, + MessageEventBusDestinationTypeNames +> = { + webhook: MessageEventBusDestinationTypeNames.webhook, + syslog: MessageEventBusDestinationTypeNames.syslog, + sentry: MessageEventBusDestinationTypeNames.sentry, +}; + export function toLogStreamingDestinationPublic(options: MessageEventBusDestinationOptions) { const type = options.__type ? INTERNAL_TO_PUBLIC[options.__type] : undefined; return LogStreamingDestinationPublicDto.parse({ ...options, type }); } + +export function toInternalDestinationOptions( + input: UpdateLogStreamingDestinationPublic, +): MessageEventBusDestinationOptions { + const { type, ...rest } = input; + return { ...rest, __type: PUBLIC_TO_INTERNAL[type] }; +} diff --git a/packages/cli/src/public-api/v1/controllers/log-streaming.public.controller.ts b/packages/cli/src/public-api/v1/controllers/log-streaming.public.controller.ts index 0e2bac6953a..be80c5face3 100644 --- a/packages/cli/src/public-api/v1/controllers/log-streaming.public.controller.ts +++ b/packages/cli/src/public-api/v1/controllers/log-streaming.public.controller.ts @@ -1,11 +1,16 @@ import { + CreateLogStreamingDestinationPublicDto, + UpdateLogStreamingDestinationPublicDto, LogStreamingDestinationListPublicDto, LogStreamingDestinationPublicDto, LogStreamingEventTypesPublicDto, logStreamingDestinationIdParamSchema, } from '@n8n/api-types'; +import { OutboundHttp } from '@n8n/backend-network'; +import { CredentialsFinderService } from '@n8n/backend-services'; +import { InstanceSettingsLoaderConfig } from '@n8n/config'; import { LICENSE_FEATURES } from '@n8n/constants'; -import type { AuthenticatedRequest } from '@n8n/db'; +import type { AuthenticatedRequest, User } from '@n8n/db'; import { ApiDescription, ApiErrorResponse, @@ -13,25 +18,40 @@ import { ApiResponse, ApiSummary, ApiTags, + Body, Get, Licensed, Param, + Post, PublicApiController, + Put, } from '@n8n/decorators'; import type { Response } from 'express'; import type { MessageEventBusDestinationOptions } from 'n8n-workflow'; -import { NotFoundError } from '@n8n/errors'; +import { ConflictError, NotFoundError } from '@n8n/errors'; import { eventNamesAll } from '@/eventbus/event-message-classes'; +import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; +import { createMessageEventBusDestination } from '@/modules/log-streaming.ee/create-message-event-bus-destination'; +import { assertUserCanUseDestinationCredentials } from '@/modules/log-streaming.ee/destinations/destination-credentials-access'; import { LogStreamingDestinationService } from '@/modules/log-streaming.ee/log-streaming-destination.service'; -import { toLogStreamingDestinationPublic } from './log-streaming.mapper'; +import { + toInternalDestinationOptions, + toLogStreamingDestinationPublic, +} from './log-streaming.mapper'; const tags = ['LogStreaming']; @PublicApiController('/settings/log-streaming') export class LogStreamingPublicController { - constructor(private readonly destinationService: LogStreamingDestinationService) {} + constructor( + private readonly destinationService: LogStreamingDestinationService, + private readonly eventBus: MessageEventBus, + private readonly instanceSettingsLoaderConfig: InstanceSettingsLoaderConfig, + private readonly outboundHttp: OutboundHttp, + private readonly credentialsFinderService: CredentialsFinderService, + ) {} @Get('/event-types') @ApiKeyScope('eventBusDestination:list') @@ -81,6 +101,70 @@ export class LogStreamingPublicController { return toLogStreamingDestinationPublic(destination); } + @Post('/destinations') + @ApiKeyScope('eventBusDestination:create') + @Licensed(LICENSE_FEATURES.LOG_STREAMING) + @ApiSummary('Create a log streaming destination') + @ApiDescription( + 'Create a log streaming destination. The destination takes effect exactly as it would from the UI, using the same validation. Requires the `eventBusDestination:create` scope and the Log Streaming feature to be licensed. When destinations are managed via environment variables, the write is rejected with 409 and nothing is created; reads still return the current values.', + ) + @ApiTags(tags) + @ApiResponse(200, LogStreamingDestinationPublicDto) + @ApiErrorResponse(409) + async createLogStreamingDestination( + req: AuthenticatedRequest, + _res: Response, + @Body body: CreateLogStreamingDestinationPublicDto, + ): Promise { + this.assertNotManagedByEnv(); + + return await this.saveDestination(req.user, toInternalDestinationOptions(body)); + } + + @Put('/destinations/:id') + @ApiKeyScope('eventBusDestination:update') + @Licensed(LICENSE_FEATURES.LOG_STREAMING) + @ApiSummary('Update a log streaming destination') + @ApiDescription( + 'Replace an existing log streaming destination. The update takes effect exactly as it would from the UI, using the same validation. Requires the `eventBusDestination:update` scope and the Log Streaming feature to be licensed. When destinations are managed via environment variables, the write is rejected with 409 and nothing is changed; reads still return the current values.', + ) + @ApiTags(tags) + @ApiResponse(200, LogStreamingDestinationPublicDto) + @ApiErrorResponse(404) + @ApiErrorResponse(409) + async updateLogStreamingDestination( + req: AuthenticatedRequest, + _res: Response, + @Param('id', logStreamingDestinationIdParamSchema) id: string, + @Body body: UpdateLogStreamingDestinationPublicDto, + ): Promise { + this.assertNotManagedByEnv(); + await this.findDestinationOrFail(id); + + // `addDestination` replaces the stored destination that has this id. + return await this.saveDestination(req.user, { ...toInternalDestinationOptions(body), id }); + } + + private assertNotManagedByEnv() { + if (this.instanceSettingsLoaderConfig.logStreamingManagedByEnv) { + throw new ConflictError( + 'Log streaming destinations are managed via environment variables and cannot be modified through the API', + ); + } + } + + private async saveDestination( + user: User, + options: MessageEventBusDestinationOptions, + ): Promise { + await assertUserCanUseDestinationCredentials(this.credentialsFinderService, user, options); + + const destination = createMessageEventBusDestination(this.eventBus, this.outboundHttp, options); + const result = await this.destinationService.addDestination(destination); + + return toLogStreamingDestinationPublic(result.serialize()); + } + private async findDestinationOrFail(id: string): Promise { const [destination] = await this.destinationService.findDestination(id); if (!destination) { diff --git a/packages/cli/src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts b/packages/cli/src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts index 93c17fd9f8a..3bf6d38f905 100644 --- a/packages/cli/src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts +++ b/packages/cli/src/public-api/v1/handlers/log-streaming/log-streaming.handler.ts @@ -1,16 +1,11 @@ -import { PublicCreateDestinationDto } from '@n8n/api-types'; -import { OutboundHttp } from '@n8n/backend-network'; import { InstanceSettingsLoaderConfig } from '@n8n/config'; import { Container } from '@n8n/di'; import type { MessageEventBusDestinationOptions } from 'n8n-workflow'; -import { BadRequestError, ConflictError, NotFoundError } from '@n8n/errors'; -import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; -import { createMessageEventBusDestination } from '@/modules/log-streaming.ee/create-message-event-bus-destination'; +import { ConflictError, NotFoundError } from '@n8n/errors'; import { assertUserCanUseDestinationCredentials } from '@/modules/log-streaming.ee/destinations/destination-credentials-access'; import { LogStreamingDestinationService } from '@/modules/log-streaming.ee/log-streaming-destination.service'; -import { toInternalDestinationOptions } from './log-streaming.mapper'; import { toLogStreamingDestinationPublic } from '../../controllers/log-streaming.mapper'; import type { LogStreamingRequest } from '../../../types'; import type { PublicAPIEndpoint } from '../../shared/handler.types'; @@ -41,79 +36,11 @@ const getCredentialsFinderService = async () => { }; type LogStreamingHandlers = { - createDestination: PublicAPIEndpoint; - updateDestination: PublicAPIEndpoint; testDestination: PublicAPIEndpoint; deleteDestination: PublicAPIEndpoint; }; const logStreamingHandlers: LogStreamingHandlers = { - createDestination: [ - isLicensed('feat:logStreaming'), - apiKeyHasScopeWithGlobalScopeFallback({ scope: 'eventBusDestination:create' }), - async (req, res) => { - assertNotManagedByEnv(); - - const parseResult = PublicCreateDestinationDto.safeParse(req.body); - if (!parseResult.success) { - throw new BadRequestError(parseResult.error.errors[0].message); - } - - const options = toInternalDestinationOptions(parseResult.data); - - await assertUserCanUseDestinationCredentials( - await getCredentialsFinderService(), - req.user, - options, - ); - - const destination = createMessageEventBusDestination( - Container.get(MessageEventBus), - Container.get(OutboundHttp), - options, - ); - const result = await Container.get(LogStreamingDestinationService).addDestination( - destination, - ); - - return res.json(toLogStreamingDestinationPublic(result.serialize())); - }, - ], - - updateDestination: [ - isLicensed('feat:logStreaming'), - apiKeyHasScopeWithGlobalScopeFallback({ scope: 'eventBusDestination:update' }), - async (req, res) => { - assertNotManagedByEnv(); - await findDestinationOrFail(req.params.id); - - const parseResult = PublicCreateDestinationDto.safeParse(req.body); - if (!parseResult.success) { - throw new BadRequestError(parseResult.error.errors[0].message); - } - - // the path id is the update target; addDestination upserts on it - const options = { ...toInternalDestinationOptions(parseResult.data), id: req.params.id }; - - await assertUserCanUseDestinationCredentials( - await getCredentialsFinderService(), - req.user, - options, - ); - - const destination = createMessageEventBusDestination( - Container.get(MessageEventBus), - Container.get(OutboundHttp), - options, - ); - const result = await Container.get(LogStreamingDestinationService).addDestination( - destination, - ); - - return res.json(toLogStreamingDestinationPublic(result.serialize())); - }, - ], - testDestination: [ isLicensed('feat:logStreaming'), apiKeyHasScopeWithGlobalScopeFallback({ scope: 'eventBusDestination:test' }), diff --git a/packages/cli/src/public-api/v1/handlers/log-streaming/log-streaming.mapper.ts b/packages/cli/src/public-api/v1/handlers/log-streaming/log-streaming.mapper.ts deleted file mode 100644 index 7c95c5eb32a..00000000000 --- a/packages/cli/src/public-api/v1/handlers/log-streaming/log-streaming.mapper.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type PublicCreateDestination, type PublicDestinationType } from '@n8n/api-types'; -import type { MessageEventBusDestinationOptions } from 'n8n-workflow'; -import { MessageEventBusDestinationTypeNames } from 'n8n-workflow'; - -// The public API uses a friendly `type` discriminator; the internal service uses `__type`. -const PUBLIC_TO_INTERNAL: Record = { - webhook: MessageEventBusDestinationTypeNames.webhook, - syslog: MessageEventBusDestinationTypeNames.syslog, - sentry: MessageEventBusDestinationTypeNames.sentry, -}; - -export function toInternalDestinationOptions( - input: PublicCreateDestination, -): MessageEventBusDestinationOptions { - const { type, ...rest } = input; - return { ...rest, __type: PUBLIC_TO_INTERNAL[type] }; -} diff --git a/packages/cli/src/public-api/v1/handlers/log-streaming/spec/paths/destinations.id.yml b/packages/cli/src/public-api/v1/handlers/log-streaming/spec/paths/destinations.id.yml index 0a758ce4769..9757c3bf31f 100644 --- a/packages/cli/src/public-api/v1/handlers/log-streaming/spec/paths/destinations.id.yml +++ b/packages/cli/src/public-api/v1/handlers/log-streaming/spec/paths/destinations.id.yml @@ -1,47 +1,3 @@ -put: - x-eov-operation-id: updateDestination - x-required-scope: eventBusDestination:update - x-eov-operation-handler: v1/handlers/log-streaming/log-streaming.handler - tags: - - LogStreaming - summary: Update a log streaming destination - description: > - Replace an existing log streaming destination. The update takes effect exactly as it would - from the UI, using the same validation. Requires the `eventBusDestination:update` scope and - the Log Streaming feature to be licensed. When destinations are managed via environment - variables, the write is rejected with 409 and nothing is changed; reads still return the - current values. - parameters: - - name: id - in: path - description: The id of the log streaming destination. - required: true - schema: - type: string - requestBody: - description: The destination fields to apply. - required: true - content: - application/json: - schema: - $ref: '../schemas/destination.yml' - responses: - '200': - description: Operation successful. - content: - application/json: - schema: - $ref: '../schemas/destination.yml' - '400': - $ref: '../../../../shared/spec/responses/badRequest.yml' - '401': - $ref: '../../../../shared/spec/responses/unauthorized.yml' - '403': - $ref: '../../../../shared/spec/responses/forbidden.yml' - '404': - $ref: '../../../../shared/spec/responses/notFound.yml' - '409': - $ref: '../../../../shared/spec/responses/conflict.yml' delete: x-eov-operation-id: deleteDestination x-required-scope: eventBusDestination:delete diff --git a/packages/cli/src/public-api/v1/handlers/log-streaming/spec/paths/destinations.yml b/packages/cli/src/public-api/v1/handlers/log-streaming/spec/paths/destinations.yml deleted file mode 100644 index 1d54807c8f9..00000000000 --- a/packages/cli/src/public-api/v1/handlers/log-streaming/spec/paths/destinations.yml +++ /dev/null @@ -1,35 +0,0 @@ -post: - x-eov-operation-id: createDestination - x-required-scope: eventBusDestination:create - x-eov-operation-handler: v1/handlers/log-streaming/log-streaming.handler - tags: - - LogStreaming - summary: Create a log streaming destination - description: > - Create a log streaming destination. The destination takes effect exactly as it would from - the UI, using the same validation. Requires the `eventBusDestination:create` scope and the - Log Streaming feature to be licensed. When destinations are managed via environment - variables, the write is rejected with 409 and nothing is created; reads still return the - current values. - requestBody: - description: The destination to create. - required: true - content: - application/json: - schema: - $ref: '../schemas/destination.yml' - responses: - '200': - description: Operation successful. - content: - application/json: - schema: - $ref: '../schemas/destination.yml' - '400': - $ref: '../../../../shared/spec/responses/badRequest.yml' - '401': - $ref: '../../../../shared/spec/responses/unauthorized.yml' - '403': - $ref: '../../../../shared/spec/responses/forbidden.yml' - '409': - $ref: '../../../../shared/spec/responses/conflict.yml' diff --git a/packages/cli/src/public-api/v1/handlers/settings/spec/paths/createLogStreamingDestination.generated.yml b/packages/cli/src/public-api/v1/handlers/settings/spec/paths/createLogStreamingDestination.generated.yml new file mode 100644 index 00000000000..abda62b9cc7 --- /dev/null +++ b/packages/cli/src/public-api/v1/handlers/settings/spec/paths/createLogStreamingDestination.generated.yml @@ -0,0 +1,385 @@ +operationId: createLogStreamingDestination +tags: + - LogStreaming +summary: Create a log streaming destination +description: Create a log streaming destination. The destination takes effect exactly as it would from the UI, using the same validation. Requires the `eventBusDestination:create` scope and the Log Streaming feature to be licensed. When destinations are managed via environment variables, the write is rejected with 409 and nothing is created; reads still return the current values. +x-required-scope: eventBusDestination:create +x-eov-operation-id: unreachable +x-eov-operation-handler: v1/handlers/decorator-routed.handler +x-decorator-routed: true +requestBody: + required: true + content: + application/json: + schema: + oneOf: + - type: object + properties: + label: + type: string + minLength: 1 + description: Human-readable name shown in the UI. + example: My destination + enabled: + type: boolean + description: Whether the destination currently receives events. + example: true + subscribedEvents: + type: array + items: + type: string + description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. + example: + - n8n.workflow + - n8n.audit + anonymizeAuditMessages: + type: boolean + description: Whether audit message payloads are anonymized before being sent. + example: false + circuitBreaker: + type: object + properties: + maxFailures: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum failures within the sliding window before the breaker opens. + failureWindow: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Sliding window, in milliseconds, over which failures are counted. + description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. + type: + type: string + enum: + - webhook + description: Sends each event as an HTTP request to a URL. + example: webhook + url: + type: string + format: uri + description: Target URL that receives the event payload. + example: https://example.com/n8n-events + method: + type: string + description: HTTP method used for the request. + example: POST + sendHeaders: + type: boolean + description: Whether to attach custom headers to the request. + example: false + specifyHeaders: + type: string + description: How custom headers are provided — `keypair` or `json`. + example: keypair + headerParameters: + type: object + properties: + parameters: + type: array + items: + type: object + properties: + name: + type: string + example: Authorization + value: + anyOf: + - type: string + - type: number + - type: boolean + - {} + - {} + description: Parameter value. Usually a string; numbers, booleans and null are also accepted. + example: Bearer + required: + - name + - value + required: + - parameters + description: Key-value pairs used when the matching `specify*` field is `keypair`. + jsonHeaders: + type: string + description: Custom headers as a JSON string when `specifyHeaders` is `json`. + sendQuery: + type: boolean + description: Whether to attach query parameters to the request. + example: false + specifyQuery: + type: string + description: How query parameters are provided — `keypair` or `json`. + queryParameters: + type: object + properties: + parameters: + type: array + items: + type: object + properties: + name: + type: string + example: Authorization + value: + anyOf: + - type: string + - type: number + - type: boolean + - {} + - {} + description: Parameter value. Usually a string; numbers, booleans and null are also accepted. + example: Bearer + required: + - name + - value + required: + - parameters + description: Key-value pairs used when the matching `specify*` field is `keypair`. + jsonQuery: + type: string + description: Query parameters as a JSON string when `specifyQuery` is `json`. + options: + type: object + properties: + timeout: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Request timeout in milliseconds. + allowUnauthorizedCerts: + type: boolean + description: Accept self-signed or otherwise invalid TLS certificates. + queryParameterArrays: + type: string + enum: + - indices + - brackets + - repeat + description: How array-valued query parameters are serialized into the query string. + redirect: + type: object + properties: + redirect: + type: object + properties: + followRedirects: + type: boolean + description: Whether to follow HTTP redirects. + maxRedirects: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum number of redirects to follow. + required: + - redirect + description: Redirect-following behaviour. + proxy: + type: object + properties: + proxy: + type: object + properties: + protocol: + type: string + enum: + - https + - http + description: Proxy protocol. + host: + type: string + description: Proxy host, without protocol or port. + port: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Proxy port. + required: + - protocol + - host + - port + required: + - proxy + description: Outbound proxy configuration. + socket: + type: object + properties: + keepAlive: + type: boolean + description: Whether to keep sockets open for reuse. + maxSockets: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum sockets per host kept open at once. + maxFreeSockets: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum idle sockets per host kept open. + description: Connection socket options. + description: Additional HTTP request options. + id: + type: string + description: Server-generated unique id of the destination. Returned in responses; not accepted in requests. + readOnly: true + example: 88be6560-bfb4-455c-8aa1-06971e9e5522 + required: + - type + - url + - type: object + properties: + label: + type: string + minLength: 1 + description: Human-readable name shown in the UI. + example: My destination + enabled: + type: boolean + description: Whether the destination currently receives events. + example: true + subscribedEvents: + type: array + items: + type: string + description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. + example: + - n8n.workflow + - n8n.audit + anonymizeAuditMessages: + type: boolean + description: Whether audit message payloads are anonymized before being sent. + example: false + circuitBreaker: + type: object + properties: + maxFailures: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum failures within the sliding window before the breaker opens. + failureWindow: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Sliding window, in milliseconds, over which failures are counted. + description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. + type: + type: string + enum: + - syslog + description: Sends each event to a syslog server. + example: syslog + host: + type: string + minLength: 1 + description: Syslog server host. + example: syslog.example.com + port: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Syslog server port. + example: 514 + protocol: + type: string + enum: + - udp + - tcp + - tls + description: Transport protocol. + example: udp + facility: + type: integer + minimum: 0 + maximum: 23 + description: 'Syslog facility code (0–23, per RFC 5424). Common values: 0 Kernel, 1 User, 3 System, 13 Audit, 14 Alert, 16 Local0, 17 Local1, 18 Local2, 19 Local3, 20 Local4, 21 Local5, 22 Local6, 23 Local7.' + example: 16 + app_name: + type: string + description: Application name reported in the syslog message. + example: n8n + tlsCa: + type: string + description: PEM-encoded CA certificate used when `protocol` is `tls`. + id: + type: string + description: Server-generated unique id of the destination. Returned in responses; not accepted in requests. + readOnly: true + example: 88be6560-bfb4-455c-8aa1-06971e9e5522 + required: + - type + - host + - type: object + properties: + label: + type: string + minLength: 1 + description: Human-readable name shown in the UI. + example: My destination + enabled: + type: boolean + description: Whether the destination currently receives events. + example: true + subscribedEvents: + type: array + items: + type: string + description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. + example: + - n8n.workflow + - n8n.audit + anonymizeAuditMessages: + type: boolean + description: Whether audit message payloads are anonymized before being sent. + example: false + circuitBreaker: + type: object + properties: + maxFailures: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum failures within the sliding window before the breaker opens. + failureWindow: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Sliding window, in milliseconds, over which failures are counted. + description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. + type: + type: string + enum: + - sentry + description: Sends each event to a Sentry project. + example: sentry + dsn: + type: string + format: uri + description: Sentry DSN the events are sent to. + example: https://examplePublicKey@o0.ingest.sentry.io/0 + id: + type: string + description: Server-generated unique id of the destination. Returned in responses; not accepted in requests. + readOnly: true + example: 88be6560-bfb4-455c-8aa1-06971e9e5522 + required: + - type + - dsn + description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.' +responses: + '200': + description: Operation successful. + content: + application/json: + schema: + $ref: ../../../../shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml + '400': + $ref: ../../../../shared/spec/responses/badRequest.yml + '401': + $ref: ../../../../shared/spec/responses/unauthorized.yml + '403': + $ref: ../../../../shared/spec/responses/forbidden.yml + '409': + $ref: ../../../../shared/spec/responses/conflict.yml + '415': + $ref: ../../../../shared/spec/responses/unsupportedMediaType.yml diff --git a/packages/cli/src/public-api/v1/handlers/settings/spec/paths/getLogStreamingDestination.generated.yml b/packages/cli/src/public-api/v1/handlers/settings/spec/paths/getLogStreamingDestination.generated.yml index 12fd053a261..1d0ea7863c2 100644 --- a/packages/cli/src/public-api/v1/handlers/settings/spec/paths/getLogStreamingDestination.generated.yml +++ b/packages/cli/src/public-api/v1/handlers/settings/spec/paths/getLogStreamingDestination.generated.yml @@ -20,360 +20,7 @@ responses: content: application/json: schema: - oneOf: - - type: object - properties: - label: - type: string - minLength: 1 - description: Human-readable name shown in the UI. - example: My destination - enabled: - type: boolean - description: Whether the destination currently receives events. - example: true - subscribedEvents: - type: array - items: - type: string - description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. - example: - - n8n.workflow - - n8n.audit - anonymizeAuditMessages: - type: boolean - description: Whether audit message payloads are anonymized before being sent. - example: false - circuitBreaker: - type: object - properties: - maxFailures: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Maximum failures within the sliding window before the breaker opens. - failureWindow: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Sliding window, in milliseconds, over which failures are counted. - description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. - type: - type: string - enum: - - webhook - description: Sends each event as an HTTP request to a URL. - example: webhook - url: - type: string - format: uri - description: Target URL that receives the event payload. - example: https://example.com/n8n-events - method: - type: string - description: HTTP method used for the request. - example: POST - sendHeaders: - type: boolean - description: Whether to attach custom headers to the request. - example: false - specifyHeaders: - type: string - description: How custom headers are provided — `keypair` or `json`. - example: keypair - headerParameters: - type: object - properties: - parameters: - type: array - items: - type: object - properties: - name: - type: string - example: Authorization - value: - anyOf: - - type: string - - type: number - - type: boolean - - {} - - {} - description: Parameter value. Usually a string; numbers, booleans and null are also accepted. - example: Bearer - required: - - name - - value - required: - - parameters - description: Key-value pairs used when the matching `specify*` field is `keypair`. - jsonHeaders: - type: string - description: Custom headers as a JSON string when `specifyHeaders` is `json`. - sendQuery: - type: boolean - description: Whether to attach query parameters to the request. - example: false - specifyQuery: - type: string - description: How query parameters are provided — `keypair` or `json`. - queryParameters: - type: object - properties: - parameters: - type: array - items: - type: object - properties: - name: - type: string - example: Authorization - value: - anyOf: - - type: string - - type: number - - type: boolean - - {} - - {} - description: Parameter value. Usually a string; numbers, booleans and null are also accepted. - example: Bearer - required: - - name - - value - required: - - parameters - description: Key-value pairs used when the matching `specify*` field is `keypair`. - jsonQuery: - type: string - description: Query parameters as a JSON string when `specifyQuery` is `json`. - options: - type: object - properties: - timeout: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Request timeout in milliseconds. - allowUnauthorizedCerts: - type: boolean - description: Accept self-signed or otherwise invalid TLS certificates. - queryParameterArrays: - type: string - enum: - - indices - - brackets - - repeat - description: How array-valued query parameters are serialized into the query string. - redirect: - type: object - properties: - redirect: - type: object - properties: - followRedirects: - type: boolean - description: Whether to follow HTTP redirects. - maxRedirects: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Maximum number of redirects to follow. - required: - - redirect - description: Redirect-following behaviour. - proxy: - type: object - properties: - proxy: - type: object - properties: - protocol: - type: string - enum: - - https - - http - description: Proxy protocol. - host: - type: string - description: Proxy host, without protocol or port. - port: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Proxy port. - required: - - protocol - - host - - port - required: - - proxy - description: Outbound proxy configuration. - socket: - type: object - properties: - keepAlive: - type: boolean - description: Whether to keep sockets open for reuse. - maxSockets: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Maximum sockets per host kept open at once. - maxFreeSockets: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Maximum idle sockets per host kept open. - description: Connection socket options. - description: Additional HTTP request options. - id: - type: string - description: Server-generated unique id of the destination. Returned in responses; not accepted in requests. - example: 88be6560-bfb4-455c-8aa1-06971e9e5522 - required: - - type - - url - - id - - type: object - properties: - label: - type: string - minLength: 1 - description: Human-readable name shown in the UI. - example: My destination - enabled: - type: boolean - description: Whether the destination currently receives events. - example: true - subscribedEvents: - type: array - items: - type: string - description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. - example: - - n8n.workflow - - n8n.audit - anonymizeAuditMessages: - type: boolean - description: Whether audit message payloads are anonymized before being sent. - example: false - circuitBreaker: - type: object - properties: - maxFailures: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Maximum failures within the sliding window before the breaker opens. - failureWindow: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Sliding window, in milliseconds, over which failures are counted. - description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. - type: - type: string - enum: - - syslog - description: Sends each event to a syslog server. - example: syslog - host: - type: string - minLength: 1 - description: Syslog server host. - example: syslog.example.com - port: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Syslog server port. - example: 514 - protocol: - type: string - enum: - - udp - - tcp - - tls - description: Transport protocol. - example: udp - facility: - type: integer - minimum: 0 - maximum: 23 - description: 'Syslog facility code (0–23, per RFC 5424). Common values: 0 Kernel, 1 User, 3 System, 13 Audit, 14 Alert, 16 Local0, 17 Local1, 18 Local2, 19 Local3, 20 Local4, 21 Local5, 22 Local6, 23 Local7.' - example: 16 - app_name: - type: string - description: Application name reported in the syslog message. - example: n8n - tlsCa: - type: string - description: PEM-encoded CA certificate used when `protocol` is `tls`. - id: - type: string - description: Server-generated unique id of the destination. Returned in responses; not accepted in requests. - example: 88be6560-bfb4-455c-8aa1-06971e9e5522 - required: - - type - - host - - id - - type: object - properties: - label: - type: string - minLength: 1 - description: Human-readable name shown in the UI. - example: My destination - enabled: - type: boolean - description: Whether the destination currently receives events. - example: true - subscribedEvents: - type: array - items: - type: string - description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. - example: - - n8n.workflow - - n8n.audit - anonymizeAuditMessages: - type: boolean - description: Whether audit message payloads are anonymized before being sent. - example: false - circuitBreaker: - type: object - properties: - maxFailures: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Maximum failures within the sliding window before the breaker opens. - failureWindow: - type: integer - minimum: 0 - exclusiveMinimum: true - description: Sliding window, in milliseconds, over which failures are counted. - description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. - type: - type: string - enum: - - sentry - description: Sends each event to a Sentry project. - example: sentry - dsn: - type: string - format: uri - description: Sentry DSN the events are sent to. - example: https://examplePublicKey@o0.ingest.sentry.io/0 - id: - type: string - description: Server-generated unique id of the destination. Returned in responses; not accepted in requests. - example: 88be6560-bfb4-455c-8aa1-06971e9e5522 - required: - - type - - dsn - - id - description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.' + $ref: ../../../../shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml '401': $ref: ../../../../shared/spec/responses/unauthorized.yml '403': diff --git a/packages/cli/src/public-api/v1/handlers/settings/spec/paths/updateLogStreamingDestination.generated.yml b/packages/cli/src/public-api/v1/handlers/settings/spec/paths/updateLogStreamingDestination.generated.yml new file mode 100644 index 00000000000..d0bf68dc3d2 --- /dev/null +++ b/packages/cli/src/public-api/v1/handlers/settings/spec/paths/updateLogStreamingDestination.generated.yml @@ -0,0 +1,379 @@ +operationId: updateLogStreamingDestination +tags: + - LogStreaming +summary: Update a log streaming destination +description: Replace an existing log streaming destination. The update takes effect exactly as it would from the UI, using the same validation. Requires the `eventBusDestination:update` scope and the Log Streaming feature to be licensed. When destinations are managed via environment variables, the write is rejected with 409 and nothing is changed; reads still return the current values. +x-required-scope: eventBusDestination:update +x-eov-operation-id: unreachable +x-eov-operation-handler: v1/handlers/decorator-routed.handler +x-decorator-routed: true +parameters: + - schema: + type: string + required: true + description: The id of the log streaming destination. + name: id + in: path +requestBody: + required: true + content: + application/json: + schema: + oneOf: + - type: object + properties: + label: + type: string + minLength: 1 + description: Human-readable name shown in the UI. + example: My destination + enabled: + type: boolean + description: Whether the destination currently receives events. + example: true + subscribedEvents: + type: array + items: + type: string + description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. + example: + - n8n.workflow + - n8n.audit + anonymizeAuditMessages: + type: boolean + description: Whether audit message payloads are anonymized before being sent. + example: false + circuitBreaker: + type: object + properties: + maxFailures: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum failures within the sliding window before the breaker opens. + failureWindow: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Sliding window, in milliseconds, over which failures are counted. + description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. + type: + type: string + enum: + - webhook + description: Sends each event as an HTTP request to a URL. + example: webhook + url: + type: string + format: uri + description: Target URL that receives the event payload. + example: https://example.com/n8n-events + method: + type: string + description: HTTP method used for the request. + example: POST + sendHeaders: + type: boolean + description: Whether to attach custom headers to the request. + example: false + specifyHeaders: + type: string + description: How custom headers are provided — `keypair` or `json`. + example: keypair + headerParameters: + type: object + properties: + parameters: + type: array + items: + type: object + properties: + name: + type: string + example: Authorization + value: + anyOf: + - type: string + - type: number + - type: boolean + - {} + - {} + description: Parameter value. Usually a string; numbers, booleans and null are also accepted. + example: Bearer + required: + - name + - value + required: + - parameters + description: Key-value pairs used when the matching `specify*` field is `keypair`. + jsonHeaders: + type: string + description: Custom headers as a JSON string when `specifyHeaders` is `json`. + sendQuery: + type: boolean + description: Whether to attach query parameters to the request. + example: false + specifyQuery: + type: string + description: How query parameters are provided — `keypair` or `json`. + queryParameters: + type: object + properties: + parameters: + type: array + items: + type: object + properties: + name: + type: string + example: Authorization + value: + anyOf: + - type: string + - type: number + - type: boolean + - {} + - {} + description: Parameter value. Usually a string; numbers, booleans and null are also accepted. + example: Bearer + required: + - name + - value + required: + - parameters + description: Key-value pairs used when the matching `specify*` field is `keypair`. + jsonQuery: + type: string + description: Query parameters as a JSON string when `specifyQuery` is `json`. + options: + type: object + properties: + timeout: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Request timeout in milliseconds. + allowUnauthorizedCerts: + type: boolean + description: Accept self-signed or otherwise invalid TLS certificates. + queryParameterArrays: + type: string + enum: + - indices + - brackets + - repeat + description: How array-valued query parameters are serialized into the query string. + redirect: + type: object + properties: + redirect: + type: object + properties: + followRedirects: + type: boolean + description: Whether to follow HTTP redirects. + maxRedirects: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum number of redirects to follow. + required: + - redirect + description: Redirect-following behaviour. + proxy: + type: object + properties: + proxy: + type: object + properties: + protocol: + type: string + enum: + - https + - http + description: Proxy protocol. + host: + type: string + description: Proxy host, without protocol or port. + port: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Proxy port. + required: + - protocol + - host + - port + required: + - proxy + description: Outbound proxy configuration. + socket: + type: object + properties: + keepAlive: + type: boolean + description: Whether to keep sockets open for reuse. + maxSockets: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum sockets per host kept open at once. + maxFreeSockets: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum idle sockets per host kept open. + description: Connection socket options. + description: Additional HTTP request options. + required: + - type + - url + - type: object + properties: + label: + type: string + minLength: 1 + description: Human-readable name shown in the UI. + example: My destination + enabled: + type: boolean + description: Whether the destination currently receives events. + example: true + subscribedEvents: + type: array + items: + type: string + description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. + example: + - n8n.workflow + - n8n.audit + anonymizeAuditMessages: + type: boolean + description: Whether audit message payloads are anonymized before being sent. + example: false + circuitBreaker: + type: object + properties: + maxFailures: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum failures within the sliding window before the breaker opens. + failureWindow: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Sliding window, in milliseconds, over which failures are counted. + description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. + type: + type: string + enum: + - syslog + description: Sends each event to a syslog server. + example: syslog + host: + type: string + minLength: 1 + description: Syslog server host. + example: syslog.example.com + port: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Syslog server port. + example: 514 + protocol: + type: string + enum: + - udp + - tcp + - tls + description: Transport protocol. + example: udp + facility: + type: integer + minimum: 0 + maximum: 23 + description: 'Syslog facility code (0–23, per RFC 5424). Common values: 0 Kernel, 1 User, 3 System, 13 Audit, 14 Alert, 16 Local0, 17 Local1, 18 Local2, 19 Local3, 20 Local4, 21 Local5, 22 Local6, 23 Local7.' + example: 16 + app_name: + type: string + description: Application name reported in the syslog message. + example: n8n + tlsCa: + type: string + description: PEM-encoded CA certificate used when `protocol` is `tls`. + required: + - type + - host + - type: object + properties: + label: + type: string + minLength: 1 + description: Human-readable name shown in the UI. + example: My destination + enabled: + type: boolean + description: Whether the destination currently receives events. + example: true + subscribedEvents: + type: array + items: + type: string + description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. + example: + - n8n.workflow + - n8n.audit + anonymizeAuditMessages: + type: boolean + description: Whether audit message payloads are anonymized before being sent. + example: false + circuitBreaker: + type: object + properties: + maxFailures: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum failures within the sliding window before the breaker opens. + failureWindow: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Sliding window, in milliseconds, over which failures are counted. + description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. + type: + type: string + enum: + - sentry + description: Sends each event to a Sentry project. + example: sentry + dsn: + type: string + format: uri + description: Sentry DSN the events are sent to. + example: https://examplePublicKey@o0.ingest.sentry.io/0 + required: + - type + - dsn + description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.' +responses: + '200': + description: Operation successful. + content: + application/json: + schema: + $ref: ../../../../shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml + '400': + $ref: ../../../../shared/spec/responses/badRequest.yml + '401': + $ref: ../../../../shared/spec/responses/unauthorized.yml + '403': + $ref: ../../../../shared/spec/responses/forbidden.yml + '404': + $ref: ../../../../shared/spec/responses/notFound.yml + '409': + $ref: ../../../../shared/spec/responses/conflict.yml + '415': + $ref: ../../../../shared/spec/responses/unsupportedMediaType.yml diff --git a/packages/cli/src/public-api/v1/openapi.decorator-routes.generated.yml b/packages/cli/src/public-api/v1/openapi.decorator-routes.generated.yml index 4c09a51f789..60bbb605bba 100644 --- a/packages/cli/src/public-api/v1/openapi.decorator-routes.generated.yml +++ b/packages/cli/src/public-api/v1/openapi.decorator-routes.generated.yml @@ -171,9 +171,13 @@ paths: /settings/log-streaming/destinations: get: $ref: ./handlers/settings/spec/paths/getLogStreamingDestinations.generated.yml + post: + $ref: ./handlers/settings/spec/paths/createLogStreamingDestination.generated.yml /settings/log-streaming/destinations/{id}: get: $ref: ./handlers/settings/spec/paths/getLogStreamingDestination.generated.yml + put: + $ref: ./handlers/settings/spec/paths/updateLogStreamingDestination.generated.yml /node-type-policies/instance: get: $ref: ./handlers/node-type-policies/spec/paths/getNodeTypeInstancePolicy.generated.yml diff --git a/packages/cli/src/public-api/v1/openapi.yml b/packages/cli/src/public-api/v1/openapi.yml index 4f8a1e2ab97..4d7418b5062 100644 --- a/packages/cli/src/public-api/v1/openapi.yml +++ b/packages/cli/src/public-api/v1/openapi.yml @@ -85,8 +85,6 @@ paths: $ref: './handlers/n8n-packages/spec/paths/n8n-packages.import.yml' /n8n-packages/import-selection: $ref: './handlers/n8n-packages/spec/paths/n8n-packages.import-selection.yml' - /settings/log-streaming/destinations: - $ref: './handlers/log-streaming/spec/paths/destinations.yml' /settings/log-streaming/destinations/{id}/test: $ref: './handlers/log-streaming/spec/paths/destinations.id.test.yml' /settings/log-streaming/destinations/{id}: diff --git a/packages/cli/src/public-api/v1/shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml b/packages/cli/src/public-api/v1/shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml new file mode 100644 index 00000000000..9fa39458b63 --- /dev/null +++ b/packages/cli/src/public-api/v1/shared/spec/schemas/logStreamingDestinationPublicDto.generated.yml @@ -0,0 +1,354 @@ +oneOf: + - type: object + properties: + label: + type: string + minLength: 1 + description: Human-readable name shown in the UI. + example: My destination + enabled: + type: boolean + description: Whether the destination currently receives events. + example: true + subscribedEvents: + type: array + items: + type: string + description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. + example: + - n8n.workflow + - n8n.audit + anonymizeAuditMessages: + type: boolean + description: Whether audit message payloads are anonymized before being sent. + example: false + circuitBreaker: + type: object + properties: + maxFailures: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum failures within the sliding window before the breaker opens. + failureWindow: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Sliding window, in milliseconds, over which failures are counted. + description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. + type: + type: string + enum: + - webhook + description: Sends each event as an HTTP request to a URL. + example: webhook + url: + type: string + format: uri + description: Target URL that receives the event payload. + example: https://example.com/n8n-events + method: + type: string + description: HTTP method used for the request. + example: POST + sendHeaders: + type: boolean + description: Whether to attach custom headers to the request. + example: false + specifyHeaders: + type: string + description: How custom headers are provided — `keypair` or `json`. + example: keypair + headerParameters: + type: object + properties: + parameters: + type: array + items: + type: object + properties: + name: + type: string + example: Authorization + value: + anyOf: + - type: string + - type: number + - type: boolean + - {} + - {} + description: Parameter value. Usually a string; numbers, booleans and null are also accepted. + example: Bearer + required: + - name + - value + required: + - parameters + description: Key-value pairs used when the matching `specify*` field is `keypair`. + jsonHeaders: + type: string + description: Custom headers as a JSON string when `specifyHeaders` is `json`. + sendQuery: + type: boolean + description: Whether to attach query parameters to the request. + example: false + specifyQuery: + type: string + description: How query parameters are provided — `keypair` or `json`. + queryParameters: + type: object + properties: + parameters: + type: array + items: + type: object + properties: + name: + type: string + example: Authorization + value: + anyOf: + - type: string + - type: number + - type: boolean + - {} + - {} + description: Parameter value. Usually a string; numbers, booleans and null are also accepted. + example: Bearer + required: + - name + - value + required: + - parameters + description: Key-value pairs used when the matching `specify*` field is `keypair`. + jsonQuery: + type: string + description: Query parameters as a JSON string when `specifyQuery` is `json`. + options: + type: object + properties: + timeout: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Request timeout in milliseconds. + allowUnauthorizedCerts: + type: boolean + description: Accept self-signed or otherwise invalid TLS certificates. + queryParameterArrays: + type: string + enum: + - indices + - brackets + - repeat + description: How array-valued query parameters are serialized into the query string. + redirect: + type: object + properties: + redirect: + type: object + properties: + followRedirects: + type: boolean + description: Whether to follow HTTP redirects. + maxRedirects: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum number of redirects to follow. + required: + - redirect + description: Redirect-following behaviour. + proxy: + type: object + properties: + proxy: + type: object + properties: + protocol: + type: string + enum: + - https + - http + description: Proxy protocol. + host: + type: string + description: Proxy host, without protocol or port. + port: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Proxy port. + required: + - protocol + - host + - port + required: + - proxy + description: Outbound proxy configuration. + socket: + type: object + properties: + keepAlive: + type: boolean + description: Whether to keep sockets open for reuse. + maxSockets: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum sockets per host kept open at once. + maxFreeSockets: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum idle sockets per host kept open. + description: Connection socket options. + description: Additional HTTP request options. + id: + type: string + description: Server-generated unique id of the destination. Returned in responses; not accepted in requests. + example: 88be6560-bfb4-455c-8aa1-06971e9e5522 + required: + - type + - url + - id + - type: object + properties: + label: + type: string + minLength: 1 + description: Human-readable name shown in the UI. + example: My destination + enabled: + type: boolean + description: Whether the destination currently receives events. + example: true + subscribedEvents: + type: array + items: + type: string + description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. + example: + - n8n.workflow + - n8n.audit + anonymizeAuditMessages: + type: boolean + description: Whether audit message payloads are anonymized before being sent. + example: false + circuitBreaker: + type: object + properties: + maxFailures: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum failures within the sliding window before the breaker opens. + failureWindow: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Sliding window, in milliseconds, over which failures are counted. + description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. + type: + type: string + enum: + - syslog + description: Sends each event to a syslog server. + example: syslog + host: + type: string + minLength: 1 + description: Syslog server host. + example: syslog.example.com + port: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Syslog server port. + example: 514 + protocol: + type: string + enum: + - udp + - tcp + - tls + description: Transport protocol. + example: udp + facility: + type: integer + minimum: 0 + maximum: 23 + description: 'Syslog facility code (0–23, per RFC 5424). Common values: 0 Kernel, 1 User, 3 System, 13 Audit, 14 Alert, 16 Local0, 17 Local1, 18 Local2, 19 Local3, 20 Local4, 21 Local5, 22 Local6, 23 Local7.' + example: 16 + app_name: + type: string + description: Application name reported in the syslog message. + example: n8n + tlsCa: + type: string + description: PEM-encoded CA certificate used when `protocol` is `tls`. + id: + type: string + description: Server-generated unique id of the destination. Returned in responses; not accepted in requests. + example: 88be6560-bfb4-455c-8aa1-06971e9e5522 + required: + - type + - host + - id + - type: object + properties: + label: + type: string + minLength: 1 + description: Human-readable name shown in the UI. + example: My destination + enabled: + type: boolean + description: Whether the destination currently receives events. + example: true + subscribedEvents: + type: array + items: + type: string + description: Event names (or group prefixes, e.g. `n8n.workflow`, which matches all `n8n.workflow.*` events) this destination receives. Retrieve the full list of streamable event names from `GET /settings/log-streaming/event-types`. + example: + - n8n.workflow + - n8n.audit + anonymizeAuditMessages: + type: boolean + description: Whether audit message payloads are anonymized before being sent. + example: false + circuitBreaker: + type: object + properties: + maxFailures: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Maximum failures within the sliding window before the breaker opens. + failureWindow: + type: integer + minimum: 0 + exclusiveMinimum: true + description: Sliding window, in milliseconds, over which failures are counted. + description: Circuit-breaker tuning for delivery. After repeated failures the destination stops sending for a cool-down window instead of hammering an unhealthy target. All fields are optional; sensible defaults apply when omitted. + type: + type: string + enum: + - sentry + description: Sends each event to a Sentry project. + example: sentry + dsn: + type: string + format: uri + description: Sentry DSN the events are sent to. + example: https://examplePublicKey@o0.ingest.sentry.io/0 + id: + type: string + description: Server-generated unique id of the destination. Returned in responses; not accepted in requests. + example: 88be6560-bfb4-455c-8aa1-06971e9e5522 + required: + - type + - dsn + - id +description: 'A log streaming destination. The `type` field selects the variant and its type-specific fields: `webhook`, `syslog`, or `sentry`.' diff --git a/packages/cli/test/integration/public-api/discover.test.ts b/packages/cli/test/integration/public-api/discover.test.ts index 8f29533f8bb..c899cbbd1c7 100644 --- a/packages/cli/test/integration/public-api/discover.test.ts +++ b/packages/cli/test/integration/public-api/discover.test.ts @@ -151,7 +151,8 @@ describe('GET /discover', () => { expect(withSchema.length).toBeGreaterThan(0); for (const endpoint of withSchema) { - expect((endpoint as Record).requestSchema).toHaveProperty('type'); + const schema = (endpoint as { requestSchema: Record }).requestSchema; + expect('type' in schema || 'oneOf' in schema).toBe(true); } }); diff --git a/packages/cli/test/integration/public-api/log-streaming.test.ts b/packages/cli/test/integration/public-api/log-streaming.test.ts index 48611d9f85c..5924b088a98 100644 --- a/packages/cli/test/integration/public-api/log-streaming.test.ts +++ b/packages/cli/test/integration/public-api/log-streaming.test.ts @@ -279,6 +279,22 @@ describe('Log streaming in Public API', () => { expect(response.status).toBe(400); }); + it('drops fields that do not belong to the destination type', async () => { + const response = await testServer + .publicApiAgentFor(owner) + .post('/settings/log-streaming/destinations') + .send({ + ...webhookPayload, + dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', + unknownField: 'value', + }); + + expect(response.status).toBe(200); + expect(response.body.type).toBe('webhook'); + expect(response.body).not.toHaveProperty('dsn'); + expect(response.body).not.toHaveProperty('unknownField'); + }); + it('creates a fully-populated webhook destination', async () => { const payload = { type: 'webhook', @@ -304,12 +320,14 @@ describe('Log streaming in Public API', () => { .send(payload); expect(response.status).toBe(200); - expect(response.body).toHaveProperty('id'); - expect(response.body).toMatchObject(payload); - expect(response.body).not.toHaveProperty('responseCodeMustMatch'); - expect(response.body).not.toHaveProperty('sendPayload'); - expect(response.body).not.toHaveProperty('authentication'); - expect(response.body).not.toHaveProperty('credentials'); + // Strict match: `responseCodeMustMatch`, `sendPayload`, `authentication` and + // `credentials` must not appear. + expect(response.body).toStrictEqual({ + id: expect.any(String), + ...payload, + jsonHeaders: '', + jsonQuery: '', + }); }); it('creates a fully-populated syslog destination', async () => { @@ -333,8 +351,7 @@ describe('Log streaming in Public API', () => { .send(payload); expect(response.status).toBe(200); - expect(response.body).toHaveProperty('id'); - expect(response.body).toMatchObject(payload); + expect(response.body).toStrictEqual({ id: expect.any(String), ...payload }); }); it('creates a fully-populated sentry destination', async () => { @@ -354,8 +371,7 @@ describe('Log streaming in Public API', () => { .send(payload); expect(response.status).toBe(200); - expect(response.body).toHaveProperty('id'); - expect(response.body).toMatchObject(payload); + expect(response.body).toStrictEqual({ id: expect.any(String), ...payload }); }); it('rejects a malformed body with 400', async () => { @@ -399,6 +415,22 @@ describe('Log streaming in Public API', () => { expect(listResponse.body.data[0].label).toBe('Renamed webhook'); }); + it('accepts a GET response as the body and keeps the path id', async () => { + const created = await createDestination(); + const read = await testServer + .publicApiAgentFor(owner) + .get(`/settings/log-streaming/destinations/${created.id}`); + + const response = await testServer + .publicApiAgentFor(owner) + .put(`/settings/log-streaming/destinations/${created.id}`) + .send({ ...read.body, id: '99999999-9999-4999-8999-999999999999', label: 'Round trip' }); + + expect(response.status).toBe(200); + expect(response.body.id).toBe(created.id); + expect(response.body.label).toBe('Round trip'); + }); + it('returns 404 for an unknown destination id', async () => { const response = await testServer .publicApiAgentFor(owner)