diff --git a/app/api/externalIntegrations.v2/automaticTranslation/AutomaticTranslationFactory.ts b/app/api/externalIntegrations.v2/automaticTranslation/AutomaticTranslationFactory.ts index 80433d762c..1e9b7f2188 100644 --- a/app/api/externalIntegrations.v2/automaticTranslation/AutomaticTranslationFactory.ts +++ b/app/api/externalIntegrations.v2/automaticTranslation/AutomaticTranslationFactory.ts @@ -1,9 +1,13 @@ -import { DefaultTransactionManager } from 'api/common.v2/database/data_source_defaults'; import { getConnection } from 'api/common.v2/database/getConnectionForCurrentTenant'; import { MongoTemplatesDataSource } from 'api/templates.v2/database/MongoTemplatesDataSource'; +import { DefaultTransactionManager } from 'api/common.v2/database/data_source_defaults'; +import { DefaultTemplatesDataSource } from 'api/templates.v2/database/data_source_defaults'; +import { DefaultEntitiesDataSource } from 'api/entities.v2/database/data_source_defaults'; import { GenerateAutomaticTranslationsCofig } from './GenerateAutomaticTranslationConfig'; import { MongoATConfigDataSource } from './infrastructure/MongoATConfigDataSource'; import { AJVATConfigValidator } from './infrastructure/AJVATConfigValidator'; +import { SaveEntityTranslations } from './SaveEntityTranslations'; +import { AJVTranslationResultValidator } from './infrastructure/AJVTranslationResultValidator'; const AutomaticTranslationFactory = { defaultGenerateATConfig() { @@ -13,6 +17,15 @@ const AutomaticTranslationFactory = { new AJVATConfigValidator() ); }, + + defaultSaveEntityTranslations() { + const transactionManager = DefaultTransactionManager(); + return new SaveEntityTranslations( + DefaultTemplatesDataSource(transactionManager), + DefaultEntitiesDataSource(transactionManager), + new AJVTranslationResultValidator() + ); + }, }; export { AutomaticTranslationFactory }; diff --git a/app/api/externalIntegrations.v2/automaticTranslation/adapters/driving/ATServiceListener.ts b/app/api/externalIntegrations.v2/automaticTranslation/adapters/driving/ATServiceListener.ts new file mode 100644 index 0000000000..744a98cee8 --- /dev/null +++ b/app/api/externalIntegrations.v2/automaticTranslation/adapters/driving/ATServiceListener.ts @@ -0,0 +1,36 @@ +import { tenants } from 'api/tenants'; +import { TaskManager } from 'api/services/tasksmanager/TaskManager'; +import { ATTranslationResultValidator } from '../../contracts/ATTranslationResultValidator'; +import { AJVTranslationResultValidator } from '../../infrastructure/AJVTranslationResultValidator'; +import { InvalidATServerResponse } from '../../errors/generateATErrors'; +import { AutomaticTranslationFactory } from '../../AutomaticTranslationFactory'; + +export class ATServiceListener { + static SERVICE_NAME = 'AutomaticTranslation'; + + private taskManager: TaskManager; + + constructor(ATFactory: typeof AutomaticTranslationFactory = AutomaticTranslationFactory) { + const validator: ATTranslationResultValidator = new AJVTranslationResultValidator(); + this.taskManager = new TaskManager({ + serviceName: ATServiceListener.SERVICE_NAME, + processResults: async result => { + if (!validator.validate(result)) { + throw new InvalidATServerResponse(validator.getErrors()[0]); + } + + await tenants.run(async () => { + await ATFactory.defaultSaveEntityTranslations().execute(result); + }, result.key[0]); + }, + }); + } + + start(interval = 500) { + this.taskManager.subscribeToResults(interval); + } + + async stop() { + await this.taskManager.stop(); + } +} diff --git a/app/api/externalIntegrations.v2/automaticTranslation/adapters/driving/specs/ATServiceListener.spec.ts b/app/api/externalIntegrations.v2/automaticTranslation/adapters/driving/specs/ATServiceListener.spec.ts new file mode 100644 index 0000000000..aa74f45a1c --- /dev/null +++ b/app/api/externalIntegrations.v2/automaticTranslation/adapters/driving/specs/ATServiceListener.spec.ts @@ -0,0 +1,98 @@ +import { config } from 'api/config'; +import { tenants } from 'api/tenants'; +import Redis from 'redis'; +import RedisSMQ from 'rsmq'; +import waitForExpect from 'wait-for-expect'; +import { AutomaticTranslationFactory } from 'api/externalIntegrations.v2/automaticTranslation/AutomaticTranslationFactory'; +import { testingEnvironment } from 'api/utils/testingEnvironment'; +import { GenerateAutomaticTranslationsCofig } from 'api/externalIntegrations.v2/automaticTranslation/GenerateAutomaticTranslationConfig'; +import { SaveEntityTranslations } from 'api/externalIntegrations.v2/automaticTranslation/SaveEntityTranslations'; +import { ATServiceListener } from '../ATServiceListener'; + +const prepareATFactory = (executeSpy: jest.Mock) => { + const ATFactory: typeof AutomaticTranslationFactory = { + defaultGenerateATConfig() { + return {} as GenerateAutomaticTranslationsCofig; + }, + defaultSaveEntityTranslations() { + return { execute: executeSpy } as unknown as SaveEntityTranslations; + }, + }; + + return ATFactory; +}; + +describe('ATServiceListener', () => { + let listener: ATServiceListener; + let redisClient: Redis.RedisClient; + let redisSMQ: RedisSMQ; + let executeSpy: jest.Mock; + + beforeEach(async () => { + await testingEnvironment.setUp({ + settings: [{ features: { automaticTranslation: { active: true } } }], + }); + await testingEnvironment.setTenant('tenant'); + + executeSpy = jest.fn(); + + listener = new ATServiceListener(prepareATFactory(executeSpy)); + const redisUrl = `redis://${config.redis.host}:${config.redis.port}`; + redisClient = Redis.createClient(redisUrl); + redisSMQ = new RedisSMQ({ client: redisClient }); + + const recreateQueue = async (queueName: string): Promise => { + try { + await redisSMQ.getQueueAttributesAsync({ qname: queueName }); + await redisSMQ.deleteQueueAsync({ qname: queueName }); + } catch (error: any) { + if (error.name === 'queueNotFound') { + // No action needed + } else { + throw error; + } + } + + await redisSMQ.createQueueAsync({ qname: queueName }); + }; + + await recreateQueue('AutomaticTranslation_results').catch(error => { + throw error; + }); + + listener.start(0); + }); + + afterAll(async () => { + redisClient.end(true); + await listener.stop(); + await testingEnvironment.tearDown(); + }); + + describe('Save Translations', () => { + it('should call on saveEntityTranslations after validating the result', async () => { + const message = { + key: ['tenant', 'sharedId', 'propName'], + text: 'original text', + language_from: 'en', + languages_to: ['es'], + translations: [ + { text: 'texto traducido', language: 'es', success: true, error_message: '' }, + ], + }; + + executeSpy.mockClear(); + + await redisSMQ.sendMessageAsync({ + qname: 'AutomaticTranslation_results', + message: JSON.stringify(message), + }); + + await waitForExpect(async () => { + await tenants.run(async () => { + expect(executeSpy).toHaveBeenCalledWith(message); + }, 'tenant'); + }); + }); + }); +}); diff --git a/app/api/externalIntegrations.v2/automaticTranslation/errors/generateATErrors.ts b/app/api/externalIntegrations.v2/automaticTranslation/errors/generateATErrors.ts index dbe3da8275..7c4545ccae 100644 --- a/app/api/externalIntegrations.v2/automaticTranslation/errors/generateATErrors.ts +++ b/app/api/externalIntegrations.v2/automaticTranslation/errors/generateATErrors.ts @@ -1,3 +1,4 @@ /* eslint-disable max-classes-per-file */ export class GenerateATConfigError extends Error {} export class InvalidInputDataFormat extends Error {} +export class InvalidATServerResponse extends Error {}