mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix: resolve file conflicts
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -111,7 +111,7 @@ export class BotStatsService extends BaseService<BotStats> {
|
||||
* @param name - The name or identifier of the statistics entry (e.g., a specific feature or component being tracked).
|
||||
*/
|
||||
@OnEvent('hook:stats:entry')
|
||||
async handleStatEntry(type: BotStatsType, name: string) {
|
||||
async handleStatEntry(type: BotStatsType, name: string): Promise<void> {
|
||||
const day = new Date();
|
||||
day.setMilliseconds(0);
|
||||
day.setSeconds(0);
|
||||
|
||||
@@ -16,8 +16,13 @@ import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Request } from 'express';
|
||||
|
||||
import LocalStorageHelper from '@/extensions/helpers/local-storage/index.helper';
|
||||
import { HelperService } from '@/helper/helper.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { SettingRepository } from '@/setting/repositories/setting.repository';
|
||||
import { SettingModel } from '@/setting/schemas/setting.schema';
|
||||
import { SettingSeeder } from '@/setting/seeds/setting.seed';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { ModelRepository } from '@/user/repositories/model.repository';
|
||||
import { PermissionRepository } from '@/user/repositories/permission.repository';
|
||||
import { ModelModel } from '@/user/schemas/model.schema';
|
||||
@@ -30,6 +35,7 @@ import {
|
||||
attachmentFixtures,
|
||||
installAttachmentFixtures,
|
||||
} from '@/utils/test/fixtures/attachment';
|
||||
import { installSettingFixtures } from '@/utils/test/fixtures/setting';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
@@ -51,16 +57,23 @@ describe('AttachmentController', () => {
|
||||
let attachmentController: AttachmentController;
|
||||
let attachmentService: AttachmentService;
|
||||
let attachmentToDelete: Attachment;
|
||||
let helperService: HelperService;
|
||||
let settingService: SettingService;
|
||||
let loggerService: LoggerService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AttachmentController],
|
||||
imports: [
|
||||
rootMongooseTestModule(installAttachmentFixtures),
|
||||
rootMongooseTestModule(async () => {
|
||||
await installSettingFixtures();
|
||||
await installAttachmentFixtures();
|
||||
}),
|
||||
MongooseModule.forFeature([
|
||||
AttachmentModel,
|
||||
PermissionModel,
|
||||
ModelModel,
|
||||
SettingModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -68,11 +81,14 @@ describe('AttachmentController', () => {
|
||||
AttachmentRepository,
|
||||
PermissionService,
|
||||
PermissionRepository,
|
||||
SettingRepository,
|
||||
ModelService,
|
||||
ModelRepository,
|
||||
LoggerService,
|
||||
EventEmitter2,
|
||||
PluginService,
|
||||
SettingSeeder,
|
||||
SettingService,
|
||||
HelperService,
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
@@ -89,6 +105,14 @@ describe('AttachmentController', () => {
|
||||
attachmentToDelete = (await attachmentService.findOne({
|
||||
name: 'store1.jpg',
|
||||
}))!;
|
||||
|
||||
helperService = module.get<HelperService>(HelperService);
|
||||
settingService = module.get<SettingService>(SettingService);
|
||||
loggerService = module.get<LoggerService>(LoggerService);
|
||||
|
||||
helperService.register(
|
||||
new LocalStorageHelper(settingService, helperService, loggerService),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(closeInMongodConnection);
|
||||
|
||||
@@ -6,312 +6,87 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import { join, normalize, resolve } from 'path';
|
||||
import { Readable, Stream } from 'stream';
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
Optional,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import fetch from 'node-fetch';
|
||||
import sanitizeFilename from 'sanitize-filename';
|
||||
import { Injectable, Optional, StreamableFile } from '@nestjs/common';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { HelperService } from '@/helper/helper.service';
|
||||
import { HelperType } from '@/helper/types';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginInstance } from '@/plugins/map-types';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { PluginType } from '@/plugins/types';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
import { AttachmentMetadataDto } from '../dto/attachment.dto';
|
||||
import { AttachmentRepository } from '../repositories/attachment.repository';
|
||||
import { Attachment } from '../schemas/attachment.schema';
|
||||
import { AttachmentResourceRef } from '../types';
|
||||
import {
|
||||
fileExists,
|
||||
generateUniqueFilename,
|
||||
getStreamableFile,
|
||||
} from '../utilities';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentService extends BaseService<Attachment> {
|
||||
private storagePlugin: PluginInstance<PluginType.storage> | null = null;
|
||||
|
||||
constructor(
|
||||
readonly repository: AttachmentRepository,
|
||||
private readonly logger: LoggerService,
|
||||
@Optional() private readonly pluginService: PluginService,
|
||||
@Optional() private readonly helperService: HelperService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* A storage plugin is a alternative way to store files, instead of local filesystem, you can
|
||||
* have a plugin that would store files in a 3rd party system (Minio, AWS S3, ...)
|
||||
* Stores a file using the default storage helper and creates an attachment record.
|
||||
*
|
||||
* @param foreign_id The unique identifier of the user, used to locate the profile picture.
|
||||
* @returns A singleton instance of the storage plugin
|
||||
*/
|
||||
getStoragePlugin() {
|
||||
if (!this.storagePlugin) {
|
||||
const plugins = this.pluginService.getAllByType(PluginType.storage);
|
||||
|
||||
if (plugins.length === 1) {
|
||||
this.storagePlugin = plugins[0];
|
||||
} else if (plugins.length > 1) {
|
||||
throw new Error(
|
||||
'Multiple storage plugins are detected, please ensure only one is available',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.storagePlugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a user's profile picture either from a 3rd party storage system or from a local directory based on configuration.
|
||||
* This method retrieves the default storage helper via the `HelperService` and
|
||||
* delegates the file storage operation to it. The returned metadata is then used
|
||||
* to create a new `Attachment` record in the database.
|
||||
*
|
||||
* @deprecated Use AttachmentService.download() instead
|
||||
* @param foreign_id The unique identifier of the user, used to locate the profile picture.
|
||||
* @returns A `StreamableFile` containing the user's profile picture.
|
||||
*/
|
||||
async downloadProfilePic(
|
||||
foreign_id: string,
|
||||
): Promise<StreamableFile | undefined> {
|
||||
if (this.getStoragePlugin()) {
|
||||
try {
|
||||
const pict = foreign_id + '.jpeg';
|
||||
const picture =
|
||||
await this.getStoragePlugin()?.downloadProfilePic?.(pict);
|
||||
return picture;
|
||||
} catch (err) {
|
||||
this.logger.error('Error downloading profile picture', err);
|
||||
throw new NotFoundException('Profile picture not found');
|
||||
}
|
||||
} else {
|
||||
const path = resolve(
|
||||
join(config.parameters.avatarDir, `${foreign_id}.jpeg`),
|
||||
);
|
||||
if (fs.existsSync(path)) {
|
||||
const picturetream = fs.createReadStream(path);
|
||||
return new StreamableFile(picturetream);
|
||||
} else {
|
||||
throw new NotFoundException('Profile picture not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a profile picture to either 3rd party storage system or locally based on the configuration.
|
||||
*
|
||||
* @deprecated use store() method instead
|
||||
* @param res - The response object from which the profile picture will be buffered or piped.
|
||||
* @param filename - The filename
|
||||
*/
|
||||
async uploadProfilePic(data: Buffer | fetch.Response, filename: string) {
|
||||
if (this.getStoragePlugin()) {
|
||||
// Upload profile picture
|
||||
const picture = {
|
||||
originalname: filename,
|
||||
buffer: Buffer.isBuffer(data) ? data : await data.buffer(),
|
||||
} as Express.Multer.File;
|
||||
try {
|
||||
await this.getStoragePlugin()?.uploadAvatar?.(picture);
|
||||
this.logger.log(
|
||||
`Profile picture uploaded successfully to ${
|
||||
this.getStoragePlugin()?.name
|
||||
}`,
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Error while uploading profile picture to ${
|
||||
this.getStoragePlugin()?.name
|
||||
}`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Save profile picture locally
|
||||
const dirPath = resolve(join(config.parameters.avatarDir, filename));
|
||||
|
||||
try {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
await fs.promises.writeFile(dirPath, data);
|
||||
} else {
|
||||
const dest = fs.createWriteStream(dirPath);
|
||||
data.body.pipe(dest);
|
||||
}
|
||||
this.logger.debug(
|
||||
'Messenger Channel Handler : Profile picture fetched successfully',
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
'Messenger Channel Handler : Error while creating directory',
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment root directory given the resource reference
|
||||
*
|
||||
* @param ref The attachment resource reference
|
||||
* @returns The root directory path
|
||||
*/
|
||||
getRootDirByResourceRef(ref: AttachmentResourceRef) {
|
||||
return ref === AttachmentResourceRef.SubscriberAvatar ||
|
||||
ref === AttachmentResourceRef.UserAvatar
|
||||
? config.parameters.avatarDir
|
||||
: config.parameters.uploadDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads files to the server. If a storage plugin is configured it uploads files accordingly.
|
||||
* Otherwise, uploads files to the local directory.
|
||||
*
|
||||
* @param file - The file
|
||||
* @param metadata - The attachment metadata informations.
|
||||
* @param rootDir - The root directory where attachment shoud be located.
|
||||
* @returns A promise that resolves to an array of uploaded attachments.
|
||||
* @param file - The file to be stored. This can be a buffer, a stream, a readable, or a file from an Express Multer upload.
|
||||
* @param metadata - The metadata associated with the file, such as name, size, and type.
|
||||
* @returns A promise resolving to the created `Attachment` record.
|
||||
*/
|
||||
async store(
|
||||
file: Buffer | Stream | Readable | Express.Multer.File,
|
||||
metadata: AttachmentMetadataDto,
|
||||
): Promise<Attachment | null> {
|
||||
if (this.getStoragePlugin()) {
|
||||
const storedDto = await this.getStoragePlugin()?.store?.(file, metadata);
|
||||
return storedDto ? await this.create(storedDto) : null;
|
||||
} else {
|
||||
const rootDir = this.getRootDirByResourceRef(metadata.resourceRef);
|
||||
const uniqueFilename = generateUniqueFilename(metadata.name);
|
||||
const filePath = resolve(join(rootDir, sanitizeFilename(uniqueFilename)));
|
||||
|
||||
if (Buffer.isBuffer(file)) {
|
||||
await fs.promises.writeFile(filePath, file);
|
||||
} else if (file instanceof Readable || file instanceof Stream) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
file.pipe(writeStream);
|
||||
// @TODO: Calc size here?
|
||||
writeStream.on('finish', resolve);
|
||||
writeStream.on('error', reject);
|
||||
});
|
||||
} else {
|
||||
if (file.path) {
|
||||
// For example, if the file is an instance of `Express.Multer.File` (diskStorage case)
|
||||
const srcFilePath = fs.realpathSync(resolve(file.path));
|
||||
// Get the system's temporary directory in a cross-platform way
|
||||
const tempDir = os.tmpdir();
|
||||
const normalizedTempDir = normalize(tempDir);
|
||||
|
||||
if (!srcFilePath.startsWith(normalizedTempDir)) {
|
||||
throw new Error('Invalid file path');
|
||||
}
|
||||
|
||||
await fs.promises.copyFile(srcFilePath, filePath);
|
||||
await fs.promises.unlink(srcFilePath);
|
||||
} else {
|
||||
await fs.promises.writeFile(filePath, file.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
const location = filePath.replace(rootDir, '');
|
||||
return await this.create({
|
||||
...metadata,
|
||||
location,
|
||||
});
|
||||
}
|
||||
): Promise<Attachment> {
|
||||
const storageHelper = await this.helperService.getDefaultHelper(
|
||||
HelperType.STORAGE,
|
||||
);
|
||||
const dto = await storageHelper.store(file, metadata);
|
||||
return await this.create(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an attachment identified by the provided parameters.
|
||||
* Downloads the specified attachment using the default storage helper.
|
||||
*
|
||||
* @param attachment - The attachment to download.
|
||||
* @param rootDir - The root directory where attachment shoud be located.
|
||||
* @returns A promise that resolves to a StreamableFile representing the downloaded attachment.
|
||||
* @param The attachment object containing the metadata required for the download.
|
||||
* @returns A promise resolving to a `StreamableFile` instance of the downloaded attachment.
|
||||
*/
|
||||
async download(attachment: Attachment): Promise<StreamableFile> {
|
||||
if (this.getStoragePlugin()) {
|
||||
const streamableFile =
|
||||
await this.getStoragePlugin()?.download(attachment);
|
||||
if (!streamableFile) {
|
||||
throw new NotFoundException('No file was found');
|
||||
}
|
||||
|
||||
return streamableFile;
|
||||
} else {
|
||||
const rootDir = this.getRootDirByResourceRef(attachment.resourceRef);
|
||||
const path = resolve(join(rootDir, attachment.location));
|
||||
|
||||
if (!fileExists(path)) {
|
||||
throw new NotFoundException('No file was found');
|
||||
}
|
||||
|
||||
const disposition = `attachment; filename="${encodeURIComponent(
|
||||
attachment.name,
|
||||
)}"`;
|
||||
|
||||
return getStreamableFile({
|
||||
path,
|
||||
options: {
|
||||
type: attachment.type,
|
||||
length: attachment.size,
|
||||
disposition,
|
||||
},
|
||||
});
|
||||
}
|
||||
const storageHelper = await this.helperService.getDefaultHelper(
|
||||
HelperType.STORAGE,
|
||||
);
|
||||
return await storageHelper.download(attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an attachment identified by the provided parameters as a Buffer.
|
||||
* Reads the specified attachment as a buffer using the default storage helper.
|
||||
*
|
||||
* @param attachment - The attachment to download.
|
||||
* @param rootDir - Root folder path where the attachment should be located.
|
||||
* @returns A promise that resolves to a Buffer representing the attachment file.
|
||||
* @param attachment - The attachment object containing the metadata required to locate the file.
|
||||
* @returns A promise resolving to the file content as a `Buffer`, or `undefined` if the file cannot be read.
|
||||
*/
|
||||
async readAsBuffer(
|
||||
attachment: Attachment,
|
||||
rootDir = config.parameters.uploadDir,
|
||||
): Promise<Buffer | undefined> {
|
||||
if (this.getStoragePlugin()) {
|
||||
return await this.getStoragePlugin()?.readAsBuffer?.(attachment);
|
||||
} else {
|
||||
const path = resolve(join(rootDir, attachment.location));
|
||||
|
||||
if (!fileExists(path)) {
|
||||
throw new NotFoundException('No file was found');
|
||||
}
|
||||
|
||||
return await fs.promises.readFile(path); // Reads the file content as a Buffer
|
||||
}
|
||||
async readAsBuffer(attachment: Attachment): Promise<Buffer | undefined> {
|
||||
const storageHelper = await this.helperService.getDefaultHelper(
|
||||
HelperType.STORAGE,
|
||||
);
|
||||
return await storageHelper.readAsBuffer(attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an attachment identified by the provided parameters as a Stream.
|
||||
* Reads the specified attachment as a stream using the default storage helper.
|
||||
*
|
||||
* @param attachment - The attachment to download.
|
||||
* @param rootDir - Root folder path where the attachment should be located.
|
||||
* @returns A promise that resolves to a Stream representing the attachment file.
|
||||
* @param attachment - The attachment object containing the metadata required to locate the file.
|
||||
* @returns A promise resolving to the file content as a `Stream`, or `undefined` if the file cannot be read.
|
||||
*/
|
||||
async readAsStream(
|
||||
attachment: Attachment,
|
||||
rootDir = config.parameters.uploadDir,
|
||||
): Promise<Stream | undefined> {
|
||||
if (this.getStoragePlugin()) {
|
||||
return await this.getStoragePlugin()?.readAsStream?.(attachment);
|
||||
} else {
|
||||
const path = resolve(join(rootDir, attachment.location));
|
||||
|
||||
if (!fileExists(path)) {
|
||||
throw new NotFoundException('No file was found');
|
||||
}
|
||||
|
||||
return fs.createReadStream(path); // Reads the file content as a Buffer
|
||||
}
|
||||
async readAsStream(attachment: Attachment): Promise<Stream | undefined> {
|
||||
const storageHelper = await this.helperService.getDefaultHelper(
|
||||
HelperType.STORAGE,
|
||||
);
|
||||
return await storageHelper.readAsStream(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -35,6 +35,7 @@ import { PermissionService } from '@/user/services/permission.service';
|
||||
import { RoleService } from '@/user/services/role.service';
|
||||
import { UserService } from '@/user/services/user.service';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import {
|
||||
blockFixtures,
|
||||
installBlockFixtures,
|
||||
@@ -327,9 +328,7 @@ describe('BlockController', () => {
|
||||
|
||||
await expect(
|
||||
blockController.updateOne(blockToDelete.id, updateBlock),
|
||||
).rejects.toThrow(
|
||||
new NotFoundException(`Block with ID ${blockToDelete.id} not found`),
|
||||
);
|
||||
).rejects.toThrow(getUpdateOneError(Block.name, blockToDelete.id));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -296,12 +296,7 @@ export class BlockController extends BaseController<
|
||||
@Param('id') id: string,
|
||||
@Body() blockUpdate: BlockUpdateDto,
|
||||
): Promise<Block> {
|
||||
const result = await this.blockService.updateOne(id, blockUpdate);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update Block by id ${id}`);
|
||||
throw new NotFoundException(`Block with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.blockService.updateOne(id, blockUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -114,12 +114,7 @@ export class CategoryController extends BaseController<Category> {
|
||||
@Param('id') id: string,
|
||||
@Body() categoryUpdate: CategoryUpdateDto,
|
||||
): Promise<Category> {
|
||||
const result = await this.categoryService.updateOne(id, categoryUpdate);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update Category by id ${id}`);
|
||||
throw new NotFoundException(`Category with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.categoryService.updateOne(id, categoryUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -12,6 +12,7 @@ import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import {
|
||||
contextVarFixtures,
|
||||
installContextVarFixtures,
|
||||
@@ -211,9 +212,7 @@ describe('ContextVarController', () => {
|
||||
contextVarUpdatedDto,
|
||||
),
|
||||
).rejects.toThrow(
|
||||
new NotFoundException(
|
||||
`ContextVar with ID ${contextVarToDelete.id} not found`,
|
||||
),
|
||||
getUpdateOneError(ContextVar.name, contextVarToDelete.id),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -120,12 +120,7 @@ export class ContextVarController extends BaseController<ContextVar> {
|
||||
@Param('id') id: string,
|
||||
@Body() contextVarUpdate: ContextVarUpdateDto,
|
||||
): Promise<ContextVar> {
|
||||
const result = await this.contextVarService.updateOne(id, contextVarUpdate);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update ContextVar by id ${id}`);
|
||||
throw new NotFoundException(`ContextVar with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.contextVarService.updateOne(id, contextVarUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,7 @@ import { UserModel } from '@/user/schemas/user.schema';
|
||||
import { RoleService } from '@/user/services/role.service';
|
||||
import { UserService } from '@/user/services/user.service';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import { labelFixtures } from '@/utils/test/fixtures/label';
|
||||
import { installSubscriberFixtures } from '@/utils/test/fixtures/subscriber';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
@@ -223,12 +224,10 @@ describe('LabelController', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw a NotFoundException when attempting to update a non existing label by id', async () => {
|
||||
it('should throw a NotFoundException when attempting to update a non existing label by id', async () => {
|
||||
await expect(
|
||||
labelController.updateOne(labelToDelete.id, labelUpdateDto),
|
||||
).rejects.toThrow(
|
||||
new NotFoundException(`Label with ID ${labelToDelete.id} not found`),
|
||||
);
|
||||
).rejects.toThrow(getUpdateOneError(Label.name, labelToDelete.id));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -111,12 +111,7 @@ export class LabelController extends BaseController<
|
||||
@Param('id') id: string,
|
||||
@Body() labelUpdate: LabelUpdateDto,
|
||||
) {
|
||||
const result = await this.labelService.updateOne(id, labelUpdate);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update Label by id ${id}`);
|
||||
throw new NotFoundException(`Label with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.labelService.updateOne(id, labelUpdate);
|
||||
}
|
||||
|
||||
@CsrfCheck(true)
|
||||
|
||||
@@ -174,11 +174,6 @@ export class SubscriberController extends BaseController<
|
||||
@Param('id') id: string,
|
||||
@Body() subscriberUpdate: SubscriberUpdateDto,
|
||||
) {
|
||||
const result = await this.subscriberService.updateOne(id, subscriberUpdate);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update Subscriber by id ${id}`);
|
||||
throw new NotFoundException(`Subscriber with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.subscriberService.updateOne(id, subscriberUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -36,6 +36,7 @@ describe('BlockRepository', () => {
|
||||
let hasNextBlocks: Block;
|
||||
let validIds: string[];
|
||||
let validCategory: string;
|
||||
let blockValidIds: string[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
@@ -58,6 +59,7 @@ describe('BlockRepository', () => {
|
||||
hasNextBlocks = (await blockRepository.findOne({
|
||||
name: 'hasNextBlocks',
|
||||
}))!;
|
||||
blockValidIds = (await blockRepository.findAll()).map(({ id }) => id);
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
@@ -169,22 +171,22 @@ describe('BlockRepository', () => {
|
||||
describe('prepareBlocksInCategoryUpdateScope', () => {
|
||||
it('should update blocks within the scope based on category and ids', async () => {
|
||||
jest.spyOn(blockRepository, 'findOne').mockResolvedValue({
|
||||
id: validIds[0],
|
||||
id: blockValidIds[0],
|
||||
category: 'oldCategory',
|
||||
nextBlocks: [validIds[1]],
|
||||
attachedBlock: validIds[1],
|
||||
nextBlocks: [blockValidIds[1]],
|
||||
attachedBlock: blockValidIds[1],
|
||||
} as Block);
|
||||
|
||||
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');
|
||||
|
||||
await blockRepository.prepareBlocksInCategoryUpdateScope(
|
||||
validCategory,
|
||||
validIds,
|
||||
blockValidIds,
|
||||
);
|
||||
|
||||
expect(mockUpdateOne).toHaveBeenCalledWith(validIds[0], {
|
||||
nextBlocks: [validIds[1]],
|
||||
attachedBlock: validIds[1],
|
||||
expect(mockUpdateOne).toHaveBeenCalledWith(blockValidIds[0], {
|
||||
nextBlocks: [blockValidIds[1]],
|
||||
attachedBlock: blockValidIds[1],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -211,9 +213,9 @@ describe('BlockRepository', () => {
|
||||
it('should update blocks outside the scope by removing references from attachedBlock', async () => {
|
||||
const otherBlocks = [
|
||||
{
|
||||
id: '64abc1234def567890fedcab',
|
||||
attachedBlock: validIds[0],
|
||||
nextBlocks: [validIds[0]],
|
||||
id: blockValidIds[1],
|
||||
attachedBlock: blockValidIds[0],
|
||||
nextBlocks: [blockValidIds[0]],
|
||||
},
|
||||
] as Block[];
|
||||
|
||||
@@ -221,10 +223,10 @@ describe('BlockRepository', () => {
|
||||
|
||||
await blockRepository.prepareBlocksOutOfCategoryUpdateScope(
|
||||
otherBlocks,
|
||||
validIds,
|
||||
blockValidIds,
|
||||
);
|
||||
|
||||
expect(mockUpdateOne).toHaveBeenCalledWith('64abc1234def567890fedcab', {
|
||||
expect(mockUpdateOne).toHaveBeenCalledWith(blockValidIds[1], {
|
||||
attachedBlock: null,
|
||||
});
|
||||
});
|
||||
@@ -232,20 +234,20 @@ describe('BlockRepository', () => {
|
||||
it('should update blocks outside the scope by removing references from nextBlocks', async () => {
|
||||
const otherBlocks = [
|
||||
{
|
||||
id: '64abc1234def567890fedcab',
|
||||
id: blockValidIds[1],
|
||||
attachedBlock: null,
|
||||
nextBlocks: [validIds[0], validIds[1]],
|
||||
nextBlocks: [blockValidIds[0], blockValidIds[1]],
|
||||
},
|
||||
] as unknown as Block[];
|
||||
|
||||
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');
|
||||
|
||||
await blockRepository.prepareBlocksOutOfCategoryUpdateScope(otherBlocks, [
|
||||
validIds[0],
|
||||
blockValidIds[0],
|
||||
]);
|
||||
|
||||
expect(mockUpdateOne).toHaveBeenCalledWith('64abc1234def567890fedcab', {
|
||||
nextBlocks: [validIds[1]],
|
||||
expect(mockUpdateOne).toHaveBeenCalledWith(blockValidIds[1], {
|
||||
nextBlocks: [blockValidIds[1]],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -254,7 +256,7 @@ describe('BlockRepository', () => {
|
||||
it('should update blocks in and out of the scope', async () => {
|
||||
const mockFind = jest.spyOn(blockRepository, 'find').mockResolvedValue([
|
||||
{
|
||||
id: '64abc1234def567890fedcab',
|
||||
id: blockValidIds[1],
|
||||
attachedBlock: validIds[0],
|
||||
nextBlocks: [validIds[0]],
|
||||
},
|
||||
@@ -278,17 +280,17 @@ describe('BlockRepository', () => {
|
||||
expect(mockFind).toHaveBeenCalled();
|
||||
expect(prepareBlocksInCategoryUpdateScope).toHaveBeenCalledWith(
|
||||
validCategory,
|
||||
['64abc1234def567890fedcab'],
|
||||
[blockValidIds[1]],
|
||||
);
|
||||
expect(prepareBlocksOutOfCategoryUpdateScope).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
id: '64abc1234def567890fedcab',
|
||||
id: blockValidIds[1],
|
||||
attachedBlock: validIds[0],
|
||||
nextBlocks: [validIds[0]],
|
||||
},
|
||||
],
|
||||
['64abc1234def567890fedcab'],
|
||||
[blockValidIds[1]],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -48,7 +48,7 @@ export class ConversationRepository extends BaseRepository<
|
||||
*
|
||||
* @returns A promise resolving to the result of the update operation.
|
||||
*/
|
||||
async end(convo: Conversation | ConversationFull) {
|
||||
async end(convo: Conversation | ConversationFull): Promise<Conversation> {
|
||||
return await this.updateOne(convo.id, { active: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -151,7 +151,7 @@ export class SubscriberRepository extends BaseRepository<
|
||||
async updateOneByForeignIdQuery(
|
||||
id: string,
|
||||
updates: SubscriberUpdateDto,
|
||||
): Promise<Subscriber | null> {
|
||||
): Promise<Subscriber> {
|
||||
return await this.updateOne({ foreign_id: id }, updates);
|
||||
}
|
||||
|
||||
@@ -162,9 +162,7 @@ export class SubscriberRepository extends BaseRepository<
|
||||
*
|
||||
* @returns The updated subscriber entity.
|
||||
*/
|
||||
async handBackByForeignIdQuery(
|
||||
foreignId: string,
|
||||
): Promise<Subscriber | null> {
|
||||
async handBackByForeignIdQuery(foreignId: string): Promise<Subscriber> {
|
||||
return await this.updateOne(
|
||||
{
|
||||
foreign_id: foreignId,
|
||||
@@ -187,7 +185,7 @@ export class SubscriberRepository extends BaseRepository<
|
||||
async handOverByForeignIdQuery(
|
||||
foreignId: string,
|
||||
userId: string,
|
||||
): Promise<Subscriber | null> {
|
||||
): Promise<Subscriber> {
|
||||
return await this.updateOne(
|
||||
{
|
||||
foreign_id: foreignId,
|
||||
|
||||
@@ -173,11 +173,6 @@ export class ConversationService extends BaseService<
|
||||
const updatedConversation = await this.updateOne(convo.id, {
|
||||
context: convo.context,
|
||||
});
|
||||
if (!updatedConversation) {
|
||||
throw new Error(
|
||||
'Conversation Model : No conversation has been updated',
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: add check if nothing changed don't update
|
||||
const criteria =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -17,6 +17,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { BlockService } from '@/chat/services/block.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NOT_FOUND_ID } from '@/utils/constants/mock';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import { installContentFixtures } from '@/utils/test/fixtures/content';
|
||||
import { contentTypeFixtures } from '@/utils/test/fixtures/contenttype';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
@@ -174,7 +175,7 @@ describe('ContentTypeController', () => {
|
||||
jest.spyOn(contentTypeService, 'updateOne');
|
||||
await expect(
|
||||
contentTypeController.updateOne(updatedContent, NOT_FOUND_ID),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(getUpdateOneError(ContentType.name, NOT_FOUND_ID));
|
||||
expect(contentTypeService.updateOne).toHaveBeenCalledWith(
|
||||
NOT_FOUND_ID,
|
||||
updatedContent,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -148,17 +148,6 @@ export class ContentTypeController extends BaseController<ContentType> {
|
||||
@Body() contentTypeDto: ContentTypeUpdateDto,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
const updatedContentType = await this.contentTypeService.updateOne(
|
||||
id,
|
||||
contentTypeDto,
|
||||
);
|
||||
|
||||
if (!updatedContentType) {
|
||||
this.logger.warn(
|
||||
`Failed to update content type with id ${id}. Content type not found.`,
|
||||
);
|
||||
throw new NotFoundException(`Content type with id ${id} not found`);
|
||||
}
|
||||
return updatedContentType;
|
||||
return await this.contentTypeService.updateOne(id, contentTypeDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -23,6 +23,7 @@ import { LoggerService } from '@/logger/logger.service';
|
||||
import { NOT_FOUND_ID } from '@/utils/constants/mock';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import {
|
||||
contentFixtures,
|
||||
installContentFixtures,
|
||||
@@ -194,7 +195,7 @@ describe('ContentController', () => {
|
||||
it('should throw NotFoundException if the content is not found', async () => {
|
||||
await expect(
|
||||
contentController.updateOne(updatedContent, NOT_FOUND_ID),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(getUpdateOneError(Content.name, NOT_FOUND_ID));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -300,13 +300,6 @@ export class ContentController extends BaseController<
|
||||
@Body() contentDto: ContentUpdateDto,
|
||||
@Param('id') id: string,
|
||||
): Promise<Content> {
|
||||
const updatedContent = await this.contentService.updateOne(id, contentDto);
|
||||
if (!updatedContent) {
|
||||
this.logger.warn(
|
||||
`Failed to update content with id ${id}. Content not found.`,
|
||||
);
|
||||
throw new NotFoundException(`Content of id ${id} not found`);
|
||||
}
|
||||
return updatedContent;
|
||||
return await this.contentService.updateOne(id, contentDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -165,7 +165,10 @@ export class MenuController extends BaseController<
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Patch(':id')
|
||||
async updateOne(@Body() body: MenuCreateDto, @Param('id') id: string) {
|
||||
async updateOne(
|
||||
@Body() body: MenuCreateDto,
|
||||
@Param('id') id: string,
|
||||
): Promise<Menu> {
|
||||
if (!id) return await this.create(body);
|
||||
return await this.menuService.updateOne(id, body);
|
||||
}
|
||||
|
||||
191
api/src/extensions/helpers/local-storage/index.helper.ts
Normal file
191
api/src/extensions/helpers/local-storage/index.helper.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import { join, normalize, resolve } from 'path';
|
||||
import { Readable, Stream } from 'stream';
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
OnModuleInit,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import sanitizeFilename from 'sanitize-filename';
|
||||
|
||||
import {
|
||||
AttachmentCreateDto,
|
||||
AttachmentMetadataDto,
|
||||
} from '@/attachment/dto/attachment.dto';
|
||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentResourceRef } from '@/attachment/types';
|
||||
import {
|
||||
fileExists,
|
||||
generateUniqueFilename,
|
||||
getStreamableFile,
|
||||
} from '@/attachment/utilities';
|
||||
import { config } from '@/config';
|
||||
import { HelperService } from '@/helper/helper.service';
|
||||
import BaseStorageHelper from '@/helper/lib/base-storage-helper';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import { LOCAL_STORAGE_HELPER_NAME } from './settings';
|
||||
|
||||
@Injectable()
|
||||
export default class LocalStorageHelper
|
||||
extends BaseStorageHelper<typeof LOCAL_STORAGE_HELPER_NAME>
|
||||
implements OnModuleInit
|
||||
{
|
||||
constructor(
|
||||
settingService: SettingService,
|
||||
helperService: HelperService,
|
||||
logger: LoggerService,
|
||||
) {
|
||||
super(LOCAL_STORAGE_HELPER_NAME, settingService, helperService, logger);
|
||||
}
|
||||
|
||||
getPath() {
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment root directory given the resource reference
|
||||
*
|
||||
* @param ref The attachment resource reference
|
||||
* @returns The root directory path
|
||||
*/
|
||||
private getRootDirByResourceRef(ref: AttachmentResourceRef) {
|
||||
return ref === AttachmentResourceRef.SubscriberAvatar ||
|
||||
ref === AttachmentResourceRef.UserAvatar
|
||||
? config.parameters.avatarDir
|
||||
: config.parameters.uploadDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a attachment file to the local directory.
|
||||
*
|
||||
* @param file - The file
|
||||
* @param metadata - The attachment metadata informations.
|
||||
* @returns A promise that resolves to the uploaded attachment.
|
||||
*/
|
||||
async store(
|
||||
file: Buffer | Stream | Readable | Express.Multer.File,
|
||||
metadata: AttachmentMetadataDto,
|
||||
): Promise<AttachmentCreateDto> {
|
||||
const rootDir = this.getRootDirByResourceRef(metadata.resourceRef);
|
||||
const uniqueFilename = generateUniqueFilename(metadata.name);
|
||||
const filePath = resolve(join(rootDir, sanitizeFilename(uniqueFilename)));
|
||||
|
||||
if (Buffer.isBuffer(file)) {
|
||||
await fs.promises.writeFile(filePath, file);
|
||||
} else if (file instanceof Readable || file instanceof Stream) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
file.pipe(writeStream);
|
||||
// @TODO: Calc size here?
|
||||
writeStream.on('finish', resolve);
|
||||
writeStream.on('error', reject);
|
||||
});
|
||||
} else {
|
||||
if (file.path) {
|
||||
// For example, if the file is an instance of `Express.Multer.File` (diskStorage case)
|
||||
const srcFilePath = fs.realpathSync(resolve(file.path));
|
||||
// Get the system's temporary directory in a cross-platform way
|
||||
const tempDir = os.tmpdir();
|
||||
const normalizedTempDir = normalize(tempDir);
|
||||
|
||||
if (!srcFilePath.startsWith(normalizedTempDir)) {
|
||||
throw new Error('Invalid file path');
|
||||
}
|
||||
|
||||
await fs.promises.copyFile(srcFilePath, filePath);
|
||||
await fs.promises.unlink(srcFilePath);
|
||||
} else {
|
||||
await fs.promises.writeFile(filePath, file.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
const location = filePath.replace(rootDir, '');
|
||||
return {
|
||||
...metadata,
|
||||
location,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an attachment identified by the provided parameters.
|
||||
*
|
||||
* @param attachment - The attachment to download.
|
||||
* @returns A promise that resolves to a StreamableFile representing the downloaded attachment.
|
||||
*/
|
||||
async download(attachment: Attachment): Promise<StreamableFile> {
|
||||
const rootDir = this.getRootDirByResourceRef(attachment.resourceRef);
|
||||
const path = resolve(join(rootDir, attachment.location));
|
||||
|
||||
if (!fileExists(path)) {
|
||||
throw new NotFoundException('No file was found');
|
||||
}
|
||||
|
||||
const disposition = `attachment; filename="${encodeURIComponent(
|
||||
attachment.name,
|
||||
)}"`;
|
||||
|
||||
return getStreamableFile({
|
||||
path,
|
||||
options: {
|
||||
type: attachment.type,
|
||||
length: attachment.size,
|
||||
disposition,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an attachment identified by the provided parameters as a Buffer.
|
||||
*
|
||||
* @param attachment - The attachment to download.
|
||||
* @returns A promise that resolves to a Buffer representing the attachment file.
|
||||
*/
|
||||
async readAsBuffer(attachment: Attachment): Promise<Buffer | undefined> {
|
||||
const path = resolve(
|
||||
join(
|
||||
this.getRootDirByResourceRef(attachment.resourceRef),
|
||||
attachment.location,
|
||||
),
|
||||
);
|
||||
|
||||
if (!fileExists(path)) {
|
||||
throw new NotFoundException('No file was found');
|
||||
}
|
||||
|
||||
return await fs.promises.readFile(path); // Reads the file content as a Buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an attachment identified by the provided parameters as a Stream.
|
||||
*
|
||||
* @param attachment - The attachment to download.
|
||||
* @returns A promise that resolves to a Stream representing the attachment file.
|
||||
*/
|
||||
async readAsStream(attachment: Attachment): Promise<Stream | undefined> {
|
||||
const path = resolve(
|
||||
join(
|
||||
this.getRootDirByResourceRef(attachment.resourceRef),
|
||||
attachment.location,
|
||||
),
|
||||
);
|
||||
|
||||
if (!fileExists(path)) {
|
||||
throw new NotFoundException('No file was found');
|
||||
}
|
||||
|
||||
return fs.createReadStream(path);
|
||||
}
|
||||
}
|
||||
8
api/src/extensions/helpers/local-storage/package.json
Normal file
8
api/src/extensions/helpers/local-storage/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "hexabot-helper-local-storage",
|
||||
"version": "2.2.0",
|
||||
"description": "The default Hexabot Helper Extension for Hexabot to enable local storage for attachment files",
|
||||
"dependencies": {},
|
||||
"author": "Hexastack",
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
17
api/src/extensions/helpers/local-storage/settings.ts
Normal file
17
api/src/extensions/helpers/local-storage/settings.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { HelperSetting } from '@/helper/types';
|
||||
|
||||
export const LOCAL_STORAGE_HELPER_NAME = 'local-storage-helper';
|
||||
|
||||
export const LOCAL_STORAGE_HELPER_NAMESPACE = 'local-storage-helper';
|
||||
|
||||
export default [] as const satisfies HelperSetting<
|
||||
typeof LOCAL_STORAGE_HELPER_NAME
|
||||
>[];
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -108,6 +108,7 @@ export class HelperService {
|
||||
/**
|
||||
* Get default NLU helper.
|
||||
*
|
||||
* @deprecated Use getDefaultHelper() instead
|
||||
* @returns - The helper
|
||||
*/
|
||||
async getDefaultNluHelper() {
|
||||
@@ -128,6 +129,7 @@ export class HelperService {
|
||||
/**
|
||||
* Get default LLM helper.
|
||||
*
|
||||
* @deprecated Use getDefaultHelper() instead
|
||||
* @returns - The helper
|
||||
*/
|
||||
async getDefaultLlmHelper() {
|
||||
@@ -144,4 +146,31 @@ export class HelperService {
|
||||
|
||||
return defaultHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default helper for a specific type.
|
||||
*
|
||||
* @param type - The type of the helper (e.g., NLU, LLM, STORAGE).
|
||||
* @returns - The helper
|
||||
*/
|
||||
async getDefaultHelper<T extends HelperType>(type: T) {
|
||||
if (type === HelperType.UTIL) {
|
||||
throw new Error(
|
||||
`Default helpers are not available for type: ${HelperType.UTIL}`,
|
||||
);
|
||||
}
|
||||
|
||||
const settings = await this.settingService.getSettings();
|
||||
const defaultHelperName = settings.chatbot_settings[
|
||||
`default_${type}_helper` as any
|
||||
] as HelperName;
|
||||
|
||||
const defaultHelper = this.get<T>(type, defaultHelperName);
|
||||
|
||||
if (!defaultHelper) {
|
||||
throw new Error(`Unable to find default ${type.toUpperCase()} helper`);
|
||||
}
|
||||
|
||||
return defaultHelper;
|
||||
}
|
||||
}
|
||||
|
||||
76
api/src/helper/lib/base-storage-helper.ts
Normal file
76
api/src/helper/lib/base-storage-helper.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Readable, Stream } from 'stream';
|
||||
|
||||
import { StreamableFile } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
AttachmentCreateDto,
|
||||
AttachmentMetadataDto,
|
||||
} from '@/attachment/dto/attachment.dto';
|
||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import { HelperService } from '../helper.service';
|
||||
import { HelperName, HelperType } from '../types';
|
||||
|
||||
import BaseHelper from './base-helper';
|
||||
|
||||
export default abstract class BaseStorageHelper<
|
||||
N extends HelperName = HelperName,
|
||||
> extends BaseHelper<N> {
|
||||
protected readonly type: HelperType = HelperType.STORAGE;
|
||||
|
||||
constructor(
|
||||
name: N,
|
||||
settingService: SettingService,
|
||||
helperService: HelperService,
|
||||
logger: LoggerService,
|
||||
) {
|
||||
super(name, settingService, helperService, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads files to the server. If a storage helper is configured it uploads files accordingly.
|
||||
* Otherwise, uploads files to the local directory.
|
||||
*
|
||||
* @param file - The file
|
||||
* @param metadata - The attachment metadata informations.
|
||||
* @returns A promise that resolves to an array of uploaded attachments.
|
||||
*/
|
||||
abstract store(
|
||||
_file: Buffer | Stream | Readable | Express.Multer.File,
|
||||
_metadata: AttachmentMetadataDto,
|
||||
): Promise<AttachmentCreateDto>;
|
||||
|
||||
/**
|
||||
* Downloads an attachment identified by the provided parameters.
|
||||
*
|
||||
* @param attachment - The attachment to download.
|
||||
* @returns A promise that resolves to a StreamableFile representing the downloaded attachment.
|
||||
*/
|
||||
abstract download(attachment: Attachment): Promise<StreamableFile>;
|
||||
|
||||
/**
|
||||
* Downloads an attachment identified by the provided parameters as a Buffer.
|
||||
*
|
||||
* @param attachment - The attachment to download.
|
||||
* @returns A promise that resolves to a Buffer representing the attachment file.
|
||||
*/
|
||||
abstract readAsBuffer(attachment: Attachment): Promise<Buffer | undefined>;
|
||||
|
||||
/**
|
||||
* Returns an attachment identified by the provided parameters as a Stream.
|
||||
*
|
||||
* @param attachment - The attachment to download.
|
||||
* @returns A promise that resolves to a Stream representing the attachment file.
|
||||
*/
|
||||
abstract readAsStream(attachment: Attachment): Promise<Stream | undefined>;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -12,6 +12,7 @@ import { HyphenToUnderscore } from '@/utils/types/extension';
|
||||
import BaseHelper from './lib/base-helper';
|
||||
import BaseLlmHelper from './lib/base-llm-helper';
|
||||
import BaseNlpHelper from './lib/base-nlp-helper';
|
||||
import BaseStorageHelper from './lib/base-storage-helper';
|
||||
|
||||
export namespace NLU {
|
||||
export interface ParseEntity {
|
||||
@@ -84,16 +85,20 @@ export namespace LLM {
|
||||
export enum HelperType {
|
||||
NLU = 'nlu',
|
||||
LLM = 'llm',
|
||||
STORAGE = 'storage',
|
||||
UTIL = 'util',
|
||||
}
|
||||
|
||||
export type HelperName = `${string}-helper`;
|
||||
|
||||
export type TypeOfHelper<T extends HelperType> = T extends HelperType.LLM
|
||||
? BaseLlmHelper<HelperName>
|
||||
: T extends HelperType.NLU
|
||||
? BaseNlpHelper<HelperName>
|
||||
: BaseHelper;
|
||||
interface HelperTypeMap {
|
||||
[HelperType.NLU]: BaseNlpHelper<HelperName>;
|
||||
[HelperType.LLM]: BaseLlmHelper<HelperName>;
|
||||
[HelperType.STORAGE]: BaseStorageHelper<HelperName>;
|
||||
[HelperType.UTIL]: BaseHelper;
|
||||
}
|
||||
|
||||
export type TypeOfHelper<T extends HelperType> = HelperTypeMap[T];
|
||||
|
||||
export type HelperRegistry<H extends BaseHelper = BaseHelper> = Map<
|
||||
HelperType,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
@@ -15,6 +15,7 @@ import { Test } from '@nestjs/testing';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NOT_FOUND_ID } from '@/utils/constants/mock';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import {
|
||||
installLanguageFixtures,
|
||||
languageFixtures,
|
||||
@@ -169,7 +170,7 @@ describe('LanguageController', () => {
|
||||
jest.spyOn(languageService, 'updateOne');
|
||||
await expect(
|
||||
languageController.updateOne(NOT_FOUND_ID, translationUpdateDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(getUpdateOneError(Language.name, NOT_FOUND_ID));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -123,12 +123,7 @@ export class LanguageController extends BaseController<Language> {
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.languageService.updateOne(id, languageUpdate);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update Language by id ${id}`);
|
||||
throw new NotFoundException(`Language with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.languageService.updateOne(id, languageUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
@@ -38,6 +37,7 @@ import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { NOT_FOUND_ID } from '@/utils/constants/mock';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import {
|
||||
installTranslationFixtures,
|
||||
translationFixtures,
|
||||
@@ -209,7 +209,7 @@ describe('TranslationController', () => {
|
||||
jest.spyOn(translationService, 'updateOne');
|
||||
await expect(
|
||||
translationController.updateOne(NOT_FOUND_ID, translationUpdateDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(getUpdateOneError(Translation.name, NOT_FOUND_ID));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -90,15 +90,7 @@ export class TranslationController extends BaseController<Translation> {
|
||||
@Param('id') id: string,
|
||||
@Body() translationUpdate: TranslationUpdateDto,
|
||||
) {
|
||||
const result = await this.translationService.updateOne(
|
||||
id,
|
||||
translationUpdate,
|
||||
);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update Translation by id ${id}`);
|
||||
throw new NotFoundException(`Translation with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.translationService.updateOne(id, translationUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -522,7 +522,7 @@ module.exports = {
|
||||
version,
|
||||
action,
|
||||
migrationDocument,
|
||||
}: MigrationSuccessCallback) {
|
||||
}: MigrationSuccessCallback): Promise<void> {
|
||||
await this.updateStatus({ version, action, migrationDocument });
|
||||
const migrationDisplayName = `${version} [${action}]`;
|
||||
this.logger.log(`"${migrationDisplayName}" migration done`);
|
||||
|
||||
@@ -772,6 +772,51 @@ const migrateAndPopulateAttachmentMessages = async ({
|
||||
}
|
||||
};
|
||||
|
||||
const addDefaultStorageHelper = async ({ logger }: MigrationServices) => {
|
||||
const SettingModel = mongoose.model<Setting>(Setting.name, settingSchema);
|
||||
try {
|
||||
await SettingModel.updateOne(
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'default_storage_helper',
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'default_storage_helper',
|
||||
value: 'local-storage-helper',
|
||||
type: SettingType.select,
|
||||
config: {
|
||||
multiple: false,
|
||||
allowCreate: false,
|
||||
entity: 'Helper',
|
||||
idKey: 'name',
|
||||
labelKey: 'name',
|
||||
},
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
},
|
||||
);
|
||||
logger.log('Successfuly added the default local storage helper setting');
|
||||
} catch (err) {
|
||||
logger.error('Unable to add the default local storage helper setting');
|
||||
}
|
||||
};
|
||||
|
||||
const removeDefaultStorageHelper = async ({ logger }: MigrationServices) => {
|
||||
const SettingModel = mongoose.model<Setting>(Setting.name, settingSchema);
|
||||
try {
|
||||
await SettingModel.deleteOne({
|
||||
group: 'chatbot_settings',
|
||||
label: 'default_storage_helper',
|
||||
});
|
||||
logger.log('Successfuly removed the default local storage helper setting');
|
||||
} catch (err) {
|
||||
logger.error('Unable to remove the default local storage helper setting');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
async up(services: MigrationServices) {
|
||||
await updateOldAvatarsPath(services);
|
||||
@@ -784,6 +829,7 @@ module.exports = {
|
||||
await populateSettingAttachments(services);
|
||||
await populateUserAvatars(services);
|
||||
await populateSubscriberAvatars(services);
|
||||
await addDefaultStorageHelper(services);
|
||||
return true;
|
||||
},
|
||||
async down(services: MigrationServices) {
|
||||
@@ -792,6 +838,7 @@ module.exports = {
|
||||
await restoreOldAvatarsPath(services);
|
||||
await migrateAttachmentBlocks(MigrationAction.DOWN, services);
|
||||
await migrateAttachmentContents(MigrationAction.DOWN, services);
|
||||
await removeDefaultStorageHelper(services);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -167,17 +167,7 @@ export class NlpEntityController extends BaseController<
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.nlpEntityService.updateOne(
|
||||
id,
|
||||
updateNlpEntityDto,
|
||||
);
|
||||
if (!result) {
|
||||
this.logger.warn(`Failed to update NLP Entity by id ${id}`);
|
||||
throw new InternalServerErrorException(
|
||||
`Failed to update NLP Entity with ID ${id}`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
return await this.nlpEntityService.updateOne(id, updateNlpEntityDto);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -22,6 +22,7 @@ import { SettingRepository } from '@/setting/repositories/setting.repository';
|
||||
import { SettingModel } from '@/setting/schemas/setting.schema';
|
||||
import { SettingSeeder } from '@/setting/seeds/setting.seed';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import { installAttachmentFixtures } from '@/utils/test/fixtures/attachment';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
|
||||
@@ -310,7 +311,7 @@ describe('NlpSampleController', () => {
|
||||
type: NlpSampleState.test,
|
||||
language: 'fr',
|
||||
}),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(getUpdateOneError(NlpSample.name, byeJhonSampleId!));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -305,11 +305,6 @@ export class NlpSampleController extends BaseController<
|
||||
trained: false,
|
||||
});
|
||||
|
||||
if (!sample) {
|
||||
this.logger.warn(`Unable to update NLP Sample by id ${id}`);
|
||||
throw new NotFoundException(`NLP Sample with ID ${id} not found`);
|
||||
}
|
||||
|
||||
await this.nlpSampleEntityService.deleteMany({ sample: id });
|
||||
|
||||
const updatedSampleEntities =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -12,6 +12,7 @@ import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { getUpdateOneError } from '@/utils/test/errors/messages';
|
||||
import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity';
|
||||
import {
|
||||
installNlpValueFixtures,
|
||||
@@ -241,7 +242,7 @@ describe('NlpValueController', () => {
|
||||
expressions: [],
|
||||
builtin: true,
|
||||
}),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(getUpdateOneError(NlpValue.name, jhonNlpValue!.id));
|
||||
});
|
||||
});
|
||||
describe('deleteMany', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -168,12 +168,7 @@ export class NlpValueController extends BaseController<
|
||||
@Param('id') id: string,
|
||||
@Body() updateNlpValueDto: NlpValueUpdateDto,
|
||||
): Promise<NlpValue> {
|
||||
const result = await this.nlpValueService.updateOne(id, updateNlpValueDto);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update NLP Value by id ${id}`);
|
||||
throw new NotFoundException(`NLP Value with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.nlpValueService.updateOne(id, updateNlpValueDto);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -63,7 +63,7 @@ export class NlpValueService extends BaseService<
|
||||
sampleText: string,
|
||||
sampleEntities: NlpSampleEntityValue[],
|
||||
storedEntities: NlpEntity[],
|
||||
) {
|
||||
): Promise<NlpSampleEntityValue[]> {
|
||||
const eMap: Record<string, NlpEntity> = storedEntities.reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.name) acc[curr?.name] = curr;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Readable, Stream } from 'stream';
|
||||
|
||||
import { Injectable, StreamableFile } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
AttachmentCreateDto,
|
||||
AttachmentMetadataDto,
|
||||
} from '@/attachment/dto/attachment.dto';
|
||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
|
||||
import { BasePlugin } from './base-plugin.service';
|
||||
import { PluginService } from './plugins.service';
|
||||
import { PluginName, PluginType } from './types';
|
||||
|
||||
@Injectable()
|
||||
export abstract class BaseStoragePlugin extends BasePlugin {
|
||||
public readonly type: PluginType = PluginType.storage;
|
||||
|
||||
constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
|
||||
super(name, pluginService);
|
||||
}
|
||||
|
||||
/** @deprecated use download() instead */
|
||||
fileExists?(attachment: Attachment): Promise<boolean>;
|
||||
|
||||
/** @deprecated use store() instead */
|
||||
upload?(file: Express.Multer.File): Promise<AttachmentCreateDto>;
|
||||
|
||||
/** @deprecated use store() instead */
|
||||
uploadAvatar?(file: Express.Multer.File): Promise<any>;
|
||||
|
||||
abstract download(attachment: Attachment): Promise<StreamableFile>;
|
||||
|
||||
/** @deprecated use download() instead */
|
||||
downloadProfilePic?(name: string): Promise<StreamableFile>;
|
||||
|
||||
readAsBuffer?(attachment: Attachment): Promise<Buffer>;
|
||||
|
||||
readAsStream?(attachment: Attachment): Promise<Stream>;
|
||||
|
||||
store?(
|
||||
file: Buffer | Stream | Readable | Express.Multer.File,
|
||||
metadata: AttachmentMetadataDto,
|
||||
): Promise<Attachment>;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -9,13 +9,11 @@
|
||||
import { BaseBlockPlugin } from './base-block-plugin';
|
||||
import { BaseEventPlugin } from './base-event-plugin';
|
||||
import { BasePlugin } from './base-plugin.service';
|
||||
import { BaseStoragePlugin } from './base-storage-plugin';
|
||||
import { PluginType } from './types';
|
||||
|
||||
const PLUGIN_TYPE_MAP = {
|
||||
[PluginType.event]: BaseEventPlugin,
|
||||
[PluginType.block]: BaseBlockPlugin,
|
||||
[PluginType.storage]: BaseStoragePlugin,
|
||||
};
|
||||
|
||||
export type PluginTypeMap = typeof PLUGIN_TYPE_MAP;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -17,7 +17,6 @@ export type PluginName = `${string}-plugin`;
|
||||
export enum PluginType {
|
||||
event = 'event',
|
||||
block = 'block',
|
||||
storage = 'storage',
|
||||
}
|
||||
|
||||
export interface CustomBlocks {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Query,
|
||||
@@ -71,11 +70,6 @@ export class SettingController {
|
||||
@Param('id') id: string,
|
||||
@Body() settingUpdateDto: { value: any },
|
||||
): Promise<Setting> {
|
||||
const result = await this.settingService.updateOne(id, settingUpdateDto);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update setting by id ${id}`);
|
||||
throw new NotFoundException(`Setting with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.settingService.updateOne(id, settingUpdateDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -38,12 +38,26 @@ export const DEFAULT_SETTINGS = [
|
||||
},
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'default_storage_helper',
|
||||
value: 'local-storage-helper',
|
||||
type: SettingType.select,
|
||||
config: {
|
||||
multiple: false,
|
||||
allowCreate: false,
|
||||
entity: 'Helper',
|
||||
idKey: 'name',
|
||||
labelKey: 'name',
|
||||
},
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'global_fallback',
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 3,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
@@ -58,7 +72,7 @@ export const DEFAULT_SETTINGS = [
|
||||
idKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
weight: 4,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
@@ -68,7 +82,7 @@ export const DEFAULT_SETTINGS = [
|
||||
"I'm really sorry but i don't quite understand what you are saying :(",
|
||||
] as string[],
|
||||
type: SettingType.multiple_text,
|
||||
weight: 5,
|
||||
weight: 6,
|
||||
translatable: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -134,12 +134,7 @@ export class RoleController extends BaseController<
|
||||
@CsrfCheck(true)
|
||||
@Patch(':id')
|
||||
async updateOne(@Param('id') id: string, @Body() roleUpdate: RoleUpdateDto) {
|
||||
const result = await this.roleService.updateOne(id, roleUpdate);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update Role by id ${id}`);
|
||||
throw new NotFoundException(`Role with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
return await this.roleService.updateOne(id, roleUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -306,7 +306,7 @@ export class ReadWriteUserController extends ReadOnlyUserController {
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const result = await this.userService.updateOne(
|
||||
return await this.userService.updateOne(
|
||||
req.user.id,
|
||||
avatar
|
||||
? {
|
||||
@@ -315,12 +315,6 @@ export class ReadWriteUserController extends ReadOnlyUserController {
|
||||
}
|
||||
: userUpdate,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update User by id ${id}`);
|
||||
throw new NotFoundException(`User with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -482,7 +482,7 @@ export abstract class BaseRepository<
|
||||
options: QueryOptions<D> | null = {
|
||||
new: true,
|
||||
},
|
||||
): Promise<T | null> {
|
||||
): Promise<T> {
|
||||
const query = this.model.findOneAndUpdate<T>(
|
||||
{
|
||||
...(typeof criteria === 'string' ? { _id: criteria } : criteria),
|
||||
@@ -512,7 +512,14 @@ export abstract class BaseRepository<
|
||||
filterCriteria,
|
||||
queryUpdates,
|
||||
);
|
||||
return await this.executeOne(query, this.cls);
|
||||
const result = await this.executeOne(query, this.cls);
|
||||
|
||||
if (!result) {
|
||||
const errorMessage = `Unable to update ${this.cls.name} with ${typeof criteria === 'string' ? 'ID' : 'criteria'} ${JSON.stringify(criteria)}`;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateMany<D extends Partial<U>>(
|
||||
|
||||
@@ -179,7 +179,7 @@ export abstract class BaseService<
|
||||
criteria: string | TFilterQuery<T>,
|
||||
dto: DtoInfer<DtoAction.Update, Dto, Partial<U>>,
|
||||
options?: QueryOptions<Partial<U>> | null,
|
||||
): Promise<T | null> {
|
||||
): Promise<T> {
|
||||
return await this.repository.updateOne(criteria, dto, options);
|
||||
}
|
||||
|
||||
|
||||
10
api/src/utils/test/errors/messages.ts
Normal file
10
api/src/utils/test/errors/messages.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
export const getUpdateOneError = (entity: string, id?: string) =>
|
||||
new Error(`Unable to update ${entity}${id ? ` with ID \"${id}\"` : ''}`);
|
||||
9
api/src/utils/test/fixtures/setting.ts
vendored
9
api/src/utils/test/fixtures/setting.ts
vendored
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -13,6 +13,13 @@ import { SettingModel } from '@/setting/schemas/setting.schema';
|
||||
import { SettingType } from '@/setting/schemas/types';
|
||||
|
||||
export const settingFixtures: SettingCreateDto[] = [
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'default_storage_helper',
|
||||
value: 'local-storage-helper',
|
||||
type: SettingType.text,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'contact_email_recipient',
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
"fallback_message": "Fallback Message",
|
||||
"fallback_block": "Fallback Block",
|
||||
"default_nlu_helper": "Default NLU Helper",
|
||||
"default_llm_helper": "Default LLM Helper"
|
||||
"default_llm_helper": "Default LLM Helper",
|
||||
"default_storage_helper": "Default Storage Helper"
|
||||
},
|
||||
"help": {
|
||||
"global_fallback": "Global fallback allows you to send custom messages when user entry does not match any of the block messages.",
|
||||
"fallback_message": "If no fallback block is selected, then one of these messages will be sent.",
|
||||
"default_nlu_helper": "The NLU helper is responsible for processing and understanding user inputs, including tasks like intent prediction, language detection, and entity recognition.",
|
||||
"default_llm_helper": "The LLM helper leverages advanced generative AI to perform tasks such as text generation, chat completion, and complex query responses."
|
||||
"default_llm_helper": "The LLM helper leverages advanced generative AI to perform tasks such as text generation, chat completion, and complex query responses.",
|
||||
"default_storage_helper": "The storage helper defines where to store attachment files. By default, the default local storage helper stores them locally, but you can choose to use Minio or any other storage solution."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
"fallback_message": "Message de secours",
|
||||
"fallback_block": "Bloc de secours",
|
||||
"default_nlu_helper": "Utilitaire NLU par défaut",
|
||||
"default_llm_helper": "Utilitaire LLM par défaut"
|
||||
"default_llm_helper": "Utilitaire LLM par défaut",
|
||||
"default_storage_helper": "Utilitaire de stockage par défaut"
|
||||
},
|
||||
"help": {
|
||||
"global_fallback": "La réponse de secours globale vous permet d'envoyer des messages personnalisés lorsque l'entrée de l'utilisateur ne correspond à aucun des messages des blocs.",
|
||||
"fallback_message": "Si aucun bloc de secours n'est sélectionné, l'un de ces messages sera envoyé.",
|
||||
"default_nlu_helper": "Utilitaire du traitement et de la compréhension des entrées des utilisateurs, incluant des tâches telles que la prédiction d'intention, la détection de langue et la reconnaissance d'entités.",
|
||||
"default_llm_helper": "Utilitaire responsable de l'intelligence artificielle générative avancée pour effectuer des tâches telles que la génération de texte, la complétion de chat et les réponses à des requêtes complexes."
|
||||
"default_llm_helper": "Utilitaire responsable de l'intelligence artificielle générative avancée pour effectuer des tâches telles que la génération de texte, la complétion de chat et les réponses à des requêtes complexes.",
|
||||
"default_storage_helper": "Utilitaire de stockage définit l'emplacement où stocker les fichiers joints. Par défaut, le stockage local les conserve localement, mais vous pouvez choisir d'utiliser Minio ou toute autre solution de stockage."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,25 +11,15 @@ import { Box, Grid, Paper } from "@mui/material";
|
||||
import { GridColDef, GridEventListener } from "@mui/x-data-grid";
|
||||
|
||||
import AttachmentThumbnail from "@/app-components/attachment/AttachmentThumbnail";
|
||||
import { DeleteDialog } from "@/app-components/dialogs/DeleteDialog";
|
||||
import { deleteCallbackHandler } from "@/app-components/dialogs/utils/deleteHandlers";
|
||||
import { FilterTextfield } from "@/app-components/inputs/FilterTextfield";
|
||||
import {
|
||||
ActionColumnLabel,
|
||||
useActionColumns,
|
||||
} from "@/app-components/tables/columns/getColumns";
|
||||
import { renderHeader } from "@/app-components/tables/columns/renderHeader";
|
||||
import { DataGrid } from "@/app-components/tables/DataGrid";
|
||||
import { useDelete } from "@/hooks/crud/useDelete";
|
||||
import { useFind } from "@/hooks/crud/useFind";
|
||||
import { useDialog } from "@/hooks/useDialog";
|
||||
import useFormattedFileSize from "@/hooks/useFormattedFileSize";
|
||||
import { useSearch } from "@/hooks/useSearch";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { PageHeader } from "@/layout/content/PageHeader";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
import { TFilterStringFields } from "@/types/search.types";
|
||||
import { getDateTimeFormatter } from "@/utils/date";
|
||||
|
||||
@@ -46,8 +36,6 @@ type MediaLibraryProps = {
|
||||
|
||||
export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
||||
const { t } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const deleteDialogCtl = useDialog<string[]>(false);
|
||||
const formatFileSize = useFormattedFileSize();
|
||||
const { onSearch, searchPayload } = useSearch<IAttachment>({
|
||||
$iLike: ["name"],
|
||||
@@ -77,26 +65,6 @@ export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
||||
},
|
||||
},
|
||||
);
|
||||
const { mutateAsync: deleteCategory } = useDelete(EntityType.ATTACHMENT, {
|
||||
onError: () => {
|
||||
toast.error(t("message.internal_server_error"));
|
||||
},
|
||||
onSuccess: () => {
|
||||
deleteDialogCtl.closeDialog();
|
||||
toast.success(t("message.item_delete_success"));
|
||||
},
|
||||
});
|
||||
const actionColumns = useActionColumns<IAttachment>(
|
||||
EntityType.ATTACHMENT,
|
||||
[
|
||||
{
|
||||
label: ActionColumnLabel.Delete,
|
||||
action: (row) => deleteDialogCtl.openDialog([row.id]),
|
||||
requires: [PermissionAction.DELETE],
|
||||
},
|
||||
],
|
||||
t("label.operations"),
|
||||
);
|
||||
const columns: GridColDef<IAttachment>[] = [
|
||||
{ field: "id", headerName: "ID" },
|
||||
{
|
||||
@@ -169,15 +137,10 @@ export const MediaLibrary = ({ onSelect, accept }: MediaLibraryProps) => {
|
||||
valueGetter: (params) =>
|
||||
t("datetime.updated_at", getDateTimeFormatter(params)),
|
||||
},
|
||||
actionColumns,
|
||||
];
|
||||
|
||||
return (
|
||||
<Grid container gap={3} flexDirection="column">
|
||||
<DeleteDialog
|
||||
{...deleteDialogCtl}
|
||||
callback={deleteCallbackHandler(deleteCategory)}
|
||||
/>
|
||||
<PageHeader title={t("title.media_library")} icon={DriveFolderUploadIcon}>
|
||||
<Grid
|
||||
justifyContent="flex-end"
|
||||
|
||||
@@ -160,6 +160,23 @@ const SettingInput: React.FC<RenderSettingInputProps> = ({
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
} else if (setting.label === "default_storage_helper") {
|
||||
const { onChange, ...rest } = field;
|
||||
|
||||
return (
|
||||
<AutoCompleteEntitySelect<IHelper, "name", false>
|
||||
searchFields={["name"]}
|
||||
entity={EntityType.STORAGE_HELPER}
|
||||
format={Format.BASIC}
|
||||
labelKey="name"
|
||||
idKey="name"
|
||||
label={t("label.default_storage_helper")}
|
||||
helperText={t("help.default_storage_helper")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected, ..._) => onChange(selected?.name)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import {
|
||||
Divider,
|
||||
FormControl,
|
||||
@@ -181,17 +182,19 @@ const ListMessageForm = () => {
|
||||
}}
|
||||
defaultValue={content?.fields?.title}
|
||||
render={({ field }) => {
|
||||
const { onChange, ...rest } = field;
|
||||
const { onChange, value, ...rest } = field;
|
||||
const options = (contentType?.fields || []).filter(
|
||||
({ type }) => ContentFieldType.TEXT === type,
|
||||
);
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<ContentField, "label", false>
|
||||
options={(contentType?.fields || []).filter(
|
||||
({ type }) => ContentFieldType.TEXT === type,
|
||||
)}
|
||||
options={options}
|
||||
idKey="name"
|
||||
labelKey="label"
|
||||
label={t("label.title")}
|
||||
multiple={false}
|
||||
{...(options.length && { value })}
|
||||
{...rest}
|
||||
onChange={(_e, selected) => onChange(selected?.name)}
|
||||
error={!!errors?.options?.["content"]?.fields?.title}
|
||||
@@ -209,20 +212,22 @@ const ListMessageForm = () => {
|
||||
control={control}
|
||||
defaultValue={content?.fields?.subtitle}
|
||||
render={({ field }) => {
|
||||
const { onChange, ...rest } = field;
|
||||
const { onChange, value, ...rest } = field;
|
||||
const options = (contentType?.fields || []).filter(
|
||||
({ type }) =>
|
||||
ContentFieldType.TEXT === type ||
|
||||
ContentFieldType.TEXTAREA === type,
|
||||
);
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<ContentField, "label", false>
|
||||
options={(contentType?.fields || []).filter(
|
||||
({ type }) =>
|
||||
ContentFieldType.TEXT === type ||
|
||||
ContentFieldType.TEXTAREA === type,
|
||||
)}
|
||||
options={options}
|
||||
idKey="name"
|
||||
labelKey="label"
|
||||
label={t("label.subtitle")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected) => onChange(selected?.name)}
|
||||
{...(options.length && { value })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
@@ -235,18 +240,20 @@ const ListMessageForm = () => {
|
||||
control={control}
|
||||
defaultValue={content?.fields?.image_url}
|
||||
render={({ field }) => {
|
||||
const { onChange, ...rest } = field;
|
||||
const { onChange, value, ...rest } = field;
|
||||
const options = (contentType?.fields || []).filter(({ type }) =>
|
||||
[ContentFieldType.FILE].includes(type),
|
||||
);
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<ContentField, "label", false>
|
||||
options={(contentType?.fields || []).filter(({ type }) =>
|
||||
[ContentFieldType.FILE].includes(type),
|
||||
)}
|
||||
options={options}
|
||||
idKey="name"
|
||||
labelKey="label"
|
||||
label={t("label.image_url")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected) => onChange(selected?.name)}
|
||||
{...(options.length && { value })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
@@ -259,18 +266,20 @@ const ListMessageForm = () => {
|
||||
control={control}
|
||||
defaultValue={content?.fields?.url}
|
||||
render={({ field }) => {
|
||||
const { onChange, ...rest } = field;
|
||||
const { onChange, value, ...rest } = field;
|
||||
const options = (contentType?.fields || []).filter(({ type }) =>
|
||||
[ContentFieldType.URL].includes(type),
|
||||
);
|
||||
|
||||
return (
|
||||
<AutoCompleteSelect<ContentField, "label", false>
|
||||
options={(contentType?.fields || []).filter(({ type }) =>
|
||||
[ContentFieldType.URL].includes(type),
|
||||
)}
|
||||
options={options}
|
||||
idKey="name"
|
||||
labelKey="label"
|
||||
label={t("label.url")}
|
||||
multiple={false}
|
||||
onChange={(_e, selected) => onChange(selected?.name)}
|
||||
{...(options.length && { value })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
82
frontend/src/hooks/useAvailableMenuItems.ts
Normal file
82
frontend/src/hooks/useAvailableMenuItems.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { MenuItem } from "@/layout/VerticalMenu";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
|
||||
import { useHasPermission } from "./useHasPermission";
|
||||
|
||||
/**
|
||||
* Helper function to check permissions for a menu item
|
||||
* @param menuItem - The menu item
|
||||
* @param hasPermission - Callback function
|
||||
* @returns True if hasPermission() is true for all required permissions.
|
||||
*/
|
||||
const isMenuItemAllowed = (
|
||||
menuItem: MenuItem,
|
||||
hasPermission: (entityType: EntityType, action: PermissionAction) => boolean,
|
||||
): boolean => {
|
||||
const requiredPermissions = Object.entries(menuItem.requires || {});
|
||||
|
||||
return (
|
||||
requiredPermissions.length === 0 ||
|
||||
requiredPermissions.every(([entityType, actions]) =>
|
||||
actions.every((action) =>
|
||||
hasPermission(entityType as EntityType, action),
|
||||
),
|
||||
)
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Filters menu items based on user permissions.
|
||||
* @param menuItems - The list of menu items to filter.
|
||||
* @returns A filtered list of menu items that the user is allowed to access.
|
||||
*/
|
||||
const filterMenuItems = (
|
||||
menuItems: MenuItem[],
|
||||
hasPermission: (entityType: EntityType, action: PermissionAction) => boolean,
|
||||
): MenuItem[] => {
|
||||
return menuItems
|
||||
.map((menuItem) => {
|
||||
// Validate top-level menu item without submenu
|
||||
if (
|
||||
menuItem &&
|
||||
!menuItem.submenuItems &&
|
||||
isMenuItemAllowed(menuItem, hasPermission)
|
||||
) {
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
// Recursively process submenu items
|
||||
if (menuItem.submenuItems) {
|
||||
const filteredSubmenuItems = filterMenuItems(
|
||||
menuItem.submenuItems,
|
||||
hasPermission,
|
||||
);
|
||||
|
||||
if (filteredSubmenuItems.length > 0) {
|
||||
return { ...menuItem, submenuItems: filteredSubmenuItems };
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Exclude invalid menu items
|
||||
})
|
||||
.filter((menuItem): menuItem is MenuItem => !!menuItem);
|
||||
};
|
||||
const useAvailableMenuItems = (menuItems: MenuItem[]): MenuItem[] => {
|
||||
const hasPermission = useHasPermission();
|
||||
|
||||
return useMemo(() => {
|
||||
return filterMenuItems(menuItems, hasPermission);
|
||||
}, [menuItems, hasPermission]);
|
||||
};
|
||||
|
||||
export default useAvailableMenuItems;
|
||||
@@ -1,11 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { useCallback, useContext } from "react";
|
||||
|
||||
import { PermissionContext } from "@/contexts/permission.context";
|
||||
@@ -18,7 +19,7 @@ export const useHasPermission = () => {
|
||||
(type: EntityType, action: PermissionAction) => {
|
||||
const allowedActions = getAllowedActions(type);
|
||||
|
||||
return allowedActions?.includes(action);
|
||||
return !!allowedActions && allowedActions?.includes(action);
|
||||
},
|
||||
[getAllowedActions],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@@ -32,14 +32,13 @@ import { CSSObject, Grid, IconButton, styled, Theme } from "@mui/material";
|
||||
import MuiDrawer from "@mui/material/Drawer";
|
||||
import { OverridableComponent } from "@mui/material/OverridableComponent";
|
||||
import { useRouter } from "next/router";
|
||||
import { FC, useMemo } from "react";
|
||||
import { FC } from "react";
|
||||
|
||||
import { HexabotLogo } from "@/app-components/logos/HexabotLogo";
|
||||
import { Sidebar } from "@/app-components/menus/Sidebar";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import useAvailableMenuItems from "@/hooks/useAvailableMenuItems";
|
||||
import { useConfig } from "@/hooks/useConfig";
|
||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
import { getLayout } from "@/utils/laylout";
|
||||
@@ -206,7 +205,6 @@ const getMenuItems = (ssoEnabled: boolean): MenuItem[] => [
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// {
|
||||
// text: 'menu.broadcast',
|
||||
// href: "/subscribers/broadcast",
|
||||
@@ -287,25 +285,10 @@ export const VerticalMenu: FC<VerticalMenuProps> = ({
|
||||
onToggleOut,
|
||||
}) => {
|
||||
const { ssoEnabled } = useConfig();
|
||||
const { t } = useTranslate();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const router = useRouter();
|
||||
const hasPermission = useHasPermission();
|
||||
const menuItems = getMenuItems(ssoEnabled);
|
||||
// Filter menu item to which user is allowed access
|
||||
const availableMenuItems = useMemo(() => {
|
||||
return menuItems.filter(({ requires: requiredPermissions }) => {
|
||||
return (
|
||||
!requiredPermissions ||
|
||||
Object.entries(requiredPermissions).every((permission) => {
|
||||
const entityType = permission[0] as EntityType;
|
||||
const actions = permission[1];
|
||||
|
||||
return actions.every((action) => hasPermission(entityType, action));
|
||||
})
|
||||
);
|
||||
});
|
||||
}, [t, hasPermission]);
|
||||
const availableMenuItems = useAvailableMenuItems(menuItems);
|
||||
const hasTemporaryDrawer =
|
||||
getLayout(router.pathname.slice(1)) === "full_width";
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ export const ROUTES = {
|
||||
[EntityType.HELPER]: "/helper",
|
||||
[EntityType.NLU_HELPER]: "/helper/nlu",
|
||||
[EntityType.LLM_HELPER]: "/helper/llm",
|
||||
[EntityType.STORAGE_HELPER]: "/helper/storage",
|
||||
} as const;
|
||||
|
||||
export class ApiClient {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { schema } from "normalizr";
|
||||
|
||||
import { IBaseSchema } from "@/types/base.types";
|
||||
@@ -304,6 +305,15 @@ export const LlmHelperEntity = new schema.Entity(
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
export const StorageHelperEntity = new schema.Entity(
|
||||
EntityType.STORAGE_HELPER,
|
||||
undefined,
|
||||
{
|
||||
idAttribute: ({ name }) => name,
|
||||
},
|
||||
);
|
||||
|
||||
export const ENTITY_MAP = {
|
||||
[EntityType.SUBSCRIBER]: SubscriberEntity,
|
||||
[EntityType.LABEL]: LabelEntity,
|
||||
@@ -333,4 +343,5 @@ export const ENTITY_MAP = {
|
||||
[EntityType.HELPER]: HelperEntity,
|
||||
[EntityType.NLU_HELPER]: NluHelperEntity,
|
||||
[EntityType.LLM_HELPER]: LlmHelperEntity,
|
||||
[EntityType.STORAGE_HELPER]: StorageHelperEntity,
|
||||
} as const;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
* Copyright © 2025 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
|
||||
import { UseMutationOptions } from "react-query";
|
||||
|
||||
export enum EntityType {
|
||||
@@ -38,6 +39,7 @@ export enum EntityType {
|
||||
HELPER = "Helper",
|
||||
NLU_HELPER = "NluHelper",
|
||||
LLM_HELPER = "LlmHelper",
|
||||
STORAGE_HELPER = "StorageHelper",
|
||||
}
|
||||
|
||||
export type NormalizedEntities = Record<string, Record<string, any>>;
|
||||
|
||||
@@ -117,6 +117,7 @@ export const POPULATE_BY_TYPE = {
|
||||
[EntityType.HELPER]: [],
|
||||
[EntityType.NLU_HELPER]: [],
|
||||
[EntityType.LLM_HELPER]: [],
|
||||
[EntityType.STORAGE_HELPER]: [],
|
||||
} as const;
|
||||
|
||||
export type Populate<C extends EntityType> =
|
||||
@@ -208,6 +209,7 @@ export interface IEntityMapTypes {
|
||||
[EntityType.HELPER]: IEntityTypes<IHelperAttributes, IHelper>;
|
||||
[EntityType.NLU_HELPER]: IEntityTypes<IHelperAttributes, IHelper>;
|
||||
[EntityType.LLM_HELPER]: IEntityTypes<IHelperAttributes, IHelper>;
|
||||
[EntityType.STORAGE_HELPER]: IEntityTypes<IHelperAttributes, IHelper>;
|
||||
}
|
||||
|
||||
export type TType<TParam extends keyof IEntityMapTypes> =
|
||||
|
||||
Reference in New Issue
Block a user