From 32c432dd1376b9faec51376294f27d116b1f6370 Mon Sep 17 00:00:00 2001 From: paultranvan Date: Thu, 23 Jun 2022 18:29:39 +0200 Subject: [PATCH] feat: Add the partial filter fields in the index name When using a partial index, its fields weren't part of the index name. Therefore, we might have a conflict situation where there is already an existing index without the partial index. Fix https://github.com/cozy/cozy-client/issues/1054 --- docs/api/cozy-stack-client.md | 23 ++++++ .../src/DocumentCollection.js | 8 +- .../src/DocumentCollection.spec.js | 2 +- packages/cozy-stack-client/src/mangoIndex.js | 27 ++++++- .../cozy-stack-client/src/mangoIndex.spec.js | 81 ++++++++++++++++++- 5 files changed, 134 insertions(+), 7 deletions(-) diff --git a/docs/api/cozy-stack-client.md b/docs/api/cozy-stack-client.md index 82598af0a..88f6f9cca 100644 --- a/docs/api/cozy-stack-client.md +++ b/docs/api/cozy-stack-client.md @@ -73,6 +73,11 @@ through OAuth.

getIllegalCharactersstring

Get the list of illegal characters in the file name

+
getIndexNameFromFieldsstring
+

Name an index, based on its indexed fields and partial filter.

+

