Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic translation config schema #7112

Merged
merged 33 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5a1a0dd
Automatic translation config schema
daneryl Aug 7, 2024
2231dbf
WIP, GetAutomaticTranslationConfig
daneryl Aug 7, 2024
4fe84aa
Wip, GetAutomaticTranslationConfig service
daneryl Aug 7, 2024
d439447
Filter out wrong configs
daneryl Aug 8, 2024
4a9d204
GetAutomaticTranslation filters languages.
daneryl Aug 8, 2024
65fc2cb
Typo and naming fix
RafaPolit Aug 8, 2024
22e0304
Moved service to externalIntegrations folder
RafaPolit Aug 8, 2024
0df2e4e
Fixed ESLint suggestion
RafaPolit Aug 8, 2024
53e784b
Create Models for the service and refined tests
RafaPolit Aug 8, 2024
e7c32e8
Fixed Typo
RafaPolit Aug 8, 2024
da3f43f
Moved logic to model and updated type definitions
RafaPolit Aug 8, 2024
12675fe
Implemented getByNames method.
RafaPolit Aug 8, 2024
a5ea146
Prepared script for Generating AT Config from Semantic data
RafaPolit Aug 8, 2024
ca2c9c9
commonProperties on the Template model
daneryl Aug 9, 2024
b1b5923
Fix Test
daneryl Aug 9, 2024
ed6a9cc
Remove AT config from settingsDS
daneryl Aug 9, 2024
1ddfff9
WIP Red test to implement invalid config test case
daneryl Aug 9, 2024
b165917
Merge remote-tracking branch 'origin/production' into 6950_AT_settings
daneryl Aug 12, 2024
566d3a4
AutomaticTranslationConfigDataSource fix
daneryl Aug 12, 2024
c9bd2cd
GenerateAutomaticTranslationConfig use case
daneryl Aug 12, 2024
65b02a7
generateAutomaticTranslation cli script
daneryl Aug 12, 2024
9ae446e
tenants tearDown, closes mongo model change stream
daneryl Aug 12, 2024
00d6164
Fixed type errors
daneryl Aug 12, 2024
6953fa5
WIP, input data shape validation
daneryl Aug 12, 2024
e18184d
Renamed to AT most of the internal module files.
RafaPolit Aug 12, 2024
6f55e24
Moved validator to own contract
RafaPolit Aug 12, 2024
61333ca
Changed Validator to return array of strings
RafaPolit Aug 12, 2024
a3c212e
Return all error params.
RafaPolit Aug 12, 2024
a6e8aed
Implemented typeguard for validator
RafaPolit Aug 13, 2024
7eec97c
Fixed prettier desync with ESLint
RafaPolit Aug 13, 2024
6ba7869
Fixed eslint errors
RafaPolit Aug 13, 2024
c79a333
Removed prettier exceptions
RafaPolit Aug 13, 2024
83080d0
Removed extra eslintrc rule.
RafaPolit Aug 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/api/entities.v2/database/MongoEntitiesDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { Entity, MetadataValue } from '../model/Entity';

