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',
|
||||
|
||||
Reference in New Issue
Block a user