diff --git a/backend/src/v5/models/tickets.constants.js b/backend/src/v5/models/tickets.constants.js index 55e8c0d6db..a4bb2b9b59 100644 --- a/backend/src/v5/models/tickets.constants.js +++ b/backend/src/v5/models/tickets.constants.js @@ -43,16 +43,25 @@ Tickets.operatorToQuery = { }) }, }), [queryOperators.NOT_EQUALS]: (propertyName, value) => ({ - [propertyName]: { $not: { $in: value.flatMap((v) => { - if (isNumberString(v)) return [Number(v), new Date(Number(v))]; - return toBoolean(v); - }) } }, - }), + $or: [ + { [propertyName]: { $not: { $in: value.flatMap((v) => { + if (isNumberString(v)) return [Number(v), new Date(Number(v))]; + return toBoolean(v); + }) } } }, + { [propertyName]: { $exists: false } }, + ], + } + ), [queryOperators.CONTAINS]: (propertyName, value) => ({ $or: value.map((val) => ({ [propertyName]: { $regex: val, $options: 'i' } })), }), [queryOperators.NOT_CONTAINS]: (propertyName, value) => ({ - $nor: value.map((val) => ({ [propertyName]: { $regex: val, $options: 'i' } })), + $or: [ + { $nor: value.map((val) => ({ + [propertyName]: { $regex: val, $options: 'i' }, + })) }, + { [propertyName]: { $exists: false } }, + ], }), [queryOperators.RANGE]: (propertyName, value) => ({ $or: value.flatMap((range) => [ @@ -61,10 +70,15 @@ Tickets.operatorToQuery = { ]), }), [queryOperators.NOT_IN_RANGE]: (propertyName, value) => ({ - $nor: value.flatMap((range) => [ - { [propertyName]: { $gte: range[0], $lte: range[1] } }, - { [propertyName]: { $gte: new Date(range[0]), $lte: new Date(range[1]) } }, - ]), + $or: [ + { + $nor: value.flatMap((range) => [ + { [propertyName]: { $gte: range[0], $lte: range[1] } }, + { [propertyName]: { $gte: new Date(range[0]), $lte: new Date(range[1]) } }, + ]), + }, + { [propertyName]: { $exists: false } }, + ], }), [queryOperators.GREATER_OR_EQUAL_TO]: (propertyName, value) => ({ $or: [ diff --git a/backend/src/v5/processors/teamspaces/projects/models/commons/tickets.js b/backend/src/v5/processors/teamspaces/projects/models/commons/tickets.js index 90ead0c7ec..60dc7b6ec5 100644 --- a/backend/src/v5/processors/teamspaces/projects/models/commons/tickets.js +++ b/backend/src/v5/processors/teamspaces/projects/models/commons/tickets.js @@ -387,9 +387,9 @@ Tickets.getTicketList = async (teamspace, project, model, if (sortBy && propertyToFilterName(sortBy)) { sort = { [propertyToFilterName(sortBy)]: sortDesc ? -1 : 1 }; } - if (queryFilters.length) { const queryInfo = await getQueryInfoFromQueryFilters(teamspace, queryFilters); + return getTicketsByFilter(teamspace, project, model, deleteIfUndefined({ projection, updatedSince, sort, limit, skip, ...queryInfo })); } diff --git a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/tickets.test.js b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/tickets.test.js index f36a7a7155..5eb5516548 100644 --- a/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/tickets.test.js +++ b/backend/tests/v5/e2e/routes/teamspaces/projects/models/common/tickets.test.js @@ -652,8 +652,22 @@ const testGetTicketList = () => { templateWithAllProps.properties.push(textProp, longTextProp, numberProp, boolProp, dateProp, oneOfProp, manyOfProp); - con.tickets = times(10, (n) => ServiceHelper.generateTicket(templatesToUse[n % templatesToUse.length])); - fed.tickets = times(10, (n) => ServiceHelper.generateTicket(templatesToUse[n % templatesToUse.length])); + con.tickets = times(13, (n) => ServiceHelper.generateTicket(templatesToUse[n % templatesToUse.length])); + fed.tickets = times(13, (n) => ServiceHelper.generateTicket(templatesToUse[n % templatesToUse.length])); + + delete con.tickets[12].properties[textProp.name]; + delete con.tickets[12].properties[longTextProp.name]; + delete con.tickets[12].properties[numberProp.name]; + delete con.tickets[12].properties[dateProp.name]; + delete con.tickets[12].properties[oneOfProp.name]; + delete con.tickets[12].properties[manyOfProp.name]; + + delete fed.tickets[12].properties[textProp.name]; + delete fed.tickets[12].properties[longTextProp.name]; + delete fed.tickets[12].properties[numberProp.name]; + delete fed.tickets[12].properties[dateProp.name]; + delete fed.tickets[12].properties[oneOfProp.name]; + delete fed.tickets[12].properties[manyOfProp.name]; beforeAll(async () => { await setupBasicData(users, teamspace, project, [con, fed, conNoTickets, fedNoTickets], @@ -711,10 +725,12 @@ const testGetTicketList = () => { const existsPropertyFilters = (propType, propertyName) => [ [`${queryOperators.EXISTS} operator is used in ${propType} property`, { ...baseRouteParams, options: { query: `'${propertyName}::${queryOperators.EXISTS}'` } }, true, - model.tickets.filter((t) => t.type === templateWithAllProps._id)], + model.tickets.filter((t) => t.type === templateWithAllProps._id + && Object.hasOwn(t.properties, propertyName))], [`${queryOperators.NOT_EXISTS} operator is used in ${propType} property`, { ...baseRouteParams, options: { query: `'${propertyName}::${queryOperators.NOT_EXISTS}'` } }, true, - model.tickets.filter((t) => t.type !== templateWithAllProps._id)], + model.tickets.filter((t) => t.type !== templateWithAllProps._id + || !Object.hasOwn(t.properties, propertyName))], ]; const equalsPropertyFilters = (propType, propertyName) => [ @@ -745,9 +761,11 @@ const testGetTicketList = () => { model.tickets.filter((t) => t.properties[propertyName] === model.tickets[0].properties[propertyName])], [`${queryOperators.NOT_CONTAINS} operator is used in ${propType} property`, - { ...baseRouteParams, options: { query: `'${propertyName}::${queryOperators.NOT_CONTAINS}::${model.tickets[0].properties[propertyName]}'` } }, true, - model.tickets.filter((t) => t.properties[propertyName] - !== model.tickets[0].properties[propertyName])], + { ...baseRouteParams, + options: { query: `'${propertyName}::${queryOperators.NOT_CONTAINS}::${model.tickets[0].properties[propertyName]}'` } }, + true, + model.tickets + .filter((t) => t.properties[propertyName] !== model.tickets[0].properties[propertyName])], ]; }; @@ -769,7 +787,9 @@ const testGetTicketList = () => { .filter((t) => t.properties[manyOfProp.name]?.some((val) => val.slice(0, 5) === model.tickets[0].properties[manyOfProp.name][0].slice(0, 5)))], [`${queryOperators.NOT_CONTAINS} operator is used in ${propTypes.MANY_OF} property`, - { ...baseRouteParams, options: { query: `'${manyOfProp.name}::${queryOperators.NOT_CONTAINS}::${model.tickets[0].properties[manyOfProp.name][0].slice(0, 5)}'` } }, true, + { ...baseRouteParams, + options: { query: `'${manyOfProp.name}::${queryOperators.NOT_CONTAINS}::${model.tickets[0].properties[manyOfProp.name][0].slice(0, 5)}'` } }, + true, model.tickets .filter((t) => !t.properties[manyOfProp.name] ?.some((val) => val.slice(0, 5) @@ -793,7 +813,9 @@ const testGetTicketList = () => { >= model.tickets[0].properties[propertyName] - 500 && t.properties[propertyName] <= model.tickets[0].properties[propertyName] + 500)], [`${queryOperators.NOT_IN_RANGE} operator is used in ${propType} property`, - { ...baseRouteParams, options: { query: `'${propertyName}::${queryOperators.NOT_IN_RANGE}::[${model.tickets[0].properties[propertyName] - 500},${model.tickets[0].properties[propertyName] + 500}]'` } }, true, + { ...baseRouteParams, + options: { query: `'${propertyName}::${queryOperators.NOT_IN_RANGE}::[${model.tickets[0].properties[propertyName] - 500},${model.tickets[0].properties[propertyName] + 500}]'` } }, + true, model.tickets.filter((t) => !t.properties[propertyName] || (t.properties[propertyName] < model.tickets[0].properties[propertyName] - 500 || t.properties[propertyName] > model.tickets[0].properties[propertyName] + 500))], diff --git a/backend/tests/v5/unit/processors/teamspaces/projects/models/commons/tickets.test.js b/backend/tests/v5/unit/processors/teamspaces/projects/models/commons/tickets.test.js index de85f8de7f..6437a7cdbb 100644 --- a/backend/tests/v5/unit/processors/teamspaces/projects/models/commons/tickets.test.js +++ b/backend/tests/v5/unit/processors/teamspaces/projects/models/commons/tickets.test.js @@ -1220,12 +1220,19 @@ const testGetTicketList = () => { }], [`${queryOperators.NOT_EQUALS} query filter and boolean value`, [], {}, undefined, { }, { queryFilters: [{ propertyName, operator: queryOperators.NOT_EQUALS, value: ['true'] }], - expectedQuery: { $and: [{ [propertyName]: { $not: { $in: [true] } } }] }, + expectedQuery: { $and: [{ $or: [ + { [propertyName]: { $not: { $in: [true] } } }, + { [propertyName]: { $exists: false } }, + ] }] }, }], [`${queryOperators.NOT_EQUALS} query filter and number value`, [], {}, undefined, { }, { queryFilters: [{ propertyName, operator: queryOperators.NOT_EQUALS, value: [`${propertyNumberValue}`] }], expectedQuery: { - $and: [{ [propertyName]: { $not: { $in: [propertyNumberValue, new Date(propertyNumberValue)] } } }], + $and: [{ $or: [ + { [propertyName]: { $not: { $in: [propertyNumberValue, new Date(propertyNumberValue)] } } }, + { [propertyName]: { $exists: false } }, + ], + }], }, }], [`${queryOperators.CONTAINS} query filter`, [], {}, undefined, { }, { @@ -1236,7 +1243,17 @@ const testGetTicketList = () => { queryFilters: [ { propertyName, operator: queryOperators.NOT_CONTAINS, value: [propertyValue, propertyValue2] }, ], - expectedQuery: { $and: [{ $nor: [{ [propertyName]: { $regex: propertyValue, $options: 'i' } }, { [propertyName]: { $regex: propertyValue2, $options: 'i' } }] }] }, + expectedQuery: { $and: [{ + $or: [ + { + $nor: [ + { [propertyName]: { $regex: propertyValue, $options: 'i' } }, + { [propertyName]: { $regex: propertyValue2, $options: 'i' } }, + ], + }, + { [propertyName]: { $exists: false } }, + ], + }] }, }], [`${queryOperators.RANGE} query filter`, [], {}, undefined, { }, { queryFilters: [{ propertyName, operator: queryOperators.RANGE, value: [[0, 10], [20, 30]] }], @@ -1249,12 +1266,19 @@ const testGetTicketList = () => { }], [`${queryOperators.NOT_IN_RANGE} query filter`, [], {}, undefined, { }, { queryFilters: [{ propertyName, operator: queryOperators.NOT_IN_RANGE, value: [[0, 10], [20, 30]] }], - expectedQuery: { $and: [{ $nor: [ - { [propertyName]: { $gte: 0, $lte: 10 } }, - { [propertyName]: { $gte: new Date(0), $lte: new Date(10) } }, - { [propertyName]: { $gte: 20, $lte: 30 } }, - { [propertyName]: { $gte: new Date(20), $lte: new Date(30) } }, - ] }] }, + expectedQuery: { $and: [{ + $or: [ + { + $nor: [ + { [propertyName]: { $gte: 0, $lte: 10 } }, + { [propertyName]: { $gte: new Date(0), $lte: new Date(10) } }, + { [propertyName]: { $gte: 20, $lte: 30 } }, + { [propertyName]: { $gte: new Date(20), $lte: new Date(30) } }, + ], + }, + { [propertyName]: { $exists: false } }, + ], + }] }, }], [`${queryOperators.GREATER_OR_EQUAL_TO} query filter`, [], {}, undefined, { }, { queryFilters: [{ propertyName, operator: queryOperators.GREATER_OR_EQUAL_TO, value: propertyNumberValue }],