[Fix] AI suggestion stage (#8996)

* chore(cspell): remove typo from cspell dictionary

* chore(camshot): update delete command type

* refactor(camshot): include id in camshot not found error message

* docs(config): add jsdoc to activepieces property

* refactor(camshot): standardize find options type for findById

Replace TypeORM's FindOneOptions with the application's standardized
FindOptionsQueryDTO for consistency in query parameter handling.

* feat(integration-activepieces): use dto for activepieces query params

Create ActivepiecesQueryDto to define the structure, typing,
and validation for query parameters handled by the
ActivepiecesAuthorizationController.
Replace the 'any' type annotation with ActivepiecesQueryDto
in the authorize and callback methods. This improves type safety
and enables automatic validation and Swagger documentation
generation for the API endpoints.

* chore(integration-activepieces): remove empty entities array from plugin config

The `entities` array in the Activepieces VendurePlugin configuration was declared
but empty and served no purpose. Remove the array and the associated comment
to clean up the configuration.

* fix(activepieces): improve error handling for connection

Update error handling in `createActivePiecesConnection` to provide more specific exception types. Catch and re-throw `UnauthorizedException` explicitly, and wrap other errors in `InternalServerErrorException` for clearer reporting downstream.

* docs(common): fix typo in callback url documentation

* refactor(activepieces): remove unused ID import

* style(camshot): improve not found error message

Add 'id' before the variable for clarity.
Adjust formatting in the constructor.

* docs(camshot): add api docs for get camshot by id

add @ApiResponse decorators for status codes 200, 404, and 500
add @ApiQuery decorator for the optional options parameter

* refactor(activepieces): rename query dto and add validation

Rename ActivepiecesQueryDto to ActivepiecesQueryDTO for naming
consistency.
Add ValidationPipe to the authorize endpoint to ensure incoming query
parameters are validated against the DTO schema. This improves input
handling robustness.

* fix(activepieces): correctly handle boolean setting values

The previous implementation of `isIntegrationEnabled` only handled boolean values stored as JSON strings. This update adds support for boolean values stored as native booleans or other types, ensuring the check works correctly regardless of how the setting value is stored.

* fix(activepieces): fix error handling and reporting

Extract detailed error messages from the ActivePieces API response body
when handling errors during connection creation. This provides users
with more specific information about why a connection failed.
Also, ensure that specific HTTP exceptions caught within the HTTP request
observable's catchError are not subsequently wrapped in a BadRequestException
by the outer try-catch block. This preserves the original HTTP status code
and error type.
This commit is contained in:
Kifungo A
2025-06-11 22:45:10 +02:00
committed by GitHub
parent 33810da039
commit 62be88a755
11 changed files with 100 additions and 38 deletions
-1
View File
@@ -3,7 +3,6 @@
"language": "en",
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"words": [
"accesibility",
"activitywatch",
"adminer",
"activepieces",
@@ -19,7 +19,7 @@ export interface IActivepiecesConfig {
/**
* The redirect URI registered with Activepieces for OAuth callback.
* This URL receives he authorization code during the OAuth.
* This URL receives the authorization code during the OAuth.
*/
readonly callbackUrl: string;
@@ -124,8 +124,8 @@ export interface IEnvironment {
hubstaff?: IHubstaffConfig;
zapier?: IZapierConfig;
makeCom?: IMakeComConfig;
activepieces?: IActivepiecesConfig; // ActivePieces Configuration
/** ActivePieces Configuration */
activepieces?: IActivepiecesConfig;
isElectron?: boolean;
gauzyUserPath?: string;
allowSuperAdminRole?: boolean;
@@ -3,6 +3,7 @@ import {
BaseQueryDTO,
FileStorage,
FileStorageFactory,
FindOptionsQueryDTO,
LazyFileInterceptor,
PermissionGuard,
Permissions,
@@ -26,7 +27,6 @@ import {
} from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { ApiConsumes, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { FindOneOptions } from 'typeorm';
import { CreateCamshotCommand } from './commands/create-camshot.command';
import { DeleteCamshotCommand } from './commands/delete-camshot.command';
import { CountCamshotDTO } from './dtos/count-camshot.dto';
@@ -43,7 +43,7 @@ import { GetCamshotQuery } from './queries/get-camshot.query';
@Permissions(PermissionsEnum.TIME_TRACKER)
@Controller('/plugins/camshots')
export class CamshotController {
constructor(private readonly commandBus: CommandBus, private readonly queryBus: QueryBus) {}
constructor(private readonly commandBus: CommandBus, private readonly queryBus: QueryBus) { }
/**
* Get a paginated list of camshots.
@@ -184,6 +184,19 @@ export class CamshotController {
status: HttpStatus.BAD_REQUEST,
description: 'Invalid input, The response body may contain clues as to what went wrong'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Camshot successfully fetched by ID.'
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Camshot with the given ID not found.'
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: 'An error occurred while retrieving the camshot.'
})
@ApiQuery({ name: 'options', type: FindOptionsQueryDTO, required: false })
@UseValidationPipe({
whitelist: true,
transform: true,
@@ -192,7 +205,7 @@ export class CamshotController {
@Get(':id')
public async findById(
@Param('id', UUIDValidationPipe) id: ID,
@Query() options: FindOneOptions<ICamshot>
@Query() options: FindOptionsQueryDTO<ICamshot>
): Promise<ICamshot> {
return this.queryBus.execute(new GetCamshotQuery(id, options));
}
@@ -1,8 +1,8 @@
import { ICommand } from "@nestjs/cqrs";
import { DeleteCamshotDTO } from "../dtos/delete-camshot.dto";
import { ID } from "@gauzy/contracts";
import { ICommand } from '@nestjs/cqrs';
import { DeleteCamshotDTO } from '../dtos/delete-camshot.dto';
import { ID } from '@gauzy/contracts';
export class DeleteCamshotCommand implements ICommand {
public static readonly type = '[Delete] Camshot';
constructor(public readonly id: ID, public readonly input: DeleteCamshotDTO) { }
public static readonly type = '[Camshot] Delete';
constructor(public readonly id: ID, public readonly input: DeleteCamshotDTO) {}
}
@@ -1,8 +1,8 @@
import { ICommandHandler, CommandHandler } from "@nestjs/cqrs";
import { CamshotService } from "../../services/camshot.service";
import { DeleteCamshotCommand } from "../delete-camshot.command";
import { NotFoundException } from "@nestjs/common";
import { RequestContext } from "@gauzy/core";
import { ICommandHandler, CommandHandler } from '@nestjs/cqrs';
import { CamshotService } from '../../services/camshot.service';
import { DeleteCamshotCommand } from '../delete-camshot.command';
import { NotFoundException } from '@nestjs/common';
import { RequestContext } from '@gauzy/core';
@CommandHandler(DeleteCamshotCommand)
export class DeleteCamshotCommandHandler implements ICommandHandler<DeleteCamshotCommand> {
@@ -24,7 +24,7 @@ export class DeleteCamshotCommandHandler implements ICommandHandler<DeleteCamsho
const camshot = await this.camshotService.findOneByWhereOptions({ id, organizationId, tenantId });
if (!camshot) {
throw new NotFoundException('Camshot not found');
throw new NotFoundException(`Camshot with id ${id} not found`);
}
if (forceDelete) {
@@ -1,4 +1,4 @@
import { Controller, Get, HttpException, HttpStatus, Query, Res } from '@nestjs/common';
import { Controller, Get, HttpException, HttpStatus, Query, Res, UsePipes, ValidationPipe } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Response } from 'express';
import { ConfigService } from '@gauzy/config';
@@ -6,6 +6,7 @@ import { Public, IActivepiecesConfig } from '@gauzy/common';
import { IntegrationEnum } from '@gauzy/contracts';
import { buildQueryString } from '@gauzy/utils';
import { ACTIVEPIECES_OAUTH_AUTHORIZE_URL, ACTIVEPIECES_SCOPES, OAUTH_RESPONSE_TYPE } from './activepieces.config';
import { ActivepiecesQueryDTO } from './dto/activepieces-query.dto';
@ApiTags('ActivePieces Integration')
@Public()
@@ -46,7 +47,8 @@ export class ActivepiecesAuthorizationController {
}
})
@Get('/authorize')
async authorize(@Query() query: any, @Res() response: Response) {
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
async authorize(@Query() query: ActivepiecesQueryDTO, @Res() response: Response) {
try {
// Get ActivePieces configuration
const activepiecesConfig = this.configService.get('activepieces') as IActivepiecesConfig;
@@ -95,7 +97,7 @@ export class ActivepiecesAuthorizationController {
description: 'Redirects to the application with authorization code'
})
@Get('/callback')
async callback(@Query() query: any, @Res() response: Response) {
async callback(@Query() query: ActivepiecesQueryDTO, @Res() response: Response) {
try {
// Validate the input data
if (!query || !query.code || !query.state) {
@@ -1,4 +1,12 @@
import { Injectable, BadRequestException, Logger, HttpException, HttpStatus, UnauthorizedException } from '@nestjs/common';
import {
Injectable,
BadRequestException,
Logger,
HttpException,
HttpStatus,
UnauthorizedException,
InternalServerErrorException
} from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@gauzy/config';
import { firstValueFrom, catchError, throwError } from 'rxjs';
@@ -10,7 +18,8 @@ import {
IActivepiecesConnectionRequest,
ActivepiecesSettingName,
ActivepiecesConnectionType,
ICreateActivepiecesIntegrationInput
ICreateActivepiecesIntegrationInput,
IActivepiecesErrorResponse
} from './activepieces.type';
import { ACTIVEPIECES_CONNECTIONS_URL, ACTIVEPIECES_PIECE_NAME } from './activepieces.config';
@@ -24,7 +33,7 @@ export class ActivepiecesService {
private readonly integrationSettingService: IntegrationSettingService,
private readonly integrationService: IntegrationService,
private readonly integrationTenantService: IntegrationTenantService
) {}
) { }
/**
* Create a new ActivePieces connection for the tenant
@@ -85,15 +94,29 @@ export class ActivepiecesService {
}
})
.pipe(
catchError((error: AxiosError) => {
this.logger.error('Error creating ActivePieces connection:', error.response?.data);
const status = error.response?.status;
catchError((error) => {
const status = error?.response?.status;
const data = error?.response?.data as IActivepiecesErrorResponse;
const errorMessage = data?.error?.message || error.message;
this.logger.error('Error creating ActivePieces connection:', data);
if (status === HttpStatus.UNAUTHORIZED) {
throw new UnauthorizedException(
`Unauthorized to create ActivePieces connection: ${error.message}`
)
return throwError(
() =>
new UnauthorizedException(
data?.error?.message ?? `Unauthorized to create ActivePieces connection: ${error.message}`
)
);
}
return throwError(() => error);
// Optionally wrap and throw a more descriptive internal server error
return throwError(
() =>
new InternalServerErrorException(
`Failed to create ActivePieces connection: ${errorMessage}`
)
);
})
)
);
@@ -104,6 +127,9 @@ export class ActivepiecesService {
this.logger.log(`Successfully created ActivePieces connection: ${response.data.id}`);
return response.data;
} catch (error: any) {
if (error instanceof HttpException) {
throw error;
}
this.logger.error('Failed to create ActivePieces connection:', error);
throw new BadRequestException(`Failed to create connection: ${error.message}`);
}
@@ -249,7 +275,12 @@ export class ActivepiecesService {
}
});
return enabledSetting?.settingsValue ? JSON.parse(enabledSetting.settingsValue) === true: false;
if (typeof enabledSetting?.settingsValue === 'boolean') {
return enabledSetting.settingsValue;
}
return !!(typeof enabledSetting?.settingsValue === 'string'
? JSON.parse(enabledSetting.settingsValue)
: enabledSetting?.settingsValue);
} catch (error) {
this.logger.error('Error checking if integration is enabled:', error);
return false;
@@ -1,4 +1,4 @@
import { IBasePerTenantAndOrganizationEntityModel, ID } from '@gauzy/contracts';
import { IBasePerTenantAndOrganizationEntityModel } from '@gauzy/contracts';
/**
* ActivePieces OAuth token response interface
@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class ActivepiecesQueryDTO {
@ApiProperty({
description: 'The state parameter for CSRF protection',
required: false,
type: String
})
@IsOptional()
@IsString()
readonly state?: string;
@ApiProperty({
description: 'The authorization code received from ActivePieces',
required: false,
type: String
})
@IsOptional()
@IsString()
readonly code?: string;
}
@@ -7,11 +7,6 @@ import { ActivepiecesModule } from './activepieces.module';
* An array of modules that will be imported and registered with the plugin.
*/
imports: [ActivepiecesModule],
/**
* Entity needed for ActivePieces integration that extends the existing
* IntegrationSetting entity to store connection data
*/
entities: [],
/**
* A callback that receives the main plugin configuration object and allows
* custom modifications before returning the final configuration.