mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
Merge branch 'main' into 48-request-context-vars-permanent-option
This commit is contained in:
@@ -32,7 +32,7 @@ import { ChannelModule } from './channel/channel.module';
|
||||
import { ChatModule } from './chat/chat.module';
|
||||
import { CmsModule } from './cms/cms.module';
|
||||
import { config } from './config';
|
||||
import { ExtendedI18nModule } from './extended-18n.module';
|
||||
import { I18nModule } from './i18n/i18n.module';
|
||||
import { LoggerModule } from './logger/logger.module';
|
||||
import { DtoUpdateMiddleware } from './middlewares/dto.update.middleware';
|
||||
import { NlpModule } from './nlp/nlp.module';
|
||||
@@ -44,7 +44,7 @@ import idPlugin from './utils/schema-plugin/id.plugin';
|
||||
import { WebsocketModule } from './websocket/websocket.module';
|
||||
|
||||
const i18nOptions: I18nOptions = {
|
||||
fallbackLanguage: config.chatbot.lang.default,
|
||||
fallbackLanguage: 'en',
|
||||
loaderOptions: {
|
||||
path: path.join(__dirname, '/config/i18n/'),
|
||||
watch: true,
|
||||
@@ -120,7 +120,7 @@ const i18nOptions: I18nOptions = {
|
||||
ignoreErrors: false,
|
||||
}),
|
||||
CsrfModule,
|
||||
ExtendedI18nModule.forRoot(i18nOptions),
|
||||
I18nModule.forRoot(i18nOptions),
|
||||
CacheModule.register({
|
||||
isGlobal: true,
|
||||
ttl: config.cache.ttl,
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ExtendedI18nService } from './extended-i18n.service';
|
||||
import { I18nService } from './i18n/services/i18n.service';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
constructor(private readonly i18n: ExtendedI18nService) {}
|
||||
constructor(private readonly i18n: I18nService) {}
|
||||
|
||||
getHello(): string {
|
||||
return this.i18n.t('welcome', { lang: 'en' });
|
||||
|
||||
@@ -13,6 +13,7 @@ import { THydratedDocument } from 'mongoose';
|
||||
import { FileType } from '@/chat/schemas/types/attachment';
|
||||
import { config } from '@/config';
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { buildURL } from '@/utils/helpers/URL';
|
||||
|
||||
import { MIME_REGEX } from '../utilities';
|
||||
|
||||
@@ -89,7 +90,10 @@ export class Attachment extends BaseSchema {
|
||||
attachmentId: string,
|
||||
attachmentName: string = '',
|
||||
): string {
|
||||
return `${config.parameters.apiUrl}/attachment/download/${attachmentId}/${attachmentName}`;
|
||||
return buildURL(
|
||||
config.parameters.apiUrl,
|
||||
`/attachment/download/${attachmentId}/${attachmentName}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +123,10 @@ export const AttachmentModel: ModelDefinition = {
|
||||
|
||||
AttachmentModel.schema.virtual('url').get(function () {
|
||||
if (this._id && this.name)
|
||||
return `${config.apiPath}/attachment/download/${this._id}/${this.name}`;
|
||||
return buildURL(
|
||||
config.apiPath,
|
||||
`/attachment/download/${this._id}/${this.name}`,
|
||||
);
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
@@ -23,7 +23,6 @@ import { ContextVarController } from './controllers/context-var.controller';
|
||||
import { LabelController } from './controllers/label.controller';
|
||||
import { MessageController } from './controllers/message.controller';
|
||||
import { SubscriberController } from './controllers/subscriber.controller';
|
||||
import { TranslationController } from './controllers/translation.controller';
|
||||
import { BlockRepository } from './repositories/block.repository';
|
||||
import { CategoryRepository } from './repositories/category.repository';
|
||||
import { ContextVarRepository } from './repositories/context-var.repository';
|
||||
@@ -31,7 +30,6 @@ import { ConversationRepository } from './repositories/conversation.repository';
|
||||
import { LabelRepository } from './repositories/label.repository';
|
||||
import { MessageRepository } from './repositories/message.repository';
|
||||
import { SubscriberRepository } from './repositories/subscriber.repository';
|
||||
import { TranslationRepository } from './repositories/translation.repository';
|
||||
import { BlockModel } from './schemas/block.schema';
|
||||
import { CategoryModel } from './schemas/category.schema';
|
||||
import { ContextVarModel } from './schemas/context-var.schema';
|
||||
@@ -39,10 +37,8 @@ import { ConversationModel } from './schemas/conversation.schema';
|
||||
import { LabelModel } from './schemas/label.schema';
|
||||
import { MessageModel } from './schemas/message.schema';
|
||||
import { SubscriberModel } from './schemas/subscriber.schema';
|
||||
import { TranslationModel } from './schemas/translation.schema';
|
||||
import { CategorySeeder } from './seeds/category.seed';
|
||||
import { ContextVarSeeder } from './seeds/context-var.seed';
|
||||
import { TranslationSeeder } from './seeds/translation.seed';
|
||||
import { BlockService } from './services/block.service';
|
||||
import { BotService } from './services/bot.service';
|
||||
import { CategoryService } from './services/category.service';
|
||||
@@ -52,7 +48,6 @@ import { ConversationService } from './services/conversation.service';
|
||||
import { LabelService } from './services/label.service';
|
||||
import { MessageService } from './services/message.service';
|
||||
import { SubscriberService } from './services/subscriber.service';
|
||||
import { TranslationService } from './services/translation.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -63,7 +58,6 @@ import { TranslationService } from './services/translation.service';
|
||||
BlockModel,
|
||||
MessageModel,
|
||||
SubscriberModel,
|
||||
TranslationModel,
|
||||
ConversationModel,
|
||||
SubscriberModel,
|
||||
]),
|
||||
@@ -81,7 +75,6 @@ import { TranslationService } from './services/translation.service';
|
||||
BlockController,
|
||||
MessageController,
|
||||
SubscriberController,
|
||||
TranslationController,
|
||||
],
|
||||
providers: [
|
||||
CategoryRepository,
|
||||
@@ -90,7 +83,6 @@ import { TranslationService } from './services/translation.service';
|
||||
BlockRepository,
|
||||
MessageRepository,
|
||||
SubscriberRepository,
|
||||
TranslationRepository,
|
||||
ConversationRepository,
|
||||
CategoryService,
|
||||
ContextVarService,
|
||||
@@ -98,13 +90,11 @@ import { TranslationService } from './services/translation.service';
|
||||
BlockService,
|
||||
MessageService,
|
||||
SubscriberService,
|
||||
TranslationService,
|
||||
CategorySeeder,
|
||||
ContextVarSeeder,
|
||||
ConversationService,
|
||||
ChatService,
|
||||
BotService,
|
||||
TranslationSeeder,
|
||||
],
|
||||
exports: [SubscriberService, MessageService, LabelService, BlockService],
|
||||
})
|
||||
|
||||
@@ -19,7 +19,10 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ContentRepository } from '@/cms/repositories/content.repository';
|
||||
import { ContentModel } from '@/cms/schemas/content.schema';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -86,6 +89,7 @@ describe('BlockController', () => {
|
||||
UserModel,
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -97,6 +101,7 @@ describe('BlockController', () => {
|
||||
UserRepository,
|
||||
RoleRepository,
|
||||
PermissionRepository,
|
||||
LanguageRepository,
|
||||
BlockService,
|
||||
LabelService,
|
||||
CategoryService,
|
||||
@@ -105,10 +110,11 @@ describe('BlockController', () => {
|
||||
UserService,
|
||||
RoleService,
|
||||
PermissionService,
|
||||
LanguageService,
|
||||
PluginService,
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ContentRepository } from '@/cms/repositories/content.repository';
|
||||
import { ContentModel } from '@/cms/schemas/content.schema';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -77,7 +77,7 @@ describe('CategoryController', () => {
|
||||
},
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ChannelService } from '@/channel/channel.service';
|
||||
import { MenuRepository } from '@/cms/repositories/menu.repository';
|
||||
import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -92,7 +92,7 @@ describe('MessageController', () => {
|
||||
MenuService,
|
||||
MenuRepository,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Injectable, Optional } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpSampleCreateDto } from '@/nlp/dto/nlp-sample.dto';
|
||||
import { NlpSampleState } from '@/nlp/schemas/types';
|
||||
@@ -36,10 +37,13 @@ export class MessageRepository extends BaseRepository<
|
||||
|
||||
private readonly logger: LoggerService;
|
||||
|
||||
private readonly languageService: LanguageService;
|
||||
|
||||
constructor(
|
||||
@InjectModel(Message.name) readonly model: Model<AnyMessage>,
|
||||
@Optional() nlpSampleService?: NlpSampleService,
|
||||
@Optional() logger?: LoggerService,
|
||||
@Optional() languageService?: LanguageService,
|
||||
) {
|
||||
super(
|
||||
model,
|
||||
@@ -49,6 +53,7 @@ export class MessageRepository extends BaseRepository<
|
||||
);
|
||||
this.logger = logger;
|
||||
this.nlpSampleService = nlpSampleService;
|
||||
this.languageService = languageService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,10 +77,13 @@ export class MessageRepository extends BaseRepository<
|
||||
'message' in _doc &&
|
||||
'text' in _doc.message
|
||||
) {
|
||||
const defaultLang = await this.languageService?.getDefaultLanguage();
|
||||
const record: NlpSampleCreateDto = {
|
||||
text: _doc.message.text,
|
||||
type: NlpSampleState.inbox,
|
||||
trained: false,
|
||||
// @TODO : We need to define the language in the message entity
|
||||
language: defaultLang.id,
|
||||
};
|
||||
try {
|
||||
await this.nlpSampleService.findOneOrCreate(record, record);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
@@ -24,11 +25,14 @@ import { ContentTypeModel } from '@/cms/schemas/content-type.schema';
|
||||
import { Content, ContentModel } from '@/cms/schemas/content.schema';
|
||||
import { ContentTypeService } from '@/cms/services/content-type.service';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import OfflineHandler from '@/extensions/channels/offline/index.channel';
|
||||
import { OFFLINE_CHANNEL_NAME } from '@/extensions/channels/offline/settings';
|
||||
import { Offline } from '@/extensions/channels/offline/types';
|
||||
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { Settings } from '@/setting/schemas/types';
|
||||
@@ -94,6 +98,7 @@ describe('BlockService', () => {
|
||||
ContentModel,
|
||||
AttachmentModel,
|
||||
LabelModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -102,18 +107,20 @@ describe('BlockService', () => {
|
||||
ContentTypeRepository,
|
||||
ContentRepository,
|
||||
AttachmentRepository,
|
||||
LanguageRepository,
|
||||
BlockService,
|
||||
CategoryService,
|
||||
ContentTypeService,
|
||||
ContentService,
|
||||
AttachmentService,
|
||||
LanguageService,
|
||||
{
|
||||
provide: PluginService,
|
||||
useValue: {},
|
||||
},
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => {
|
||||
return t === 'Welcome' ? 'Bienvenue' : t;
|
||||
@@ -132,6 +139,14 @@ describe('BlockService', () => {
|
||||
},
|
||||
},
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
blockService = module.get<BlockService>(BlockService);
|
||||
|
||||
@@ -13,7 +13,8 @@ import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { Nlp } from '@/nlp/lib/types';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
@@ -44,7 +45,8 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
private readonly settingService: SettingService,
|
||||
private readonly pluginService: PluginService,
|
||||
private readonly logger: LoggerService,
|
||||
protected readonly i18n: ExtendedI18nService,
|
||||
protected readonly i18n: I18nService,
|
||||
protected readonly languageService: LanguageService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
@@ -109,12 +111,9 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
// Check & catch user language through NLP
|
||||
const nlp = event.getNLP();
|
||||
if (nlp) {
|
||||
const settings = await this.settingService.getSettings();
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const lang = nlp.entities.find((e) => e.entity === 'language');
|
||||
if (
|
||||
lang &&
|
||||
settings.nlp_settings.languages.indexOf(lang.value) !== -1
|
||||
) {
|
||||
if (lang && Object.keys(languages).indexOf(lang.value) !== -1) {
|
||||
const profile = event.getSender();
|
||||
profile.language = lang.value;
|
||||
event.setSender(profile);
|
||||
@@ -372,12 +371,11 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
subscriberContext: SubscriberContext,
|
||||
settings: Settings,
|
||||
): string {
|
||||
const lang =
|
||||
context && context.user && context.user.language
|
||||
? context.user.language
|
||||
: settings.nlp_settings.default_lang;
|
||||
// Translate
|
||||
text = this.i18n.t(text, { lang, defaultValue: text });
|
||||
text = this.i18n.t(text, {
|
||||
lang: context.user.language,
|
||||
defaultValue: text,
|
||||
});
|
||||
// Replace context tokens
|
||||
text = this.processTokenReplacements(
|
||||
text,
|
||||
|
||||
@@ -25,10 +25,13 @@ import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { ContentTypeService } from '@/cms/services/content-type.service';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { offlineEventText } from '@/extensions/channels/offline/__test__/events.mock';
|
||||
import OfflineHandler from '@/extensions/channels/offline/index.channel';
|
||||
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpEntityRepository } from '@/nlp/repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '@/nlp/repositories/nlp-sample-entity.repository';
|
||||
@@ -111,6 +114,7 @@ describe('BlockService', () => {
|
||||
NlpSampleEntityModel,
|
||||
NlpSampleModel,
|
||||
ContextVarModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -130,6 +134,7 @@ describe('BlockService', () => {
|
||||
NlpEntityRepository,
|
||||
NlpSampleEntityRepository,
|
||||
NlpSampleRepository,
|
||||
LanguageRepository,
|
||||
BlockService,
|
||||
CategoryService,
|
||||
ContentTypeService,
|
||||
@@ -149,13 +154,14 @@ describe('BlockService', () => {
|
||||
NlpService,
|
||||
ContextVarService,
|
||||
ContextVarRepository,
|
||||
LanguageService,
|
||||
{
|
||||
provide: PluginService,
|
||||
useValue: {},
|
||||
},
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ export const config: Config = {
|
||||
translationFilename: process.env.I18N_TRANSLATION_FILENAME || 'messages',
|
||||
},
|
||||
appPath: process.cwd(),
|
||||
apiPath: process.env.API_ORIGIN,
|
||||
apiPath: process.env.API_ORIGIN || 'http://localhost:4000',
|
||||
frontendPath: process.env.FRONTEND_ORIGIN
|
||||
? process.env.FRONTEND_ORIGIN.split(',')[0]
|
||||
: 'http://localhost:8080',
|
||||
@@ -120,10 +120,6 @@ export const config: Config = {
|
||||
limit: 10,
|
||||
},
|
||||
chatbot: {
|
||||
lang: {
|
||||
default: 'en',
|
||||
available: ['en', 'fr'],
|
||||
},
|
||||
messages: {
|
||||
track_delivery: false,
|
||||
track_read: false,
|
||||
|
||||
@@ -15,7 +15,6 @@ type TJwtOptions = {
|
||||
secret: string;
|
||||
expiresIn: string;
|
||||
};
|
||||
type TLanguage = 'en' | 'fr' | 'ar' | 'tn';
|
||||
type TMethods = 'GET' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD';
|
||||
type TLogLevel = 'log' | 'fatal' | 'error' | 'warn' | 'debug' | 'verbose';
|
||||
type TCacheConfig = {
|
||||
@@ -87,10 +86,6 @@ export type Config = {
|
||||
limit: number;
|
||||
};
|
||||
chatbot: {
|
||||
lang: {
|
||||
default: TLanguage;
|
||||
available: TLanguage[];
|
||||
};
|
||||
messages: {
|
||||
track_delivery: boolean;
|
||||
track_read: boolean;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { DynamicModule, Global, Inject, Module } from '@nestjs/common';
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
import {
|
||||
I18N_OPTIONS,
|
||||
I18N_TRANSLATIONS,
|
||||
I18nModule,
|
||||
I18nOptions,
|
||||
I18nTranslation,
|
||||
} from 'nestjs-i18n';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ExtendedI18nService } from './extended-i18n.service';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class ExtendedI18nModule extends I18nModule {
|
||||
constructor(
|
||||
i18n: ExtendedI18nService,
|
||||
@Inject(I18N_TRANSLATIONS)
|
||||
translations: Observable<I18nTranslation>,
|
||||
@Inject(I18N_OPTIONS) i18nOptions: I18nOptions,
|
||||
adapter: HttpAdapterHost,
|
||||
) {
|
||||
super(i18n, translations, i18nOptions, adapter);
|
||||
}
|
||||
|
||||
static forRoot(options: I18nOptions): DynamicModule {
|
||||
const { providers, exports } = super.forRoot(options);
|
||||
return {
|
||||
module: ExtendedI18nModule,
|
||||
providers: providers.concat(ExtendedI18nService),
|
||||
exports: exports.concat(ExtendedI18nService),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import { ChannelService } from '@/channel/channel.service';
|
||||
import { MessageService } from '@/chat/services/message.service';
|
||||
import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
@@ -38,7 +38,7 @@ export default class LiveChatTesterHandler extends OfflineHandler {
|
||||
nlpService: NlpService,
|
||||
logger: LoggerService,
|
||||
eventEmitter: EventEmitter2,
|
||||
i18n: ExtendedI18nService,
|
||||
i18n: I18nService,
|
||||
subscriberService: SubscriberService,
|
||||
attachmentService: AttachmentService,
|
||||
messageService: MessageService,
|
||||
|
||||
@@ -35,7 +35,7 @@ import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { MenuRepository } from '@/cms/repositories/menu.repository';
|
||||
import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -113,7 +113,7 @@ describe('Offline Handler', () => {
|
||||
EventEmitter2,
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { MenuRepository } from '@/cms/repositories/menu.repository';
|
||||
import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
@@ -90,7 +90,7 @@ describe(`Offline event wrapper`, () => {
|
||||
EventEmitter2,
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { Content } from '@/cms/schemas/content.schema';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
@@ -73,7 +73,7 @@ export default class OfflineHandler extends ChannelHandler {
|
||||
nlpService: NlpService,
|
||||
logger: LoggerService,
|
||||
protected readonly eventEmitter: EventEmitter2,
|
||||
protected readonly i18n: ExtendedI18nService,
|
||||
protected readonly i18n: I18nService,
|
||||
protected readonly subscriberService: SubscriberService,
|
||||
protected readonly attachmentService: AttachmentService,
|
||||
protected readonly messageService: MessageService,
|
||||
@@ -477,7 +477,7 @@ export default class OfflineHandler extends ChannelHandler {
|
||||
...channelData,
|
||||
name: this.getChannel(),
|
||||
},
|
||||
language: config.chatbot.lang.default,
|
||||
language: '',
|
||||
locale: '',
|
||||
timezone: 0,
|
||||
gender: 'male',
|
||||
|
||||
@@ -29,6 +29,13 @@ export const baseNlpEntity = {
|
||||
builtin: true,
|
||||
};
|
||||
|
||||
export const baseLanguage = {
|
||||
...modelInstance,
|
||||
title: 'English',
|
||||
code: 'en',
|
||||
isDefault: true,
|
||||
};
|
||||
|
||||
export const entitiesMock: NlpEntityFull[] = [
|
||||
{
|
||||
...baseNlpEntity,
|
||||
@@ -89,6 +96,7 @@ export const samplesMock: NlpSampleFull[] = [
|
||||
],
|
||||
trained: false,
|
||||
type: NlpSampleState.train,
|
||||
language: baseLanguage,
|
||||
},
|
||||
{
|
||||
...modelInstance,
|
||||
@@ -112,5 +120,6 @@ export const samplesMock: NlpSampleFull[] = [
|
||||
],
|
||||
trained: false,
|
||||
type: NlpSampleState.train,
|
||||
language: baseLanguage,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -23,6 +23,10 @@ export const nlpEmptyFormated: DatasetType = {
|
||||
name: 'product',
|
||||
elements: ['pizza', 'sandwich'],
|
||||
},
|
||||
{
|
||||
elements: ['en', 'fr'],
|
||||
name: 'language',
|
||||
},
|
||||
],
|
||||
entity_synonyms: [
|
||||
{
|
||||
@@ -34,17 +38,33 @@ export const nlpEmptyFormated: DatasetType = {
|
||||
|
||||
export const nlpFormatted: DatasetType = {
|
||||
common_examples: [
|
||||
{ text: 'Hello', intent: 'greeting', entities: [] },
|
||||
{
|
||||
text: 'Hello',
|
||||
intent: 'greeting',
|
||||
entities: [
|
||||
{
|
||||
entity: 'language',
|
||||
value: 'en',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'i want to order a pizza',
|
||||
intent: 'order',
|
||||
entities: [{ entity: 'product', value: 'pizza', start: 19, end: 23 }],
|
||||
entities: [
|
||||
{ entity: 'product', value: 'pizza', start: 19, end: 23 },
|
||||
{
|
||||
entity: 'language',
|
||||
value: 'en',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
regex_features: [],
|
||||
lookup_tables: [
|
||||
{ name: 'intent', elements: ['greeting', 'order'] },
|
||||
{ name: 'product', elements: ['pizza', 'sandwich'] },
|
||||
{ name: 'language', elements: ['en', 'fr'] },
|
||||
],
|
||||
entity_synonyms: [
|
||||
{
|
||||
|
||||
@@ -8,10 +8,14 @@
|
||||
*/
|
||||
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpEntityRepository } from '@/nlp/repositories/nlp-entity.repository';
|
||||
import { NlpSampleEntityRepository } from '@/nlp/repositories/nlp-sample-entity.repository';
|
||||
@@ -56,26 +60,11 @@ describe('NLP Default Helper', () => {
|
||||
NlpValueModel,
|
||||
NlpSampleModel,
|
||||
NlpSampleEntityModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
HttpModule,
|
||||
],
|
||||
providers: [
|
||||
LoggerService,
|
||||
{
|
||||
provide: SettingService,
|
||||
useValue: {
|
||||
getSettings: jest.fn(() => ({
|
||||
nlp_settings: {
|
||||
provider: 'default',
|
||||
endpoint: 'path',
|
||||
token: 'token',
|
||||
languages: ['fr', 'ar', 'tn'],
|
||||
default_lang: 'fr',
|
||||
threshold: '0.5',
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
NlpService,
|
||||
NlpSampleService,
|
||||
NlpSampleRepository,
|
||||
@@ -85,8 +74,32 @@ describe('NLP Default Helper', () => {
|
||||
NlpValueRepository,
|
||||
NlpSampleEntityService,
|
||||
NlpSampleEntityRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
EventEmitter2,
|
||||
DefaultNlpHelper,
|
||||
LoggerService,
|
||||
{
|
||||
provide: SettingService,
|
||||
useValue: {
|
||||
getSettings: jest.fn(() => ({
|
||||
nlp_settings: {
|
||||
provider: 'default',
|
||||
endpoint: 'path',
|
||||
token: 'token',
|
||||
threshold: '0.5',
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
settingService = module.get<SettingService>(SettingService);
|
||||
@@ -103,15 +116,15 @@ describe('NLP Default Helper', () => {
|
||||
expect(nlp).toBeDefined();
|
||||
});
|
||||
|
||||
it('should format empty training set properly', () => {
|
||||
it('should format empty training set properly', async () => {
|
||||
const nlp = nlpService.getNLP();
|
||||
const results = nlp.format([], entitiesMock);
|
||||
const results = await nlp.format([], entitiesMock);
|
||||
expect(results).toEqual(nlpEmptyFormated);
|
||||
});
|
||||
|
||||
it('should format training set properly', () => {
|
||||
it('should format training set properly', async () => {
|
||||
const nlp = nlpService.getNLP();
|
||||
const results = nlp.format(samplesMock, entitiesMock);
|
||||
const results = await nlp.format(samplesMock, entitiesMock);
|
||||
expect(results).toEqual(nlpFormatted);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,21 +13,14 @@ import { Injectable } from '@nestjs/common';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import BaseNlpHelper from '@/nlp/lib/BaseNlpHelper';
|
||||
import { Nlp } from '@/nlp/lib/types';
|
||||
import { NlpEntity, NlpEntityFull } from '@/nlp/schemas/nlp-entity.schema';
|
||||
import { NlpEntityFull } from '@/nlp/schemas/nlp-entity.schema';
|
||||
import { NlpSampleFull } from '@/nlp/schemas/nlp-sample.schema';
|
||||
import { NlpValue } from '@/nlp/schemas/nlp-value.schema';
|
||||
import { NlpEntityService } from '@/nlp/services/nlp-entity.service';
|
||||
import { NlpSampleService } from '@/nlp/services/nlp-sample.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { buildURL } from '@/utils/helpers/URL';
|
||||
|
||||
import {
|
||||
CommonExample,
|
||||
DatasetType,
|
||||
EntitySynonym,
|
||||
ExampleEntity,
|
||||
LookupTable,
|
||||
NlpParseResultType,
|
||||
} from './types';
|
||||
import { DatasetType, NlpParseResultType } from './types';
|
||||
|
||||
@Injectable()
|
||||
export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
@@ -61,69 +54,16 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
* @param entities - All available entities
|
||||
* @returns {DatasetType} - The formatted RASA training set
|
||||
*/
|
||||
format(samples: NlpSampleFull[], entities: NlpEntityFull[]): DatasetType {
|
||||
const entityMap = NlpEntity.getEntityMap(entities);
|
||||
const valueMap = NlpValue.getValueMap(
|
||||
NlpValue.getValuesFromEntities(entities),
|
||||
async format(
|
||||
samples: NlpSampleFull[],
|
||||
entities: NlpEntityFull[],
|
||||
): Promise<DatasetType> {
|
||||
const nluData = await this.nlpSampleService.formatRasaNlu(
|
||||
samples,
|
||||
entities,
|
||||
);
|
||||
|
||||
const common_examples: CommonExample[] = samples
|
||||
.filter((s) => s.entities.length > 0)
|
||||
.map((s) => {
|
||||
const intent = s.entities.find(
|
||||
(e) => entityMap[e.entity].name === 'intent',
|
||||
);
|
||||
if (!intent) {
|
||||
throw new Error('Unable to find the `intent` nlp entity.');
|
||||
}
|
||||
const sampleEntities: ExampleEntity[] = s.entities
|
||||
.filter((e) => entityMap[<string>e.entity].name !== 'intent')
|
||||
.map((e) => {
|
||||
const res: ExampleEntity = {
|
||||
entity: entityMap[<string>e.entity].name,
|
||||
value: valueMap[<string>e.value].value,
|
||||
};
|
||||
if ('start' in e && 'end' in e) {
|
||||
Object.assign(res, {
|
||||
start: e.start,
|
||||
end: e.end,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
});
|
||||
return {
|
||||
text: s.text,
|
||||
intent: valueMap[intent.value].value,
|
||||
entities: sampleEntities,
|
||||
};
|
||||
});
|
||||
const lookup_tables: LookupTable[] = entities.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
elements: e.values.map((v) => {
|
||||
return v.value;
|
||||
}),
|
||||
};
|
||||
});
|
||||
const entity_synonyms = entities
|
||||
.reduce((acc, e) => {
|
||||
const synonyms = e.values.map((v) => {
|
||||
return {
|
||||
value: v.value,
|
||||
synonyms: v.expressions,
|
||||
};
|
||||
});
|
||||
return acc.concat(synonyms);
|
||||
}, [] as EntitySynonym[])
|
||||
.filter((s) => {
|
||||
return s.synonyms.length > 0;
|
||||
});
|
||||
return {
|
||||
common_examples,
|
||||
regex_features: [],
|
||||
lookup_tables,
|
||||
entity_synonyms,
|
||||
};
|
||||
return nluData;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,10 +78,10 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
entities: NlpEntityFull[],
|
||||
): Promise<any> {
|
||||
const self = this;
|
||||
const nluData: DatasetType = self.format(samples, entities);
|
||||
const nluData: DatasetType = await self.format(samples, entities);
|
||||
// Train samples
|
||||
const result = await this.httpService.axiosRef.post(
|
||||
`${this.settings.endpoint}/train`,
|
||||
buildURL(this.settings.endpoint, `/train`),
|
||||
nluData,
|
||||
{
|
||||
params: {
|
||||
@@ -169,10 +109,10 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
entities: NlpEntityFull[],
|
||||
): Promise<any> {
|
||||
const self = this;
|
||||
const nluTestData: DatasetType = self.format(samples, entities);
|
||||
const nluTestData: DatasetType = await self.format(samples, entities);
|
||||
// Evaluate model with test samples
|
||||
return await this.httpService.axiosRef.post(
|
||||
`${this.settings.endpoint}/evaluate`,
|
||||
buildURL(this.settings.endpoint, `/evaluate`),
|
||||
nluTestData,
|
||||
{
|
||||
params: {
|
||||
@@ -251,7 +191,7 @@ export default class DefaultNlpHelper extends BaseNlpHelper {
|
||||
try {
|
||||
const { data: nlp } =
|
||||
await this.httpService.axiosRef.post<NlpParseResultType>(
|
||||
`${this.settings.endpoint}/parse`,
|
||||
buildURL(this.settings.endpoint, '/parse'),
|
||||
{
|
||||
q: text,
|
||||
project,
|
||||
|
||||
181
api/src/i18n/controllers/language.controller.spec.ts
Normal file
181
api/src/i18n/controllers/language.controller.spec.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
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 {
|
||||
installLanguageFixtures,
|
||||
languageFixtures,
|
||||
} from '@/utils/test/fixtures/language';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { LanguageController } from './language.controller';
|
||||
import { LanguageUpdateDto } from '../dto/language.dto';
|
||||
import { LanguageRepository } from '../repositories/language.repository';
|
||||
import { Language, LanguageModel } from '../schemas/language.schema';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
|
||||
describe('LanguageController', () => {
|
||||
let languageController: LanguageController;
|
||||
let languageService: LanguageService;
|
||||
let language: Language;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installLanguageFixtures),
|
||||
MongooseModule.forFeature([LanguageModel]),
|
||||
],
|
||||
providers: [
|
||||
LanguageController,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
initDynamicLanguages: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
LoggerService,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
languageService = module.get<LanguageService>(LanguageService);
|
||||
languageController = module.get<LanguageController>(LanguageController);
|
||||
language = await languageService.findOne({ code: 'en' });
|
||||
});
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
afterAll(closeInMongodConnection);
|
||||
|
||||
describe('count', () => {
|
||||
it('should count languages', async () => {
|
||||
jest.spyOn(languageService, 'count');
|
||||
const result = await languageController.filterCount();
|
||||
|
||||
expect(languageService.count).toHaveBeenCalled();
|
||||
expect(result).toEqual({ count: languageFixtures.length });
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('should find one translation by id', async () => {
|
||||
jest.spyOn(languageService, 'findOne');
|
||||
const result = await languageController.findOne(language.id);
|
||||
|
||||
expect(languageService.findOne).toHaveBeenCalledWith(language.id);
|
||||
expect(result).toEqualPayload(
|
||||
languageFixtures.find(({ code }) => code === language.code),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPage', () => {
|
||||
const pageQuery = getPageQuery<Language>({ sort: ['code', 'asc'] });
|
||||
it('should find languages', async () => {
|
||||
jest.spyOn(languageService, 'findPage');
|
||||
const result = await languageController.findPage(pageQuery, {});
|
||||
|
||||
expect(languageService.findPage).toHaveBeenCalledWith({}, pageQuery);
|
||||
expect(result).toEqualPayload(
|
||||
languageFixtures.sort(({ code: codeA }, { code: codeB }) => {
|
||||
if (codeA < codeB) {
|
||||
return -1;
|
||||
}
|
||||
if (codeA > codeB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
const translationUpdateDto: LanguageUpdateDto = {
|
||||
title: 'English (US)',
|
||||
};
|
||||
it('should update one language by id', async () => {
|
||||
jest.spyOn(languageService, 'updateOne');
|
||||
const result = await languageController.updateOne(
|
||||
language.id,
|
||||
translationUpdateDto,
|
||||
);
|
||||
|
||||
expect(languageService.updateOne).toHaveBeenCalledWith(
|
||||
language.id,
|
||||
translationUpdateDto,
|
||||
);
|
||||
expect(result).toEqualPayload({
|
||||
...languageFixtures.find(({ code }) => code === language.code),
|
||||
...translationUpdateDto,
|
||||
});
|
||||
});
|
||||
|
||||
it('should mark a language as default', async () => {
|
||||
jest.spyOn(languageService, 'updateOne');
|
||||
const translationUpdateDto = { isDefault: true };
|
||||
const frLang = await languageService.findOne({ code: 'fr' });
|
||||
const result = await languageController.updateOne(
|
||||
frLang.id,
|
||||
translationUpdateDto,
|
||||
);
|
||||
|
||||
expect(languageService.updateOne).toHaveBeenCalledWith(
|
||||
frLang.id,
|
||||
translationUpdateDto,
|
||||
);
|
||||
expect(result).toEqualPayload({
|
||||
...languageFixtures.find(({ code }) => code === frLang.code),
|
||||
...translationUpdateDto,
|
||||
});
|
||||
|
||||
const enLang = await languageService.findOne({ code: 'en' });
|
||||
expect(enLang.isDefault).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw a NotFoundException when attempting to update a translation by id', async () => {
|
||||
jest.spyOn(languageService, 'updateOne');
|
||||
await expect(
|
||||
languageController.updateOne(NOT_FOUND_ID, translationUpdateDto),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should throw when attempting to delete the default language', async () => {
|
||||
const defaultLang = await languageService.findOne({ isDefault: true });
|
||||
|
||||
await expect(
|
||||
languageController.deleteOne(defaultLang.id),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
});
|
||||
154
api/src/i18n/controllers/language.controller.ts
Normal file
154
api/src/i18n/controllers/language.controller.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
import { DeleteResult } from '@/utils/generics/base-repository';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
||||
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { LanguageCreateDto, LanguageUpdateDto } from '../dto/language.dto';
|
||||
import { Language } from '../schemas/language.schema';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('language')
|
||||
export class LanguageController extends BaseController<Language> {
|
||||
constructor(
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly logger: LoggerService,
|
||||
) {
|
||||
super(languageService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a paginated list of categories based on provided filters and pagination settings.
|
||||
* @param pageQuery - The pagination settings.
|
||||
* @param filters - The filters to apply to the language search.
|
||||
* @returns A Promise that resolves to a paginated list of categories.
|
||||
*/
|
||||
@Get()
|
||||
async findPage(
|
||||
@Query(PageQueryPipe) pageQuery: PageQueryDto<Language>,
|
||||
@Query(new SearchFilterPipe<Language>({ allowedFields: ['title', 'code'] }))
|
||||
filters: TFilterQuery<Language>,
|
||||
) {
|
||||
return await this.languageService.findPage(filters, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the filtered number of categories.
|
||||
* @returns A promise that resolves to an object representing the filtered number of categories.
|
||||
*/
|
||||
@Get('count')
|
||||
async filterCount(
|
||||
@Query(
|
||||
new SearchFilterPipe<Language>({
|
||||
allowedFields: ['title', 'code'],
|
||||
}),
|
||||
)
|
||||
filters?: TFilterQuery<Language>,
|
||||
) {
|
||||
return await this.count(filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a language by its ID.
|
||||
* @param id - The ID of the language to find.
|
||||
* @returns A Promise that resolves to the found language.
|
||||
*/
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string): Promise<Language> {
|
||||
const doc = await this.languageService.findOne(id);
|
||||
if (!doc) {
|
||||
this.logger.warn(`Unable to find Language by id ${id}`);
|
||||
throw new NotFoundException(`Language with ID ${id} not found`);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new language.
|
||||
* @param language - The data of the language to be created.
|
||||
* @returns A Promise that resolves to the created language.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Post()
|
||||
async create(@Body() language: LanguageCreateDto): Promise<Language> {
|
||||
return await this.languageService.create(language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing language.
|
||||
* @param id - The ID of the language to be updated.
|
||||
* @param languageUpdate - The updated data for the language.
|
||||
* @returns A Promise that resolves to the updated language.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Patch(':id')
|
||||
async updateOne(
|
||||
@Param('id') id: string,
|
||||
@Body() languageUpdate: LanguageUpdateDto,
|
||||
): Promise<Language> {
|
||||
if ('isDefault' in languageUpdate) {
|
||||
if (languageUpdate.isDefault) {
|
||||
// A new default language is define, make sure that only one is marked as default
|
||||
await this.languageService.updateMany({}, { isDefault: false });
|
||||
} else {
|
||||
throw new BadRequestException('Should not be able to disable default');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a language by its ID.
|
||||
* @param id - The ID of the language to be deleted.
|
||||
* @returns A Promise that resolves to the deletion result.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async deleteOne(@Param('id') id: string): Promise<DeleteResult> {
|
||||
const result = await this.languageService.deleteOne({
|
||||
isDefault: false, // Prevent deleting the default language
|
||||
_id: id,
|
||||
});
|
||||
if (result.deletedCount === 0) {
|
||||
this.logger.warn(`Unable to delete Language by id ${id}`);
|
||||
throw new BadRequestException(`Unable to delete Language with ID ${id}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,23 @@ import { AttachmentRepository } from '@/attachment/repositories/attachment.repos
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ChannelService } from '@/channel/channel.service';
|
||||
import { MessageController } from '@/chat/controllers/message.controller';
|
||||
import { BlockRepository } from '@/chat/repositories/block.repository';
|
||||
import { MessageRepository } from '@/chat/repositories/message.repository';
|
||||
import { SubscriberRepository } from '@/chat/repositories/subscriber.repository';
|
||||
import { BlockModel } from '@/chat/schemas/block.schema';
|
||||
import { MessageModel } from '@/chat/schemas/message.schema';
|
||||
import { SubscriberModel } from '@/chat/schemas/subscriber.schema';
|
||||
import { BlockService } from '@/chat/services/block.service';
|
||||
import { MessageService } from '@/chat/services/message.service';
|
||||
import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { ContentRepository } from '@/cms/repositories/content.repository';
|
||||
import { MenuRepository } from '@/cms/repositories/menu.repository';
|
||||
import { ContentModel } from '@/cms/schemas/content.schema';
|
||||
import { MenuModel } from '@/cms/schemas/menu.schema';
|
||||
import { ContentService } from '@/cms/services/content.service';
|
||||
import { MenuService } from '@/cms/services/menu.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { NlpService } from '@/nlp/services/nlp.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
@@ -39,20 +49,13 @@ import {
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { MessageController } from './message.controller';
|
||||
import { TranslationController } from './translation.controller';
|
||||
import { TranslationUpdateDto } from '../dto/translation.dto';
|
||||
import { BlockRepository } from '../repositories/block.repository';
|
||||
import { MessageRepository } from '../repositories/message.repository';
|
||||
import { SubscriberRepository } from '../repositories/subscriber.repository';
|
||||
import { LanguageRepository } from '../repositories/language.repository';
|
||||
import { TranslationRepository } from '../repositories/translation.repository';
|
||||
import { BlockModel } from '../schemas/block.schema';
|
||||
import { MessageModel } from '../schemas/message.schema';
|
||||
import { SubscriberModel } from '../schemas/subscriber.schema';
|
||||
import { LanguageModel } from '../schemas/language.schema';
|
||||
import { Translation, TranslationModel } from '../schemas/translation.schema';
|
||||
import { BlockService } from '../services/block.service';
|
||||
import { MessageService } from '../services/message.service';
|
||||
import { SubscriberService } from '../services/subscriber.service';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
describe('TranslationController', () => {
|
||||
@@ -73,6 +76,7 @@ describe('TranslationController', () => {
|
||||
MenuModel,
|
||||
BlockModel,
|
||||
ContentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -114,10 +118,10 @@ describe('TranslationController', () => {
|
||||
EventEmitter2,
|
||||
LoggerService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
initDynamicTranslations: jest.fn(),
|
||||
refreshDynamicTranslations: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -129,6 +133,8 @@ describe('TranslationController', () => {
|
||||
},
|
||||
},
|
||||
LoggerService,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
],
|
||||
}).compile();
|
||||
translationService = module.get<TranslationService>(TranslationService);
|
||||
@@ -8,15 +8,18 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
@@ -25,18 +28,21 @@ import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
import { DeleteResult } from '@/utils/generics/base-repository';
|
||||
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
||||
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { TranslationUpdateDto } from '../dto/translation.dto';
|
||||
import { Translation } from '../schemas/translation.schema';
|
||||
import { LanguageService } from '../services/language.service';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('translation')
|
||||
export class TranslationController extends BaseController<Translation> {
|
||||
constructor(
|
||||
private readonly languageService: LanguageService,
|
||||
private readonly translationService: TranslationService,
|
||||
private readonly settingService: SettingService,
|
||||
private readonly logger: LoggerService,
|
||||
@@ -103,40 +109,56 @@ export class TranslationController extends BaseController<Translation> {
|
||||
@CsrfCheck(true)
|
||||
@Post('refresh')
|
||||
async refresh(): Promise<any> {
|
||||
const settings = await this.settingService.getSettings();
|
||||
const languages = settings.nlp_settings.languages;
|
||||
const defaultTrans: Translation['translations'] = languages.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr] = '';
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const defaultTrans: Translation['translations'] = Object.keys(languages)
|
||||
.filter((lang) => lang !== defaultLanguage.code)
|
||||
.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr] = '';
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: string },
|
||||
);
|
||||
// Scan Blocks
|
||||
return this.translationService
|
||||
.getAllBlockStrings()
|
||||
.then(async (strings: string[]) => {
|
||||
const settingStrings =
|
||||
await this.translationService.getSettingStrings();
|
||||
// Scan global settings
|
||||
strings = strings.concat(settingStrings);
|
||||
// Filter unique and not empty messages
|
||||
strings = strings.filter((str, pos) => {
|
||||
return str && strings.indexOf(str) == pos;
|
||||
});
|
||||
// Perform refresh
|
||||
const queue = strings.map((str) =>
|
||||
this.translationService.findOneOrCreate(
|
||||
{ str },
|
||||
{ str, translations: defaultTrans as any, translated: 100 },
|
||||
),
|
||||
);
|
||||
return Promise.all(queue).then(() => {
|
||||
// Purge non existing translations
|
||||
return this.translationService.deleteMany({
|
||||
str: { $nin: strings },
|
||||
});
|
||||
});
|
||||
});
|
||||
let strings = await this.translationService.getAllBlockStrings();
|
||||
const settingStrings = await this.translationService.getSettingStrings();
|
||||
// Scan global settings
|
||||
strings = strings.concat(settingStrings);
|
||||
// Filter unique and not empty messages
|
||||
strings = strings.filter((str, pos) => {
|
||||
return str && strings.indexOf(str) == pos;
|
||||
});
|
||||
// Perform refresh
|
||||
const queue = strings.map((str) =>
|
||||
this.translationService.findOneOrCreate(
|
||||
{ str },
|
||||
{ str, translations: defaultTrans },
|
||||
),
|
||||
);
|
||||
await Promise.all(queue);
|
||||
// Purge non existing translations
|
||||
return this.translationService.deleteMany({
|
||||
str: { $nin: strings },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a translation by its ID.
|
||||
* @param id - The ID of the translation to be deleted.
|
||||
* @returns A Promise that resolves to the deletion result.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async deleteOne(@Param('id') id: string): Promise<DeleteResult> {
|
||||
const result = await this.translationService.deleteOne(id);
|
||||
if (result.deletedCount === 0) {
|
||||
this.logger.warn(`Unable to delete Translation by id ${id}`);
|
||||
throw new BadRequestException(
|
||||
`Unable to delete Translation with ID ${id}`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
35
api/src/i18n/dto/language.dto.ts
Normal file
35
api/src/i18n/dto/language.dto.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class LanguageCreateDto {
|
||||
@ApiProperty({ description: 'Language Title', type: String })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: 'Language Code', type: String })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
code: string;
|
||||
|
||||
@ApiProperty({ description: 'Whether Language is RTL', type: Boolean })
|
||||
@IsBoolean()
|
||||
isRTL: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Is Default Language ?', type: Boolean })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export class LanguageUpdateDto extends PartialType(LanguageCreateDto) {}
|
||||
79
api/src/i18n/i18n.module.ts
Normal file
79
api/src/i18n/i18n.module.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import {
|
||||
DynamicModule,
|
||||
forwardRef,
|
||||
Global,
|
||||
Inject,
|
||||
Module,
|
||||
} from '@nestjs/common';
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import {
|
||||
I18N_OPTIONS,
|
||||
I18N_TRANSLATIONS,
|
||||
I18nOptions,
|
||||
I18nTranslation,
|
||||
I18nModule as NativeI18nModule,
|
||||
} from 'nestjs-i18n';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ChatModule } from '@/chat/chat.module';
|
||||
|
||||
import { LanguageController } from './controllers/language.controller';
|
||||
import { TranslationController } from './controllers/translation.controller';
|
||||
import { LanguageRepository } from './repositories/language.repository';
|
||||
import { TranslationRepository } from './repositories/translation.repository';
|
||||
import { LanguageModel } from './schemas/language.schema';
|
||||
import { TranslationModel } from './schemas/translation.schema';
|
||||
import { LanguageSeeder } from './seeds/language.seed';
|
||||
import { TranslationSeeder } from './seeds/translation.seed';
|
||||
import { I18nService } from './services/i18n.service';
|
||||
import { LanguageService } from './services/language.service';
|
||||
import { TranslationService } from './services/translation.service';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
export class I18nModule extends NativeI18nModule {
|
||||
constructor(
|
||||
i18n: I18nService,
|
||||
@Inject(I18N_TRANSLATIONS)
|
||||
translations: Observable<I18nTranslation>,
|
||||
@Inject(I18N_OPTIONS) i18nOptions: I18nOptions,
|
||||
adapter: HttpAdapterHost,
|
||||
) {
|
||||
super(i18n, translations, i18nOptions, adapter);
|
||||
}
|
||||
|
||||
static forRoot(options: I18nOptions): DynamicModule {
|
||||
const { imports, providers, controllers, exports } = super.forRoot(options);
|
||||
return {
|
||||
module: I18nModule,
|
||||
imports: (imports || []).concat([
|
||||
MongooseModule.forFeature([LanguageModel, TranslationModel]),
|
||||
forwardRef(() => ChatModule),
|
||||
]),
|
||||
controllers: (controllers || []).concat([
|
||||
LanguageController,
|
||||
TranslationController,
|
||||
]),
|
||||
providers: providers.concat([
|
||||
I18nService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
LanguageSeeder,
|
||||
TranslationRepository,
|
||||
TranslationService,
|
||||
TranslationSeeder,
|
||||
]),
|
||||
exports: exports.concat(I18nService, LanguageService),
|
||||
};
|
||||
}
|
||||
}
|
||||
53
api/src/i18n/repositories/language.repository.ts
Normal file
53
api/src/i18n/repositories/language.repository.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query, TFilterQuery } from 'mongoose';
|
||||
|
||||
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
|
||||
|
||||
import { Language } from '../schemas/language.schema';
|
||||
|
||||
@Injectable()
|
||||
export class LanguageRepository extends BaseRepository<Language> {
|
||||
constructor(
|
||||
@InjectModel(Language.name) readonly model: Model<Language>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {
|
||||
super(model, Language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-delete hook that triggers before an language is deleted.
|
||||
*
|
||||
* @param query The query used to delete the language.
|
||||
* @param criteria The filter criteria used to find the language for deletion.
|
||||
*/
|
||||
async preDelete(
|
||||
_query: Query<
|
||||
DeleteResult,
|
||||
Document<Language, any, any>,
|
||||
unknown,
|
||||
Language,
|
||||
'deleteOne' | 'deleteMany'
|
||||
>,
|
||||
criteria: TFilterQuery<Language>,
|
||||
): Promise<void> {
|
||||
if (criteria._id) {
|
||||
const language = await this.findOne(
|
||||
typeof criteria === 'string' ? { _id: criteria } : criteria,
|
||||
);
|
||||
this.eventEmitter.emit('hook:language:delete', language);
|
||||
} else {
|
||||
throw new Error('Attempted to delete language using unknown criteria');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { Document, Model, Query, Types } from 'mongoose';
|
||||
|
||||
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
|
||||
|
||||
import { Translation } from '../schemas/translation.schema';
|
||||
import { Translation } from '../../i18n/schemas/translation.schema';
|
||||
|
||||
@Injectable()
|
||||
export class TranslationRepository extends BaseRepository<Translation> {
|
||||
52
api/src/i18n/schemas/language.schema.ts
Normal file
52
api/src/i18n/schemas/language.schema.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { THydratedDocument } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Language extends BaseSchema {
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
})
|
||||
code: string;
|
||||
|
||||
@Prop({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
isDefault?: boolean;
|
||||
|
||||
@Prop({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
})
|
||||
isRTL?: boolean;
|
||||
}
|
||||
|
||||
export const LanguageModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: Language.name,
|
||||
schema: SchemaFactory.createForClass(Language),
|
||||
});
|
||||
|
||||
export type LanguageDocument = THydratedDocument<Language>;
|
||||
|
||||
export default LanguageModel.schema;
|
||||
@@ -11,6 +11,7 @@ import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { THydratedDocument } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Translation extends BaseSchema {
|
||||
@@ -26,17 +27,12 @@ export class Translation extends BaseSchema {
|
||||
required: true,
|
||||
})
|
||||
translations: Record<string, string>;
|
||||
|
||||
@Prop({
|
||||
type: Number,
|
||||
})
|
||||
translated: number;
|
||||
}
|
||||
|
||||
export const TranslationModel: ModelDefinition = {
|
||||
export const TranslationModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: Translation.name,
|
||||
schema: SchemaFactory.createForClass(Translation),
|
||||
};
|
||||
});
|
||||
|
||||
export type TranslationDocument = THydratedDocument<Translation>;
|
||||
|
||||
24
api/src/i18n/seeds/language.seed-model.ts
Normal file
24
api/src/i18n/seeds/language.seed-model.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { LanguageCreateDto } from '../dto/language.dto';
|
||||
|
||||
export const languageModels: LanguageCreateDto[] = [
|
||||
{
|
||||
title: 'English',
|
||||
code: 'en',
|
||||
isRTL: false,
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
title: 'Français',
|
||||
code: 'fr',
|
||||
isRTL: false,
|
||||
},
|
||||
];
|
||||
22
api/src/i18n/seeds/language.seed.ts
Normal file
22
api/src/i18n/seeds/language.seed.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { BaseSeeder } from '@/utils/generics/base-seeder';
|
||||
|
||||
import { LanguageRepository } from '../repositories/language.repository';
|
||||
import { Language } from '../schemas/language.schema';
|
||||
|
||||
@Injectable()
|
||||
export class LanguageSeeder extends BaseSeeder<Language> {
|
||||
constructor(private readonly languageRepository: LanguageRepository) {
|
||||
super(languageRepository);
|
||||
}
|
||||
}
|
||||
@@ -8,22 +8,22 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { I18nService, Path, PathValue, TranslateOptions } from 'nestjs-i18n';
|
||||
import {
|
||||
I18nService as NativeI18nService,
|
||||
Path,
|
||||
PathValue,
|
||||
TranslateOptions,
|
||||
} from 'nestjs-i18n';
|
||||
import { IfAnyOrNever } from 'nestjs-i18n/dist/types';
|
||||
|
||||
import { Translation } from './chat/schemas/translation.schema';
|
||||
import { config } from './config';
|
||||
import { config } from '@/config';
|
||||
import { Translation } from '@/i18n/schemas/translation.schema';
|
||||
|
||||
@Injectable()
|
||||
export class ExtendedI18nService<
|
||||
export class I18nService<
|
||||
K = Record<string, unknown>,
|
||||
> extends I18nService<K> {
|
||||
private dynamicTranslations: Record<string, Record<string, string>> =
|
||||
config.chatbot.lang.available.reduce(
|
||||
(acc, curr) => ({ ...acc, [curr]: {} }),
|
||||
{},
|
||||
);
|
||||
> extends NativeI18nService<K> {
|
||||
private dynamicTranslations: Record<string, Record<string, string>> = {};
|
||||
|
||||
t<P extends Path<K> = any, R = PathValue<K, P>>(
|
||||
key: P,
|
||||
@@ -35,17 +35,19 @@ export class ExtendedI18nService<
|
||||
...options,
|
||||
};
|
||||
let { lang } = options;
|
||||
lang = lang ?? this.i18nOptions.fallbackLanguage;
|
||||
lang = this.resolveLanguage(lang);
|
||||
|
||||
// Translate block message, button text, ...
|
||||
if (lang in this.dynamicTranslations) {
|
||||
if (key in this.dynamicTranslations[lang]) {
|
||||
return this.dynamicTranslations[lang][key] as IfAnyOrNever<
|
||||
R,
|
||||
string,
|
||||
R
|
||||
>;
|
||||
if (this.dynamicTranslations[lang][key]) {
|
||||
return this.dynamicTranslations[lang][key] as IfAnyOrNever<
|
||||
R,
|
||||
string,
|
||||
R
|
||||
>;
|
||||
}
|
||||
return options.defaultValue as IfAnyOrNever<R, string, R>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,15 +56,13 @@ export class ExtendedI18nService<
|
||||
return super.t<P, R>(key, options);
|
||||
}
|
||||
|
||||
@OnEvent('hook:i18n:refresh')
|
||||
initDynamicTranslations(translations: Translation[]) {
|
||||
refreshDynamicTranslations(translations: Translation[]) {
|
||||
this.dynamicTranslations = translations.reduce((acc, curr) => {
|
||||
const { str, translations } = curr;
|
||||
Object.entries(translations)
|
||||
.filter(([lang]) => lang in acc)
|
||||
.forEach(([lang, t]) => {
|
||||
acc[lang][str] = t;
|
||||
});
|
||||
Object.entries(translations).forEach(([lang, t]) => {
|
||||
acc[lang] = acc[lang] || {};
|
||||
acc[lang][str] = t;
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, this.dynamicTranslations);
|
||||
68
api/src/i18n/services/language.service.ts
Normal file
68
api/src/i18n/services/language.service.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Cache } from 'cache-manager';
|
||||
|
||||
import {
|
||||
DEFAULT_LANGUAGE_CACHE_KEY,
|
||||
LANGUAGES_CACHE_KEY,
|
||||
} from '@/utils/constants/cache';
|
||||
import { Cacheable } from '@/utils/decorators/cacheable.decorator';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
import { LanguageRepository } from '../repositories/language.repository';
|
||||
import { Language } from '../schemas/language.schema';
|
||||
|
||||
@Injectable()
|
||||
export class LanguageService extends BaseService<Language> {
|
||||
constructor(
|
||||
readonly repository: LanguageRepository,
|
||||
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all available languages from the repository.
|
||||
*
|
||||
* @returns A promise that resolves to an object where each key is a language code
|
||||
* and the corresponding value is the `Language` object.
|
||||
*/
|
||||
@Cacheable(LANGUAGES_CACHE_KEY)
|
||||
async getLanguages() {
|
||||
const languages = await this.findAll();
|
||||
return languages.reduce((acc, curr) => {
|
||||
return {
|
||||
...acc,
|
||||
[curr.code]: curr,
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the default language from the repository.
|
||||
*
|
||||
* @returns A promise that resolves to the default `Language` object.
|
||||
*/
|
||||
@Cacheable(DEFAULT_LANGUAGE_CACHE_KEY)
|
||||
async getDefaultLanguage() {
|
||||
return await this.findOne({ isDefault: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the language by code.
|
||||
*
|
||||
* @returns A promise that resolves to the `Language` object.
|
||||
*/
|
||||
async getLanguageByCode(code: string) {
|
||||
return await this.findOne({ code });
|
||||
}
|
||||
}
|
||||
266
api/src/i18n/services/translation.service.spec.ts
Normal file
266
api/src/i18n/services/translation.service.spec.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { Settings } from '@/setting/schemas/types';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import { Block } from '../../chat/schemas/block.schema';
|
||||
import { BlockOptions } from '../../chat/schemas/types/options';
|
||||
import { BlockService } from '../../chat/services/block.service';
|
||||
import { TranslationRepository } from '../repositories/translation.repository';
|
||||
import { TranslationService } from '../services/translation.service';
|
||||
|
||||
describe('TranslationService', () => {
|
||||
let service: TranslationService;
|
||||
let settingService: SettingService;
|
||||
let i18nService: I18nService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
TranslationService,
|
||||
{
|
||||
provide: TranslationRepository,
|
||||
useValue: {
|
||||
findAll: jest.fn().mockResolvedValue([
|
||||
{
|
||||
key: 'test',
|
||||
value: 'test',
|
||||
lang: 'en',
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: BlockService,
|
||||
useValue: {
|
||||
find: jest.fn().mockResolvedValue([
|
||||
{
|
||||
id: 'blockId',
|
||||
message: ['Test message'],
|
||||
options: {
|
||||
fallback: {
|
||||
message: ['Fallback message'],
|
||||
},
|
||||
},
|
||||
} as Block,
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SettingService,
|
||||
useValue: {
|
||||
getSettings: jest.fn().mockResolvedValue({
|
||||
chatbot_settings: {
|
||||
global_fallback: true,
|
||||
fallback_message: ['Global fallback message'],
|
||||
},
|
||||
} as Settings),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
refreshDynamicTranslations: jest.fn(),
|
||||
},
|
||||
},
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TranslationService>(TranslationService);
|
||||
settingService = module.get<SettingService>(SettingService);
|
||||
i18nService = module.get<I18nService>(I18nService);
|
||||
});
|
||||
|
||||
it('should call refreshDynamicTranslations with translations from findAll', async () => {
|
||||
jest.spyOn(i18nService, 'refreshDynamicTranslations');
|
||||
await service.resetI18nTranslations();
|
||||
expect(i18nService.refreshDynamicTranslations).toHaveBeenCalledWith([
|
||||
{
|
||||
key: 'test',
|
||||
value: 'test',
|
||||
lang: 'en',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array of strings from all blocks', async () => {
|
||||
const strings = await service.getAllBlockStrings();
|
||||
expect(strings).toEqual(['Test message', 'Fallback message']);
|
||||
});
|
||||
|
||||
it('should return an array of strings from the settings when global fallback is enabled', async () => {
|
||||
const strings = await service.getSettingStrings();
|
||||
expect(strings).toEqual(['Global fallback message']);
|
||||
});
|
||||
|
||||
it('should return an empty array from the settings when global fallback is disabled', async () => {
|
||||
jest.spyOn(settingService, 'getSettings').mockResolvedValueOnce({
|
||||
chatbot_settings: {
|
||||
global_fallback: false,
|
||||
fallback_message: ['Global fallback message'],
|
||||
},
|
||||
} as Settings);
|
||||
|
||||
const strings = await service.getSettingStrings();
|
||||
expect(strings).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an array of strings from a block with a quick reply message', () => {
|
||||
const block = {
|
||||
id: 'blockId',
|
||||
name: 'Test Block',
|
||||
category: 'Test Category',
|
||||
position: { x: 0, y: 0 },
|
||||
message: {
|
||||
text: 'Test message',
|
||||
quickReplies: [
|
||||
{
|
||||
title: 'Quick reply 1',
|
||||
},
|
||||
{
|
||||
title: 'Quick reply 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
fallback: {
|
||||
active: true,
|
||||
message: ['Fallback message'],
|
||||
max_attempts: 3,
|
||||
} as BlockOptions,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as Block;
|
||||
const strings = service.getBlockStrings(block);
|
||||
expect(strings).toEqual([
|
||||
'Test message',
|
||||
'Quick reply 1',
|
||||
'Quick reply 2',
|
||||
'Fallback message',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array of strings from a block with a button message', () => {
|
||||
const block = {
|
||||
id: 'blockId',
|
||||
name: 'Test Block',
|
||||
category: 'Test Category',
|
||||
position: { x: 0, y: 0 },
|
||||
message: {
|
||||
text: 'Test message',
|
||||
buttons: [
|
||||
{
|
||||
title: 'Button 1',
|
||||
},
|
||||
{
|
||||
title: 'Button 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
fallback: {
|
||||
active: true,
|
||||
message: ['Fallback message'],
|
||||
max_attempts: 3,
|
||||
} as BlockOptions,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as Block;
|
||||
const strings = service.getBlockStrings(block);
|
||||
expect(strings).toEqual([
|
||||
'Test message',
|
||||
'Button 1',
|
||||
'Button 2',
|
||||
'Fallback message',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array of strings from a block with a text message', () => {
|
||||
const block = {
|
||||
id: 'blockId',
|
||||
name: 'Test Block',
|
||||
category: 'Test Category',
|
||||
position: { x: 0, y: 0 },
|
||||
message: ['Test message'], // Text message as an array
|
||||
options: {
|
||||
fallback: {
|
||||
active: true,
|
||||
message: ['Fallback message'],
|
||||
max_attempts: 3,
|
||||
} as BlockOptions,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as Block;
|
||||
const strings = service.getBlockStrings(block);
|
||||
expect(strings).toEqual(['Test message', 'Fallback message']);
|
||||
});
|
||||
|
||||
it('should return an array of strings from a block with a nested message object', () => {
|
||||
const block = {
|
||||
id: 'blockId',
|
||||
name: 'Test Block',
|
||||
category: 'Test Category',
|
||||
position: { x: 0, y: 0 },
|
||||
message: {
|
||||
text: 'Test message', // Nested text message
|
||||
},
|
||||
options: {
|
||||
fallback: {
|
||||
active: true,
|
||||
message: ['Fallback message'],
|
||||
max_attempts: 3,
|
||||
} as BlockOptions,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as Block;
|
||||
const strings = service.getBlockStrings(block);
|
||||
expect(strings).toEqual(['Test message', 'Fallback message']);
|
||||
});
|
||||
|
||||
it('should handle different message formats in getBlockStrings', () => {
|
||||
// Covers lines 54-60, 65
|
||||
|
||||
// Test with an array message (line 54-57)
|
||||
const block1 = {
|
||||
id: 'blockId1',
|
||||
message: ['This is a text message'],
|
||||
options: { fallback: { message: ['Fallback message'] } },
|
||||
} as Block;
|
||||
const strings1 = service.getBlockStrings(block1);
|
||||
expect(strings1).toEqual(['This is a text message', 'Fallback message']);
|
||||
|
||||
// Test with an object message (line 58-60)
|
||||
const block2 = {
|
||||
id: 'blockId2',
|
||||
message: { text: 'Another text message' },
|
||||
options: { fallback: { message: ['Fallback message'] } },
|
||||
} as Block;
|
||||
const strings2 = service.getBlockStrings(block2);
|
||||
expect(strings2).toEqual(['Another text message', 'Fallback message']);
|
||||
|
||||
// Test a block without a fallback (line 65)
|
||||
const block3 = {
|
||||
id: 'blockId3',
|
||||
message: { text: 'Another test message' },
|
||||
options: {},
|
||||
} as Block;
|
||||
const strings3 = service.getBlockStrings(block3);
|
||||
expect(strings3).toEqual(['Another test message']);
|
||||
});
|
||||
});
|
||||
@@ -10,13 +10,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
import { BlockService } from './block.service';
|
||||
import { Block } from '../../chat/schemas/block.schema';
|
||||
import { BlockService } from '../../chat/services/block.service';
|
||||
import { TranslationRepository } from '../repositories/translation.repository';
|
||||
import { Block } from '../schemas/block.schema';
|
||||
import { Translation } from '../schemas/translation.schema';
|
||||
|
||||
@Injectable()
|
||||
@@ -25,7 +25,7 @@ export class TranslationService extends BaseService<Translation> {
|
||||
readonly repository: TranslationRepository,
|
||||
private readonly blockService: BlockService,
|
||||
private readonly settingService: SettingService,
|
||||
private readonly i18n: ExtendedI18nService,
|
||||
private readonly i18n: I18nService,
|
||||
) {
|
||||
super(repository);
|
||||
this.resetI18nTranslations();
|
||||
@@ -33,7 +33,7 @@ export class TranslationService extends BaseService<Translation> {
|
||||
|
||||
public async resetI18nTranslations() {
|
||||
const translations = await this.findAll();
|
||||
this.i18n.initDynamicTranslations(translations);
|
||||
this.i18n.refreshDynamicTranslations(translations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +51,7 @@ async function bootstrap() {
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
// forbidNonWhitelisted: true,
|
||||
}),
|
||||
new ObjectIdPipe(),
|
||||
|
||||
@@ -18,7 +18,10 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingRepository } from '@/setting/repositories/setting.repository';
|
||||
import { SettingModel } from '@/setting/schemas/setting.schema';
|
||||
@@ -57,7 +60,9 @@ describe('NlpSampleController', () => {
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let nlpValueService: NlpValueService;
|
||||
let attachmentService: AttachmentService;
|
||||
let languageService: LanguageService;
|
||||
let byeJhonSampleId: string;
|
||||
let languages: Language[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -74,6 +79,7 @@ describe('NlpSampleController', () => {
|
||||
NlpEntityModel,
|
||||
NlpValueModel,
|
||||
SettingModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -88,13 +94,15 @@ describe('NlpSampleController', () => {
|
||||
NlpValueRepository,
|
||||
NlpSampleService,
|
||||
NlpSampleEntityService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
EventEmitter2,
|
||||
NlpService,
|
||||
SettingRepository,
|
||||
SettingService,
|
||||
SettingSeeder,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
@@ -122,6 +130,8 @@ describe('NlpSampleController', () => {
|
||||
})
|
||||
).id;
|
||||
attachmentService = module.get<AttachmentService>(AttachmentService);
|
||||
languageService = module.get<LanguageService>(LanguageService);
|
||||
languages = await languageService.findAll();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await closeInMongodConnection();
|
||||
@@ -134,7 +144,7 @@ describe('NlpSampleController', () => {
|
||||
const pageQuery = getPageQuery<NlpSample>({ sort: ['text', 'desc'] });
|
||||
const result = await nlpSampleController.findPage(
|
||||
pageQuery,
|
||||
['entities'],
|
||||
['language', 'entities'],
|
||||
{},
|
||||
);
|
||||
const nlpSamples = await nlpSampleService.findAll();
|
||||
@@ -146,6 +156,7 @@ describe('NlpSampleController', () => {
|
||||
entities: nlpSampleEntities.filter((currSampleEntity) => {
|
||||
return currSampleEntity.sample === currSample.id;
|
||||
}),
|
||||
language: languages.find((lang) => lang.id === currSample.language),
|
||||
};
|
||||
acc.push(sampleWithEntities);
|
||||
return acc;
|
||||
@@ -163,7 +174,12 @@ describe('NlpSampleController', () => {
|
||||
['invalidCriteria'],
|
||||
{},
|
||||
);
|
||||
expect(result).toEqualPayload(nlpSampleFixtures);
|
||||
expect(result).toEqualPayload(
|
||||
nlpSampleFixtures.map((sample) => ({
|
||||
...sample,
|
||||
language: languages[sample.language].id,
|
||||
})),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -177,14 +193,19 @@ describe('NlpSampleController', () => {
|
||||
|
||||
describe('create', () => {
|
||||
it('should create nlp sample', async () => {
|
||||
const enLang = await languageService.findOne({ code: 'en' });
|
||||
const nlSample: NlpSampleDto = {
|
||||
text: 'text1',
|
||||
trained: true,
|
||||
type: NlpSampleState.test,
|
||||
entities: [],
|
||||
language: 'en',
|
||||
};
|
||||
const result = await nlpSampleController.create(nlSample);
|
||||
expect(result).toEqualPayload(nlSample);
|
||||
expect(result).toEqualPayload({
|
||||
...nlSample,
|
||||
language: enLang,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -209,7 +230,10 @@ describe('NlpSampleController', () => {
|
||||
const result = await nlpSampleController.findOne(yessSample.id, [
|
||||
'invalidCreteria',
|
||||
]);
|
||||
expect(result).toEqualPayload(nlpSampleFixtures[0]);
|
||||
expect(result).toEqualPayload({
|
||||
...nlpSampleFixtures[0],
|
||||
language: languages[nlpSampleFixtures[0].language].id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should find a nlp sample and populate its entities', async () => {
|
||||
@@ -225,6 +249,7 @@ describe('NlpSampleController', () => {
|
||||
const samplesWithEntities = {
|
||||
...nlpSampleFixtures[0],
|
||||
entities: [yessSampleEntity],
|
||||
language: languages[nlpSampleFixtures[0].language],
|
||||
};
|
||||
expect(result).toEqualPayload(samplesWithEntities);
|
||||
});
|
||||
@@ -241,6 +266,9 @@ describe('NlpSampleController', () => {
|
||||
const yessSample = await nlpSampleService.findOne({
|
||||
text: 'yess',
|
||||
});
|
||||
const frLang = await languageService.findOne({
|
||||
code: 'fr',
|
||||
});
|
||||
const result = await nlpSampleController.updateOne(yessSample.id, {
|
||||
text: 'updated',
|
||||
trained: true,
|
||||
@@ -251,6 +279,7 @@ describe('NlpSampleController', () => {
|
||||
value: 'update',
|
||||
},
|
||||
],
|
||||
language: 'fr',
|
||||
});
|
||||
const updatedSample = {
|
||||
text: 'updated',
|
||||
@@ -263,11 +292,13 @@ describe('NlpSampleController', () => {
|
||||
value: expect.stringMatching(/^[a-z0-9]+$/),
|
||||
},
|
||||
],
|
||||
language: frLang,
|
||||
};
|
||||
expect(result.text).toEqual(updatedSample.text);
|
||||
expect(result.type).toEqual(updatedSample.type);
|
||||
expect(result.trained).toEqual(updatedSample.trained);
|
||||
expect(result.entities).toMatchObject(updatedSample.entities);
|
||||
expect(result.language).toEqualPayload(updatedSample.language);
|
||||
});
|
||||
|
||||
it('should throw exception when nlp sample id not found', async () => {
|
||||
@@ -276,6 +307,7 @@ describe('NlpSampleController', () => {
|
||||
text: 'updated',
|
||||
trained: true,
|
||||
type: NlpSampleState.test,
|
||||
language: 'fr',
|
||||
}),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
@@ -352,7 +384,7 @@ describe('NlpSampleController', () => {
|
||||
).id;
|
||||
const mockCsvData: string = [
|
||||
`text,intent,language`,
|
||||
`Was kostet dieser bmw,preis,de`,
|
||||
`How much does a BMW cost?,price,en`,
|
||||
].join('\n');
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true);
|
||||
jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockCsvData);
|
||||
@@ -361,17 +393,14 @@ describe('NlpSampleController', () => {
|
||||
const intentEntityResult = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const languageEntityResult = await nlpEntityService.findOne({
|
||||
name: 'language',
|
||||
});
|
||||
const preisValueResult = await nlpValueService.findOne({
|
||||
value: 'preis',
|
||||
});
|
||||
const deValueResult = await nlpValueService.findOne({
|
||||
value: 'de',
|
||||
const priceValueResult = await nlpValueService.findOne({
|
||||
value: 'price',
|
||||
});
|
||||
const textSampleResult = await nlpSampleService.findOne({
|
||||
text: 'Was kostet dieser bmw',
|
||||
text: 'How much does a BMW cost?',
|
||||
});
|
||||
const language = await languageService.findOne({
|
||||
code: 'en',
|
||||
});
|
||||
const intentEntity = {
|
||||
name: 'intent',
|
||||
@@ -379,40 +408,24 @@ describe('NlpSampleController', () => {
|
||||
doc: '',
|
||||
builtin: false,
|
||||
};
|
||||
const languageEntity = {
|
||||
name: 'language',
|
||||
lookups: ['trait'],
|
||||
builtin: false,
|
||||
doc: '',
|
||||
};
|
||||
const preisVlueEntity = await nlpEntityService.findOne({
|
||||
const priceValueEntity = await nlpEntityService.findOne({
|
||||
name: 'intent',
|
||||
});
|
||||
const preisValue = {
|
||||
value: 'preis',
|
||||
const priceValue = {
|
||||
value: 'price',
|
||||
expressions: [],
|
||||
builtin: false,
|
||||
entity: preisVlueEntity.id,
|
||||
};
|
||||
const deValueEntity = await nlpEntityService.findOne({
|
||||
name: 'language',
|
||||
});
|
||||
const deValue = {
|
||||
value: 'de',
|
||||
expressions: [],
|
||||
builtin: false,
|
||||
entity: deValueEntity.id,
|
||||
entity: priceValueEntity.id,
|
||||
};
|
||||
const textSample = {
|
||||
text: 'Was kostet dieser bmw',
|
||||
text: 'How much does a BMW cost?',
|
||||
trained: false,
|
||||
type: 'train',
|
||||
language: language.id,
|
||||
};
|
||||
|
||||
expect(languageEntityResult).toEqualPayload(languageEntity);
|
||||
expect(intentEntityResult).toEqualPayload(intentEntity);
|
||||
expect(preisValueResult).toEqualPayload(preisValue);
|
||||
expect(deValueResult).toEqualPayload(deValue);
|
||||
expect(priceValueResult).toEqualPayload(priceValue);
|
||||
expect(textSampleResult).toEqualPayload(textSample);
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ import Papa from 'papaparse';
|
||||
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { config } from '@/config';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
@@ -70,6 +71,7 @@ export class NlpSampleController extends BaseController<
|
||||
private readonly nlpEntityService: NlpEntityService,
|
||||
private readonly logger: LoggerService,
|
||||
private readonly nlpService: NlpService,
|
||||
private readonly languageService: LanguageService,
|
||||
) {
|
||||
super(nlpSampleService);
|
||||
}
|
||||
@@ -91,7 +93,7 @@ export class NlpSampleController extends BaseController<
|
||||
type ? { type } : {},
|
||||
);
|
||||
const entities = await this.nlpEntityService.findAllAndPopulate();
|
||||
const result = this.nlpSampleService.formatRasaNlu(samples, entities);
|
||||
const result = await this.nlpSampleService.formatRasaNlu(samples, entities);
|
||||
|
||||
// Sending the JSON data as a file
|
||||
const buffer = Buffer.from(JSON.stringify(result));
|
||||
@@ -120,11 +122,18 @@ export class NlpSampleController extends BaseController<
|
||||
@CsrfCheck(true)
|
||||
@Post()
|
||||
async create(
|
||||
@Body() { entities: nlpEntities, ...createNlpSampleDto }: NlpSampleDto,
|
||||
@Body()
|
||||
{
|
||||
entities: nlpEntities,
|
||||
language: languageCode,
|
||||
...createNlpSampleDto
|
||||
}: NlpSampleDto,
|
||||
): Promise<NlpSampleFull> {
|
||||
const nlpSample = await this.nlpSampleService.create(
|
||||
createNlpSampleDto as NlpSampleCreateDto,
|
||||
);
|
||||
const language = await this.languageService.getLanguageByCode(languageCode);
|
||||
const nlpSample = await this.nlpSampleService.create({
|
||||
...createNlpSampleDto,
|
||||
language: language.id,
|
||||
});
|
||||
|
||||
const entities = await this.nlpSampleEntityService.storeSampleEntities(
|
||||
nlpSample,
|
||||
@@ -134,6 +143,7 @@ export class NlpSampleController extends BaseController<
|
||||
return {
|
||||
...nlpSample,
|
||||
entities,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -243,7 +253,11 @@ export class NlpSampleController extends BaseController<
|
||||
async findPage(
|
||||
@Query(PageQueryPipe) pageQuery: PageQueryDto<NlpSample>,
|
||||
@Query(PopulatePipe) populate: string[],
|
||||
@Query(new SearchFilterPipe<NlpSample>({ allowedFields: ['text', 'type'] }))
|
||||
@Query(
|
||||
new SearchFilterPipe<NlpSample>({
|
||||
allowedFields: ['text', 'type', 'language'],
|
||||
}),
|
||||
)
|
||||
filters: TFilterQuery<NlpSample>,
|
||||
) {
|
||||
return this.canPopulate(populate)
|
||||
@@ -263,12 +277,12 @@ export class NlpSampleController extends BaseController<
|
||||
@Patch(':id')
|
||||
async updateOne(
|
||||
@Param('id') id: string,
|
||||
@Body() updateNlpSampleDto: NlpSampleDto,
|
||||
@Body() { entities, language: languageCode, ...sampleAttrs }: NlpSampleDto,
|
||||
): Promise<NlpSampleFull> {
|
||||
const { text, type, entities } = updateNlpSampleDto;
|
||||
const language = await this.languageService.getLanguageByCode(languageCode);
|
||||
const sample = await this.nlpSampleService.updateOne(id, {
|
||||
text,
|
||||
type,
|
||||
...sampleAttrs,
|
||||
language: language.id,
|
||||
trained: false,
|
||||
});
|
||||
|
||||
@@ -284,6 +298,7 @@ export class NlpSampleController extends BaseController<
|
||||
|
||||
return {
|
||||
...sample,
|
||||
language,
|
||||
entities: updatedSampleEntities,
|
||||
};
|
||||
}
|
||||
@@ -366,6 +381,8 @@ export class NlpSampleController extends BaseController<
|
||||
}
|
||||
// Remove data with no intent
|
||||
const filteredData = result.data.filter((d) => d.intent !== 'none');
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
// Reduce function to ensure executing promises one by one
|
||||
for (const d of filteredData) {
|
||||
try {
|
||||
@@ -375,15 +392,25 @@ export class NlpSampleController extends BaseController<
|
||||
});
|
||||
|
||||
// Skip if sample already exists
|
||||
|
||||
if (Array.isArray(existingSamples) && existingSamples.length > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback to default language if 'language' is missing or invalid
|
||||
if (!d.language || !(d.language in languages)) {
|
||||
if (d.language) {
|
||||
this.logger.warn(
|
||||
`Language "${d.language}" does not exist, falling back to default.`,
|
||||
);
|
||||
}
|
||||
d.language = defaultLanguage.code;
|
||||
}
|
||||
|
||||
// Create a new sample dto
|
||||
const sample: NlpSampleCreateDto = {
|
||||
text: d.text,
|
||||
trained: false,
|
||||
language: languages[d.language].id,
|
||||
};
|
||||
|
||||
// Create a new sample entity dto
|
||||
|
||||
@@ -16,27 +16,38 @@ import {
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
import { IsObjectId } from '@/utils/validation-rules/is-object-id';
|
||||
|
||||
import { NlpSampleEntityValue, NlpSampleState } from '../schemas/types';
|
||||
|
||||
export class NlpSampleCreateDto {
|
||||
@ApiProperty({ description: 'nlp sample text', type: String })
|
||||
@ApiProperty({ description: 'NLP sample text', type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
text: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'nlp sample is trained', type: Boolean })
|
||||
@ApiPropertyOptional({
|
||||
description: 'If NLP sample is trained',
|
||||
type: Boolean,
|
||||
})
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
trained?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'nlp sample type',
|
||||
description: 'NLP sample type',
|
||||
enum: Object.values(NlpSampleState),
|
||||
})
|
||||
@IsString()
|
||||
@IsIn(Object.values(NlpSampleState))
|
||||
@IsOptional()
|
||||
type?: NlpSampleState;
|
||||
|
||||
@ApiProperty({ description: 'NLP sample language id', type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsObjectId({ message: 'Language must be a valid ObjectId' })
|
||||
language: string;
|
||||
}
|
||||
|
||||
export class NlpSampleDto extends NlpSampleCreateDto {
|
||||
@@ -45,6 +56,11 @@ export class NlpSampleDto extends NlpSampleCreateDto {
|
||||
})
|
||||
@IsOptional()
|
||||
entities?: NlpSampleEntityValue[];
|
||||
|
||||
@ApiProperty({ description: 'NLP sample language code', type: String })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
language: string;
|
||||
}
|
||||
|
||||
export class NlpSampleUpdateDto extends PartialType(NlpSampleCreateDto) {}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import {
|
||||
installNlpSampleEntityFixtures,
|
||||
@@ -37,8 +39,10 @@ import { NlpValueModel } from '../schemas/nlp-value.schema';
|
||||
describe('NlpSampleEntityRepository', () => {
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpEntityRepository: NlpEntityRepository;
|
||||
let languageRepository: LanguageRepository;
|
||||
let nlpSampleEntities: NlpSampleEntity[];
|
||||
let nlpEntities: NlpEntity[];
|
||||
let languages: Language[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -49,12 +53,14 @@ describe('NlpSampleEntityRepository', () => {
|
||||
NlpEntityModel,
|
||||
NlpValueModel,
|
||||
NlpSampleModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
LanguageRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
@@ -62,8 +68,10 @@ describe('NlpSampleEntityRepository', () => {
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
nlpEntityRepository = module.get<NlpEntityRepository>(NlpEntityRepository);
|
||||
languageRepository = module.get<LanguageRepository>(LanguageRepository);
|
||||
nlpSampleEntities = await nlpSampleEntityRepository.findAll();
|
||||
nlpEntities = await nlpEntityRepository.findAll();
|
||||
languages = await languageRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -81,7 +89,10 @@ describe('NlpSampleEntityRepository', () => {
|
||||
...nlpSampleEntityFixtures[0],
|
||||
entity: nlpEntities[0],
|
||||
value: { ...nlpValueFixtures[0], entity: nlpEntities[0].id },
|
||||
sample: nlpSampleFixtures[0],
|
||||
sample: {
|
||||
...nlpSampleFixtures[0],
|
||||
language: languages[nlpSampleFixtures[0].language].id,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -117,7 +128,10 @@ describe('NlpSampleEntityRepository', () => {
|
||||
...curr,
|
||||
entity: nlpEntities[curr.entity],
|
||||
value: nlpValueFixturesWithEntities[curr.value],
|
||||
sample: nlpSampleFixtures[curr.sample],
|
||||
sample: {
|
||||
...nlpSampleFixtures[curr.sample],
|
||||
language: languages[nlpSampleFixtures[curr.sample].language].id,
|
||||
},
|
||||
};
|
||||
acc.push(sampleEntityWithPopulate);
|
||||
return acc;
|
||||
|
||||
@@ -11,6 +11,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
@@ -30,18 +32,25 @@ import { NlpSampleModel, NlpSample } from '../schemas/nlp-sample.schema';
|
||||
describe('NlpSampleRepository', () => {
|
||||
let nlpSampleRepository: NlpSampleRepository;
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let languageRepository: LanguageRepository;
|
||||
let nlpSampleEntity: NlpSampleEntity;
|
||||
let noNlpSample: NlpSample;
|
||||
let languages: Language[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installNlpSampleEntityFixtures),
|
||||
MongooseModule.forFeature([NlpSampleModel, NlpSampleEntityModel]),
|
||||
MongooseModule.forFeature([
|
||||
NlpSampleModel,
|
||||
NlpSampleEntityModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleRepository,
|
||||
NlpSampleEntityRepository,
|
||||
LanguageRepository,
|
||||
EventEmitter2,
|
||||
],
|
||||
}).compile();
|
||||
@@ -49,10 +58,12 @@ describe('NlpSampleRepository', () => {
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
languageRepository = module.get<LanguageRepository>(LanguageRepository);
|
||||
noNlpSample = await nlpSampleRepository.findOne({ text: 'No' });
|
||||
nlpSampleEntity = await nlpSampleEntityRepository.findOne({
|
||||
sample: noNlpSample.id,
|
||||
});
|
||||
languages = await languageRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -69,6 +80,7 @@ describe('NlpSampleRepository', () => {
|
||||
expect(result).toEqualPayload({
|
||||
...nlpSampleFixtures[1],
|
||||
entities: [nlpSampleEntity],
|
||||
language: languages[nlpSampleFixtures[1].language],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -92,6 +104,7 @@ describe('NlpSampleRepository', () => {
|
||||
entities: nlpSampleEntities.filter((currSampleEntity) => {
|
||||
return currSampleEntity.sample === currSample.id;
|
||||
}),
|
||||
language: languages.find((lang) => currSample.language === lang.id),
|
||||
};
|
||||
acc.push(sampleWithEntities);
|
||||
return acc;
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
*/
|
||||
|
||||
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Exclude, Type } from 'class-transformer';
|
||||
import { THydratedDocument } from 'mongoose';
|
||||
import { Exclude, Transform, Type } from 'class-transformer';
|
||||
import { THydratedDocument, Schema as MongooseSchema } from 'mongoose';
|
||||
|
||||
import { Language } from '@/i18n/schemas/language.schema';
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
import { TFilterPopulateFields } from '@/utils/types/filter.types';
|
||||
@@ -41,16 +42,32 @@ export class NlpSampleStub extends BaseSchema {
|
||||
default: NlpSampleState.train,
|
||||
})
|
||||
type?: keyof typeof NlpSampleState;
|
||||
|
||||
/**
|
||||
* The language of the sample.
|
||||
*/
|
||||
@Prop({
|
||||
type: MongooseSchema.Types.ObjectId,
|
||||
ref: 'Language',
|
||||
required: false,
|
||||
})
|
||||
language: unknown | null;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSample extends NlpSampleStub {
|
||||
@Transform(({ obj }) => obj.language.toString())
|
||||
language: string | null;
|
||||
|
||||
@Exclude()
|
||||
entities?: never;
|
||||
}
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class NlpSampleFull extends NlpSampleStub {
|
||||
@Type(() => Language)
|
||||
language: Language | null;
|
||||
|
||||
@Type(() => NlpSampleEntity)
|
||||
entities: NlpSampleEntity[];
|
||||
}
|
||||
@@ -75,4 +92,7 @@ export type NlpSamplePopulate = keyof TFilterPopulateFields<
|
||||
NlpSampleStub
|
||||
>;
|
||||
|
||||
export const NLP_SAMPLE_POPULATE: NlpSamplePopulate[] = ['entities'];
|
||||
export const NLP_SAMPLE_POPULATE: NlpSamplePopulate[] = [
|
||||
'language',
|
||||
'entities',
|
||||
];
|
||||
|
||||
@@ -10,12 +10,6 @@
|
||||
import { NlpEntityCreateDto } from '../dto/nlp-entity.dto';
|
||||
|
||||
export const nlpEntityModels: NlpEntityCreateDto[] = [
|
||||
{
|
||||
name: 'language',
|
||||
lookups: ['trait'],
|
||||
doc: `"language" refers to the language of the text sent by the end user`,
|
||||
builtin: true,
|
||||
},
|
||||
{
|
||||
name: 'intent',
|
||||
lookups: ['trait'],
|
||||
|
||||
@@ -7,16 +7,6 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { config } from '@/config';
|
||||
|
||||
import { NlpValueCreateDto } from '../dto/nlp-value.dto';
|
||||
|
||||
export const nlpValueModels: NlpValueCreateDto[] = [
|
||||
...config.chatbot.lang.available.map((lang: string) => {
|
||||
return {
|
||||
entity: 'language',
|
||||
value: lang,
|
||||
builtin: true,
|
||||
};
|
||||
}),
|
||||
];
|
||||
export const nlpValueModels: NlpValueCreateDto[] = [];
|
||||
|
||||
@@ -11,6 +11,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import {
|
||||
installNlpSampleEntityFixtures,
|
||||
@@ -42,7 +44,9 @@ describe('NlpSampleEntityService', () => {
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpSampleEntities: NlpSampleEntity[];
|
||||
let nlpEntityRepository: NlpEntityRepository;
|
||||
let languageRepository: LanguageRepository;
|
||||
let nlpEntities: NlpEntity[];
|
||||
let languages: Language[];
|
||||
let nlpEntityService: NlpEntityService;
|
||||
let nlpValueService: NlpValueService;
|
||||
|
||||
@@ -55,12 +59,14 @@ describe('NlpSampleEntityService', () => {
|
||||
NlpEntityModel,
|
||||
NlpSampleModel,
|
||||
NlpValueModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
LanguageRepository,
|
||||
NlpSampleEntityService,
|
||||
NlpEntityService,
|
||||
NlpValueService,
|
||||
@@ -74,6 +80,7 @@ describe('NlpSampleEntityService', () => {
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
nlpEntityRepository = module.get<NlpEntityRepository>(NlpEntityRepository);
|
||||
languageRepository = module.get<LanguageRepository>(LanguageRepository);
|
||||
nlpSampleEntityService = module.get<NlpSampleEntityService>(
|
||||
NlpSampleEntityService,
|
||||
);
|
||||
@@ -81,6 +88,7 @@ describe('NlpSampleEntityService', () => {
|
||||
nlpValueService = module.get<NlpValueService>(NlpValueService);
|
||||
nlpSampleEntities = await nlpSampleEntityRepository.findAll();
|
||||
nlpEntities = await nlpEntityRepository.findAll();
|
||||
languages = await languageRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -98,7 +106,10 @@ describe('NlpSampleEntityService', () => {
|
||||
...nlpSampleEntityFixtures[0],
|
||||
entity: nlpEntities[0],
|
||||
value: { ...nlpValueFixtures[0], entity: nlpEntities[0].id },
|
||||
sample: nlpSampleFixtures[0],
|
||||
sample: {
|
||||
...nlpSampleFixtures[0],
|
||||
language: languages[nlpSampleFixtures[0].language].id,
|
||||
},
|
||||
};
|
||||
expect(result).toEqualPayload(sampleEntityWithPopulate);
|
||||
});
|
||||
@@ -135,7 +146,10 @@ describe('NlpSampleEntityService', () => {
|
||||
...curr,
|
||||
entity: nlpEntities[curr.entity],
|
||||
value: nlpValueFixturesWithEntities[curr.value],
|
||||
sample: nlpSampleFixtures[curr.sample],
|
||||
sample: {
|
||||
...nlpSampleFixtures[curr.sample],
|
||||
language: languages[nlpSampleFixtures[curr.sample].language].id,
|
||||
},
|
||||
};
|
||||
acc.push(sampleEntityWithPopulate);
|
||||
return acc;
|
||||
|
||||
@@ -7,10 +7,14 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { Language, LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { nlpSampleFixtures } from '@/utils/test/fixtures/nlpsample';
|
||||
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
|
||||
import { getPageQuery } from '@/utils/test/pagination';
|
||||
@@ -39,8 +43,10 @@ describe('NlpSampleService', () => {
|
||||
let nlpSampleService: NlpSampleService;
|
||||
let nlpSampleEntityRepository: NlpSampleEntityRepository;
|
||||
let nlpSampleRepository: NlpSampleRepository;
|
||||
let languageRepository: LanguageRepository;
|
||||
let noNlpSample: NlpSample;
|
||||
let nlpSampleEntity: NlpSampleEntity;
|
||||
let languages: Language[];
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -51,6 +57,7 @@ describe('NlpSampleService', () => {
|
||||
NlpSampleEntityModel,
|
||||
NlpValueModel,
|
||||
NlpEntityModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -58,11 +65,21 @@ describe('NlpSampleService', () => {
|
||||
NlpSampleEntityRepository,
|
||||
NlpEntityRepository,
|
||||
NlpValueRepository,
|
||||
LanguageRepository,
|
||||
NlpSampleService,
|
||||
NlpSampleEntityService,
|
||||
NlpEntityService,
|
||||
NlpValueService,
|
||||
LanguageService,
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
nlpSampleService = module.get<NlpSampleService>(NlpSampleService);
|
||||
@@ -73,10 +90,12 @@ describe('NlpSampleService', () => {
|
||||
nlpSampleEntityRepository = module.get<NlpSampleEntityRepository>(
|
||||
NlpSampleEntityRepository,
|
||||
);
|
||||
languageRepository = module.get<LanguageRepository>(LanguageRepository);
|
||||
noNlpSample = await nlpSampleService.findOne({ text: 'No' });
|
||||
nlpSampleEntity = await nlpSampleEntityRepository.findOne({
|
||||
sample: noNlpSample.id,
|
||||
});
|
||||
languages = await languageRepository.findAll();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -91,6 +110,7 @@ describe('NlpSampleService', () => {
|
||||
const sampleWithEntities = {
|
||||
...nlpSampleFixtures[1],
|
||||
entities: [nlpSampleEntity],
|
||||
language: languages[nlpSampleFixtures[1].language],
|
||||
};
|
||||
expect(result).toEqualPayload(sampleWithEntities);
|
||||
});
|
||||
@@ -110,6 +130,7 @@ describe('NlpSampleService', () => {
|
||||
entities: nlpSampleEntities.filter((currSampleEntity) => {
|
||||
return currSampleEntity.sample === currSample.id;
|
||||
}),
|
||||
language: languages.find((lang) => lang.id === currSample.language),
|
||||
};
|
||||
acc.push(sampleWithEntities);
|
||||
return acc;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import {
|
||||
CommonExample,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
ExampleEntity,
|
||||
LookupTable,
|
||||
} from '@/extensions/helpers/nlp/default/types';
|
||||
import { Language } from '@/i18n/schemas/language.schema';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
import { NlpSampleRepository } from '../repositories/nlp-sample.repository';
|
||||
@@ -33,7 +36,10 @@ export class NlpSampleService extends BaseService<
|
||||
NlpSamplePopulate,
|
||||
NlpSampleFull
|
||||
> {
|
||||
constructor(readonly repository: NlpSampleRepository) {
|
||||
constructor(
|
||||
readonly repository: NlpSampleRepository,
|
||||
private readonly languageService: LanguageService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
@@ -56,10 +62,10 @@ export class NlpSampleService extends BaseService<
|
||||
*
|
||||
* @returns The formatted Rasa NLU training dataset.
|
||||
*/
|
||||
formatRasaNlu(
|
||||
async formatRasaNlu(
|
||||
samples: NlpSampleFull[],
|
||||
entities: NlpEntityFull[],
|
||||
): DatasetType {
|
||||
): Promise<DatasetType> {
|
||||
const entityMap = NlpEntity.getEntityMap(entities);
|
||||
const valueMap = NlpValue.getValueMap(
|
||||
NlpValue.getValuesFromEntities(entities),
|
||||
@@ -88,21 +94,34 @@ export class NlpSampleService extends BaseService<
|
||||
});
|
||||
}
|
||||
return res;
|
||||
})
|
||||
// TODO : place language at the same level as the intent
|
||||
.concat({
|
||||
entity: 'language',
|
||||
value: s.language.code,
|
||||
});
|
||||
|
||||
return {
|
||||
text: s.text,
|
||||
intent: valueMap[intent.value].value,
|
||||
entities: sampleEntities,
|
||||
};
|
||||
});
|
||||
const lookup_tables: LookupTable[] = entities.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
elements: e.values.map((v) => {
|
||||
return v.value;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const languages = await this.languageService.getLanguages();
|
||||
const lookup_tables: LookupTable[] = entities
|
||||
.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
elements: e.values.map((v) => {
|
||||
return v.value;
|
||||
}),
|
||||
};
|
||||
})
|
||||
.concat({
|
||||
name: 'language',
|
||||
elements: Object.keys(languages),
|
||||
});
|
||||
const entity_synonyms = entities
|
||||
.reduce((acc, e) => {
|
||||
const synonyms = e.values.map((v) => {
|
||||
@@ -123,4 +142,21 @@ export class NlpSampleService extends BaseService<
|
||||
entity_synonyms,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* When a language gets deleted, we need to set related samples to null
|
||||
*
|
||||
* @param language The language that has been deleted.
|
||||
*/
|
||||
@OnEvent('hook:language:delete')
|
||||
async handleLanguageDelete(language: Language) {
|
||||
await this.updateMany(
|
||||
{
|
||||
language: language.id,
|
||||
},
|
||||
{
|
||||
language: null,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,10 @@ import { CategorySeeder } from './chat/seeds/category.seed';
|
||||
import { categoryModels } from './chat/seeds/category.seed-model';
|
||||
import { ContextVarSeeder } from './chat/seeds/context-var.seed';
|
||||
import { contextVarModels } from './chat/seeds/context-var.seed-model';
|
||||
import { TranslationSeeder } from './chat/seeds/translation.seed';
|
||||
import { translationModels } from './chat/seeds/translation.seed-model';
|
||||
import { LanguageSeeder } from './i18n/seeds/language.seed';
|
||||
import { languageModels } from './i18n/seeds/language.seed-model';
|
||||
import { TranslationSeeder } from './i18n/seeds/translation.seed';
|
||||
import { translationModels } from './i18n/seeds/translation.seed-model';
|
||||
import { LoggerService } from './logger/logger.service';
|
||||
import { NlpEntitySeeder } from './nlp/seeds/nlp-entity.seed';
|
||||
import { nlpEntityModels } from './nlp/seeds/nlp-entity.seed-model';
|
||||
@@ -40,6 +42,7 @@ export async function seedDatabase(app: INestApplicationContext) {
|
||||
const settingSeeder = app.get(SettingSeeder);
|
||||
const permissionSeeder = app.get(PermissionSeeder);
|
||||
const userSeeder = app.get(UserSeeder);
|
||||
const languageSeeder = app.get(LanguageSeeder);
|
||||
const translationSeeder = app.get(TranslationSeeder);
|
||||
const nlpEntitySeeder = app.get(NlpEntitySeeder);
|
||||
const nlpValueSeeder = app.get(NlpValueSeeder);
|
||||
@@ -127,6 +130,14 @@ export async function seedDatabase(app: INestApplicationContext) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Seed languages
|
||||
try {
|
||||
await languageSeeder.seed(languageModels);
|
||||
} catch (e) {
|
||||
logger.error('Unable to seed the database with languages!');
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Seed translations
|
||||
try {
|
||||
await translationSeeder.seed(translationModels);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import {
|
||||
installSettingFixtures,
|
||||
@@ -47,7 +47,7 @@ describe('SettingController', () => {
|
||||
LoggerService,
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -12,8 +12,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query, Types } from 'mongoose';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { BaseRepository } from '@/utils/generics/base-repository';
|
||||
|
||||
import { Setting } from '../schemas/setting.schema';
|
||||
@@ -23,7 +22,7 @@ export class SettingRepository extends BaseRepository<Setting> {
|
||||
constructor(
|
||||
@InjectModel(Setting.name) readonly model: Model<Setting>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly i18n: ExtendedI18nService,
|
||||
private readonly i18n: I18nService,
|
||||
) {
|
||||
super(model, Setting);
|
||||
}
|
||||
@@ -65,8 +64,7 @@ export class SettingRepository extends BaseRepository<Setting> {
|
||||
* Emits an event after a `Setting` has been updated.
|
||||
*
|
||||
* This method is used to synchronize global settings by emitting an event
|
||||
* based on the `group` and `label` of the `Setting`. It also updates the i18n
|
||||
* default language setting when the `default_lang` label is updated.
|
||||
* based on the `group` and `label` of the `Setting`.
|
||||
*
|
||||
* @param _query The Mongoose query object used to find and update the document.
|
||||
* @param setting The updated `Setting` object.
|
||||
@@ -86,33 +84,5 @@ export class SettingRepository extends BaseRepository<Setting> {
|
||||
'hook:settings:' + setting.group + ':' + setting.label,
|
||||
setting,
|
||||
);
|
||||
|
||||
if (setting.label === 'default_lang') {
|
||||
// @todo : check if this actually updates the default lang
|
||||
this.i18n.resolveLanguage(setting.value as string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default values before creating a `Setting` document.
|
||||
*
|
||||
* If the setting is part of the `nlp_settings` group, it sets specific values
|
||||
* for `languages` and `default_lang` labels, using configuration values from the
|
||||
* chatbot settings.
|
||||
*
|
||||
* @param setting The `Setting` document to be created.
|
||||
*/
|
||||
async preCreate(
|
||||
setting: Document<unknown, unknown, Setting> &
|
||||
Setting & { _id: Types.ObjectId },
|
||||
) {
|
||||
if (setting.group === 'nlp_settings') {
|
||||
if (setting.label === 'languages') {
|
||||
setting.value = config.chatbot.lang.available;
|
||||
} else if (setting.label === 'default_lang') {
|
||||
setting.value = config.chatbot.lang.default;
|
||||
setting.options = config.chatbot.lang.available;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +98,6 @@ export type SettingDict = { [group: string]: Setting[] };
|
||||
|
||||
export type Settings = {
|
||||
nlp_settings: {
|
||||
default_lang: string;
|
||||
languages: string[];
|
||||
threshold: string;
|
||||
provider: string;
|
||||
endpoint: string;
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { config } from '@/config';
|
||||
|
||||
import { SettingCreateDto } from '../dto/setting.dto';
|
||||
import { SettingType } from '../schemas/types';
|
||||
|
||||
@@ -67,26 +65,6 @@ export const settingModels: SettingCreateDto[] = [
|
||||
type: SettingType.text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'languages',
|
||||
value: [],
|
||||
options: [],
|
||||
type: SettingType.select,
|
||||
config: {
|
||||
multiple: true,
|
||||
allowCreate: true,
|
||||
},
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'default_lang',
|
||||
value: config.chatbot.lang.default,
|
||||
options: [], // NOTE : will be set onBeforeCreate from config
|
||||
type: SettingType.select,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'threshold',
|
||||
@@ -97,7 +75,7 @@ export const settingModels: SettingCreateDto[] = [
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
weight: 6,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import {
|
||||
installSettingFixtures,
|
||||
@@ -51,7 +51,7 @@ describe('SettingService', () => {
|
||||
SettingSeeder,
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -23,7 +23,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { installUserFixtures } from '@/utils/test/fixtures/user';
|
||||
import {
|
||||
@@ -69,6 +72,7 @@ describe('AuthController', () => {
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
],
|
||||
providers: [
|
||||
@@ -86,6 +90,8 @@ describe('AuthController', () => {
|
||||
PermissionRepository,
|
||||
InvitationRepository,
|
||||
InvitationService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
JwtService,
|
||||
{
|
||||
provide: MailerService,
|
||||
@@ -106,7 +112,7 @@ describe('AuthController', () => {
|
||||
EventEmitter2,
|
||||
ValidateAccountService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -20,7 +20,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import { installPermissionFixtures } from '@/utils/test/fixtures/permission';
|
||||
@@ -75,6 +78,7 @@ describe('UserController', () => {
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@@ -108,9 +112,11 @@ describe('UserController', () => {
|
||||
},
|
||||
AttachmentService,
|
||||
AttachmentRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
ValidateAccountService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('PermissionRepository', () => {
|
||||
let permissionRepository: PermissionRepository;
|
||||
let permissionModel: Model<Permission>;
|
||||
let permission: Permission;
|
||||
let permissionToDelete: Permission;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -59,6 +60,9 @@ describe('PermissionRepository', () => {
|
||||
permission = await permissionRepository.findOne({
|
||||
action: Action.CREATE,
|
||||
});
|
||||
permissionToDelete = await permissionRepository.findOne({
|
||||
action: Action.UPDATE,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -112,4 +116,36 @@ describe('PermissionRepository', () => {
|
||||
expect(result).toEqualPayload(permissionsWithRolesAndModels);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should delete a permission by id', async () => {
|
||||
jest.spyOn(permissionModel, 'deleteOne');
|
||||
const result = await permissionRepository.deleteOne(
|
||||
permissionToDelete.id,
|
||||
);
|
||||
|
||||
expect(permissionModel.deleteOne).toHaveBeenCalledWith({
|
||||
_id: permissionToDelete.id,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
acknowledged: true,
|
||||
deletedCount: 1,
|
||||
});
|
||||
|
||||
const permissions = await permissionRepository.find({
|
||||
role: permissionToDelete.id,
|
||||
});
|
||||
expect(permissions.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should fail to delete a permission that does not exist', async () => {
|
||||
expect(
|
||||
await permissionRepository.deleteOne(permissionToDelete.id),
|
||||
).toEqual({
|
||||
acknowledged: true,
|
||||
deletedCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ import { PermissionRepository } from '../repositories/permission.repository';
|
||||
import { RoleRepository } from '../repositories/role.repository';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { PermissionModel } from '../schemas/permission.schema';
|
||||
import { RoleModel, Role } from '../schemas/role.schema';
|
||||
import { UserModel, User } from '../schemas/user.schema';
|
||||
import { Role, RoleModel } from '../schemas/role.schema';
|
||||
import { User, UserModel } from '../schemas/user.schema';
|
||||
|
||||
describe('RoleRepository', () => {
|
||||
let roleRepository: RoleRepository;
|
||||
@@ -34,6 +34,7 @@ describe('RoleRepository', () => {
|
||||
let roleModel: Model<Role>;
|
||||
let role: Role;
|
||||
let users: User[];
|
||||
let roleToDelete: Role;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -57,6 +58,9 @@ describe('RoleRepository', () => {
|
||||
users = (await userRepository.findAll()).filter((user) =>
|
||||
user.roles.includes(role.id),
|
||||
);
|
||||
roleToDelete = await roleRepository.findOne({
|
||||
name: 'manager',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -106,4 +110,31 @@ describe('RoleRepository', () => {
|
||||
expect(result).toEqualPayload(rolesWithPermissionsAndUsers);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteOne', () => {
|
||||
it('should delete a role by id', async () => {
|
||||
jest.spyOn(roleModel, 'deleteOne');
|
||||
const result = await roleRepository.deleteOne(roleToDelete.id);
|
||||
|
||||
expect(roleModel.deleteOne).toHaveBeenCalledWith({
|
||||
_id: roleToDelete.id,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
acknowledged: true,
|
||||
deletedCount: 1,
|
||||
});
|
||||
|
||||
const permissions = await permissionRepository.find({
|
||||
role: roleToDelete.id,
|
||||
});
|
||||
expect(permissions.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should fail to delete a role that does not exist', async () => {
|
||||
expect(await roleRepository.deleteOne(roleToDelete.id)).toEqual({
|
||||
acknowledged: true,
|
||||
deletedCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ export class RoleRepository extends BaseRepository<
|
||||
*
|
||||
* @returns The result of the delete operation.
|
||||
*/
|
||||
async deleteOneQuery(id: string) {
|
||||
async deleteOne(id: string) {
|
||||
const result = await this.model.deleteOne({ _id: id }).exec();
|
||||
if (result.deletedCount > 0) {
|
||||
await this.permissionModel.deleteMany({ role: id });
|
||||
|
||||
@@ -100,6 +100,11 @@ export const modelModels: ModelCreateDto[] = [
|
||||
identity: 'subscriber',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
name: 'Language',
|
||||
identity: 'language',
|
||||
attributes: {},
|
||||
},
|
||||
{
|
||||
name: 'Translation',
|
||||
identity: 'translation',
|
||||
|
||||
@@ -16,7 +16,10 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ISendMailOptions, MailerService } from '@nestjs-modules/mailer';
|
||||
import { SentMessageInfo } from 'nodemailer';
|
||||
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
|
||||
import {
|
||||
@@ -55,6 +58,7 @@ describe('InvitationService', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
InvitationModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@@ -66,10 +70,12 @@ describe('InvitationService', () => {
|
||||
PermissionRepository,
|
||||
InvitationRepository,
|
||||
InvitationService,
|
||||
LanguageRepository,
|
||||
LanguageService,
|
||||
JwtService,
|
||||
Logger,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -17,7 +17,8 @@ import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
@@ -41,7 +42,8 @@ export class InvitationService extends BaseService<
|
||||
@Inject(JwtService) private readonly jwtService: JwtService,
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private logger: LoggerService,
|
||||
protected readonly i18n: ExtendedI18nService,
|
||||
protected readonly i18n: I18nService,
|
||||
public readonly languageService: LanguageService,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
@@ -63,6 +65,7 @@ export class InvitationService extends BaseService<
|
||||
const jwt = await this.sign(dto);
|
||||
if (this.mailerService) {
|
||||
try {
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'invitation.mjml',
|
||||
@@ -70,7 +73,7 @@ export class InvitationService extends BaseService<
|
||||
token: jwt,
|
||||
// TODO: Which language should we use?
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
this.i18n.t(key, { lang: defaultLanguage.code }),
|
||||
},
|
||||
subject: this.i18n.t('invitation_subject'),
|
||||
});
|
||||
|
||||
@@ -21,7 +21,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { installUserFixtures, users } from '@/utils/test/fixtures/user';
|
||||
import {
|
||||
@@ -52,6 +55,7 @@ describe('PasswordResetService', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@@ -62,6 +66,8 @@ describe('PasswordResetService', () => {
|
||||
AttachmentService,
|
||||
AttachmentRepository,
|
||||
RoleRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
PasswordResetService,
|
||||
JwtService,
|
||||
@@ -75,7 +81,7 @@ describe('PasswordResetService', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
|
||||
@@ -21,7 +21,8 @@ import { MailerService } from '@nestjs-modules/mailer';
|
||||
import { compareSync } from 'bcryptjs';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
@@ -34,7 +35,8 @@ export class PasswordResetService {
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private logger: LoggerService,
|
||||
private readonly userService: UserService,
|
||||
public readonly i18n: ExtendedI18nService,
|
||||
public readonly i18n: I18nService,
|
||||
public readonly languageService: LanguageService,
|
||||
) {}
|
||||
|
||||
public readonly jwtSignOptions: JwtSignOptions = {
|
||||
@@ -59,6 +61,7 @@ export class PasswordResetService {
|
||||
|
||||
if (this.mailerService) {
|
||||
try {
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'password_reset.mjml',
|
||||
@@ -66,7 +69,7 @@ export class PasswordResetService {
|
||||
token: jwt,
|
||||
first_name: user.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
this.i18n.t(key, { lang: defaultLanguage.code }),
|
||||
},
|
||||
subject: this.i18n.t('password_reset_subject'),
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
@@ -17,7 +18,10 @@ import { SentMessageInfo } from 'nodemailer';
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LanguageRepository } from '@/i18n/repositories/language.repository';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { installUserFixtures, users } from '@/utils/test/fixtures/user';
|
||||
import {
|
||||
@@ -46,6 +50,7 @@ describe('ValidateAccountService', () => {
|
||||
RoleModel,
|
||||
PermissionModel,
|
||||
AttachmentModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
@@ -56,6 +61,8 @@ describe('ValidateAccountService', () => {
|
||||
UserRepository,
|
||||
RoleService,
|
||||
RoleRepository,
|
||||
LanguageService,
|
||||
LanguageRepository,
|
||||
LoggerService,
|
||||
{
|
||||
provide: MailerService,
|
||||
@@ -69,11 +76,19 @@ describe('ValidateAccountService', () => {
|
||||
EventEmitter2,
|
||||
ValidateAccountService,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
validateAccountService = module.get<ValidateAccountService>(
|
||||
|
||||
@@ -18,7 +18,9 @@ import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
import { UserCreateDto } from '../dto/user.dto';
|
||||
@@ -35,7 +37,9 @@ export class ValidateAccountService {
|
||||
@Inject(JwtService) private readonly jwtService: JwtService,
|
||||
private readonly userService: UserService,
|
||||
@Optional() private readonly mailerService: MailerService | undefined,
|
||||
private readonly i18n: ExtendedI18nService,
|
||||
private logger: LoggerService,
|
||||
private readonly i18n: I18nService,
|
||||
private readonly languageService: LanguageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -73,17 +77,28 @@ export class ValidateAccountService {
|
||||
const confirmationToken = await this.sign({ email: dto.email });
|
||||
|
||||
if (this.mailerService) {
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'account_confirmation.mjml',
|
||||
context: {
|
||||
token: confirmationToken,
|
||||
first_name: dto.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: config.chatbot.lang.default }),
|
||||
},
|
||||
subject: this.i18n.t('account_confirmation_subject'),
|
||||
});
|
||||
try {
|
||||
const defaultLanguage = await this.languageService.getDefaultLanguage();
|
||||
await this.mailerService.sendMail({
|
||||
to: dto.email,
|
||||
template: 'account_confirmation.mjml',
|
||||
context: {
|
||||
token: confirmationToken,
|
||||
first_name: dto.first_name,
|
||||
t: (key: string) =>
|
||||
this.i18n.t(key, { lang: defaultLanguage.code }),
|
||||
},
|
||||
subject: this.i18n.t('account_confirmation_subject'),
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
'Could not send email',
|
||||
e.message,
|
||||
e.stack,
|
||||
'ValidateAccount',
|
||||
);
|
||||
throw new InternalServerErrorException('Could not send email');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export type TModel =
|
||||
| 'conversation'
|
||||
| 'message'
|
||||
| 'subscriber'
|
||||
| 'language'
|
||||
| 'translation'
|
||||
| 'botstats'
|
||||
| 'menu'
|
||||
|
||||
@@ -13,3 +13,7 @@ export const SETTING_CACHE_KEY = 'settings';
|
||||
export const PERMISSION_CACHE_KEY = 'permissions';
|
||||
|
||||
export const MENU_CACHE_KEY = 'menu';
|
||||
|
||||
export const LANGUAGES_CACHE_KEY = 'languages';
|
||||
|
||||
export const DEFAULT_LANGUAGE_CACHE_KEY = 'default_language';
|
||||
|
||||
9
api/src/utils/helpers/URL.ts
Normal file
9
api/src/utils/helpers/URL.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const buildURL = (baseUrl: string, relativePath: string): string => {
|
||||
try {
|
||||
const url = new URL(relativePath, baseUrl);
|
||||
|
||||
return url.toString();
|
||||
} catch {
|
||||
throw new Error(`Invalid base URL: ${baseUrl}`);
|
||||
}
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ArgumentMetadata,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import { TFilterQuery, Types } from 'mongoose';
|
||||
|
||||
import {
|
||||
@@ -36,9 +37,8 @@ export class SearchFilterPipe<T>
|
||||
}
|
||||
|
||||
private getRegexValue(val: string) {
|
||||
const quote = (str: string) =>
|
||||
str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
|
||||
return new RegExp(quote(val), 'i');
|
||||
const escapedRegExp = escapeRegExp(val);
|
||||
return new RegExp(escapedRegExp, 'i');
|
||||
}
|
||||
|
||||
private isAllowedField(field: string) {
|
||||
|
||||
33
api/src/utils/test/fixtures/language.ts
vendored
Normal file
33
api/src/utils/test/fixtures/language.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright © 2024 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import { LanguageUpdateDto } from '@/i18n/dto/language.dto';
|
||||
import { LanguageModel } from '@/i18n/schemas/language.schema';
|
||||
|
||||
export const languageFixtures: LanguageUpdateDto[] = [
|
||||
{
|
||||
title: 'English',
|
||||
code: 'en',
|
||||
isDefault: true,
|
||||
isRTL: false,
|
||||
},
|
||||
{
|
||||
title: 'Français',
|
||||
code: 'fr',
|
||||
isDefault: false,
|
||||
isRTL: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const installLanguageFixtures = async () => {
|
||||
const Language = mongoose.model(LanguageModel.name, LanguageModel.schema);
|
||||
return await Language.insertMany(languageFixtures);
|
||||
};
|
||||
6
api/src/utils/test/fixtures/nlpentity.ts
vendored
6
api/src/utils/test/fixtures/nlpentity.ts
vendored
@@ -25,12 +25,6 @@ export const nlpEntityFixtures: NlpEntityCreateDto[] = [
|
||||
doc: '',
|
||||
builtin: false,
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
lookups: ['trait'],
|
||||
doc: '',
|
||||
builtin: false,
|
||||
},
|
||||
{
|
||||
name: 'built_in',
|
||||
lookups: ['trait'],
|
||||
|
||||
16
api/src/utils/test/fixtures/nlpsample.ts
vendored
16
api/src/utils/test/fixtures/nlpsample.ts
vendored
@@ -13,23 +13,28 @@ import { NlpSampleCreateDto } from '@/nlp/dto/nlp-sample.dto';
|
||||
import { NlpSampleModel, NlpSample } from '@/nlp/schemas/nlp-sample.schema';
|
||||
import { NlpSampleState } from '@/nlp/schemas/types';
|
||||
|
||||
import { installLanguageFixtures } from './language';
|
||||
import { getFixturesWithDefaultValues } from '../defaultValues';
|
||||
import { TFixturesDefaultValues } from '../types';
|
||||
|
||||
const nlpSamples: NlpSampleCreateDto[] = [
|
||||
{
|
||||
text: 'yess',
|
||||
language: '0',
|
||||
},
|
||||
{
|
||||
text: 'No',
|
||||
language: '0',
|
||||
},
|
||||
{
|
||||
text: 'Hello',
|
||||
trained: true,
|
||||
language: '0',
|
||||
},
|
||||
{
|
||||
text: 'Bye Jhon',
|
||||
trained: true,
|
||||
language: '0',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -44,6 +49,15 @@ export const nlpSampleFixtures = getFixturesWithDefaultValues<NlpSample>({
|
||||
});
|
||||
|
||||
export const installNlpSampleFixtures = async () => {
|
||||
const languages = await installLanguageFixtures();
|
||||
|
||||
const NlpSample = mongoose.model(NlpSampleModel.name, NlpSampleModel.schema);
|
||||
return await NlpSample.insertMany(nlpSampleFixtures);
|
||||
return await NlpSample.insertMany(
|
||||
nlpSampleFixtures.map((v) => {
|
||||
return {
|
||||
...v,
|
||||
language: languages[parseInt(v.language)].id,
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
6
api/src/utils/test/fixtures/nlpvalue.ts
vendored
6
api/src/utils/test/fixtures/nlpvalue.ts
vendored
@@ -45,12 +45,6 @@ export const nlpValueFixtures: NlpValueCreateDto[] = [
|
||||
expressions: ['bye', 'bye bye'],
|
||||
builtin: true,
|
||||
},
|
||||
{
|
||||
entity: '2',
|
||||
value: 'en',
|
||||
expressions: [],
|
||||
builtin: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const installNlpValueFixtures = async () => {
|
||||
|
||||
4
api/src/utils/test/fixtures/translation.ts
vendored
4
api/src/utils/test/fixtures/translation.ts
vendored
@@ -9,8 +9,8 @@
|
||||
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import { TranslationUpdateDto } from '@/chat/dto/translation.dto';
|
||||
import { TranslationModel } from '@/chat/schemas/translation.schema';
|
||||
import { TranslationUpdateDto } from '@/i18n/dto/translation.dto';
|
||||
import { TranslationModel } from '@/i18n/schemas/translation.schema';
|
||||
|
||||
export const translationFixtures: TranslationUpdateDto[] = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user