Skip to content

Commit

Permalink
fixed category validator, validation of sibling items (#3690)
Browse files Browse the repository at this point in the history
Co-authored-by: Stefano Ricci <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 10, 2024
1 parent d4fcccc commit 2cb6ac9
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 35 deletions.
26 changes: 15 additions & 11 deletions core/objectUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,21 @@ export const toIndexedObj = (array, propNameOrExtractor) =>

export const toUuidIndexedObj = R.partialRight(toIndexedObj, [keys.uuid])

export const groupByProp = (propNameOrExtractor) => (items) =>
items.reduce((acc, item) => {
const prop = _getProp(propNameOrExtractor)(item)
let itemsByProp = acc[prop]
if (!itemsByProp) {
itemsByProp = []
acc[prop] = itemsByProp
}
itemsByProp.push(item)
return acc
}, {})
export const groupByProps =
(...propNamesOrExtractors) =>
(items) =>
items.reduce((acc, item) => {
const props = propNamesOrExtractors.map((propNameOrExtractor) => _getProp(propNameOrExtractor)(item))
let itemsPartial = R.path(props)(acc)
if (!itemsPartial) {
itemsPartial = []
}
itemsPartial.push(item)
setInPath(props, itemsPartial)(acc)
return acc
}, {})

export const groupByProp = groupByProps

export const clone = (obj) => (R.isNil(obj) ? obj : JSON.parse(JSON.stringify(obj)))

Expand Down
26 changes: 15 additions & 11 deletions server/modules/category/categoryValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ const validateLevels = async ({ category, itemsCache, bigCategory }) => {

// ====== ITEMS

const validateItemCodeUniqueness = (siblingsAndSelfByCode) => (_propName, item) => {
const isUnique = siblingsAndSelfByCode[CategoryItem.getCode(item)]?.length === 1
const validateItemCodeUniqueness = (itemsByParentAndCode) => (_propName, item) => {
const siblingItems = R.path([CategoryItem.getParentUuid(item), CategoryItem.getCode(item)])(itemsByParentAndCode)
const isUnique = siblingItems?.length === 1
return isUnique ? null : { key: Validation.messageKeys.categoryEdit.codeDuplicate }
}

Expand All @@ -68,11 +69,11 @@ const validateNotEmptyChildrenItems =
? { key: Validation.messageKeys.categoryEdit.childrenEmpty, severity: ValidationResult.severity.warning }
: null

const itemValidators = ({ isLeaf, siblingsAndSelfByCode, childrenCount = 0 }) => ({
const itemValidators = ({ isLeaf, itemsByParentAndCode, childrenCount = 0 }) => ({
[`${CategoryItem.keys.props}.${CategoryItem.keysProps.code}`]: [
Validator.validateRequired(Validation.messageKeys.categoryEdit.codeRequired),
Validator.validateNotKeyword(Validation.messageKeys.categoryEdit.codeCannotBeKeyword),
validateItemCodeUniqueness(siblingsAndSelfByCode),
validateItemCodeUniqueness(itemsByParentAndCode),
],
[keys.children]: [validateNotEmptyChildrenItems({ isLeaf, childrenCount })],
})
Expand Down Expand Up @@ -182,15 +183,15 @@ const validateItemsAndDescendants = async ({
const pushItems = (items) => {
// Group sibling items by code to optimize item code uniqueness check
// do it only one time for every sibling
const siblingsAndSelfByCode = ObjectUtils.groupByProp(CategoryItem.getCode)(items)
const itemsByParentAndCode = ObjectUtils.groupByProps(CategoryItem.getParentUuid, CategoryItem.getCode)(items)
items.forEach((item) => {
item.siblingsAndSelfByCode = siblingsAndSelfByCode
item.itemsByParentAndCode = itemsByParentAndCode
stack.push(item)
})
}

const popItem = (item) => {
delete item['siblingsAndSelfByCode']
delete item['itemsByParentAndCode']
stack.pop()
processed++
onProgress?.({ total, processed })
Expand All @@ -200,7 +201,7 @@ const validateItemsAndDescendants = async ({

while (!R.isEmpty(stack) && !stopIfFn?.()) {
const item = stack[stack.length - 1] // Do not pop item: it can be visited again
const { siblingsAndSelfByCode } = item
const { itemsByParentAndCode } = item
const itemUuid = CategoryItem.getUuid(item)
const isLeaf = Category.isItemLeaf(item)(category)
const itemChildren = itemsCache.getItemChildren(itemUuid)
Expand All @@ -214,7 +215,7 @@ const validateItemsAndDescendants = async ({
/* eslint-disable no-await-in-loop */
validation = await Validator.validate(
item,
itemValidators({ isLeaf, siblingsAndSelfByCode, childrenCount }),
itemValidators({ isLeaf, itemsByParentAndCode, childrenCount }),
false
)
validation = _validateItemExtraProps({ extraDefs, validation, srsIndex })(item)
Expand Down Expand Up @@ -294,7 +295,10 @@ export const validateCategory = async ({

export const validateItems = async ({ category, itemsToValidate, itemsCountByItemUuid }) => {
const prevValidation = Category.getValidation(category)
const siblingsAndSelfByCode = ObjectUtils.groupByProp(CategoryItem.getCode)(itemsToValidate)
const itemsByParentAndCode = ObjectUtils.groupByProps(
CategoryItem.getParentUuid,
CategoryItem.getCode
)(itemsToValidate)
const prevItemsValidation = Validation.getFieldValidation(keys.items)(prevValidation)
let itemsValidationUpdated = prevItemsValidation

Expand All @@ -305,7 +309,7 @@ export const validateItems = async ({ category, itemsToValidate, itemsCountByIte

const itemValidation = await Validator.validate(
item,
itemValidators({ isLeaf, siblingsAndSelfByCode, childrenCount })
itemValidators({ isLeaf, itemsByParentAndCode, childrenCount })
)
itemsValidationUpdated = Validation.assocFieldValidation(itemUuid, itemValidation)(itemsValidationUpdated)
}
Expand Down
11 changes: 6 additions & 5 deletions server/modules/category/manager/categoryManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,12 @@ const _afterItemUpdate = async ({ surveyId, categoryUuid, itemUuid, prevItem },
}
const itemsToValidate = []
for await (const code of codes) {
const itemsCount = await CategoryRepository.countItemsByLevelAndCode({ surveyId, levelUuid, code, draft }, client)
if (itemsCount <= Category.maxCategoryItemsInIndex) {
itemsToValidate.push(
...(await CategoryRepository.fetchItemsByLevelAndCode({ surveyId, levelUuid, code, draft }, client))
)
const items = await CategoryRepository.fetchItemsByLevelParentAndCode(
{ surveyId, levelUuid, parentUuid, code, draft },
client
)
if (items.length > 0 && items.length <= Category.maxCategoryItemsInIndex) {
itemsToValidate.push(...items)
}
}
const addItemToValidate = (item) => {
Expand Down
24 changes: 16 additions & 8 deletions server/modules/category/repository/categoryRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,33 +250,41 @@ export const fetchItemsByCategoryUuid = async (
return backup || draft ? items : R.filter((item) => item.published)(items)
}

const getWhereConditionItemsWithLevelAndCode = ({ draft, tableAlias = 'i' }) => {
const getWhereConditionItemsWithLevelParentAndCode = ({ draft, parentUuid = null, tableAlias = 'i' }) => {
const codeColumn = DbUtils.getPropColCombined(CategoryItem.keysProps.code, draft, `${tableAlias}.`, true)
return `${tableAlias}.level_uuid = $/levelUuid/ AND COALESCE(${codeColumn}, '') = $/code/`
return `${tableAlias}.level_uuid = $/levelUuid/
AND parent_uuid ${parentUuid ? '= $/parentUuid/' : ' IS NULL'}
AND COALESCE(${codeColumn}, '') = $/code/`
}

export const countItemsByLevelAndCode = async ({ surveyId, levelUuid, code, draft = false }, client = db) => {
export const countItemsByLevelParentAndCode = async (
{ surveyId, levelUuid, parentUuid, code, draft = false },
client = db
) => {
const schema = Schemata.getSchemaSurvey(surveyId)
const row = await client.one(
`SELECT COUNT(*)
FROM ${schema}.category_item i
WHERE ${getWhereConditionItemsWithLevelAndCode({ draft })}
WHERE ${getWhereConditionItemsWithLevelParentAndCode({ parentUuid, draft })}
`,
{ levelUuid, code: Strings.defaultIfEmpty('')(code) }
{ levelUuid, parentUuid, code: Strings.defaultIfEmpty('')(code) }
)
return Number(row.count)
}

export const fetchItemsByLevelAndCode = async ({ surveyId, levelUuid, code, draft = false }, client = db) => {
export const fetchItemsByLevelParentAndCode = async (
{ surveyId, levelUuid, parentUuid, code, draft = false },
client = db
) => {
const schema = Schemata.getSchemaSurvey(surveyId)
const items = await client.map(
`
SELECT i.*
FROM ${schema}.category_item i
WHERE ${getWhereConditionItemsWithLevelAndCode({ draft })}
WHERE ${getWhereConditionItemsWithLevelParentAndCode({ parentUuid, draft })}
ORDER BY i.id
`,
{ levelUuid, code: Strings.defaultIfEmpty('')(code) },
{ levelUuid, parentUuid, code: Strings.defaultIfEmpty('')(code) },
(def) => DB.transformCallback(def, draft, true)
)

Expand Down

0 comments on commit 2cb6ac9

Please sign in to comment.