Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 1x 1x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 5x 5x 5x 5x 5x 5x 5x 5x 9x 9x 1x 1x 9x 9x 4x 4x 9x 9x 11x 11x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 20x 20x 20x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 9x 9x 9x 9x 9x 1x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 1x | import { Injectable } from '@nestjs/common';
import { escape, mapValues, reduce } from 'lodash-es';
import { Brackets, In, Like, type QueryRunner } from 'typeorm';
import { formulaBuilderToAmaliaFormula } from '@amalia/amalia-lang/formula/converter/shared';
import { type AmaliaFormula } from '@amalia/amalia-lang/formula/types';
import {
Filter,
getSchema,
PlanForecast,
Relationship,
Rule,
RuleAssignment,
Variable,
type Company,
type CustomObjectDefinition,
} from '@amalia/core/models';
import { assert } from '@amalia/ext/typescript';
import {
type PlanRuleFieldToDisplay,
type PlanRuleFilterDisplayConfiguration,
} from '@amalia/payout-definition/plans/types';
import { replaceInExpression, replaceInFormulaBuilder } from './replace';
@Injectable()
export class DesignerService {
/**
* Given the old machine name of anything (variable, filter, customObjectDefinition...), replace
* in formula with the new name.
*
* A parent query runner is mandatory since we'd want either to rename all of them or none of them.
*
* @param company
* @param oldMachineName
* @param newMachineName
* @param parentQueryRunner
*/
public async changeMachineName(
company: Company,
oldMachineName: string,
newMachineName: string,
parentQueryRunner: QueryRunner,
): Promise<void> {
if (!oldMachineName || !newMachineName || oldMachineName === newMachineName) {
return;
}
// Get all objects that may eventually be concerned by the renaming. Here we're doing a simple LIKE because
// we don't really care if we match too many things, they will not be renamed in the regex anyway.
const [filters, relationships, rules, variables] = await Promise.all([
parentQueryRunner.manager
.getRepository(Filter)
.findBy({ company: { id: company.id }, condition: Like(`%${escape(oldMachineName)}%` as AmaliaFormula) }),
parentQueryRunner.manager
.getRepository(Relationship)
.findBy({ company: { id: company.id }, condition: Like(`%${escape(oldMachineName)}%` as AmaliaFormula) }),
parentQueryRunner.manager
.getRepository(Rule)
.findBy({ company: { id: company.id }, formula: Like(`%${escape(oldMachineName)}%`) }),
parentQueryRunner.manager
.getRepository(Variable)
.findBy({ company: { id: company.id }, formula: Like(`%${escape(oldMachineName)}%`) }),
]);
// Now we take those elements one by one and eventually replace in formulas.
const newFilters = filters.map((filter) => {
const newBuilder = {
root: replaceInFormulaBuilder(filter.formulaBuilder.root, oldMachineName, newMachineName),
};
return {
...filter,
formulaBuilder: newBuilder,
condition: formulaBuilderToAmaliaFormula(newBuilder),
};
});
const newRelationships = relationships.map((r) => ({
...r,
condition: r.condition && replaceInExpression(r.condition, oldMachineName, newMachineName),
}));
const newRules = rules.map((r) => ({
...r,
formula: replaceInExpression(r.formula, oldMachineName, newMachineName),
}));
const newVariables = variables.map((v) => ({
...v,
formula: v.formula && replaceInExpression(v.formula, oldMachineName, newMachineName),
}));
// And finally save them all.
await Promise.all([
parentQueryRunner.manager.getRepository(Filter).save(newFilters),
parentQueryRunner.manager.getRepository(Relationship).save(newRelationships),
parentQueryRunner.manager.getRepository(Rule).save(newRules),
parentQueryRunner.manager.getRepository(Variable).save(newVariables),
]);
// Update also the formulas in the forecasts configurations
await this.updateMachineNameInForecastConfig(parentQueryRunner, company, oldMachineName, newMachineName);
}
/**
* Used to update plan configuration when a field changes of name
* @param company
* @param oldMachineName
* @param newMachineName
* @param parentQueryRunner
* @param customObjectDefinition
*/
public async onChangeFieldMachineName(
company: Company,
oldMachineName: string,
newMachineName: string,
parentQueryRunner: QueryRunner,
customObjectDefinition: CustomObjectDefinition,
) {
if (!oldMachineName || !newMachineName || oldMachineName === newMachineName) {
return;
}
const oldMachineNameWithoutDefinition = oldMachineName.split('.')[1];
const newMachineNameWithoutDefinition = newMachineName.split('.')[1];
if (!oldMachineNameWithoutDefinition || !newMachineNameWithoutDefinition) {
return;
}
const ruleAssignments = await parentQueryRunner.manager
.getRepository(RuleAssignment)
.findBy({ company: { id: company.id } });
const allFilters = await parentQueryRunner.manager
.getRepository(Filter)
.find({ where: { company: { id: company.id } }, relations: ['object'] });
const newRuleAssignments = ruleAssignments.map((oldRa: RuleAssignment) => ({
...oldRa,
filtersToDisplay: (oldRa.filtersToDisplay ?? []).map((oldFtd: PlanRuleFilterDisplayConfiguration) => {
// Find the filter definition
const oldFtdFilter = allFilters.find((f) => f.id === oldFtd.id);
// Check if the filter to display is on the same definition than the field itself. If it's not the case, we don't update it.
const oldFtdOnSameDefinition = oldFtdFilter?.object?.id === customObjectDefinition.id;
// Replicate everything. If old filter to display does not contain the field, copy as it is
const oldFtdContainsField = oldFtd.fieldsToDisplay.find((f) => f.name === oldMachineNameWithoutDefinition);
if (!oldFtdContainsField || !oldFtdOnSameDefinition) {
return oldFtd;
}
// Otherwise, remove it, then add a new one that has the same displayStatus as the old one
return {
...oldFtd,
fieldsToDisplay: oldFtd.fieldsToDisplay.map((f: PlanRuleFieldToDisplay) => {
// If it's not the field, just copy it
if (f.name !== oldMachineNameWithoutDefinition) {
return f;
}
// Otherwise, replace it with the new machineName, and with the same displayStatus
return {
name: newMachineNameWithoutDefinition,
displayStatus: oldFtdContainsField.displayStatus,
};
}),
};
}),
}));
await parentQueryRunner.manager.getRepository(RuleAssignment).save(newRuleAssignments);
// Rename also the fields in the forecasts objects to display
await this.updateFieldMachineNameInForecastObjectToDisplay(
parentQueryRunner,
company,
oldMachineNameWithoutDefinition,
newMachineNameWithoutDefinition,
);
}
/**
* Replaces all occurrences of the `oldName` string with the `newName` string in the values of the given object `obj`.
* @param obj - The object whose values should be replaced.
* @param oldName - The string to search for and replace.
* @param newName - The string to replace `oldName` with.
* @returns A new object with the same keys as `obj`, but with the values replaced as described above.
*/
private replaceExpressionInForecastConfig(
obj: Record<string, string>,
oldName: string,
newName: string,
): Record<string, string> {
return reduce(
obj,
(result, value, key) => {
result[key] = replaceInExpression(value, oldName, newName);
return result;
},
{} as Record<string, string>,
);
}
/**
* Updates the `forecastConfig` object of all `PlanForecast` that belong to the given `company` and contain the `oldMachineName`.
* @param queryRunner - The query runner to use for the database transaction.
* @param company - The company whose `PlanForecast` objects should be updated.
* @param oldMachineName - The string to search for and replace.
* @param newMachineName - The string to replace `oldMachineName` with.
* @returns
*/
private async updateMachineNameInForecastConfig(
queryRunner: QueryRunner,
company: Company,
oldMachineName: string,
newMachineName: string,
) {
// Get all plan forecasts that use the old machine name
const planForecastsToUpdate: Pick<PlanForecast, 'forecastConfig' | 'id'>[] = await queryRunner.manager
.getRepository(PlanForecast)
.createQueryBuilder('planForecast')
.select('planForecast.id, planForecast.forecastConfig')
.where('planForecast.companyId = :companyId', { companyId: company.id })
.andWhere(
new Brackets((qb) => {
qb.where("planForecast.forecastConfig ->> 'kpis'::text LIKE :oldMachineName", {
oldMachineName: `%${escape(oldMachineName)}%`,
})
.orWhere("planForecast.forecastConfig ->> 'fields'::text LIKE :oldMachineName", {
oldMachineName: `%${escape(oldMachineName)}%`,
})
.orWhere("planForecast.forecastConfig ->> 'datasets'::text LIKE :oldMachineName", {
oldMachineName: `%${escape(oldMachineName)}%`,
});
}),
)
.getRawMany();
// Replace the old machine name by the new one
const planForecastsUpdated = planForecastsToUpdate.map((pf) => {
assert(pf.forecastConfig, 'Forecast config not present');
return {
...pf,
forecastConfig: {
kpis: this.replaceExpressionInForecastConfig(pf.forecastConfig.kpis || {}, oldMachineName, newMachineName),
datasets: this.replaceExpressionInForecastConfig(
pf.forecastConfig.datasets || {},
oldMachineName,
newMachineName,
),
fields: this.replaceExpressionInForecastConfig(
pf.forecastConfig.fields || {},
oldMachineName,
newMachineName,
),
},
};
});
await Promise.all(
planForecastsUpdated.map((pf) => queryRunner.manager.getRepository(PlanForecast).update(pf.id, pf)),
);
}
private async updateFieldMachineNameInForecastObjectToDisplay(
queryRunner: QueryRunner,
company: Company,
nameToReplace: string,
newName: string,
) {
const planForecastUsingThisField = await queryRunner.manager.query<{ plan_forecast_id: string }[]>(
`
SELECT *
FROM (
SELECT
pf.id as plan_forecast_id,
jsonb_array_elements(jsonb_array_elements(value -> 'filtersToDisplay') -> 'fieldsToDisplay') ->> 'name' as field
FROM ${getSchema(queryRunner.connection)}.plan_forecast pf, lateral jsonb_each("objectsToDisplay")
WHERE pf."companyId" = $1
) planFWithField
WHERE planFWithField.field = $2;
`,
[company.id, nameToReplace],
);
const planForecastRepository = queryRunner.manager.getRepository(PlanForecast);
const planForecastsToEdit = await planForecastRepository.findBy({
id: In(planForecastUsingThisField.map((pf) => pf.plan_forecast_id)),
});
const newPlanForecasts = planForecastsToEdit.map((pf) => ({
...pf,
objectsToDisplay: mapValues(pf.objectsToDisplay, (ruleConfig) => ({
...ruleConfig,
filtersToDisplay: (ruleConfig.filtersToDisplay ?? []).map((filterToDisplay) => ({
...filterToDisplay,
fieldsToDisplay: filterToDisplay.fieldsToDisplay.map((fieldToDisplay) => ({
...fieldToDisplay,
name: fieldToDisplay.name === nameToReplace ? newName : fieldToDisplay.name,
})),
})),
})),
}));
await planForecastRepository.save(newPlanForecasts);
}
}
|