mirror of
https://github.com/hexastack/hexabot
synced 2025-01-22 10:35:37 +00:00
Merge pull request #589 from Hexastack/feat/refactor-attachment-helper
feat: refactor attachment storage to use helpers
This commit is contained in:
commit
9c0a358977
@ -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);
|
||||
}
|
||||
}
|
||||
|
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,
|
||||
|
@ -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,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.
|
||||
@ -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,
|
||||
},
|
||||
{
|
||||
|
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."
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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> =
|
||||
|
Loading…
Reference in New Issue
Block a user