From 5da0d55ab8cce0a77c881da4a7745f006c6c1465 Mon Sep 17 00:00:00 2001
From: cballevre
boolean
Check if an index is matching the given fields
object
Transform a query to make all operators explicit
+object
Build a permission set
Array
+Handle the $nor operator in a query +CouchDB transforms $nor into $and with $ne operators
+Delete outdated results from cache
Array
| The fields that the index must have |
| partialFilter | object
| An optional partial filter |
+
+
+## makeOperatorsExplicit ⇒ object
+Transform a query to make all operators explicit
+
+**Kind**: global constant
+**Returns**: object
- - The transformed query with all operators explicit
+
+| Param | Type | Description |
+| --- | --- | --- |
+| query | object
| The query to transform |
+| reverseEq | boolean
| If true, $eq will be transformed to $ne (useful for manage $nor) |
+
## getPermissionsFor ⇒ object
@@ -2312,6 +2332,19 @@ Get Icon URL using blob mechanism if OAuth connected
or using preloaded url when blob not needed
**Kind**: global function
+
+
+## handleNorOperator(conditions) ⇒ Array
+Handle the $nor operator in a query
+CouchDB transforms $nor into $and with $ne operators
+
+**Kind**: global function
+**Returns**: Array
- - The reversed conditions
+
+| Param | Type | Description |
+| --- | --- | --- |
+| conditions | Array
| The conditions inside the $nor operator |
+
## garbageCollect()
diff --git a/packages/cozy-stack-client/src/mangoIndex.js b/packages/cozy-stack-client/src/mangoIndex.js
index 38661395ef..38d662c6cb 100644
--- a/packages/cozy-stack-client/src/mangoIndex.js
+++ b/packages/cozy-stack-client/src/mangoIndex.js
@@ -1,6 +1,7 @@
import head from 'lodash/head'
import get from 'lodash/get'
import isEqual from 'lodash/isEqual'
+import isObject from 'lodash/isObject'
/**
* @typedef {Object} MangoPartialFilter
@@ -148,9 +149,73 @@ export const isMatchingIndex = (index, fields, partialFilter) => {
if (!partialFilter && !partialFilterInIndex) {
return true
}
- if (isEqual(partialFilter, partialFilterInIndex)) {
+
+ const explicitPartialFilter = makeOperatorsExplicit(partialFilter ?? {})
+ if (isEqual(explicitPartialFilter, partialFilterInIndex)) {
return true
}
}
+
return false
}
+
+/**
+ * Handle the $nor operator in a query
+ * CouchDB transforms $nor into $and with $ne operators
+ *
+ * @param {Array} conditions - The conditions inside the $nor operator
+ * @returns {Array} - The reversed conditions
+ */
+const handleNorOperator = conditions => {
+ return conditions.map(condition =>
+ Object.entries(condition).reduce((acc, [key, value]) => {
+ if (typeof value === 'string') {
+ acc[key] = { $ne: value }
+ } else {
+ acc[key] = makeOperatorsExplicit(value, true)
+ }
+ return acc
+ }, {})
+ )
+}
+
+/**
+ * Transform a query to make all operators explicit
+ *
+ * @param {object} query - The query to transform
+ * @param {boolean} reverseEq - If true, $eq will be transformed to $ne (useful for manage $nor)
+ * @returns {object} - The transformed query with all operators explicit
+ */
+export const makeOperatorsExplicit = (query, reverseEq = false) => {
+ const explicitQuery = Object.entries(query).reduce((acc, [key, value]) => {
+ if (key === '$nor') {
+ acc['$and'] = handleNorOperator(value)
+ } else if (value['$or']?.every(v => typeof v === 'string')) {
+ acc['$or'] = value['$or'].map(v =>
+ makeOperatorsExplicit({ [key]: v }, reverseEq)
+ ) // To manage $or with list of strings
+ } else if (Array.isArray(value) && value.every(isObject)) {
+ acc[key] = value.map(v => makeOperatorsExplicit(v, reverseEq)) // To manage $and and $or with multiple conditions inside
+ } else if (isObject(value) && !Array.isArray(value)) {
+ acc[key] = makeOperatorsExplicit(value, reverseEq) // To manage nested objects
+ } else if (reverseEq && key === '$eq') {
+ acc['$ne'] = value
+ } else if (!key.startsWith('$')) {
+ acc[key] = { $eq: value } // To manage implicit $eq
+ } else {
+ acc[key] = value // To manage explicit operators
+ }
+ return acc
+ }, {})
+
+ const explicitQueryKeys = Object.keys(explicitQuery)
+ if (explicitQueryKeys.length === 1) {
+ return explicitQuery
+ }
+
+ return {
+ $and: explicitQueryKeys.map(key => ({
+ [key]: explicitQuery[key]
+ }))
+ }
+}
diff --git a/packages/cozy-stack-client/src/mangoIndex.spec.js b/packages/cozy-stack-client/src/mangoIndex.spec.js
index 4a7458f3e4..e60ba422fa 100644
--- a/packages/cozy-stack-client/src/mangoIndex.spec.js
+++ b/packages/cozy-stack-client/src/mangoIndex.spec.js
@@ -1,7 +1,8 @@
import {
isMatchingIndex,
getIndexFields,
- getIndexNameFromFields
+ getIndexNameFromFields,
+ makeOperatorsExplicit
} from './mangoIndex'
const buildDesignDoc = (fields, { partialFilter, id } = {}) => {
@@ -262,3 +263,163 @@ describe('getIndexNameFromFields', () => {
)
})
})
+
+describe('makeOperatorsExplicit', () => {
+ it('Transforms implicit $eq operator to explicit', () => {
+ const query = { name: 'test' }
+ const expected = { name: { $eq: 'test' } }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Transforms implicit $and operator to explicit', () => {
+ const query = { name: 'test', age: 42 }
+ const expected = { $and: [{ name: { $eq: 'test' } }, { age: { $eq: 42 } }] }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Maintains explicit $eq operator', () => {
+ const query = { name: { $eq: 'test' } }
+ const expected = { name: { $eq: 'test' } }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Maintains explicit $and operator', () => {
+ const query = { $and: [{ name: 'test' }, { age: 42 }] }
+ const expected = { $and: [{ name: { $eq: 'test' } }, { age: { $eq: 42 } }] }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Handles nested implicit $eq operators', () => {
+ const query = { user: { name: 'test', age: 42 } }
+ const expected = {
+ user: { $and: [{ name: { $eq: 'test' } }, { age: { $eq: 42 } }] }
+ }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Handles nested explicit operators', () => {
+ const query = { user: { $or: [{ name: 'test' }, { age: { $ne: 42 } }] } }
+ const expected = {
+ user: { $or: [{ name: { $eq: 'test' } }, { age: { $ne: 42 } }] }
+ }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Handles mixed implicit and explicit operators', () => {
+ const query = { name: 'test', age: { $ne: 42 } }
+ const expected = { $and: [{ name: { $eq: 'test' } }, { age: { $ne: 42 } }] }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Handles operator with string array', () => {
+ const query = {
+ _id: {
+ $nin: ['id123', 'id456']
+ },
+ type: 'file'
+ }
+ const expected = {
+ $and: [
+ {
+ _id: {
+ $nin: ['id123', 'id456']
+ }
+ },
+ { type: { $eq: 'file' } }
+ ]
+ }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Handles $or operator with string array', () => {
+ const query = {
+ type: {
+ $or: ['konnector', 'worker']
+ }
+ }
+ const expected = {
+ $or: [{ type: { $eq: 'konnector' } }, { type: { $eq: 'worker' } }]
+ }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Handles $or operator with object array', () => {
+ const query = {
+ type: 'file',
+ $or: [
+ {
+ trashed: {
+ $exists: false
+ }
+ },
+ {
+ trashed: false
+ }
+ ]
+ }
+ const expected = {
+ $and: [
+ { type: { $eq: 'file' } },
+ { $or: [{ trashed: { $exists: false } }, { trashed: { $eq: false } }] }
+ ]
+ }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Handles explicit $and operator with nested object to make explicit', () => {
+ const query = {
+ type: 'file',
+ trashed: true,
+ 'metadata.notifiedAt': {
+ $exists: false
+ }
+ }
+ const expected = {
+ $and: [
+ { type: { $eq: 'file' } },
+ { trashed: { $eq: true } },
+ { 'metadata.notifiedAt': { $exists: false } }
+ ]
+ }
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+
+ it('Handles explicit $nor operator', () => {
+ const query = {
+ $nor: [
+ {
+ type: {
+ $eq: 'directory'
+ }
+ },
+ { dir_id: 'id1234' },
+ {
+ 'metadata.notifiedAt': {
+ $exists: false
+ }
+ }
+ ]
+ }
+ const expected = {
+ $and: [
+ {
+ type: {
+ $ne: 'directory'
+ }
+ },
+ {
+ dir_id: {
+ $ne: 'id1234'
+ }
+ },
+ {
+ 'metadata.notifiedAt': {
+ $exists: false
+ }
+ }
+ ]
+ }
+
+ expect(makeOperatorsExplicit(query)).toEqual(expected)
+ })
+})