export class MongoEntitiesDataSource
extends MongoDataSource<EntityDBO>
// eslint-disable-next-line prettier/prettier
implements EntitiesDataSource {
implements EntitiesDataSource
{
protected collectionName = 'entities';

private settingsDS: MongoSettingsDataSource;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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 { GenerateAutomaticTranslationsCofig } from './GenerateAutomaticTranslationConfig';
import { MongoATConfigDataSource } from './infrastructure/MongoATConfigDataSource';
import { AJVATConfigValidator } from './infrastructure/AJVATConfigValidator';

const AutomaticTranslationFactory = {
defaultGenerateATConfig() {
return new GenerateAutomaticTranslationsCofig(
new MongoATConfigDataSource(getConnection(), DefaultTransactionManager()),
new MongoTemplatesDataSource(getConnection(), DefaultTransactionManager()),
new AJVATConfigValidator()
);
},
};

export { AutomaticTranslationFactory };
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { TemplatesDataSource } from 'api/templates.v2/contracts/TemplatesDataSource';
import { ATConfigDataSource } from './contracts/ATConfigDataSource';
import { ATTemplateConfig } from './model/ATConfig';
import { RawATConfig } from './model/RawATConfig';
import { GenerateATConfigError, InvalidInputDataFormat } from './errors/generateATErrors';
import { ATConfigValidator } from './contracts/ATConfigValidator';
import { SemanticConfig } from './types/SemanticConfig';

export class GenerateAutomaticTranslationsCofig {
private atuomaticTranslationConfigDS: ATConfigDataSource;

private templatsDS: TemplatesDataSource;

private validator: ATConfigValidator;

constructor(
atuomaticTranslationConfigDS: ATConfigDataSource,
templatesDS: TemplatesDataSource,
validator: ATConfigValidator
) {
this.atuomaticTranslationConfigDS = atuomaticTranslationConfigDS;
this.templatsDS = templatesDS;
this.validator = validator;
}

async execute(semanticConfig: SemanticConfig | unknown) {
if (!this.validator.validate(semanticConfig)) {
throw new InvalidInputDataFormat(this.validator.getErrors()[0]);
}

const templatesData = await this.templatsDS
.getByNames(semanticConfig.templates.map(t => t.template))
.all();

const templates = semanticConfig.templates.map(configData => {
const templateData = templatesData.find(t => t.name === configData.template);
if (!templateData) {
throw new GenerateATConfigError(`Template not found: ${configData.template}`);
}
return new ATTemplateConfig(
templateData?.id,
(configData.properties || []).map(label => {
const foundProperty = templateData.properties.find(p => p.label === label);
if (!foundProperty) {
throw new GenerateATConfigError(`Property not found: ${label}`);
}
return foundProperty.id;
}),
(configData.commonProperties || []).map(label => {
const foundProperty = templateData?.commonProperties.find(p => p.label === label);
if (!foundProperty) {
throw new GenerateATConfigError(`Common property not found: ${label}`);
}
return foundProperty.id;
})
);
});

return this.atuomaticTranslationConfigDS.update(
new RawATConfig(semanticConfig.active, templates)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { RawATConfig } from '../model/RawATConfig';

export interface ATConfigDataSource {
get(): Promise<RawATConfig>;
update(config: RawATConfig): Promise<RawATConfig>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { SemanticConfig } from '../types/SemanticConfig';

export interface ATConfigValidator {
getErrors(): string[];
validate(data: unknown): data is SemanticConfig;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LanguageISO6391 } from 'shared/types/commonTypes';

export interface ATGateway {
supportedLanguages(): Promise<LanguageISO6391[]>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-disable max-classes-per-file */
export class GenerateATConfigError extends Error {}
export class InvalidInputDataFormat extends Error {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Ajv } from 'ajv';
import { JTDSchemaType } from 'ajv/dist/core';
import { ATConfigValidator } from '../contracts/ATConfigValidator';
import { SemanticConfig } from '../types/SemanticConfig';

const schema: JTDSchemaType<SemanticConfig> = {
additionalProperties: false,
properties: {
active: { type: 'boolean' },
templates: {
elements: {
properties: {
template: { type: 'string' },
},
optionalProperties: {
properties: { elements: { type: 'string' } },
commonProperties: { elements: { type: 'string' } },
},
},
},
},
};

export class AJVATConfigValidator implements ATConfigValidator {
private errors: string[] = [];

getErrors() {
return this.errors;
}

validate(data: unknown) {
const ajv = new Ajv({ strict: false });
const validate = ajv.compile<SemanticConfig>(schema);
const result = validate(data);
this.errors = validate.errors ? validate.errors?.map(e => JSON.stringify(e.params)) : [];
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { LanguageISO6391 } from 'shared/types/commonTypes';
import { ATGateway } from '../contracts/ATGateway';

export class ATExternalAPI implements ATGateway {
// eslint-disable-next-line class-methods-use-this
async supportedLanguages() {
const result: LanguageISO6391[] = ['ru', 'en', 'fr', 'es'];
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { MongoDataSource } from 'api/common.v2/database/MongoDataSource';
import { Settings as SettingsType } from 'shared/types/settingsType';
import { ATConfigDataSource } from '../contracts/ATConfigDataSource';
import { RawATConfig } from '../model/RawATConfig';
import { ATTemplateConfig } from '../model/ATConfig';

export class MongoATConfigDataSource
extends MongoDataSource<SettingsType>
implements ATConfigDataSource
{
protected collectionName = 'settings';

async get() {
const settings = await this.getCollection().findOne();
const config = settings?.features?.automaticTranslation ?? { active: false };

return new RawATConfig(
config.active,
(config.templates || []).map(
t => new ATTemplateConfig(t.template, t.properties || [], t.commonProperties)
)
);
}

async update(config: RawATConfig) {
await this.getCollection().findOneAndUpdate(
{},
{ $set: { 'features.automaticTranslation': config } }
);
return this.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* eslint-disable max-classes-per-file */
import { LanguageISO6391 } from 'shared/types/commonTypes';

class ATTemplateConfig {
readonly template: string;

readonly commonProperties: string[];

readonly properties: string[];

constructor(template: string, properties: string[], commonProperties: string[] = []) {
this.template = template;
this.commonProperties = commonProperties;
this.properties = properties;
}
}

class ATConfig {
readonly active: boolean;

readonly languages: LanguageISO6391[];

readonly templates: ATTemplateConfig[];

constructor(active: boolean, languages: LanguageISO6391[], templates: ATTemplateConfig[]) {
this.active = active;
this.languages = languages;
this.templates = templates.filter(t => t.commonProperties.length || t.properties.length);
}
}

export { ATConfig, ATTemplateConfig };
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { LanguageISO6391 } from 'shared/types/commonTypes';
import { Property } from 'api/templates.v2/model/Property';
import { ATConfig, ATTemplateConfig } from './ATConfig';

class RawATConfig {
readonly active: boolean;

readonly templates: ATTemplateConfig[];

constructor(active: boolean, templates: ATTemplateConfig[]) {
this.active = active;
this.templates = templates;
}

getCompleteConfig(languages: LanguageISO6391[], validProperties: Property[]): ATConfig {
const validPropertiesMap = validProperties.reduce(
(memo, property) => {
// eslint-disable-next-line no-param-reassign
memo[property.id] = property;
return memo;
},
{} as { [k: string]: Property }
);

const validPropertiesIds = Object.keys(validPropertiesMap);

const templates = (this.templates || []).map(
templateConfig =>
new ATTemplateConfig(
templateConfig.template,
(templateConfig.properties || []).filter(
propertyId =>
validPropertiesIds.includes(propertyId) &&
validPropertiesMap[propertyId].template === templateConfig.template
),
templateConfig.commonProperties
)
);

return new ATConfig(this.active, languages, templates);
}
}

export { RawATConfig };
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { SettingsDataSource } from 'api/settings.v2/contracts/SettingsDataSource';
import { TemplatesDataSource } from 'api/templates.v2/contracts/TemplatesDataSource';
import { ATGateway } from '../contracts/ATGateway';
import { ATConfigDataSource } from '../contracts/ATConfigDataSource';

export class GetAutomaticTranslationConfig {
private settings: SettingsDataSource;

private config: ATConfigDataSource;

private templates: TemplatesDataSource;

private automaticTranslation: ATGateway;

constructor(
settings: SettingsDataSource,
config: ATConfigDataSource,
templates: TemplatesDataSource,
automaticTranslation: ATGateway
) {
this.settings = settings;
this.config = config;
this.templates = templates;
this.automaticTranslation = automaticTranslation;
}

async execute() {
const config = await this.config.get();

const validProperties = await this.templates.getAllTextProperties().all();
const configuredLanguages = await this.settings.getLanguageKeys();
const supportedLanguages = await this.automaticTranslation.supportedLanguages();

return config.getCompleteConfig(
configuredLanguages.filter(languageKey => supportedLanguages.includes(languageKey)),
validProperties
);
}
}
Loading
Loading