It follows this naming convention: +`by_{indexed_field1}and{indexed_field2}filter{partial_filter_field1}and{partial_filter_field2}

+
getIndexFieldsArray

Compute fields that should be indexed for a mango query to work

@@ -1796,6 +1801,23 @@ Get the list of illegal characters in the file name | --- | --- | --- | | name | string | the file name | + + +## getIndexNameFromFields ⇒ string +Name an index, based on its indexed fields and partial filter. + +It follows this naming convention: +`by_{indexed_field1}_and_{indexed_field2}_filter_{partial_filter_field1}_and_{partial_filter_field2} + +**Kind**: global constant +**Returns**: string - The index name, built from the fields + +| Param | Type | Description | +| --- | --- | --- | +| fields | Array.<string> | The indexed fields | +| params | object | The additional params | +| params.partialFilterFields | Array.<string> | The partial filter fields | + ## getIndexFields ⇒ Array @@ -2402,6 +2424,7 @@ Document representing a io.cozy.jobs | --- | --- | --- | | [sort] | Array.<object> | The sorting parameters | | [fields] | Array.<string> | The fields to return | +| [partialFilterFields] | Array.<string> | The partial filter fields | | [limit] | number \| null | For pagination, the number of results to return | | [skip] | number \| null | For skip-based pagination, the number of referenced files to skip | | [indexId] | string \| null | The _id of the CouchDB index to use for this request | diff --git a/packages/cozy-stack-client/src/DocumentCollection.js b/packages/cozy-stack-client/src/DocumentCollection.js index 77d641991..96314fbe3 100644 --- a/packages/cozy-stack-client/src/DocumentCollection.js +++ b/packages/cozy-stack-client/src/DocumentCollection.js @@ -220,8 +220,14 @@ class DocumentCollection { if (!indexedFields) { indexedFields = getIndexFields({ sort: options.sort, selector }) } + const partialFilterFields = partialFilter + ? getIndexFields({ partialFilter }) + : null + const existingIndex = await this.findExistingIndex(selector, options) - const indexName = getIndexNameFromFields(indexedFields) + const indexName = getIndexNameFromFields(indexedFields, { + partialFilterFields + }) if (!existingIndex) { await this.createIndex(indexedFields, { partialFilter, diff --git a/packages/cozy-stack-client/src/DocumentCollection.spec.js b/packages/cozy-stack-client/src/DocumentCollection.spec.js index 7b398eab2..af7c2181b 100644 --- a/packages/cozy-stack-client/src/DocumentCollection.spec.js +++ b/packages/cozy-stack-client/src/DocumentCollection.spec.js @@ -759,7 +759,7 @@ describe('DocumentCollection', () => { 'POST', '/data/io.cozy.todos/_design/123456/copy?rev=1-123', null, - { headers: { Destination: '_design/by_label_and_done' } } + { headers: { Destination: '_design/by_label_and_done_filter_trashed' } } ) expect(client.fetchJSON).toHaveBeenNthCalledWith( 4, diff --git a/packages/cozy-stack-client/src/mangoIndex.js b/packages/cozy-stack-client/src/mangoIndex.js index 1063087cd..f035311af 100644 --- a/packages/cozy-stack-client/src/mangoIndex.js +++ b/packages/cozy-stack-client/src/mangoIndex.js @@ -12,6 +12,7 @@ import isEqual from 'lodash/isEqual' * * @property {Array} [sort] The sorting parameters * @property {Array} [fields] The fields to return + * @property {Array} [partialFilterFields] The partial filter fields * @property {number|null} [limit] For pagination, the number of results to return * @property {number|null} [skip] For skip-based pagination, the number of referenced files to skip * @property {string|null} [indexId] The _id of the CouchDB index to use for this request @@ -35,8 +36,25 @@ export const normalizeDesignDoc = designDoc => { return { id, _id: id, ...designDoc.doc } } -export const getIndexNameFromFields = fields => { - return `by_${fields.join('_and_')}` +/** + * Name an index, based on its indexed fields and partial filter. + * + * It follows this naming convention: + * `by_{indexed_field1}_and_{indexed_field2}_filter_{partial_filter_field1}_and_{partial_filter_field2} + * + * @param {Array} fields - The indexed fields + * @param {object} params - The additional params + * @param {Array} params.partialFilterFields - The partial filter fields + * @returns {string} The index name, built from the fields + */ +export const getIndexNameFromFields = ( + fields, + { partialFilterFields } = {} +) => { + const indexName = `by_${fields.join('_and_')}` + return partialFilterFields + ? `${indexName}_filter_${partialFilterFields.join('_and_')}` + : indexName } export const transformSort = sort => { @@ -61,11 +79,12 @@ export const transformSort = sort => { * * @returns {Array} - Fields to index */ -export const getIndexFields = ({ selector, sort = [] }) => { +export const getIndexFields = ({ selector, partialFilter, sort = [] }) => { return Array.from( new Set([ ...sort.map(sortOption => head(Object.keys(sortOption))), - ...(selector ? Object.keys(selector) : []) + ...(selector ? Object.keys(selector) : []), + ...(partialFilter ? Object.keys(partialFilter) : []) ]) ) } diff --git a/packages/cozy-stack-client/src/mangoIndex.spec.js b/packages/cozy-stack-client/src/mangoIndex.spec.js index c5ff65254..c7802c229 100644 --- a/packages/cozy-stack-client/src/mangoIndex.spec.js +++ b/packages/cozy-stack-client/src/mangoIndex.spec.js @@ -1,4 +1,9 @@ -import { isMatchingIndex, isInconsistentIndex } from './mangoIndex' +import { + isMatchingIndex, + isInconsistentIndex, + getIndexFields, + getIndexNameFromFields +} from './mangoIndex' const buildDesignDoc = (fields, { partialFilter, id } = {}) => { return { @@ -103,3 +108,77 @@ describe('inconsistent index', () => { expect(isInconsistentIndex(index2)).toBe(false) }) }) + +describe('getIndexFields', () => { + it('should return nothing', () => { + expect(getIndexFields({})).toEqual([]) + }) + + it('should return fields from selector', () => { + const selector = { + _id: { + $gt: null + }, + name: 'toto' + } + expect(getIndexFields({ selector })).toEqual(['_id', 'name']) + }) + + it('should return fields from sort', () => { + const selector = {} + const sort = [{ _id: 'asc' }, { name: 'asc' }] + expect(getIndexFields({ selector, sort })).toEqual(['_id', 'name']) + }) + + it('should return fields from partial filter', () => { + const partialFilter = { + date: { + $exists: false + }, + trashed: { + $ne: true + } + } + expect(getIndexFields({ partialFilter })).toEqual(['date', 'trashed']) + }) + + it('should return all fields', () => { + const selector = { + _id: { + $gt: null + }, + name: 'toto' + } + const sort = [{ _id: 'asc' }, { name: 'asc' }] + + const partialFilter = { + date: { + $exists: false + }, + trashed: { + $ne: true + } + } + expect(getIndexFields({ selector, sort, partialFilter })).toEqual([ + '_id', + 'name', + 'date', + 'trashed' + ]) + }) +}) + +describe('getIndexNameFromFields', () => { + it('should return index fields', () => { + const fields = ['_id', 'name'] + expect(getIndexNameFromFields(fields)).toEqual('by__id_and_name') + }) + + it('should return index fields with partial filter', () => { + const fields = ['_id', 'name'] + const partialFilterFields = ['date', 'trashed'] + expect(getIndexNameFromFields(fields, { partialFilterFields })).toEqual( + 'by__id_and_name_filter_date_and_trashed' + ) + }) +})