Skip to content

Commit

Permalink
feat: [WIP]
Browse files Browse the repository at this point in the history
  • Loading branch information
paultranvan committed Dec 5, 2024
1 parent f13f75f commit 3f703eb
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 5 deletions.
24 changes: 21 additions & 3 deletions packages/cozy-client/src/CozyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -1193,7 +1193,7 @@ client.query(Q('io.cozy.bills'))`)
if (queryDef instanceof QueryDefinition) {
definitions.push(queryDef)
} else {
documents.push(queryDef)
documents.push(doc)
}
} catch {
// eslint-disable-next-line
Expand Down Expand Up @@ -1309,6 +1309,8 @@ client.query(Q('io.cozy.bills'))`)

hydrateRelationships(document, schemaRelationships) {
const methods = this.getRelationshipStoreAccessors()
// FIXME: the association is created even though the relationships does not
// exist in the document: should we keep this behaviour?
return mapValues(schemaRelationships, (assoc, name) =>
createAssociation(document, assoc, methods)
)
Expand Down Expand Up @@ -1421,13 +1423,29 @@ client.query(Q('io.cozy.bills'))`)
return queryResults
}

const data =
const hydratedData =
hydrated && doctype
? this.hydrateDocuments(doctype, queryResults.data)
: queryResults.data

const relationships = this.schema.getDoctypeSchema(doctype)?.relationships
const relationshipNames = relationships
? Object.keys(relationships)
: null

// The `data` array contains the hydrated data with the relationships, if any.
// The `storeData` array contains the documents from the store: this is useful to preserve
// referential equality, to be later evaluated to determine whether or not the
// documents had changed.
return {
...queryResults,
data: isSingleDocQuery && singleDocData ? data[0] : data
data:
isSingleDocQuery && singleDocData ? hydratedData[0] : hydratedData,
storeData:
isSingleDocQuery && singleDocData
? queryResults.data[0]
: queryResults.data,
relationshipNames
}
} catch (e) {
logger.warn(
Expand Down
3 changes: 2 additions & 1 deletion packages/cozy-client/src/hooks/useQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useClient from './useClient'
import logger from '../logger'
import { clientContext } from '../context'
import { QueryDefinition } from '../queries/dsl'
import { equalityCheckForQuery } from './utils'

const useSelector = createSelectorHook(clientContext)

Expand Down Expand Up @@ -61,7 +62,7 @@ const useQuery = (queryDefinition, options) => {
hydrated: get(options, 'hydrated', true),
singleDocData: get(options, 'singleDocData', false)
})
})
}, equalityCheckForQuery)

useEffect(
() => {
Expand Down
82 changes: 82 additions & 0 deletions packages/cozy-client/src/hooks/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Equality check
*
* Note we do not make a shallow equality check on documents, as it is less efficient and should
* not be necessary: the queryResult.data is built by extracting documents from the state, thus
* preserving references.
*
* @param {*} queryResA

Check failure on line 8 in packages/cozy-client/src/hooks/utils.js

View workflow job for this annotation

GitHub Actions / Build and publish

Missing JSDoc @param "queryResA" description
* @param {*} queryResB

Check failure on line 9 in packages/cozy-client/src/hooks/utils.js

View workflow job for this annotation

GitHub Actions / Build and publish

Missing JSDoc @param "queryResB" description
* @returns
*/
export const equalityCheckForQuery = (queryResA, queryResB) => {
//console.log('Call equality check : ', queryResA, queryResB)
if (queryResA === queryResB) {
// Referential equality
return true
}

if (
typeof queryResA !== 'object' ||
queryResA === null ||
typeof queryResB !== 'object' ||
queryResB === null
) {
// queryResA or queryResB is not an object or null
return false
}

if (queryResA.id !== queryResB.id) {
return false
}
if (queryResA.fetchStatus !== queryResB.fetchStatus) {
return false
}

const docsA = queryResA.storeData
const docsB = queryResB.storeData
if (!docsA || !docsB) {
// No data to check
return false
}

if (docsA.length !== docsB.length) {
// A document was added or removed
return false
}

for (let i = 0; i < docsA.length; i++) {
if (docsA[i] !== docsB[i]) {
// References should be the same for non-updated documents
return false
}
}
if (queryResA.relationshipNames) {
// In case of relationships, we cannot check referential equality, because we
// "hydrate" the data by creating a new instance of the related relationship class.
// Thus, we check the document revision instead.
const hydratedDataA = queryResA.data
const hydratedDataB = queryResB.data
if (hydratedDataA.length !== hydratedDataB.length) {
return false
}
for (let i = 0; i < hydratedDataA.length; i++) {
for (const name of queryResA.relationshipNames) {
// Check hydrated relationship
const includedA = hydratedDataA[i][name]
const includedB = hydratedDataB[i][name]
if (includedA && includedB) {
if (
includedA._rev &&
includedB._rev &&
includedA._rev !== includedB._rev
) {
return false
}
}
}
}
}
//console.log('docs are same')
return true
}
110 changes: 110 additions & 0 deletions packages/cozy-client/src/hooks/utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { equalityCheckForQuery } from './utils'

const mapIdsToDocuments = (state, doctype, ids) => {
return ids.map(id => state[doctype][id])
}

describe('equalityCheckForQuery', () => {
const state = {
documents: {
'io.cozy.files': {
doc1: {
_id: 'doc1'
},
doc2: {
_id: 'doc2'
},
doc3: {
_id: 'doc3'
}
}
},
queries: {
query1: {
id: 'query1',
data: ['doc1', 'doc2']
},
query2: {
id: 'query2',
data: ['doc2']
}
}
}

const queryResultA1 = {
id: 1,
data: mapIdsToDocuments(state.documents, 'io.cozy.files', ['doc1', 'doc2'])
}
const queryResultA2 = {
id: 1,
data: mapIdsToDocuments(state.documents, 'io.cozy.files', ['doc1', 'doc2'])
}
const queryResultA3 = {
id: 1,
data: mapIdsToDocuments(state.documents, 'io.cozy.files', [
'doc1',
'doc2',
'doc3'
])
}
const queryResultA4 = {
id: 1,
data: mapIdsToDocuments(state.documents, 'io.cozy.files', ['doc2', 'doc3'])
}
const queryResultB1 = {
id: 2,
data: mapIdsToDocuments(state.documents, 'io.cozy.files', ['doc2'])
}
const queryResultB2 = {
id: 2,
data: mapIdsToDocuments(state.documents, 'io.cozy.files', ['doc3'])
}

it('should return true for referential equality', () => {
expect(equalityCheckForQuery(queryResultA1, queryResultA1)).toBe(true)
expect(equalityCheckForQuery(null, null)).toBe(true)
})

it('should return false if one object is null', () => {
expect(equalityCheckForQuery(null, queryResultA1)).toBe(false)
expect(equalityCheckForQuery(queryResultA1, null)).toBe(false)
})

it('should return false if one or both objects are not objects', () => {
expect(equalityCheckForQuery('notAnObject', queryResultA1)).toBe(false)
expect(equalityCheckForQuery(queryResultA1, 'notAnObject')).toBe(false)
})

it('should return false if `id` properties are different', () => {
expect(equalityCheckForQuery(queryResultA1, queryResultB1)).toBe(false)
})

it('should return false if one or both objects lack `data`', () => {
expect(equalityCheckForQuery({ id: 1 }, queryResultA1)).toBe(false)
expect(equalityCheckForQuery(queryResultA1, { id: 1 })).toBe(false)
})

it('should return false if `data` lengths are different', () => {
expect(equalityCheckForQuery(queryResultA1, queryResultA3)).toBe(false)
})

it('should return false if elements in `data` are different', () => {
expect(equalityCheckForQuery(queryResultA1, queryResultA3)).toBe(false)
expect(equalityCheckForQuery(queryResultA3, queryResultA4)).toBe(false)
expect(equalityCheckForQuery(queryResultB1, queryResultB2)).toBe(false)
})

it('should return true for matching data array, with equal references ', () => {
expect(equalityCheckForQuery(queryResultA1, queryResultA2)).toBe(true)
})

it('should return false for matching data array, with different references ', () => {
const queryResShallowCopyA1 = {
...queryResultA1,
data: { ...queryResultA1.data }
}
expect(equalityCheckForQuery(queryResultA1, queryResShallowCopyA1)).toBe(
false
)
})
})
1 change: 0 additions & 1 deletion packages/cozy-client/src/store/documents.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ export const extractAndMergeDocument = (data, updatedStateWithIncluded) => {

let mergedData = Object.assign({}, updatedStateWithIncluded)
mergedData[doctype] = Object.assign({}, updatedStateWithIncluded[doctype])

Object.values(sortedData).map(data => {
const id = properId(data)
if (mergedData[doctype][id]) {
Expand Down

0 comments on commit 3f703eb

Please sign in to comment.