Skip to content

Commit

Permalink
3213 paneuropean linked nodes shows wrong data 1 (#3218)
Browse files Browse the repository at this point in the history
* generate meta cache for all assessments

* refactor generate meta cache context

* use Objects.setInPath in member

* fix Objects.setInPath

* add cross cycle dependant/dependencies

* update jest config
  • Loading branch information
minotogna authored Nov 13, 2023
1 parent a3e98ad commit eedd721
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 125 deletions.
5 changes: 3 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ module.exports = {
testMatch: ['**/*.test.ts'],
moduleDirectories: ['node_modules'],
moduleNameMapper: {
'i18n/(.*)': '<rootDir>/i18n/$1',
'client/(.*)': '<rootDir>/client/$1',
'i18n/(.*)': '<rootDir>/i18n/$1',
'meta/(.*)': '<rootDir>/meta/$1',
'server/(.*)': '<rootDir>/server/$1',
'test/(.*)': '<rootDir>/test/$1',
'utils/objects(.*)': '<rootDir>/utils/objects$1',
'utils/arrays(.*)': '<rootDir>/utils/arrays$1',
'utils/dates(.*)': '<rootDir>/utils/dates$1',
'utils/functions(.*)': '<rootDir>/utils/functions$1',
'utils/numbers(.*)': '<rootDir>/utils/numbers$1',
'utils/objects(.*)': '<rootDir>/utils/objects$1',
'utils/promises(.*)': '<rootDir>/utils/promises$1',
'utils/strings(.*)': '<rootDir>/utils/strings$1',
'utils/uuids(.*)': '<rootDir>/utils/uuids$1',
},
Expand Down
13 changes: 7 additions & 6 deletions src/meta/assessment/assessmentMetaCache.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { AssessmentName } from './assessmentName'
import { CycleName } from './cycle'
import { VariableName } from './row'
import { TableName } from './table'
import { AssessmentName } from 'meta/assessment/assessmentName'
import { ColName } from 'meta/assessment/col'
import { CycleName } from 'meta/assessment/cycle'
import { VariableName } from 'meta/assessment/row'
import { TableName } from 'meta/assessment/table'

export interface VariableCache {
assessmentName?: AssessmentName
cycleName?: CycleName
tableName: TableName
variableName: VariableName
colName?: string // TODO: will colName become mandatory when handling dependencies by col ?
colName?: ColName // TODO: will colName become mandatory when handling dependencies by col ?
}

/**
Expand All @@ -24,7 +25,7 @@ export interface VariableCache {
*/
export type VariablesCache = Record<TableName, Record<VariableName, VariableCache>>

export type DependencyRecord = Record<string, Record<string, Array<VariableCache>>>
export type DependencyRecord = Record<TableName, Record<VariableName, Array<VariableCache>>>

export type DependencyCache = {
dependencies: DependencyRecord
Expand Down
13 changes: 3 additions & 10 deletions src/meta/expressionEvaluator/util/parseMemberVariable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { MemberExpression } from '@openforis/arena-core'

import { VariableCache } from 'meta/assessment'

const getExpressionDepth = (expressionNode: MemberExpression): number => {
let depth = 0
let currentExpressionNode = expressionNode
Expand All @@ -11,16 +13,7 @@ const getExpressionDepth = (expressionNode: MemberExpression): number => {
return depth
}

export const parseMemberVariable = (
expressionNode: MemberExpression
): {
tableName: string
variableName: string
colName?: string
assessmentName?: string
cycleName?: string
depth?: number
} => {
export const parseMemberVariable = (expressionNode: MemberExpression): VariableCache => {
const depth = getExpressionDepth(expressionNode)

switch (depth) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ExpressionContext } from '@openforis/arena-core'

import { AssessmentMetaCache, Row } from 'meta/assessment'
import { Assessment, AssessmentName, CycleName, RowCache } from 'meta/assessment'

export interface Context extends ExpressionContext {
assessmentMetaCache: AssessmentMetaCache
row: Row
tableName: string
assessments: Array<Assessment>
assessmentName: AssessmentName
cycleName: CycleName
row: RowCache
type: 'calculations' | 'validations'
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ExpressionNodeEvaluator, MemberExpression } from '@openforis/arena-core'
import { Objects } from 'utils/objects'

import { Row, VariableCache } from 'meta/assessment'
import { AssessmentMetaCaches, RowCache, VariableCache } from 'meta/assessment'
import { ExpressionEvaluator } from 'meta/expressionEvaluator'

import { Context } from './context'

const includesVariableCache = (variables: Array<VariableCache>, variable: VariableCache): boolean =>
const _includesVariableCache = (variables: Array<VariableCache>, variable: VariableCache): boolean =>
Boolean(
variables.find(
(v) =>
Expand All @@ -16,56 +17,69 @@ const includesVariableCache = (variables: Array<VariableCache>, variable: Variab
)
)

const excludeDependant = (row: Row, tableName: string, variableName: string): boolean =>
const _excludeDependant = (row: RowCache, tableName: string, variableName: string): boolean =>
Boolean(row.props?.dependantsExclude?.find((v) => v.tableName === tableName && v.variableName === variableName))

export class MemberEvaluator extends ExpressionNodeEvaluator<Context, MemberExpression> {
evaluate(expressionNode: MemberExpression): string {
const { assessmentMetaCache, row, tableName, type } = this.context

const memberVariable = ExpressionEvaluator.parseMemberVariable(expressionNode)

if (assessmentMetaCache.variablesByTable[memberVariable.tableName]) {
const dependantTable = assessmentMetaCache[type].dependants?.[memberVariable.tableName] ?? {}
const dependants = dependantTable[memberVariable.variableName] ?? []
const dependant: VariableCache = { variableName: row.props.variableName, tableName }
if (
!excludeDependant(row, memberVariable.tableName, memberVariable.variableName) &&
!includesVariableCache(dependants, dependant)
) {
assessmentMetaCache[type].dependants = {
...assessmentMetaCache[type].dependants,
[memberVariable.tableName]: {
...dependantTable,
[memberVariable.variableName]: [...dependants, dependant],
},
}
}
this.#addDependant(memberVariable)
this.#addDependency(memberVariable)

const dependencyTable = assessmentMetaCache[type].dependencies?.[tableName] ?? {}
const dependencies = dependencyTable[row.props.variableName] ?? []
const dependency: VariableCache = {
variableName: memberVariable.variableName,
tableName: memberVariable.tableName,
}
return `${memberVariable.tableName}.${memberVariable.variableName}`
}

#addDependant(variable: VariableCache): void {
const { assessments, assessmentName, cycleName, row, type } = this.context

const assessment = assessments.find((a) => a.props.name === (variable.assessmentName ?? assessmentName))
const cycle = assessment.cycles.find((c) => c.name === (variable.cycleName ?? cycleName))
const metaCache = AssessmentMetaCaches.getMetaCache({ assessment, cycle })
const variablesCache = AssessmentMetaCaches.getVariablesByTables({ assessment, cycle })

if (memberVariable.assessmentName && memberVariable.cycleName) {
dependency.assessmentName = memberVariable.assessmentName
dependency.cycleName = memberVariable.cycleName
if (variablesCache[variable.tableName] && !_excludeDependant(row, variable.tableName, variable.variableName)) {
const propsDependants = { assessment, cycle, tableName: variable.tableName, variableName: variable.variableName }
const dependants =
type === 'calculations'
? AssessmentMetaCaches.getCalculationsDependants(propsDependants)
: AssessmentMetaCaches.getValidationsDependants(propsDependants)
const dependant: VariableCache = {
assessmentName: assessmentName !== assessment.props.name ? assessmentName : undefined,
cycleName: cycleName !== cycle.name ? cycleName : undefined,
tableName: row.tableName,
variableName: row.props.variableName,
}

if (!includesVariableCache(dependencies, dependency)) {
assessmentMetaCache[type].dependencies = {
...assessmentMetaCache[type].dependencies,
[tableName]: {
...dependencyTable,
[row.props.variableName]: [...dependencies, dependency],
},
}
if (!_includesVariableCache(dependants, dependant)) {
const path = [type, 'dependants', variable.tableName, variable.variableName]
Objects.setInPath({ obj: metaCache, path, value: [...dependants, dependant] })
}
}
}

#addDependency(variable: VariableCache): void {
const { assessments, assessmentName, cycleName, row, type } = this.context

const assessment = assessments.find((a) => a.props.name === assessmentName)
const cycle = assessment.cycles.find((c) => c.name === cycleName)
const metaCache = AssessmentMetaCaches.getMetaCache({ assessment, cycle })
const variablesCache = AssessmentMetaCaches.getVariablesByTables({ assessment, cycle })
const { tableName } = row
const { variableName } = row.props

return `${tableName}.${memberVariable.variableName}`
// 1. parse internal dependencies
if (variablesCache[tableName]) {
const propsDependency = { assessment, cycle, tableName, variableName }
const dependencies =
type === 'calculations'
? AssessmentMetaCaches.getCalculationsDependencies(propsDependency)
: AssessmentMetaCaches.getValidationsDependencies(propsDependency)

if (!_includesVariableCache(dependencies, variable)) {
const path = [type, 'dependencies', tableName, variableName]
Objects.setInPath({ obj: metaCache, path, value: [...dependencies, variable] })
}
}
return `${memberVariable.tableName}.${memberVariable.variableName}`
}
}
124 changes: 76 additions & 48 deletions src/server/controller/assessment/generateMetaCache/index.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,100 @@
import { Assessment, AssessmentMetaCache, Cycle } from 'meta/assessment'
import { Objects } from 'utils/objects'
import { Promises } from 'utils/promises'

import { BaseProtocol } from 'server/db'
import { AssessmentMetaCache, AssessmentName, RowCache } from 'meta/assessment'

import { BaseProtocol, DB } from 'server/db'
import { AssessmentRepository } from 'server/repository/assessment/assessment'
import { RowRepository } from 'server/repository/assessment/row'
import { ValueAggregateRepository } from 'server/repository/assessmentCycle/valueAggregate'

import { DependencyEvaluator } from './dependencyEvaluator'

type Props = {
assessment: Assessment
cycle: Cycle
}
/**
* This method generates meta cache for all assessments
*/
export const generateMetaCache = async (client: BaseProtocol = DB): Promise<void> => {
// 1. init assessments meta cache and rows
const assessments = await AssessmentRepository.getAll({}, client)
const rows: Record<AssessmentName, Array<RowCache>> = {}
await Promises.each(assessments, async (assessment) => {
rows[assessment.props.name] = (await RowRepository.getManyCache({ assessment }, client)).filter(
(row) =>
Boolean(row.props.validateFns || row.props.calculateFn) ||
Boolean(row.cols.find((col) => Boolean(col.props.validateFns || col.props.calculateFn)))
)

export const generateMetaCache = async (props: Props, client: BaseProtocol): Promise<void> => {
const { assessment, cycle } = props
// init cycle meta cache
await Promises.each(assessment.cycles, async (cycle) => {
const [variables, valueAggregate] = await Promise.all([
RowRepository.getVariablesCache({ assessment, cycle }, client),
ValueAggregateRepository.getVariablesCache({ assessment, cycle }, client),
])
const metaCache: AssessmentMetaCache = {
calculations: { dependants: {}, dependencies: {} },
validations: { dependants: {}, dependencies: {} },
variablesByTable: { ...variables, ...valueAggregate },
}
Objects.setInPath({ obj: assessment, path: ['metaCache', cycle.uuid], value: metaCache })
})
})

const [variables, valueAggregate] = await Promise.all([
RowRepository.getVariablesCache({ assessment, cycle }, client),
ValueAggregateRepository.getVariablesCache({ assessment, cycle }, client),
])
// 2. generate assessments meta cache
assessments.forEach((assessment) => {
const assessmentName = assessment.props.name

const assessmentMetaCache: AssessmentMetaCache = {
calculations: { dependants: {}, dependencies: {} },
validations: { dependants: {}, dependencies: {} },
variablesByTable: { ...variables, ...valueAggregate },
}
assessment.cycles.forEach((cycle) => {
const cycleName = cycle.name

const rows = (await RowRepository.getManyCache({ assessment }, client)).filter(
(row) =>
Boolean(row.props.validateFns || row.props.calculateFn) ||
Boolean(row.cols.find((col) => Boolean(col.props.validateFns || col.props.calculateFn)))
)
rows[assessmentName].forEach((row) => {
const context = { assessments, assessmentName, cycleName, row }

rows.forEach(({ tableName, ...row }) => {
const context = { row, tableName, assessmentMetaCache }
if (row.props.calculateFn?.[cycle.uuid]) {
DependencyEvaluator.evalDependencies(row.props.calculateFn[cycle.uuid], { ...context, type: 'calculations' })
if (row.props.calculateIf?.[cycle.uuid]) {
DependencyEvaluator.evalDependencies(row.props.calculateIf[cycle.uuid], { ...context, type: 'calculations' })
}
} else {
row.cols.forEach((col) => {
if (col.props.calculateFn?.[cycle.uuid]) {
DependencyEvaluator.evalDependencies(col.props.calculateFn[cycle.uuid], { ...context, type: 'calculations' })
if (row.props.calculateFn?.[cycle.uuid]) {
DependencyEvaluator.evalDependencies(row.props.calculateFn[cycle.uuid], { ...context, type: 'calculations' })
if (row.props.calculateIf?.[cycle.uuid]) {
DependencyEvaluator.evalDependencies(row.props.calculateIf[cycle.uuid], {
...context,
type: 'calculations',
})
}
} else {
row.cols.forEach((col) => {
if (col.props.calculateFn?.[cycle.uuid]) {
DependencyEvaluator.evalDependencies(col.props.calculateFn[cycle.uuid], {
...context,
type: 'calculations',
})
}
})
}
})
}

if (row.props.validateFns?.[cycle.uuid]) {
row.props.validateFns[cycle.uuid].forEach((validateFn) =>
DependencyEvaluator.evalDependencies(validateFn, { ...context, type: 'validations' })
)
} else {
row.cols.forEach((col) => {
if (col.props.validateFns?.[cycle.uuid]) {
col.props.validateFns?.[cycle.uuid].forEach((validateFn) => {
if (row.props.validateFns?.[cycle.uuid]) {
row.props.validateFns[cycle.uuid].forEach((validateFn) =>
DependencyEvaluator.evalDependencies(validateFn, { ...context, type: 'validations' })
)
} else {
row.cols.forEach((col) => {
if (col.props.validateFns?.[cycle.uuid]) {
col.props.validateFns?.[cycle.uuid].forEach((validateFn) => {
DependencyEvaluator.evalDependencies(validateFn, { ...context, type: 'validations' })
})
}
})
}
})
}
})
})

return client.query(
`
await Promise.all(
assessments.map((assessment) =>
client.query<void>(
`
update assessment
set meta_cache = jsonb_set(meta_cache, '{${cycle.uuid}}', $1::jsonb)
set meta_cache = $1::jsonb
where id = $2
`,
[JSON.stringify(assessmentMetaCache), assessment.id]
[assessment.metaCache, assessment.id]
)
)
)
}
15 changes: 1 addition & 14 deletions src/tools/generateMetaCache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,12 @@ import { AssessmentController } from 'server/controller/assessment'
import { DB } from 'server/db'
import { Logger } from 'server/utils/logger'

const client = DB

const close = async () => {
await DB.$pool.end()
}

const exec = async () => {
const assessments = await AssessmentController.getAll({ metaCache: true }, client)
await Promise.all(
assessments.map((assessment) => {
Logger.debug(`\t---- Generating meta cache for assessment ${assessment.props.name}`)
return Promise.all(
assessment.cycles.map(async (cycle) => {
await AssessmentController.generateMetaCache({ assessment, cycle }, client)
Logger.debug(`\t\t----\tGenerated meta cache for cycle ${assessment.props.name}-${cycle.name}`)
})
)
})
)
await AssessmentController.generateMetaCache()
await close()
}

Expand Down
2 changes: 1 addition & 1 deletion src/utils/objects/setInPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const setInPath = (params: { obj: any; path: string[]; value: any; exclud
if (i === path.length - 1) {
objCurrent[pathPart] = value
} else {
if (!Object.prototype.hasOwnProperty.call(objCurrent, pathPart)) {
if (!objCurrent[pathPart]) {
objCurrent[pathPart] = {}
}
objCurrent = objCurrent[pathPart]
Expand Down

0 comments on commit eedd721

Please sign in to comment.