diff --git a/docs/api/cozy-client/README.md b/docs/api/cozy-client/README.md
index a3e0bcbd7b..e426106802 100644
--- a/docs/api/cozy-client/README.md
+++ b/docs/api/cozy-client/README.md
@@ -28,6 +28,7 @@ cozy-client
* [QueryDefinition](classes/QueryDefinition.md)
* [Registry](classes/Registry.md)
* [StackLink](classes/StackLink.md)
+* [WebFlagshipLink](classes/WebFlagshipLink.md)
## Properties
diff --git a/docs/api/cozy-client/classes/CozyClient.md b/docs/api/cozy-client/classes/CozyClient.md
index f54ff21992..052cd214fc 100644
--- a/docs/api/cozy-client/classes/CozyClient.md
+++ b/docs/api/cozy-client/classes/CozyClient.md
@@ -43,7 +43,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:153](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L153)
+[packages/cozy-client/src/CozyClient.js:155](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L155)
## Properties
@@ -53,7 +53,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:166](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L166)
+[packages/cozy-client/src/CozyClient.js:168](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L168)
***
@@ -63,7 +63,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:194](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L194)
+[packages/cozy-client/src/CozyClient.js:196](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L196)
***
@@ -73,7 +73,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:187](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L187)
+[packages/cozy-client/src/CozyClient.js:189](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L189)
***
@@ -83,7 +83,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1639](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1639)
+[packages/cozy-client/src/CozyClient.js:1700](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1700)
***
@@ -93,7 +93,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:174](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L174)
+[packages/cozy-client/src/CozyClient.js:176](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L176)
***
@@ -103,7 +103,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:173](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L173)
+[packages/cozy-client/src/CozyClient.js:175](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L175)
***
@@ -113,7 +113,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:488](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L488)
+[packages/cozy-client/src/CozyClient.js:490](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L490)
***
@@ -123,7 +123,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:184](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L184)
+[packages/cozy-client/src/CozyClient.js:186](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L186)
***
@@ -133,7 +133,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:167](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L167)
+[packages/cozy-client/src/CozyClient.js:169](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L169)
***
@@ -159,7 +159,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:170](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L170)
+[packages/cozy-client/src/CozyClient.js:172](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L172)
***
@@ -169,7 +169,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:197](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L197)
+[packages/cozy-client/src/CozyClient.js:199](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L199)
***
@@ -179,7 +179,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:172](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L172)
+[packages/cozy-client/src/CozyClient.js:174](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L174)
***
@@ -189,7 +189,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:189](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L189)
+[packages/cozy-client/src/CozyClient.js:191](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L191)
***
@@ -199,7 +199,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1614](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1614)
+[packages/cozy-client/src/CozyClient.js:1675](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1675)
***
@@ -209,7 +209,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1544](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1544)
+[packages/cozy-client/src/CozyClient.js:1605](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1605)
***
@@ -219,7 +219,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:222](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L222)
+[packages/cozy-client/src/CozyClient.js:224](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L224)
***
@@ -239,7 +239,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1297](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1297)
+[packages/cozy-client/src/CozyClient.js:1358](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1358)
***
@@ -284,7 +284,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:467](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L467)
+[packages/cozy-client/src/CozyClient.js:469](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L469)
***
@@ -304,7 +304,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:423](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L423)
+[packages/cozy-client/src/CozyClient.js:425](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L425)
***
@@ -324,7 +324,7 @@ Cozy-Client will automatically call `this.login()` if provided with a token and
*Defined in*
-[packages/cozy-client/src/CozyClient.js:568](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L568)
+[packages/cozy-client/src/CozyClient.js:570](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L570)
***
@@ -353,7 +353,7 @@ Contains the fetched token and the client information. These should be stored an
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1460](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1460)
+[packages/cozy-client/src/CozyClient.js:1521](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1521)
***
@@ -371,7 +371,7 @@ This mechanism is described in https://github.com/cozy/cozy-client/blob/master/p
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1441](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1441)
+[packages/cozy-client/src/CozyClient.js:1502](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1502)
***
@@ -387,7 +387,7 @@ Returns whether the client has been revoked on the server
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1556](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1556)
+[packages/cozy-client/src/CozyClient.js:1617](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1617)
***
@@ -412,7 +412,7 @@ Collection corresponding to the doctype
*Defined in*
-[packages/cozy-client/src/CozyClient.js:560](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L560)
+[packages/cozy-client/src/CozyClient.js:562](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L562)
***
@@ -450,7 +450,7 @@ await client.create('io.cozy.todos', {
*Defined in*
-[packages/cozy-client/src/CozyClient.js:615](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L615)
+[packages/cozy-client/src/CozyClient.js:617](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L617)
***
@@ -471,7 +471,7 @@ If `oauth` options are passed, stackClient is an OAuthStackClient.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1594](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1594)
+[packages/cozy-client/src/CozyClient.js:1655](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1655)
***
@@ -496,7 +496,7 @@ The document that has been deleted
*Defined in*
-[packages/cozy-client/src/CozyClient.js:871](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L871)
+[packages/cozy-client/src/CozyClient.js:873](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L873)
***
@@ -516,7 +516,7 @@ The document that has been deleted
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1665](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1665)
+[packages/cozy-client/src/CozyClient.js:1726](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1726)
***
@@ -542,7 +542,7 @@ a method from cozy-client
*Defined in*
-[packages/cozy-client/src/CozyClient.js:236](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L236)
+[packages/cozy-client/src/CozyClient.js:238](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L238)
***
@@ -564,7 +564,7 @@ a method from cozy-client
*Defined in*
-[packages/cozy-client/src/CozyClient.js:685](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L685)
+[packages/cozy-client/src/CozyClient.js:687](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L687)
***
@@ -588,7 +588,7 @@ Makes sure that the query exists in the store
*Defined in*
-[packages/cozy-client/src/CozyClient.js:892](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L892)
+[packages/cozy-client/src/CozyClient.js:894](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L894)
***
@@ -602,7 +602,7 @@ Makes sure that the query exists in the store
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1547](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1547)
+[packages/cozy-client/src/CozyClient.js:1608](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1608)
***
@@ -625,7 +625,7 @@ Makes sure that the query exists in the store
*Defined in*
-[packages/cozy-client/src/CozyClient.js:564](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L564)
+[packages/cozy-client/src/CozyClient.js:566](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L566)
***
@@ -654,7 +654,7 @@ Query state
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1394](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1394)
+[packages/cozy-client/src/CozyClient.js:1455](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1455)
***
@@ -675,7 +675,7 @@ Query state
*Defined in*
-[packages/cozy-client/src/CozyClient.js:577](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L577)
+[packages/cozy-client/src/CozyClient.js:579](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L579)
***
@@ -689,7 +689,7 @@ Query state
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1272](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1272)
+[packages/cozy-client/src/CozyClient.js:1333](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1333)
***
@@ -710,7 +710,7 @@ Query state
*Defined in*
-[packages/cozy-client/src/CozyClient.js:584](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L584)
+[packages/cozy-client/src/CozyClient.js:586](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L586)
***
@@ -733,7 +733,7 @@ Creates an association that is linked to the store.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1279](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1279)
+[packages/cozy-client/src/CozyClient.js:1340](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1340)
***
@@ -747,7 +747,7 @@ Creates an association that is linked to the store.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1647](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1647)
+[packages/cozy-client/src/CozyClient.js:1708](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1708)
***
@@ -771,7 +771,7 @@ Array of documents or null if the collection does not exist.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1315](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1315)
+[packages/cozy-client/src/CozyClient.js:1376](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1376)
***
@@ -796,7 +796,7 @@ Document or null if the object does not exist.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1332](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1332)
+[packages/cozy-client/src/CozyClient.js:1393](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1393)
***
@@ -831,7 +831,7 @@ One or more mutation to execute
*Defined in*
-[packages/cozy-client/src/CozyClient.js:784](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L784)
+[packages/cozy-client/src/CozyClient.js:786](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L786)
***
@@ -851,7 +851,7 @@ One or more mutation to execute
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1199](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1199)
+[packages/cozy-client/src/CozyClient.js:1260](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1260)
***
@@ -867,7 +867,7 @@ getInstanceOptions - Returns current instance options, such as domain or app slu
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1674](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1674)
+[packages/cozy-client/src/CozyClient.js:1735](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1735)
***
@@ -894,7 +894,7 @@ Get a query from the internal store.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1353](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1353)
+[packages/cozy-client/src/CozyClient.js:1414](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1414)
***
@@ -923,7 +923,7 @@ the store up, which in turn will update the ``s and re-render the data.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1295](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1295)
+[packages/cozy-client/src/CozyClient.js:1356](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1356)
***
@@ -955,7 +955,7 @@ extract the value corresponding to the given `key`
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1775](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1775)
+[packages/cozy-client/src/CozyClient.js:1836](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1836)
***
@@ -969,7 +969,7 @@ extract the value corresponding to the given `key`
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1654](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1654)
+[packages/cozy-client/src/CozyClient.js:1715](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1715)
***
@@ -991,7 +991,7 @@ Sets public attribute and emits event related to revocation
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1565](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1565)
+[packages/cozy-client/src/CozyClient.js:1626](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1626)
***
@@ -1013,7 +1013,7 @@ Emits event when token is refreshed
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1576](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1576)
+[packages/cozy-client/src/CozyClient.js:1637](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1637)
***
@@ -1039,7 +1039,7 @@ the relationship
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1242](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1242)
+[packages/cozy-client/src/CozyClient.js:1303](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1303)
***
@@ -1064,7 +1064,7 @@ Instead, the relationships will have null documents.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1219](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1219)
+[packages/cozy-client/src/CozyClient.js:1280](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1280)
***
@@ -1085,7 +1085,7 @@ Instead, the relationships will have null documents.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1253](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1253)
+[packages/cozy-client/src/CozyClient.js:1314](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1314)
***
@@ -1099,7 +1099,7 @@ Instead, the relationships will have null documents.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1417](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1417)
+[packages/cozy-client/src/CozyClient.js:1478](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1478)
***
@@ -1121,7 +1121,7 @@ loadInstanceOptionsFromDOM - Loads the dataset injected by the Stack in web page
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1685](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1685)
+[packages/cozy-client/src/CozyClient.js:1746](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1746)
***
@@ -1139,7 +1139,7 @@ This method is not iso with loadInstanceOptionsFromDOM for now.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1706](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1706)
+[packages/cozy-client/src/CozyClient.js:1767](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1767)
***
@@ -1173,7 +1173,7 @@ Emits
*Defined in*
-[packages/cozy-client/src/CozyClient.js:456](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L456)
+[packages/cozy-client/src/CozyClient.js:458](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L458)
***
@@ -1196,7 +1196,7 @@ Emits
*Defined in*
-[packages/cozy-client/src/CozyClient.js:507](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L507)
+[packages/cozy-client/src/CozyClient.js:509](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L509)
***
@@ -1220,7 +1220,7 @@ and working.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1265](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1265)
+[packages/cozy-client/src/CozyClient.js:1326](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1326)
***
@@ -1241,7 +1241,7 @@ and working.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1041](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1041)
+[packages/cozy-client/src/CozyClient.js:1043](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1043)
***
@@ -1267,7 +1267,7 @@ Mutate a document
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1059](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1059)
+[packages/cozy-client/src/CozyClient.js:1061](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1061)
***
@@ -1287,7 +1287,7 @@ Mutate a document
*Defined in*
-[packages/cozy-client/src/CozyClient.js:237](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L237)
+[packages/cozy-client/src/CozyClient.js:239](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L239)
***
@@ -1309,7 +1309,7 @@ Dehydrates and adds metadata before saving a document
*Defined in*
-[packages/cozy-client/src/CozyClient.js:755](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L755)
+[packages/cozy-client/src/CozyClient.js:757](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L757)
***
@@ -1340,7 +1340,7 @@ please use `fetchQueryAndGetFromState` instead
*Defined in*
-[packages/cozy-client/src/CozyClient.js:919](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L919)
+[packages/cozy-client/src/CozyClient.js:921](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L921)
***
@@ -1367,7 +1367,7 @@ All documents matching the query
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1001](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1001)
+[packages/cozy-client/src/CozyClient.js:1003](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1003)
***
@@ -1401,7 +1401,7 @@ All documents matching the query
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1661](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1661)
+[packages/cozy-client/src/CozyClient.js:1722](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1722)
***
@@ -1427,7 +1427,7 @@ Contains the fetched token and the client information.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1411](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1411)
+[packages/cozy-client/src/CozyClient.js:1472](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1472)
***
@@ -1441,7 +1441,7 @@ Contains the fetched token and the client information.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:427](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L427)
+[packages/cozy-client/src/CozyClient.js:429](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L429)
***
@@ -1509,7 +1509,7 @@ client.plugins.alerts
*Defined in*
-[packages/cozy-client/src/CozyClient.js:287](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L287)
+[packages/cozy-client/src/CozyClient.js:289](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L289)
***
@@ -1529,7 +1529,7 @@ client.plugins.alerts
*Defined in*
-[packages/cozy-client/src/CozyClient.js:238](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L238)
+[packages/cozy-client/src/CozyClient.js:240](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L240)
***
@@ -1548,7 +1548,7 @@ Contains the fetched token and the client information.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1506](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1506)
+[packages/cozy-client/src/CozyClient.js:1567](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1567)
***
@@ -1568,7 +1568,7 @@ Contains the fetched token and the client information.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1183](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1183)
+[packages/cozy-client/src/CozyClient.js:1244](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1244)
***
@@ -1594,7 +1594,7 @@ This method will reset the query state to its initial state and refetch it.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1804](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1804)
+[packages/cozy-client/src/CozyClient.js:1865](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1865)
***
@@ -1617,7 +1617,7 @@ Create or update a document on the server
*Defined in*
-[packages/cozy-client/src/CozyClient.js:637](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L637)
+[packages/cozy-client/src/CozyClient.js:639](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L639)
***
@@ -1652,7 +1652,7 @@ save the new resulting settings into database
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1792](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1792)
+[packages/cozy-client/src/CozyClient.js:1853](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1853)
***
@@ -1681,7 +1681,7 @@ Saves multiple documents in one batch
*Defined in*
-[packages/cozy-client/src/CozyClient.js:658](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L658)
+[packages/cozy-client/src/CozyClient.js:660](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L660)
***
@@ -1701,7 +1701,7 @@ Saves multiple documents in one batch
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1758](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1758)
+[packages/cozy-client/src/CozyClient.js:1819](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1819)
***
@@ -1725,7 +1725,7 @@ set some data in the store.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1731](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1731)
+[packages/cozy-client/src/CozyClient.js:1792](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1792)
***
@@ -1749,7 +1749,7 @@ At any time put an error function
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1744](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1744)
+[packages/cozy-client/src/CozyClient.js:1805](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1805)
***
@@ -1787,7 +1787,7 @@ use options.force = true.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1532](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1532)
+[packages/cozy-client/src/CozyClient.js:1593](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1593)
***
@@ -1811,7 +1811,7 @@ Contains the fetched token and the client information. These should be stored an
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1427](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1427)
+[packages/cozy-client/src/CozyClient.js:1488](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1488)
***
@@ -1825,7 +1825,7 @@ Contains the fetched token and the client information. These should be stored an
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1751](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1751)
+[packages/cozy-client/src/CozyClient.js:1812](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1812)
***
@@ -1846,7 +1846,7 @@ Contains the fetched token and the client information. These should be stored an
*Defined in*
-[packages/cozy-client/src/CozyClient.js:856](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L856)
+[packages/cozy-client/src/CozyClient.js:858](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L858)
***
@@ -1868,7 +1868,7 @@ Contains the fetched token and the client information. These should be stored an
*Defined in*
-[packages/cozy-client/src/CozyClient.js:881](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L881)
+[packages/cozy-client/src/CozyClient.js:883](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L883)
***
@@ -1888,7 +1888,7 @@ Contains the fetched token and the client information. These should be stored an
*Defined in*
-[packages/cozy-client/src/CozyClient.js:626](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L626)
+[packages/cozy-client/src/CozyClient.js:628](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L628)
***
@@ -1908,7 +1908,7 @@ Contains the fetched token and the client information. These should be stored an
*Defined in*
-[packages/cozy-client/src/CozyClient.js:1034](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1034)
+[packages/cozy-client/src/CozyClient.js:1036](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L1036)
***
@@ -1934,7 +1934,7 @@ the DOM.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:390](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L390)
+[packages/cozy-client/src/CozyClient.js:392](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L392)
***
@@ -1958,7 +1958,7 @@ environment variables
*Defined in*
-[packages/cozy-client/src/CozyClient.js:361](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L361)
+[packages/cozy-client/src/CozyClient.js:363](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L363)
***
@@ -1982,7 +1982,7 @@ a client with a cookie-based instance of cozy-client-js.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:311](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L311)
+[packages/cozy-client/src/CozyClient.js:313](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L313)
***
@@ -2010,7 +2010,7 @@ An instance of a client, configured from the old client
*Defined in*
-[packages/cozy-client/src/CozyClient.js:329](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L329)
+[packages/cozy-client/src/CozyClient.js:331](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L331)
***
@@ -2044,4 +2044,4 @@ There are at the moment only 2 hooks available.
*Defined in*
-[packages/cozy-client/src/CozyClient.js:850](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L850)
+[packages/cozy-client/src/CozyClient.js:852](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyClient.js#L852)
diff --git a/docs/api/cozy-client/classes/CozyLink.md b/docs/api/cozy-client/classes/CozyLink.md
index 2cefac7a5b..9c8abfae53 100644
--- a/docs/api/cozy-client/classes/CozyLink.md
+++ b/docs/api/cozy-client/classes/CozyLink.md
@@ -8,17 +8,20 @@
↳ [`StackLink`](StackLink.md)
+ ↳ [`WebFlagshipLink`](WebFlagshipLink.md)
+
## Constructors
### constructor
-• **new CozyLink**(`requestHandler`)
+• **new CozyLink**(`requestHandler`, `persistHandler`)
*Parameters*
| Name | Type |
| :------ | :------ |
| `requestHandler` | `any` |
+| `persistHandler` | `any` |
*Defined in*
@@ -26,22 +29,63 @@
## Methods
+### persistCozyData
+
+▸ **persistCozyData**(`data`, `forward`): `Promise`<`any`>
+
+Persist the given data into the links storage
+
+*Parameters*
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `data` | `any` | The document to persist |
+| `forward` | `any` | The next persistCozyData of the chain |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Defined in*
+
+[packages/cozy-client/src/CozyLink.js:31](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyLink.js#L31)
+
+***
+
### request
-▸ **request**(`operation`, `result`, `forward`): `void`
+▸ **request**(`operation`, `result`, `forward`): `Promise`<`any`>
+
+Request the given operation from the link
*Parameters*
-| Name | Type |
-| :------ | :------ |
-| `operation` | `any` |
-| `result` | `any` |
-| `forward` | `any` |
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `operation` | `any` | The operation to request |
+| `result` | `any` | The result from the previous request of the chain |
+| `forward` | `any` | The next request of the chain |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Defined in*
+
+[packages/cozy-client/src/CozyLink.js:20](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyLink.js#L20)
+
+***
+
+### reset
+
+▸ **reset**(): `Promise`<`any`>
+
+Reset the link data
*Returns*
-`void`
+`Promise`<`any`>
*Defined in*
-[packages/cozy-client/src/CozyLink.js:8](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyLink.js#L8)
+[packages/cozy-client/src/CozyLink.js:40](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/CozyLink.js#L40)
diff --git a/docs/api/cozy-client/classes/StackLink.md b/docs/api/cozy-client/classes/StackLink.md
index 7a8fb81bf5..6e69ab319d 100644
--- a/docs/api/cozy-client/classes/StackLink.md
+++ b/docs/api/cozy-client/classes/StackLink.md
@@ -20,9 +20,7 @@ Transfers queries and mutations to a remote stack
| Name | Type | Description |
| :------ | :------ | :------ |
-| `[options]` | `Object` | Options |
-| `[options].client` | `any` | - |
-| `[options].stackClient` | `any` | - |
+| `[options]` | `StackLinkOptions` | Options |
*Overrides*
@@ -30,17 +28,27 @@ Transfers queries and mutations to a remote stack
*Defined in*
-[packages/cozy-client/src/StackLink.js:62](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L62)
+[packages/cozy-client/src/StackLink.js:68](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L68)
## Properties
+### isOnline
+
+• **isOnline**: `any`
+
+*Defined in*
+
+[packages/cozy-client/src/StackLink.js:76](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L76)
+
+***
+
### stackClient
• **stackClient**: `any`
*Defined in*
-[packages/cozy-client/src/StackLink.js:69](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L69)
+[packages/cozy-client/src/StackLink.js:75](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L75)
## Methods
@@ -62,7 +70,7 @@ Transfers queries and mutations to a remote stack
*Defined in*
-[packages/cozy-client/src/StackLink.js:114](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L114)
+[packages/cozy-client/src/StackLink.js:136](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L136)
***
@@ -82,7 +90,34 @@ Transfers queries and mutations to a remote stack
*Defined in*
-[packages/cozy-client/src/StackLink.js:91](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L91)
+[packages/cozy-client/src/StackLink.js:113](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L113)
+
+***
+
+### persistCozyData
+
+▸ **persistCozyData**(`data`, `forward`): `Promise`<`any`>
+
+Persist the given data into the links storage
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `data` | `any` |
+| `forward` | `any` |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Overrides*
+
+[CozyLink](CozyLink.md).[persistCozyData](CozyLink.md#persistcozydata)
+
+*Defined in*
+
+[packages/cozy-client/src/StackLink.js:105](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L105)
***
@@ -102,7 +137,7 @@ Transfers queries and mutations to a remote stack
*Defined in*
-[packages/cozy-client/src/StackLink.js:72](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L72)
+[packages/cozy-client/src/StackLink.js:79](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L79)
***
@@ -110,6 +145,8 @@ Transfers queries and mutations to a remote stack
▸ **request**(`operation`, `result`, `forward`): `Promise`<`any`>
+Request the given operation from the link
+
*Parameters*
| Name | Type |
@@ -128,18 +165,24 @@ Transfers queries and mutations to a remote stack
*Defined in*
-[packages/cozy-client/src/StackLink.js:80](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L80)
+[packages/cozy-client/src/StackLink.js:87](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L87)
***
### reset
-▸ **reset**(): `void`
+▸ **reset**(): `Promise`<`void`>
+
+Reset the link data
*Returns*
-`void`
+`Promise`<`void`>
+
+*Overrides*
+
+[CozyLink](CozyLink.md).[reset](CozyLink.md#reset)
*Defined in*
-[packages/cozy-client/src/StackLink.js:76](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L76)
+[packages/cozy-client/src/StackLink.js:83](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/StackLink.js#L83)
diff --git a/docs/api/cozy-client/classes/WebFlagshipLink.md b/docs/api/cozy-client/classes/WebFlagshipLink.md
new file mode 100644
index 0000000000..b405fc2b31
--- /dev/null
+++ b/docs/api/cozy-client/classes/WebFlagshipLink.md
@@ -0,0 +1,135 @@
+[cozy-client](../README.md) / WebFlagshipLink
+
+# Class: WebFlagshipLink
+
+## Hierarchy
+
+* [`CozyLink`](CozyLink.md)
+
+ ↳ **`WebFlagshipLink`**
+
+## Constructors
+
+### constructor
+
+• **new WebFlagshipLink**(`[options]?`)
+
+*Parameters*
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `[options]` | `Object` | Options |
+| `[options].webviewIntent` | `WebviewService` | - |
+
+*Overrides*
+
+[CozyLink](CozyLink.md).[constructor](CozyLink.md#constructor)
+
+*Defined in*
+
+[packages/cozy-client/src/WebFlagshipLink.js:8](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/WebFlagshipLink.js#L8)
+
+## Properties
+
+### webviewIntent
+
+• **webviewIntent**: `WebviewService`
+
+*Defined in*
+
+[packages/cozy-client/src/WebFlagshipLink.js:10](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/WebFlagshipLink.js#L10)
+
+## Methods
+
+### persistCozyData
+
+▸ **persistCozyData**(`data`, `forward`): `Promise`<`void`>
+
+Persist the given data into the links storage
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `data` | `any` |
+| `forward` | `any` |
+
+*Returns*
+
+`Promise`<`void`>
+
+*Overrides*
+
+[CozyLink](CozyLink.md).[persistCozyData](CozyLink.md#persistcozydata)
+
+*Defined in*
+
+[packages/cozy-client/src/WebFlagshipLink.js:25](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/WebFlagshipLink.js#L25)
+
+***
+
+### registerClient
+
+▸ **registerClient**(`client`): `void`
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `client` | `any` |
+
+*Returns*
+
+`void`
+
+*Defined in*
+
+[packages/cozy-client/src/WebFlagshipLink.js:13](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/WebFlagshipLink.js#L13)
+
+***
+
+### request
+
+▸ **request**(`operation`, `result`, `forward`): `Promise`<`boolean`>
+
+Request the given operation from the link
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `operation` | `any` |
+| `result` | `any` |
+| `forward` | `any` |
+
+*Returns*
+
+`Promise`<`boolean`>
+
+*Overrides*
+
+[CozyLink](CozyLink.md).[request](CozyLink.md#request)
+
+*Defined in*
+
+[packages/cozy-client/src/WebFlagshipLink.js:21](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/WebFlagshipLink.js#L21)
+
+***
+
+### reset
+
+▸ **reset**(): `Promise`<`void`>
+
+Reset the link data
+
+*Returns*
+
+`Promise`<`void`>
+
+*Overrides*
+
+[CozyLink](CozyLink.md).[reset](CozyLink.md#reset)
+
+*Defined in*
+
+[packages/cozy-client/src/WebFlagshipLink.js:17](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/WebFlagshipLink.js#L17)
diff --git a/docs/api/cozy-client/interfaces/models.file.FileUploadOptions.md b/docs/api/cozy-client/interfaces/models.file.FileUploadOptions.md
index 41adfd3631..1f4eedaf66 100644
--- a/docs/api/cozy-client/interfaces/models.file.FileUploadOptions.md
+++ b/docs/api/cozy-client/interfaces/models.file.FileUploadOptions.md
@@ -14,7 +14,7 @@ Conflict options
*Defined in*
-[packages/cozy-client/src/models/file.js:494](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L494)
+[packages/cozy-client/src/models/file.js:496](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L496)
***
@@ -26,7 +26,7 @@ Erase / rename
*Defined in*
-[packages/cozy-client/src/models/file.js:493](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L493)
+[packages/cozy-client/src/models/file.js:495](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L495)
***
@@ -38,7 +38,7 @@ The file Content-Type
*Defined in*
-[packages/cozy-client/src/models/file.js:492](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L492)
+[packages/cozy-client/src/models/file.js:494](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L494)
***
@@ -50,7 +50,7 @@ The dirId to upload the file to
*Defined in*
-[packages/cozy-client/src/models/file.js:490](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L490)
+[packages/cozy-client/src/models/file.js:492](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L492)
***
@@ -62,7 +62,7 @@ An object containing the metadata to attach
*Defined in*
-[packages/cozy-client/src/models/file.js:491](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L491)
+[packages/cozy-client/src/models/file.js:493](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L493)
***
@@ -74,4 +74,4 @@ The file name to upload
*Defined in*
-[packages/cozy-client/src/models/file.js:489](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L489)
+[packages/cozy-client/src/models/file.js:491](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L491)
diff --git a/docs/api/cozy-client/interfaces/models.instance.DiskInfos.md b/docs/api/cozy-client/interfaces/models.instance.DiskInfos.md
index 2b59c4d611..332a46461d 100644
--- a/docs/api/cozy-client/interfaces/models.instance.DiskInfos.md
+++ b/docs/api/cozy-client/interfaces/models.instance.DiskInfos.md
@@ -14,7 +14,7 @@ Space used in GB rounded
*Defined in*
-[packages/cozy-client/src/models/instance.js:121](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L121)
+[packages/cozy-client/src/models/instance.js:113](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L113)
***
@@ -26,7 +26,7 @@ Maximum space available in GB rounded
*Defined in*
-[packages/cozy-client/src/models/instance.js:122](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L122)
+[packages/cozy-client/src/models/instance.js:114](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L114)
***
@@ -38,4 +38,4 @@ Usage percent of the disk rounded
*Defined in*
-[packages/cozy-client/src/models/instance.js:123](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L123)
+[packages/cozy-client/src/models/instance.js:115](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L115)
diff --git a/docs/api/cozy-client/interfaces/models.instance.DiskInfosRaw.md b/docs/api/cozy-client/interfaces/models.instance.DiskInfosRaw.md
index 5036a9d3de..a2a71efe0d 100644
--- a/docs/api/cozy-client/interfaces/models.instance.DiskInfosRaw.md
+++ b/docs/api/cozy-client/interfaces/models.instance.DiskInfosRaw.md
@@ -14,7 +14,7 @@ Space used in GB
*Defined in*
-[packages/cozy-client/src/models/instance.js:114](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L114)
+[packages/cozy-client/src/models/instance.js:106](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L106)
***
@@ -26,7 +26,7 @@ Maximum space available in GB
*Defined in*
-[packages/cozy-client/src/models/instance.js:115](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L115)
+[packages/cozy-client/src/models/instance.js:107](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L107)
***
@@ -38,4 +38,4 @@ Usage percent of the disk
*Defined in*
-[packages/cozy-client/src/models/instance.js:116](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L116)
+[packages/cozy-client/src/models/instance.js:108](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L108)
diff --git a/docs/api/cozy-client/modules/models.applications.md b/docs/api/cozy-client/modules/models.applications.md
index 1082ccbadb..defa3e3b11 100644
--- a/docs/api/cozy-client/modules/models.applications.md
+++ b/docs/api/cozy-client/modules/models.applications.md
@@ -27,7 +27,7 @@ Name of the app suitable for display
*Defined in*
-[packages/cozy-client/src/models/applications.js:73](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/applications.js#L73)
+[packages/cozy-client/src/models/applications.js:71](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/applications.js#L71)
***
@@ -99,7 +99,7 @@ url to the app
*Defined in*
-[packages/cozy-client/src/models/applications.js:61](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/applications.js#L61)
+[packages/cozy-client/src/models/applications.js:59](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/applications.js#L59)
***
diff --git a/docs/api/cozy-client/modules/models.file.md b/docs/api/cozy-client/modules/models.file.md
index efdb749cf2..9b90abfc7f 100644
--- a/docs/api/cozy-client/modules/models.file.md
+++ b/docs/api/cozy-client/modules/models.file.md
@@ -16,7 +16,7 @@
*Defined in*
-[packages/cozy-client/src/models/file.js:14](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L14)
+[packages/cozy-client/src/models/file.js:16](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L16)
## Functions
@@ -44,7 +44,7 @@ Copies a file to a specified destination.
*Defined in*
-[packages/cozy-client/src/models/file.js:662](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L662)
+[packages/cozy-client/src/models/file.js:664](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L664)
***
@@ -68,7 +68,38 @@ Upload a file on a mobile
*Defined in*
-[packages/cozy-client/src/models/file.js:599](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L599)
+[packages/cozy-client/src/models/file.js:601](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L601)
+
+***
+
+### downloadFile
+
+▸ **downloadFile**(`params`): `Promise`<`any`>
+
+Download the requested file
+
+This method can be used in a web page context or in a WebView hosted by a Flagship app
+
+When used in a FlagshipApp WebView context, then the action is redirected to the host app
+that will process the download
+
+*Parameters*
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `params` | `Object` | The download parameters |
+| `params.client` | [`CozyClient`](../classes/CozyClient.md) | Instance of CozyClient |
+| `params.file` | `IOCozyFile` | io.cozy.files metadata of the document to downloaded |
+| `params.url` | `string` | - |
+| `params.webviewIntent` | `WebviewService` | - |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Defined in*
+
+[packages/cozy-client/src/models/file.js:718](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L718)
***
@@ -93,7 +124,7 @@ file object with path attribute
*Defined in*
-[packages/cozy-client/src/models/file.js:136](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L136)
+[packages/cozy-client/src/models/file.js:138](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L138)
***
@@ -114,7 +145,7 @@ file object with path attribute
*Defined in*
-[packages/cozy-client/src/models/file.js:645](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L645)
+[packages/cozy-client/src/models/file.js:647](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L647)
***
@@ -139,7 +170,7 @@ The files found by the rules
*Defined in*
-[packages/cozy-client/src/models/file.js:256](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L256)
+[packages/cozy-client/src/models/file.js:258](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L258)
***
@@ -163,7 +194,7 @@ Generate a file name for a revision
*Defined in*
-[packages/cozy-client/src/models/file.js:479](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L479)
+[packages/cozy-client/src/models/file.js:481](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L481)
***
@@ -188,7 +219,7 @@ A filename with the right suffix
*Defined in*
-[packages/cozy-client/src/models/file.js:449](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L449)
+[packages/cozy-client/src/models/file.js:451](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L451)
***
@@ -214,7 +245,7 @@ The full path of the file in the cozy
*Defined in*
-[packages/cozy-client/src/models/file.js:291](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L291)
+[packages/cozy-client/src/models/file.js:293](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L293)
***
@@ -238,7 +269,7 @@ id of the parent folder, if any
*Defined in*
-[packages/cozy-client/src/models/file.js:150](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L150)
+[packages/cozy-client/src/models/file.js:152](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L152)
***
@@ -262,7 +293,7 @@ A description of the status
*Defined in*
-[packages/cozy-client/src/models/file.js:162](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L162)
+[packages/cozy-client/src/models/file.js:164](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L164)
***
@@ -286,7 +317,7 @@ A doctype
*Defined in*
-[packages/cozy-client/src/models/file.js:182](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L182)
+[packages/cozy-client/src/models/file.js:184](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L184)
***
@@ -310,7 +341,7 @@ The mime-type of the target file, or an empty string is the target is not a file
*Defined in*
-[packages/cozy-client/src/models/file.js:172](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L172)
+[packages/cozy-client/src/models/file.js:174](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L174)
***
@@ -330,7 +361,7 @@ The mime-type of the target file, or an empty string is the target is not a file
*Defined in*
-[packages/cozy-client/src/models/file.js:625](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L625)
+[packages/cozy-client/src/models/file.js:627](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L627)
***
@@ -354,7 +385,7 @@ Whether the file's metadata attribute exists
*Defined in*
-[packages/cozy-client/src/models/file.js:280](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L280)
+[packages/cozy-client/src/models/file.js:282](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L282)
***
@@ -374,7 +405,7 @@ Whether the file's metadata attribute exists
*Defined in*
-[packages/cozy-client/src/models/file.js:617](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L617)
+[packages/cozy-client/src/models/file.js:619](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L619)
***
@@ -394,7 +425,7 @@ Whether the file's metadata attribute exists
*Defined in*
-[packages/cozy-client/src/models/file.js:46](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L46)
+[packages/cozy-client/src/models/file.js:48](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L48)
***
@@ -416,7 +447,7 @@ Whether the file is client-side encrypted
*Defined in*
-[packages/cozy-client/src/models/file.js:74](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L74)
+[packages/cozy-client/src/models/file.js:76](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L76)
***
@@ -436,7 +467,7 @@ Whether the file is client-side encrypted
*Defined in*
-[packages/cozy-client/src/models/file.js:40](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L40)
+[packages/cozy-client/src/models/file.js:42](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L42)
***
@@ -456,7 +487,7 @@ Whether the file is client-side encrypted
*Defined in*
-[packages/cozy-client/src/models/file.js:636](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L636)
+[packages/cozy-client/src/models/file.js:638](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L638)
***
@@ -478,7 +509,7 @@ Is file param a correct note
*Defined in*
-[packages/cozy-client/src/models/file.js:54](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L54)
+[packages/cozy-client/src/models/file.js:56](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L56)
***
@@ -500,7 +531,7 @@ Whether the file is supported by Only Office
*Defined in*
-[packages/cozy-client/src/models/file.js:84](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L84)
+[packages/cozy-client/src/models/file.js:86](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L86)
***
@@ -521,7 +552,7 @@ Whether the file is supported by Only Office
*Defined in*
-[packages/cozy-client/src/models/file.js:609](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L609)
+[packages/cozy-client/src/models/file.js:611](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L611)
***
@@ -545,7 +576,7 @@ Returns whether the file is a shortcut to a sharing
*Defined in*
-[packages/cozy-client/src/models/file.js:202](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L202)
+[packages/cozy-client/src/models/file.js:204](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L204)
***
@@ -569,7 +600,7 @@ Returns whether the sharing shortcut is new
*Defined in*
-[packages/cozy-client/src/models/file.js:227](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L227)
+[packages/cozy-client/src/models/file.js:229](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L229)
***
@@ -591,7 +622,7 @@ Returns whether the file is a shortcut to a sharing
*Defined in*
-[packages/cozy-client/src/models/file.js:192](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L192)
+[packages/cozy-client/src/models/file.js:194](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L194)
***
@@ -613,7 +644,7 @@ Returns whether the sharing shortcut is new
*Defined in*
-[packages/cozy-client/src/models/file.js:216](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L216)
+[packages/cozy-client/src/models/file.js:218](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L218)
***
@@ -635,7 +666,7 @@ true if the file is a shortcut
*Defined in*
-[packages/cozy-client/src/models/file.js:109](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L109)
+[packages/cozy-client/src/models/file.js:111](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L111)
***
@@ -670,7 +701,7 @@ Manage 4 cases :
*Defined in*
-[packages/cozy-client/src/models/file.js:320](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L320)
+[packages/cozy-client/src/models/file.js:322](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L322)
***
@@ -696,7 +727,7 @@ full normalized object
*Defined in*
-[packages/cozy-client/src/models/file.js:122](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L122)
+[packages/cozy-client/src/models/file.js:124](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L124)
***
@@ -723,7 +754,7 @@ The overrided file
*Defined in*
-[packages/cozy-client/src/models/file.js:415](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L415)
+[packages/cozy-client/src/models/file.js:417](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L417)
***
@@ -745,7 +776,7 @@ Read a file on a mobile
*Defined in*
-[packages/cozy-client/src/models/file.js:552](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L552)
+[packages/cozy-client/src/models/file.js:554](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L554)
***
@@ -771,7 +802,7 @@ The saved file
*Defined in*
-[packages/cozy-client/src/models/file.js:242](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L242)
+[packages/cozy-client/src/models/file.js:244](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L244)
***
@@ -795,7 +826,7 @@ But we want to exclude .txt and .md because the CozyUI Viewer can already show t
*Defined in*
-[packages/cozy-client/src/models/file.js:99](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L99)
+[packages/cozy-client/src/models/file.js:101](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L101)
***
@@ -822,7 +853,7 @@ Returns base filename and extension
*Defined in*
-[packages/cozy-client/src/models/file.js:24](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L24)
+[packages/cozy-client/src/models/file.js:26](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L26)
***
@@ -855,4 +886,4 @@ If there is a conflict, then we apply the conflict strategy : `erase` or `rename
*Defined in*
-[packages/cozy-client/src/models/file.js:512](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L512)
+[packages/cozy-client/src/models/file.js:514](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/file.js#L514)
diff --git a/docs/api/cozy-client/modules/models.instance.md b/docs/api/cozy-client/modules/models.instance.md
index edca32687b..ea90003778 100644
--- a/docs/api/cozy-client/modules/models.instance.md
+++ b/docs/api/cozy-client/modules/models.instance.md
@@ -80,7 +80,7 @@ Returns the link to the Premium page on the Cozy's Manager
*Defined in*
-[packages/cozy-client/src/models/instance.js:72](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L72)
+[packages/cozy-client/src/models/instance.js:70](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L70)
***
@@ -100,7 +100,7 @@ Returns the link to the Premium page on the Cozy's Manager
*Defined in*
-[packages/cozy-client/src/models/instance.js:34](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L34)
+[packages/cozy-client/src/models/instance.js:32](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L32)
***
@@ -124,7 +124,7 @@ Does the cozy have offers
*Defined in*
-[packages/cozy-client/src/models/instance.js:58](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L58)
+[packages/cozy-client/src/models/instance.js:56](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L56)
***
@@ -148,7 +148,7 @@ Checks the value of the password_defined attribute
*Defined in*
-[packages/cozy-client/src/models/instance.js:92](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L92)
+[packages/cozy-client/src/models/instance.js:86](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L86)
***
@@ -168,7 +168,7 @@ Checks the value of the password_defined attribute
*Defined in*
-[packages/cozy-client/src/models/instance.js:30](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L30)
+[packages/cozy-client/src/models/instance.js:28](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L28)
***
@@ -219,7 +219,7 @@ Make human readable information from disk information (usage, quota)
*Defined in*
-[packages/cozy-client/src/models/instance.js:164](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L164)
+[packages/cozy-client/src/models/instance.js:156](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L156)
***
@@ -243,4 +243,4 @@ Should we display offers
*Defined in*
-[packages/cozy-client/src/models/instance.js:44](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L44)
+[packages/cozy-client/src/models/instance.js:42](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/instance.js#L42)
diff --git a/docs/api/cozy-pouch-link.md b/docs/api/cozy-pouch-link.md
deleted file mode 100644
index 90d59e9f1e..0000000000
--- a/docs/api/cozy-pouch-link.md
+++ /dev/null
@@ -1,593 +0,0 @@
-## Classes
-
-
-PouchLink
-Link to be passed to a CozyClient
instance to support CouchDB. It instantiates
-PouchDB collections for each doctype that it supports and knows how
-to respond to queries and mutations.
-
-Loop
-Utility to call a function (task) periodically
-and on demand immediately.
-Public API
-
-start
-stop
-scheduleImmediateTask
-waitForCurrentTask
-
-
-PouchManager
-Handles the lifecycle of several pouches
-
-Creates/Destroys the pouches
-Replicates periodically
-
-
-
-
-## Constants
-
-
-persistLastReplicatedDocID
-Persist the last replicated doc id for a doctype
-
-getLastReplicatedDocID ⇒ string
-Get the last replicated doc id for a doctype
-
-destroyAllLastReplicatedDocID
-Destroy all the replicated doc id
-
-getPersistedSyncedDoctypes ⇒ object
-Get the persisted doctypes
-
-destroySyncedDoctypes
-Destroy the synced doctypes
-
-persistDoctypeLastSequence
-Persist the last CouchDB sequence for a synced doctype
-
-getDoctypeLastSequence ⇒ string
-Get the last CouchDB sequence for a doctype
-
-destroyAllDoctypeLastSequence
-Destroy all the last sequence
-
-destroyDoctypeLastSequence
-Destroy the last sequence for a doctype
-
-persistWarmedUpQueries
-Persist the warmed up queries
-
-getPersistedWarmedUpQueries ⇒ object
-Get the warmed up queries
-
-destroyWarmedUpQueries
-Destroy the warmed queries
-
-getAdapterName ⇒ string
-Get the adapter name
-
-persistAdapterName
-Persist the adapter name
-
-fetchRemoteInstance ⇒ object
-Fetch remote instance
-
-fetchRemoteLastSequence ⇒ string
-Fetch last sequence from remote instance
-
-replicateAllDocs ⇒ Array
-Replicate all docs locally from a remote URL.
-It uses the _all_docs view, and bulk insert the docs.
-Note it saves the last replicated _id for each run and
-starts from there in case the process stops before the end.
-
-getDatabaseName ⇒ string
-Get the database name based on prefix and doctype
-
-getPrefix ⇒ string
-Get the URI prefix
-
-
-
-## Functions
-
-
-getQueryAlias(query) ⇒ string
-
-
-
-## Typedefs
-
-
-SyncStatus : "idle"
| "replicating"
-
-MigrationParams : object
-Migrate the current adapter
-
-SyncInfo : object
-Persist the synchronized doctypes
-
-MigrationParams ⇒ object
-Migrate a PouchDB database to a new adapter.
-
-
-
-
-
-## PouchLink
-Link to be passed to a `CozyClient` instance to support CouchDB. It instantiates
-PouchDB collections for each doctype that it supports and knows how
-to respond to queries and mutations.
-
-**Kind**: global class
-
-* [PouchLink](#PouchLink)
- * [new PouchLink([opts])](#new_PouchLink_new)
- * [.replicationStatus](#PouchLink+replicationStatus) : Record.<string, SyncStatus>
- * [.getPouchAdapterName](#PouchLink+getPouchAdapterName) ⇒ string
- * [.handleOnSync()](#PouchLink+handleOnSync)
- * [.startReplication()](#PouchLink+startReplication) ⇒ void
- * [.stopReplication()](#PouchLink+stopReplication) ⇒ void
- * [.needsToWaitWarmup(doctype)](#PouchLink+needsToWaitWarmup) ⇒ boolean
-
-
-
-### new PouchLink([opts])
-constructor - Initializes a new PouchLink
-
-**Returns**: object
- The PouchLink instance
-
-| Param | Type | Default | Description |
-| --- | --- | --- | --- |
-| [opts] | object
| {}
| |
-| [opts.replicationInterval] | number
| | Milliseconds between replications |
-| opts.doctypes | Array.<string>
| | Doctypes to replicate |
-| opts.doctypesReplicationOptions | Array.<object>
| | A mapping from doctypes to replication options. All pouch replication options can be used, as well as the "strategy" option that determines which way the replication is done (can be "sync", "fromRemote" or "toRemote") |
-
-
-
-### pouchLink.replicationStatus : Record.<string, SyncStatus>
-- Stores replication states per doctype
-
-**Kind**: instance property of [PouchLink
](#PouchLink)
-
-
-### pouchLink.getPouchAdapterName ⇒ string
-Return the PouchDB adapter name.
-Should be IndexedDB for newest adapters.
-
-**Kind**: instance property of [PouchLink
](#PouchLink)
-**Returns**: string
- The adapter name
-
-
-### pouchLink.handleOnSync()
-Receives PouchDB updates (documents grouped by doctype).
-Normalizes the data (.id -> ._id, .rev -> _rev).
-Passes the data to the client and to the onSync handler.
-
-Emits an event (pouchlink:sync:end) when the sync (all doctypes) is done
-
-**Kind**: instance method of [PouchLink
](#PouchLink)
-
-
-### pouchLink.startReplication() ⇒ void
-User of the link can call this to start ongoing replications.
-Typically, it can be used when the application regains focus.
-
-Emits pouchlink:sync:start event when the replication begins
-
-**Kind**: instance method of [PouchLink
](#PouchLink)
-**Access**: public
-
-
-### pouchLink.stopReplication() ⇒ void
-User of the link can call this to stop ongoing replications.
-Typically, it can be used when the applications loses focus.
-
-Emits pouchlink:sync:stop event
-
-**Kind**: instance method of [PouchLink
](#PouchLink)
-**Access**: public
-
-
-### pouchLink.needsToWaitWarmup(doctype) ⇒ boolean
-Check if there is warmup queries for this doctype
-and return if those queries are already warmed up or not
-
-**Kind**: instance method of [PouchLink
](#PouchLink)
-**Returns**: boolean
- the need to wait for the warmup
-
-| Param | Type | Description |
-| --- | --- | --- |
-| doctype | string
| Doctype to check |
-
-
-
-## Loop
-Utility to call a function (task) periodically
-and on demand immediately.
-
-Public API
-
-- start
-- stop
-- scheduleImmediateTask
-- waitForCurrentTask
-
-**Kind**: global class
-
-* [Loop](#Loop)
- * [.start()](#Loop+start)
- * [.stop()](#Loop+stop)
- * [.runImmediateTasks()](#Loop+runImmediateTasks)
- * [.scheduleImmediateTask(task)](#Loop+scheduleImmediateTask)
- * [.runTask()](#Loop+runTask)
- * [.round()](#Loop+round)
-
-
-
-### loop.start()
-Starts the loop. Will run the task periodically each `this.delay` ms.
-Ignores multiple starts.
-
-**Kind**: instance method of [Loop
](#Loop)
-
-
-### loop.stop()
-Stops the loop, clears immediate tasks.
-Cancels current task if possible
-
-**Kind**: instance method of [Loop
](#Loop)
-
-
-### loop.runImmediateTasks()
-Flushes the immediate tasks list and calls each task.
-Each task is awaited before the next is started.
-
-**Kind**: instance method of [Loop
](#Loop)
-
-
-### loop.scheduleImmediateTask(task)
-Schedules a task to be run immediately at next round.
-Ignored if loop is not started.
-If not task is passed, the default task from the loop is used.
-
-**Kind**: instance method of [Loop
](#Loop)
-
-| Param | Type | Default | Description |
-| --- | --- | --- | --- |
-| task | function
|
| Optional custom function to be run immediately |
-
-
-
-### loop.runTask()
-Calls and saves current task.
-Stops loop in case of error of the task.
-
-**Kind**: instance method of [Loop
](#Loop)
-
-
-### loop.round()
-Runs immediate tasks and then schedule the next round.
-Immediate tasks are called sequentially without delay
-There is a delay between immediate tasks and normal periodic tasks.
-
-**Kind**: instance method of [Loop
](#Loop)
-
-
-## PouchManager
-Handles the lifecycle of several pouches
-
-- Creates/Destroys the pouches
-- Replicates periodically
-
-**Kind**: global class
-
-* [PouchManager](#PouchManager)
- * [.ensureDatabasesExist()](#PouchManager+ensureDatabasesExist)
- * [.startReplicationLoop()](#PouchManager+startReplicationLoop)
- * [.stopReplicationLoop()](#PouchManager+stopReplicationLoop)
- * [.syncImmediately()](#PouchManager+syncImmediately)
- * [.replicateOnce()](#PouchManager+replicateOnce)
-
-
-
-### pouchManager.ensureDatabasesExist()
-Via a call to info() we ensure the database exist on the
-remote side. This is done only once since after the first
-call, we are sure that the databases have been created.
-
-**Kind**: instance method of [PouchManager
](#PouchManager)
-
-
-### pouchManager.startReplicationLoop()
-Starts periodic syncing of the pouches
-
-**Kind**: instance method of [PouchManager
](#PouchManager)
-
-
-### pouchManager.stopReplicationLoop()
-Stop periodic syncing of the pouches
-
-**Kind**: instance method of [PouchManager
](#PouchManager)
-
-
-### pouchManager.syncImmediately()
-If a replication is currently ongoing, will start a replication
-just after it has finished. Otherwise it will start a replication
-immediately
-
-**Kind**: instance method of [PouchManager
](#PouchManager)
-
-
-### pouchManager.replicateOnce()
-Starts replication
-
-**Kind**: instance method of [PouchManager
](#PouchManager)
-
-
-## persistLastReplicatedDocID
-Persist the last replicated doc id for a doctype
-
-**Kind**: global constant
-
-| Param | Type | Description |
-| --- | --- | --- |
-| doctype | string
| The replicated doctype |
-| id | string
| The docid |
-
-
-
-## getLastReplicatedDocID ⇒ string
-Get the last replicated doc id for a doctype
-
-**Kind**: global constant
-**Returns**: string
- The last replicated docid
-
-| Param | Type | Description |
-| --- | --- | --- |
-| doctype | string
| The doctype |
-
-
-
-## destroyAllLastReplicatedDocID
-Destroy all the replicated doc id
-
-**Kind**: global constant
-
-
-## getPersistedSyncedDoctypes ⇒ object
-Get the persisted doctypes
-
-**Kind**: global constant
-**Returns**: object
- The synced doctypes
-
-
-## destroySyncedDoctypes
-Destroy the synced doctypes
-
-**Kind**: global constant
-
-
-## persistDoctypeLastSequence
-Persist the last CouchDB sequence for a synced doctype
-
-**Kind**: global constant
-
-| Param | Type | Description |
-| --- | --- | --- |
-| doctype | string
| The synced doctype |
-| sequence | string
| The sequence hash |
-
-
-
-## getDoctypeLastSequence ⇒ string
-Get the last CouchDB sequence for a doctype
-
-**Kind**: global constant
-**Returns**: string
- the last sequence
-
-| Param | Type | Description |
-| --- | --- | --- |
-| doctype | string
| The doctype |
-
-
-
-## destroyAllDoctypeLastSequence
-Destroy all the last sequence
-
-**Kind**: global constant
-
-
-## destroyDoctypeLastSequence
-Destroy the last sequence for a doctype
-
-**Kind**: global constant
-
-| Param | Type | Description |
-| --- | --- | --- |
-| doctype | string
| The doctype |
-
-
-
-## persistWarmedUpQueries
-Persist the warmed up queries
-
-**Kind**: global constant
-
-| Param | Type | Description |
-| --- | --- | --- |
-| warmedUpQueries | object
| The warmedup queries |
-
-
-
-## getPersistedWarmedUpQueries ⇒ object
-Get the warmed up queries
-
-**Kind**: global constant
-**Returns**: object
- the warmed up queries
-
-
-## destroyWarmedUpQueries
-Destroy the warmed queries
-
-**Kind**: global constant
-
-
-## getAdapterName ⇒ string
-Get the adapter name
-
-**Kind**: global constant
-**Returns**: string
- The adapter name
-
-
-## persistAdapterName
-Persist the adapter name
-
-**Kind**: global constant
-
-| Param | Type | Description |
-| --- | --- | --- |
-| adapter | string
| The adapter name |
-
-
-
-## fetchRemoteInstance ⇒ object
-Fetch remote instance
-
-**Kind**: global constant
-**Returns**: object
- The instance response
-
-| Param | Type | Description |
-| --- | --- | --- |
-| url | URL
| The remote instance URL, including the credentials |
-| params | object
| The params to query the remote instance |
-
-
-
-## fetchRemoteLastSequence ⇒ string
-Fetch last sequence from remote instance
-
-**Kind**: global constant
-**Returns**: string
- The last sequence
-
-| Param | Type | Description |
-| --- | --- | --- |
-| baseUrl | string
| The base URL of the remote instance |
-
-
-
-## replicateAllDocs ⇒ Array
-Replicate all docs locally from a remote URL.
-
-It uses the _all_docs view, and bulk insert the docs.
-Note it saves the last replicated _id for each run and
-starts from there in case the process stops before the end.
-
-**Kind**: global constant
-**Returns**: Array
- The retrieved documents
-
-| Param | Type | Description |
-| --- | --- | --- |
-| db | object
| Pouch instance |
-| baseUrl | string
| The remote instance |
-| doctype | string
| The doctype to replicate |
-
-
-
-## getDatabaseName ⇒ string
-Get the database name based on prefix and doctype
-
-**Kind**: global constant
-**Returns**: string
- The database name
-
-| Param | Type | Description |
-| --- | --- | --- |
-| prefix | string
| The URL prefix |
-| doctype | string
| The database doctype |
-
-
-
-## getPrefix ⇒ string
-Get the URI prefix
-
-**Kind**: global constant
-**Returns**: string
- The URI prefix
-
-| Param | Type | Description |
-| --- | --- | --- |
-| uri | string
| The Cozy URI |
-
-
-
-## getQueryAlias(query) ⇒ string
-**Kind**: global function
-**Returns**: string
- alias
-
-| Param | Type | Description |
-| --- | --- | --- |
-| query | QueryDefinition
| The query definition whose name we're getting |
-
-
-
-## SyncStatus : "idle"
\| "replicating"
-**Kind**: global typedef
-
-
-## MigrationParams : object
-Migrate the current adapter
-
-**Kind**: global typedef
-
-| Param | Type | Description |
-| --- | --- | --- |
-| params | [MigrationParams
](#MigrationParams) | Migration params |
-
-**Properties**
-
-| Name | Type | Description |
-| --- | --- | --- |
-| [fromAdapter] | string
| The current adapter type, e.g. 'idb' |
-| [toAdapter] | string
| The new adapter type, e.g. 'indexeddb' |
-| [url] | string
| The Cozy URL |
-| [plugins] | Array.<object>
| The PouchDB plugins |
-
-
-
-## SyncInfo : object
-Persist the synchronized doctypes
-
-**Kind**: global typedef
-
-| Param | Type | Description |
-| --- | --- | --- |
-| syncedDoctypes | Record.<string, SyncInfo>
| The sync doctypes |
-
-**Properties**
-
-| Name | Type |
-| --- | --- |
-| Date | string
|
-
-
-
-## MigrationParams ⇒ object
-Migrate a PouchDB database to a new adapter.
-
-**Kind**: global typedef
-**Returns**: object
- - The migrated pouch
-
-| Param | Type | Description |
-| --- | --- | --- |
-| params | [MigrationParams
](#MigrationParams) | The migration params |
-
-**Properties**
-
-| Name | Type | Description |
-| --- | --- | --- |
-| [dbName] | string
| The database name |
-| [fromAdapter] | string
| The current adapter type, e.g. 'idb' |
-| [toAdapter] | string
| The new adapter type, e.g. 'indexeddb' |
-
diff --git a/docs/api/cozy-pouch-link/.nojekyll b/docs/api/cozy-pouch-link/.nojekyll
new file mode 100644
index 0000000000..e2ac6616ad
--- /dev/null
+++ b/docs/api/cozy-pouch-link/.nojekyll
@@ -0,0 +1 @@
+TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
\ No newline at end of file
diff --git a/docs/api/cozy-pouch-link/README.md b/docs/api/cozy-pouch-link/README.md
new file mode 100644
index 0000000000..547f65421b
--- /dev/null
+++ b/docs/api/cozy-pouch-link/README.md
@@ -0,0 +1,7 @@
+cozy-pouch-link
+
+# cozy-pouch-link
+
+## Classes
+
+* [PouchLink](classes/PouchLink.md)
diff --git a/docs/api/cozy-pouch-link/classes/PouchLink.md b/docs/api/cozy-pouch-link/classes/PouchLink.md
new file mode 100644
index 0000000000..6e74bf75c0
--- /dev/null
+++ b/docs/api/cozy-pouch-link/classes/PouchLink.md
@@ -0,0 +1,769 @@
+[cozy-pouch-link](../README.md) / PouchLink
+
+# Class: PouchLink
+
+Link to be passed to a `CozyClient` instance to support CouchDB. It instantiates
+PouchDB collections for each doctype that it supports and knows how
+to respond to queries and mutations.
+
+## Hierarchy
+
+* `default`
+
+ ↳ **`PouchLink`**
+
+## Constructors
+
+### constructor
+
+• **new PouchLink**(`opts`)
+
+constructor - Initializes a new PouchLink
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `opts` | `PouchLinkOptions` |
+
+*Overrides*
+
+CozyLink.constructor
+
+*Defined in*
+
+[CozyPouchLink.js:87](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L87)
+
+## Properties
+
+### client
+
+• **client**: `any`
+
+*Defined in*
+
+[CozyPouchLink.js:140](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L140)
+
+***
+
+### doctypes
+
+• **doctypes**: `string`\[]
+
+*Defined in*
+
+[CozyPouchLink.js:97](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L97)
+
+***
+
+### doctypesReplicationOptions
+
+• **doctypesReplicationOptions**: `Record`<`string`, `any`>
+
+*Defined in*
+
+[CozyPouchLink.js:98](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L98)
+
+***
+
+### indexes
+
+• **indexes**: `Object`
+
+*Defined in*
+
+[CozyPouchLink.js:99](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L99)
+
+***
+
+### options
+
+• **options**: { `replicationInterval`: `number` } & `PouchLinkOptions`
+
+*Defined in*
+
+[CozyPouchLink.js:91](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L91)
+
+***
+
+### pouches
+
+• **pouches**: `any`
+
+*Defined in*
+
+[CozyPouchLink.js:210](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L210)
+
+***
+
+### replicationStatus
+
+• **replicationStatus**: `Record`<`string`, `ReplicationStatus`>
+
+*Defined in*
+
+[CozyPouchLink.js:105](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L105)
+
+***
+
+### storage
+
+• **storage**: `PouchLocalStorage`
+
+*Defined in*
+
+[CozyPouchLink.js:100](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L100)
+
+## Methods
+
+### addReferencesTo
+
+▸ **addReferencesTo**(`mutation`): `Promise`<`void`>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `mutation` | `any` |
+
+*Returns*
+
+`Promise`<`void`>
+
+*Defined in*
+
+[CozyPouchLink.js:689](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L689)
+
+***
+
+### createDocument
+
+▸ **createDocument**(`mutation`): `Promise`<`any`>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `mutation` | `any` |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Defined in*
+
+[CozyPouchLink.js:650](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L650)
+
+***
+
+### createIndex
+
+▸ **createIndex**(`fields`, `indexOption?`): `Promise`<`PouchDbIndex`>
+
+Create the PouchDB index if not existing
+
+*Parameters*
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `fields` | `any`\[] | Fields to index |
+| `indexOption` | `Object` | Options for the index |
+| `indexOption.doctype` | `string` | - |
+| `indexOption.indexName` | `string` | - |
+| `indexOption.partialFilter` | `any` | - |
+
+*Returns*
+
+`Promise`<`PouchDbIndex`>
+
+*Defined in*
+
+[CozyPouchLink.js:494](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L494)
+
+***
+
+### dbMethod
+
+▸ **dbMethod**(`method`, `mutation`): `Promise`<`any`>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `method` | `any` |
+| `mutation` | `any` |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Defined in*
+
+[CozyPouchLink.js:693](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L693)
+
+***
+
+### deleteDocument
+
+▸ **deleteDocument**(`mutation`): `Promise`<`any`>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `mutation` | `any` |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Defined in*
+
+[CozyPouchLink.js:678](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L678)
+
+***
+
+### executeMutation
+
+▸ **executeMutation**(`mutation`, `result`, `forward`): `Promise`<`any`>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `mutation` | `any` |
+| `result` | `any` |
+| `forward` | `any` |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Defined in*
+
+[CozyPouchLink.js:620](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L620)
+
+***
+
+### executeQuery
+
+▸ **executeQuery**(`__namedParameters`): `Promise`<{ `data`: `any` = res.cozyPouchData; `meta`: `undefined` ; `next`: `undefined` ; `skip`: `undefined` = offset } | { `data`: `any` ; `meta`: { `count`: `any` = docs.length } ; `next`: `boolean` ; `skip`: `any` = offset }>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `__namedParameters` | `Object` |
+
+*Returns*
+
+`Promise`<{ `data`: `any` = res.cozyPouchData; `meta`: `undefined` ; `next`: `undefined` ; `skip`: `undefined` = offset } | { `data`: `any` ; `meta`: { `count`: `any` = docs.length } ; `next`: `boolean` ; `skip`: `any` = offset }>
+
+*Defined in*
+
+[CozyPouchLink.js:558](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L558)
+
+***
+
+### findExistingIndex
+
+▸ **findExistingIndex**(`doctype`, `options`, `indexName`): `PouchDbIndex`
+
+Retrieve the PouchDB index if exist, undefined otherwise
+
+*Parameters*
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `doctype` | `string` | The query's doctype |
+| `options` | `MangoQueryOptions` | The find options |
+| `indexName` | `string` | The index name |
+
+*Returns*
+
+`PouchDbIndex`
+
+*Defined in*
+
+[CozyPouchLink.js:518](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L518)
+
+***
+
+### getPouch
+
+▸ **getPouch**(`doctype`): `any`
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `doctype` | `any` |
+
+*Returns*
+
+`any`
+
+*Defined in*
+
+[CozyPouchLink.js:327](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L327)
+
+***
+
+### getReplicationURL
+
+▸ **getReplicationURL**(`doctype`): `string`
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `doctype` | `any` |
+
+*Returns*
+
+`string`
+
+*Defined in*
+
+[CozyPouchLink.js:120](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L120)
+
+***
+
+### getSyncInfo
+
+▸ **getSyncInfo**(`doctype`): `any`
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `doctype` | `any` |
+
+*Returns*
+
+`any`
+
+*Defined in*
+
+[CozyPouchLink.js:323](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L323)
+
+***
+
+### handleDoctypeSyncEnd
+
+▸ **handleDoctypeSyncEnd**(`doctype`): `void`
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `doctype` | `any` |
+
+*Returns*
+
+`void`
+
+*Defined in*
+
+[CozyPouchLink.js:264](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L264)
+
+***
+
+### handleDoctypeSyncStart
+
+▸ **handleDoctypeSyncStart**(`doctype`): `void`
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `doctype` | `any` |
+
+*Returns*
+
+`void`
+
+*Defined in*
+
+[CozyPouchLink.js:259](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L259)
+
+***
+
+### handleOnSync
+
+▸ **handleOnSync**(`doctypeUpdates`): `void`
+
+Receives PouchDB updates (documents grouped by doctype).
+Normalizes the data (.id -> .\_id, .rev -> \_rev).
+Passes the data to the client and to the onSync handler.
+
+Emits an event (pouchlink:sync:end) when the sync (all doctypes) is done
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `doctypeUpdates` | `any` |
+
+*Returns*
+
+`void`
+
+*Defined in*
+
+[CozyPouchLink.js:245](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L245)
+
+***
+
+### hasIndex
+
+▸ **hasIndex**(`name`): `boolean`
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `name` | `any` |
+
+*Returns*
+
+`boolean`
+
+*Defined in*
+
+[CozyPouchLink.js:480](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L480)
+
+***
+
+### migrateAdapter
+
+▸ **migrateAdapter**(`params`): `Promise`<`void`>
+
+Migrate the current adapter
+
+**`property`** {string} \[fromAdapter] - The current adapter type, e.g. 'idb'
+
+**`property`** {string} \[toAdapter] - The new adapter type, e.g. 'indexeddb'
+
+**`property`** {string} \[url] - The Cozy URL
+
+**`property`** {Array} \[plugins] - The PouchDB plugins
+
+*Parameters*
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `params` | `MigrationParams` | Migration params |
+
+*Returns*
+
+`Promise`<`void`>
+
+*Defined in*
+
+[CozyPouchLink.js:154](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L154)
+
+***
+
+### needsToWaitWarmup
+
+▸ **needsToWaitWarmup**(`doctype`): `Promise`<`boolean`>
+
+Check if there is warmup queries for this doctype
+and return if those queries are already warmed up or not
+
+*Parameters*
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `doctype` | `string` | Doctype to check |
+
+*Returns*
+
+`Promise`<`boolean`>
+
+the need to wait for the warmup
+
+*Defined in*
+
+[CozyPouchLink.js:466](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L466)
+
+***
+
+### onLogin
+
+▸ **onLogin**(): `Promise`<`void`>
+
+*Returns*
+
+`Promise`<`void`>
+
+*Defined in*
+
+[CozyPouchLink.js:173](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L173)
+
+***
+
+### onSyncError
+
+▸ **onSyncError**(`error`): `Promise`<`void`>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `error` | `any` |
+
+*Returns*
+
+`Promise`<`void`>
+
+*Defined in*
+
+[CozyPouchLink.js:303](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L303)
+
+***
+
+### persistCozyData
+
+▸ **persistCozyData**(`data`, `forward?`): `Promise`<`void`>
+
+*Parameters*
+
+| Name | Type | Default value |
+| :------ | :------ | :------ |
+| `data` | `any` | `undefined` |
+| `forward` | (`operation`: `any`, `result`: `any`) => `void` | `doNothing` |
+
+*Returns*
+
+`Promise`<`void`>
+
+*Overrides*
+
+CozyLink.persistCozyData
+
+*Defined in*
+
+[CozyPouchLink.js:421](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L421)
+
+***
+
+### registerClient
+
+▸ **registerClient**(`client`): `Promise`<`void`>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `client` | `any` |
+
+*Returns*
+
+`Promise`<`void`>
+
+*Defined in*
+
+[CozyPouchLink.js:139](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L139)
+
+***
+
+### request
+
+▸ **request**(`operation`, `result?`, `forward?`): `Promise`<`any`>
+
+*Parameters*
+
+| Name | Type | Default value |
+| :------ | :------ | :------ |
+| `operation` | `any` | `undefined` |
+| `result` | `any` | `null` |
+| `forward` | (`operation`: `any`, `result`: `any`) => `void` | `doNothing` |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Overrides*
+
+CozyLink.request
+
+*Defined in*
+
+[CozyPouchLink.js:346](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L346)
+
+***
+
+### reset
+
+▸ **reset**(): `Promise`<`void`>
+
+*Returns*
+
+`Promise`<`void`>
+
+*Overrides*
+
+CozyLink.reset
+
+*Defined in*
+
+[CozyPouchLink.js:229](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L229)
+
+***
+
+### sanitizeJsonApi
+
+▸ **sanitizeJsonApi**(`data`): `Omit`<`Pick`<`any`, `string` | `number` | `symbol`>, `"attributes"` | `"meta"`>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `data` | `any` |
+
+*Returns*
+
+`Omit`<`Pick`<`any`, `string` | `number` | `symbol`>, `"attributes"` | `"meta"`>
+
+*Defined in*
+
+[CozyPouchLink.js:394](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L394)
+
+***
+
+### startReplication
+
+▸ **startReplication**(): `void`
+
+User of the link can call this to start ongoing replications.
+Typically, it can be used when the application regains focus.
+
+Emits pouchlink:sync:start event when the replication begins
+
+*Returns*
+
+`void`
+
+*Defined in*
+
+[CozyPouchLink.js:278](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L278)
+
+***
+
+### stopReplication
+
+▸ **stopReplication**(): `void`
+
+User of the link can call this to stop ongoing replications.
+Typically, it can be used when the applications loses focus.
+
+Emits pouchlink:sync:stop event
+
+*Returns*
+
+`void`
+
+*Defined in*
+
+[CozyPouchLink.js:295](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L295)
+
+***
+
+### supportsOperation
+
+▸ **supportsOperation**(`operation`): `boolean`
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `operation` | `any` |
+
+*Returns*
+
+`boolean`
+
+*Defined in*
+
+[CozyPouchLink.js:331](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L331)
+
+***
+
+### syncImmediately
+
+▸ **syncImmediately**(): `Promise`<`void`>
+
+*Returns*
+
+`Promise`<`void`>
+
+*Defined in*
+
+[CozyPouchLink.js:715](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L715)
+
+***
+
+### updateDocument
+
+▸ **updateDocument**(`mutation`): `Promise`<`any`>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `mutation` | `any` |
+
+*Returns*
+
+`Promise`<`any`>
+
+*Defined in*
+
+[CozyPouchLink.js:655](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L655)
+
+***
+
+### updateDocuments
+
+▸ **updateDocuments**(`mutation`): `Promise`<`any`\[]>
+
+*Parameters*
+
+| Name | Type |
+| :------ | :------ |
+| `mutation` | `any` |
+
+*Returns*
+
+`Promise`<`any`\[]>
+
+*Defined in*
+
+[CozyPouchLink.js:660](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L660)
+
+***
+
+### getPouchAdapterName
+
+▸ `Static` **getPouchAdapterName**(`localStorage`): `Promise`<`string`>
+
+Return the PouchDB adapter name.
+Should be IndexedDB for newest adapters.
+
+*Parameters*
+
+| Name | Type | Description |
+| :------ | :------ | :------ |
+| `localStorage` | `LocalStorage` | Methods to access local storage |
+
+*Returns*
+
+`Promise`<`string`>
+
+The adapter name
+
+*Defined in*
+
+[CozyPouchLink.js:115](https://github.com/cozy/cozy-client/blob/master/packages/cozy-pouch-link/src/CozyPouchLink.js#L115)
diff --git a/docs/api/cozy-stack-client.md b/docs/api/cozy-stack-client.md
index 0faf81fadf..4d684b1db7 100644
--- a/docs/api/cozy-stack-client.md
+++ b/docs/api/cozy-stack-client.md
@@ -85,11 +85,15 @@ Serves to dedupe equal queries requested at the same time
makeKeyFromPartialFilter ⇒ string
Process a partial filter to generate a string key
+/!\ Warning: this method is similar to cozy-pouch-link mango.makeKeyFromPartialFilter()
+If you edit this method, please check if the change is also needed in mango file
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.key1}_{partial_filter.value1})_and_({partial_filter.key2}_{partial_filter.value2})
+/!\ Warning: this method is similar to cozy-pouch-link mango.getIndexNameFromFields()
+If you edit this method, please check if the change is also needed in mango file
transformSort ⇒ MangoSort
Transform sort into Array
@@ -2175,6 +2179,9 @@ Get the list of illegal characters in the file name
## makeKeyFromPartialFilter ⇒ string
Process a partial filter to generate a string key
+/!\ Warning: this method is similar to cozy-pouch-link mango.makeKeyFromPartialFilter()
+If you edit this method, please check if the change is also needed in mango file
+
**Kind**: global constant
**Returns**: string
- - The string key of the processed partial filter
@@ -2190,6 +2197,9 @@ 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.key1}_{partial_filter.value1})_and_({partial_filter.key2}_{partial_filter.value2})`
+/!\ Warning: this method is similar to cozy-pouch-link mango.getIndexNameFromFields()
+If you edit this method, please check if the change is also needed in mango file
+
**Kind**: global constant
**Returns**: string
- The index name, built from the fields
@@ -2560,6 +2570,7 @@ Document representing a io.cozy.files
| Name | Type | Description |
| --- | --- | --- |
| _id | string
| Id of the file |
+| _rev | string
| Rev of the file |
| attributes | [FileAttributes
](#FileAttributes) | Attributes of the file |
| meta | object
| Meta |
| relationships | object
| Relationships |
diff --git a/package.json b/package.json
index 76b46dcb98..d4cec92ad4 100644
--- a/package.json
+++ b/package.json
@@ -43,9 +43,12 @@
"build": "lerna run build --parallel",
"commitmsg": "commitlint -e $GIT_PARAMS",
"clean": "rm -rf packages/*/dist",
- "docs": "node scripts/docs.js && yarn docs:cozy-client",
+ "cleanTypes": "rm -rf packages/*/types",
+ "cleanDocsApi": "rm -rf docs/api",
+ "docs": "yarn cleanDocsApi && node scripts/docs.js && yarn docs:cozy-client && yarn docs:cozy-pouch-link",
"docs:cozy-client": "yarn typedoc --readme none --hideInPageTOC --excludeExternals --excludePrivate --tsconfig packages/cozy-client/tsconfig.json packages/cozy-client/src/index.js --out docs/api/cozy-client --gitRevision master && yarn remark -o -u ./scripts/strip-typedoc-headings.mjs docs/api/cozy-client/",
- "types": "cd packages/cozy-client && yarn typecheck"
+ "docs:cozy-pouch-link": "yarn typedoc --readme none --hideInPageTOC --excludeExternals --excludePrivate --tsconfig packages/cozy-pouch-link/tsconfig.json packages/cozy-pouch-link/src/index.js --out docs/api/cozy-pouch-link --gitRevision master && yarn remark -o -u ./scripts/strip-typedoc-headings.mjs docs/api/cozy-pouch-link/",
+ "types": "yarn cleanTypes && cd packages/cozy-client && yarn typecheck && cd ../cozy-pouch-link && yarn typecheck"
},
"commitlint": {
"extends": [
diff --git a/packages/cozy-client/package.json b/packages/cozy-client/package.json
index df85b06c96..e1a65437ba 100644
--- a/packages/cozy-client/package.json
+++ b/packages/cozy-client/package.json
@@ -50,7 +50,7 @@
"btoa": "1.2.1",
"cozy-device-helper": "2.7.0",
"cozy-flags": "2.10.2",
- "cozy-intent": "1.17.3",
+ "cozy-intent": "2.23.0",
"cozy-logger": "1.7.0",
"cozy-ui": "93.1.1",
"jsdoc-plugin-intersection": "1.0.4",
@@ -65,7 +65,7 @@
"peerDependencies": {
"cozy-device-helper": ">=2.1.0",
"cozy-flags": ">2.8.6",
- "cozy-intent": ">=1.3.0",
+ "cozy-intent": ">=2.23.0",
"cozy-logger": ">1.7.0",
"cozy-ui": ">=93.1.1",
"react": "^16.7.0",
diff --git a/packages/cozy-client/src/CozyClient.js b/packages/cozy-client/src/CozyClient.js
index cb5aa55fc3..11669ec522 100644
--- a/packages/cozy-client/src/CozyClient.js
+++ b/packages/cozy-client/src/CozyClient.js
@@ -98,6 +98,8 @@ const DOC_CREATION = 'creation'
const DOC_UPDATE = 'update'
/**
+ * @typedef {import("./types").CozyClientDocument} CozyClientDocument
+ *
* @typedef {object} ClientOptions
* @property {object} [client]
* @property {object} [link]
@@ -1090,6 +1092,9 @@ client.query(Q('io.cozy.bills'))`)
*/
async requestQuery(definition) {
const mainResponse = await this.chain.request(definition)
+
+ await this.persistVirtualDocuments(definition, mainResponse.data)
+
if (!definition.includes) {
return mainResponse
}
@@ -1100,6 +1105,62 @@ client.query(Q('io.cozy.bills'))`)
return withIncluded
}
+ /**
+ * Save the document or array of documents into the persisted storage (if any)
+ *
+ * @private
+ * @param {CozyClientDocument | Array} data - Document or array of documents to be saved
+ * @returns {Promise}
+ */
+ async persistVirtualDocuments(definition, data) {
+ const enforceList = ['io.cozy.files.shortcuts']
+
+ const enforce = enforceList.includes(definition.doctype)
+
+ if (definition.doctype === 'io.cozy.apps_registry') {
+ // io.cozy.apps_registry has a dedicated `maintenance` endpoint on cozy-stack that
+ // returns data different than the one stored in database
+ // As we want to have transparent queries, whether it uses the stack API or Pouch,
+ // we store the full response into a single doc, with a `maintenance` _id
+ // and a special `cozyPouchData` attribute, to highlight this special case
+ return await this.persistVirtualDocument(
+ {
+ _type: 'io.cozy.apps_registry',
+ _id: definition.id,
+ // @ts-ignore
+ cozyPouchData: data
+ },
+ enforce
+ )
+ }
+
+ if (!Array.isArray(data)) {
+ await this.persistVirtualDocument(data, enforce)
+ } else {
+ for (const document of data) {
+ await this.persistVirtualDocument(document, enforce)
+ }
+ }
+ }
+
+ /**
+ * Save the document or array of documents into the persisted storage (if any)
+ *
+ * @private
+ * @param {CozyClientDocument} document - Document to be saved
+ * @param {boolean} enforce - When true, save the document even if `meta.rev` or `_rev` exist
+ * @returns {Promise}
+ */
+ async persistVirtualDocument(document, enforce) {
+ if (!document || document.cozyLocalOnly) {
+ return
+ }
+
+ if ((!document.meta?.rev && !document._rev) || enforce) {
+ await this.chain.persistCozyData(document)
+ }
+ }
+
/**
* Fetch relationships for a response (can be several docs).
* Fills the `relationships` attribute of each documents.
@@ -1713,9 +1774,9 @@ instantiation of the client.`
)
this.instanceOptions = {
- capabilities: data.attributes,
- locale: instanceData.attributes?.locale,
- tracking: instanceData.attributes?.tracking
+ capabilities: data,
+ locale: instanceData.locale,
+ tracking: instanceData.tracking
}
this.capabilities = this.instanceOptions.capabilities || null
diff --git a/packages/cozy-client/src/CozyClient.spec.js b/packages/cozy-client/src/CozyClient.spec.js
index 05880d1123..69688220c7 100644
--- a/packages/cozy-client/src/CozyClient.spec.js
+++ b/packages/cozy-client/src/CozyClient.spec.js
@@ -693,7 +693,8 @@ describe('CozyClient login', () => {
describe('CozyClient', () => {
const requestHandler = jest.fn()
- const link = new CozyLink(requestHandler)
+ const persistHandler = jest.fn()
+ const link = new CozyLink(requestHandler, persistHandler)
const MOCKED_DATE = '2018-05-05T09:09:00.115Z'
@@ -724,6 +725,7 @@ describe('CozyClient', () => {
afterEach(() => {
requestHandler.mockReset()
+ persistHandler.mockReset()
})
describe('setAppMetadata', () => {
it('should update the appMetadata', () => {
@@ -1384,6 +1386,7 @@ describe('CozyClient', () => {
it('should return the same result if the query is run while she is already in loading status whithout requesting the query twice', async () => {
jest.spyOn(client, 'requestQuery')
+ requestHandler.mockResolvedValue({})
const [resp, resp2] = await Promise.all([
client.query(query, { as: 'allTodos' }),
@@ -1400,6 +1403,118 @@ describe('CozyClient', () => {
expect(executeQueryFromState).toHaveBeenCalledTimes(1)
})
+ it('should persist virtual document when no meta.rev nor _rev', async () => {
+ jest.spyOn(client, 'requestQuery')
+ requestHandler.mockResolvedValue({
+ data: {
+ _id: 'some_id'
+ }
+ })
+
+ await client.query(query, { as: 'allTodos' })
+
+ expect(persistHandler).toHaveBeenCalledWith(
+ {
+ _id: 'some_id'
+ },
+ expect.anything()
+ )
+ })
+
+ it('should not persist virtual document when meta.rev', async () => {
+ jest.spyOn(client, 'requestQuery')
+ requestHandler.mockResolvedValue({
+ data: {
+ _id: 'some_id',
+ meta: {
+ rev: 'SOME_REV'
+ }
+ }
+ })
+
+ await client.query(query, { as: 'allTodos' })
+
+ expect(persistHandler).not.toHaveBeenCalled()
+ })
+
+ it('should not persist virtual document when _rev', async () => {
+ jest.spyOn(client, 'requestQuery')
+ requestHandler.mockResolvedValue({
+ data: {
+ _id: 'some_id',
+ _rev: 'SOME_REV'
+ }
+ })
+
+ await client.query(query, { as: 'allTodos' })
+
+ expect(persistHandler).not.toHaveBeenCalled()
+ })
+
+ it('should persist array of virtual documents when no meta.rev nor _rev', async () => {
+ jest.spyOn(client, 'requestQuery')
+ requestHandler.mockResolvedValue({
+ data: [
+ {
+ _id: 'some_id'
+ },
+ {
+ _id: 'some_id2'
+ }
+ ]
+ })
+
+ await client.query(query, { as: 'allTodos' })
+
+ expect(persistHandler).toHaveBeenCalledWith(
+ {
+ _id: 'some_id'
+ },
+ expect.anything()
+ )
+ expect(persistHandler).toHaveBeenCalledWith(
+ {
+ _id: 'some_id2'
+ },
+ expect.anything()
+ )
+ })
+
+ it('should not persist virtual documents if cozyLocalOnly', async () => {
+ jest.spyOn(client, 'requestQuery')
+ requestHandler.mockResolvedValue({
+ data: [
+ {
+ _id: 'some_id',
+ cozyLocalOnly: true
+ }
+ ]
+ })
+
+ await client.query(query, { as: 'allTodos' })
+
+ expect(persistHandler).not.toHaveBeenCalled()
+ })
+
+ it('should enforce persisting io.cozy.files.shortcuts as virtual documents even if meta.rev exists', async () => {
+ jest.spyOn(client, 'requestQuery')
+ requestHandler.mockResolvedValue({
+ data: [
+ {
+ _id: 'some_id',
+ meta: {
+ rev: 'SOME_REV'
+ }
+ }
+ ]
+ })
+ const shortcutsQuery = Q('io.cozy.files.shortcuts')
+
+ await client.query(shortcutsQuery, { as: 'allShortcuts' })
+
+ expect(persistHandler).toHaveBeenCalled()
+ })
+
describe('relationship with query failure', () => {
beforeEach(() => {
jest.spyOn(HasManyFiles, 'query').mockImplementation(() => {
diff --git a/packages/cozy-client/src/CozyLink.js b/packages/cozy-client/src/CozyLink.js
index 8b4f5a13fe..055b61b1e8 100644
--- a/packages/cozy-client/src/CozyLink.js
+++ b/packages/cozy-client/src/CozyLink.js
@@ -1,19 +1,51 @@
export default class CozyLink {
- constructor(requestHandler) {
+ constructor(requestHandler, persistHandler) {
if (typeof requestHandler === 'function') {
this.request = requestHandler
}
+
+ if (typeof persistHandler === 'function') {
+ this.persistCozyData = persistHandler
+ }
}
- request(operation, result, forward) {
+ /**
+ * Request the given operation from the link
+ *
+ * @param {any} operation - The operation to request
+ * @param {any} result - The result from the previous request of the chain
+ * @param {any} forward - The next request of the chain
+ * @returns {Promise}
+ */
+ async request(operation, result, forward) {
throw new Error('request is not implemented')
}
+
+ /**
+ * Persist the given data into the links storage
+ *
+ * @param {any} data - The document to persist
+ * @param {any} forward - The next persistCozyData of the chain
+ * @returns {Promise}
+ */
+ async persistCozyData(data, forward) {
+ throw new Error('persistCozyData is not implemented')
+ }
+
+ /**
+ * Reset the link data
+ *
+ * @returns {Promise}
+ */
+ async reset() {
+ throw new Error('reset is not implemented')
+ }
}
const toLink = handler =>
typeof handler === 'function' ? new CozyLink(handler) : handler
-const defaultLinkHandler = (operation, result) => {
+const defaultLinkRequestHandler = (operation, result) => {
if (result) return result
else if (operation.execute) return operation.execute()
else
@@ -22,14 +54,32 @@ const defaultLinkHandler = (operation, result) => {
)
}
+const defaultLinkPersistHandler = (operation, result) => {
+ // Do nothing
+}
+
+const defaultLinkHandler = new CozyLink(
+ defaultLinkRequestHandler,
+ defaultLinkPersistHandler
+)
+
export const chain = links =>
[...links, defaultLinkHandler].map(toLink).reduce(concat)
const concat = (firstLink, nextLink) => {
- return new CozyLink((operation, result, forward) => {
+ const requestHandler = (operation, result, forward) => {
const nextForward = (op, res) => {
return nextLink.request(op, res, forward)
}
return firstLink.request(operation, result, nextForward)
- })
+ }
+
+ const persistHandler = (data, forward) => {
+ const nextForward = d => {
+ return nextLink.persistCozyData(d, forward)
+ }
+ return firstLink.persistCozyData(data, nextForward)
+ }
+
+ return new CozyLink(requestHandler, persistHandler)
}
diff --git a/packages/cozy-client/src/StackLink.js b/packages/cozy-client/src/StackLink.js
index 97e9a2fb92..41ea7c3245 100644
--- a/packages/cozy-client/src/StackLink.js
+++ b/packages/cozy-client/src/StackLink.js
@@ -5,6 +5,7 @@ import CozyLink from './CozyLink'
import { DOCTYPE_FILES } from './const'
import { BulkEditError } from './errors'
import logger from './logger'
+import { isReactNativeOfflineError } from './utils'
/**
*
@@ -50,16 +51,21 @@ export const transformBulkDocsResponse = (bulkResponse, originalDocuments) => {
}
}
+/**
+ * @typedef {object} StackLinkOptions
+ * @property {object} [stackClient] - A StackClient
+ * @property {object} [client] - A StackClient (deprecated)
+ * @property {import('cozy-pouch-link/dist/types').LinkPlatform} [platform] - Platform specific adapters and methods
+ */
+
/**
* Transfers queries and mutations to a remote stack
*/
export default class StackLink extends CozyLink {
/**
- * @param {object} [options] - Options
- * @param {object} [options.stackClient] - A StackClient
- * @param {object} [options.client] - A StackClient (deprecated)
+ * @param {StackLinkOptions} [options] - Options
*/
- constructor({ client, stackClient } = {}) {
+ constructor({ client, stackClient, platform } = {}) {
super()
if (client) {
logger.warn(
@@ -67,21 +73,37 @@ export default class StackLink extends CozyLink {
)
}
this.stackClient = stackClient || client
+ this.isOnline = platform?.isOnline
}
registerClient(client) {
this.stackClient = client.stackClient || client.client
}
- reset() {
+ async reset() {
this.stackClient = null
}
- request(operation, result, forward) {
- if (operation.mutationType) {
- return this.executeMutation(operation, result, forward)
+ async request(operation, result, forward) {
+ if (this.isOnline && !(await this.isOnline())) {
+ return forward(operation)
}
- return this.executeQuery(operation)
+
+ try {
+ if (operation.mutationType) {
+ return await this.executeMutation(operation, result, forward)
+ }
+ return await this.executeQuery(operation)
+ } catch (err) {
+ if (isReactNativeOfflineError(err)) {
+ return forward(operation)
+ }
+ throw err
+ }
+ }
+
+ async persistCozyData(data, forward) {
+ return forward(data)
}
/**
*
diff --git a/packages/cozy-client/src/WebFlagshipLink.js b/packages/cozy-client/src/WebFlagshipLink.js
new file mode 100644
index 0000000000..cc0e664225
--- /dev/null
+++ b/packages/cozy-client/src/WebFlagshipLink.js
@@ -0,0 +1,29 @@
+import CozyLink from './CozyLink'
+
+export default class WebFlagshipLink extends CozyLink {
+ /**
+ * @param {object} [options] - Options
+ * @param {import('cozy-intent').WebviewService} [options.webviewIntent] - The webview's intent reference
+ */
+ constructor({ webviewIntent } = {}) {
+ super()
+ this.webviewIntent = webviewIntent
+ }
+
+ registerClient(client) {
+ // does nothing, we don't need any client for this kind of link
+ }
+
+ async reset() {
+ // does nothing, we don't need any client for this kind of link
+ }
+
+ async request(operation, result, forward) {
+ return this.webviewIntent.call('flagshipLinkRequest', operation)
+ }
+
+ async persistCozyData(data, forward) {
+ // Persist data should do nothing here as data is already persisted on Flagship side
+ return
+ }
+}
diff --git a/packages/cozy-client/src/associations.spec.js b/packages/cozy-client/src/associations.spec.js
index 8d53bf42e5..d4dac6c2a2 100644
--- a/packages/cozy-client/src/associations.spec.js
+++ b/packages/cozy-client/src/associations.spec.js
@@ -7,7 +7,8 @@ import { SCHEMA, TODO_1, TODO_2 } from './__tests__/fixtures'
describe('Associations', () => {
const requestHandler = jest.fn()
- const link = new CozyLink(requestHandler)
+ const persistHandler = jest.fn()
+ const link = new CozyLink(requestHandler, persistHandler)
const client = new CozyClient({ links: [link], schema: SCHEMA })
const getTodo = id =>
diff --git a/packages/cozy-client/src/associations/HasManyFiles.js b/packages/cozy-client/src/associations/HasManyFiles.js
index ac83c044df..dac5c6b220 100644
--- a/packages/cozy-client/src/associations/HasManyFiles.js
+++ b/packages/cozy-client/src/associations/HasManyFiles.js
@@ -57,7 +57,7 @@ export default class HasManyFiles extends HasMany {
lastRelationship._type,
lastRelationship._id
)
- const lastDatetime = getFileDatetime(lastRelDoc.attributes)
+ const lastDatetime = getFileDatetime(lastRelDoc)
// cursor-based pagination
const cursor = newCursor(
[this.target._type, this.target._id, lastDatetime],
diff --git a/packages/cozy-client/src/hooks/useAppLinkWithStoreFallback.spec.jsx b/packages/cozy-client/src/hooks/useAppLinkWithStoreFallback.spec.jsx
index 922559bd9f..c782e20785 100644
--- a/packages/cozy-client/src/hooks/useAppLinkWithStoreFallback.spec.jsx
+++ b/packages/cozy-client/src/hooks/useAppLinkWithStoreFallback.spec.jsx
@@ -30,9 +30,7 @@ describe('useAppLinkWithStoreFallback', () => {
mockClient.query.mockResolvedValue({
data: [
{
- attributes: {
- slug: testAppSlug
- },
+ slug: testAppSlug,
links: { related: 'http://testapp.cozy.io' }
}
]
@@ -52,9 +50,7 @@ describe('useAppLinkWithStoreFallback', () => {
mockClient.query.mockResolvedValue({
data: [
{
- attributes: {
- slug: 'store'
- },
+ slug: 'store',
links: { related: 'http://store.cozy.io' }
}
]
diff --git a/packages/cozy-client/src/hooks/useCapabilities.jsx b/packages/cozy-client/src/hooks/useCapabilities.jsx
index ec660316cc..e61d62a583 100644
--- a/packages/cozy-client/src/hooks/useCapabilities.jsx
+++ b/packages/cozy-client/src/hooks/useCapabilities.jsx
@@ -13,7 +13,7 @@ const useCapabilities = client => {
Q('io.cozy.settings').getById('io.cozy.settings.capabilities')
)
- setCapabilities(get(capabilitiesResult, 'data.attributes', {}))
+ setCapabilities(get(capabilitiesResult, 'data', {}))
setFetchStatus('loaded')
} catch (e) {
setFetchStatus('failed')
diff --git a/packages/cozy-client/src/hooks/useCapabilities.spec.jsx b/packages/cozy-client/src/hooks/useCapabilities.spec.jsx
index 1ad9992221..297e6952f9 100644
--- a/packages/cozy-client/src/hooks/useCapabilities.spec.jsx
+++ b/packages/cozy-client/src/hooks/useCapabilities.spec.jsx
@@ -33,7 +33,8 @@ describe('useCapabilities', () => {
data: {
type: 'io.cozy.settings',
id: 'io.cozy.settings.capabilities',
- attributes: { file_versioning: true, flat_subdomains: true },
+ file_versioning: true,
+ flat_subdomains: true,
meta: {},
links: { self: '/settings/capabilities' }
}
@@ -44,8 +45,12 @@ describe('useCapabilities', () => {
await waitForNextUpdate()
expect(result.current.capabilities).toEqual({
+ type: 'io.cozy.settings',
+ id: 'io.cozy.settings.capabilities',
file_versioning: true,
- flat_subdomains: true
+ flat_subdomains: true,
+ meta: {},
+ links: { self: '/settings/capabilities' }
})
})
diff --git a/packages/cozy-client/src/hooks/useFetchShortcut.jsx b/packages/cozy-client/src/hooks/useFetchShortcut.jsx
index 8d0e35d91b..fe5a1b6977 100644
--- a/packages/cozy-client/src/hooks/useFetchShortcut.jsx
+++ b/packages/cozy-client/src/hooks/useFetchShortcut.jsx
@@ -24,8 +24,7 @@ const useFetchShortcut = (client, id) => {
}
})
- const targetApp =
- shortcutInfosResult?.data?.attributes?.metadata?.target?.app
+ const targetApp = shortcutInfosResult?.data?.metadata?.target?.app
if (targetApp) {
const targetAppIconUrl = await client.getStackClient().getIconURL({
type: 'app',
@@ -34,9 +33,7 @@ const useFetchShortcut = (client, id) => {
})
setShortcutImg(targetAppIconUrl)
} else {
- const shortcutRemoteUrl = new URL(
- shortcutInfosResult.data.attributes.url
- )
+ const shortcutRemoteUrl = new URL(shortcutInfosResult.data.url)
const imgUrl = `${client.getStackClient().uri}/bitwarden/icons/${
shortcutRemoteUrl.host
diff --git a/packages/cozy-client/src/hooks/useFetchShortcut.spec.jsx b/packages/cozy-client/src/hooks/useFetchShortcut.spec.jsx
index 4f6ebc5219..22e3168983 100644
--- a/packages/cozy-client/src/hooks/useFetchShortcut.spec.jsx
+++ b/packages/cozy-client/src/hooks/useFetchShortcut.spec.jsx
@@ -16,13 +16,11 @@ describe('useFetchShortcut', () => {
{
type: 'io.cozy.files.shortcuts',
id: 'b7470059d40c88e4bd30031d5e0109d3',
- attributes: {
- _id: '',
- name: 'cozy.url',
- dir_id: '8034db0016d0548ded99b9627e003270',
- url: 'https://cozy.io',
- metadata: { extractor_version: 2 }
- },
+ _id: 'b7470059d40c88e4bd30031d5e0109d3',
+ name: 'cozy.url',
+ dir_id: '8034db0016d0548ded99b9627e003270',
+ url: 'https://cozy.io',
+ metadata: { extractor_version: 2 },
meta: { rev: '1-60e1359e63fa7fa9fa000a2726d5d4c7' }
}
]
@@ -37,16 +35,14 @@ describe('useFetchShortcut', () => {
{
type: 'io.cozy.files.shortcuts',
id: 'linkToCozyApp',
- attributes: {
- _id: '',
- name: 'cozy.url',
- dir_id: '8034db0016d0548ded99b9627e003270',
- url: 'https://cozy.io',
- metadata: {
- extractor_version: 2,
- target: {
- app: 'notes'
- }
+ _id: '',
+ name: 'cozy.url',
+ dir_id: '8034db0016d0548ded99b9627e003270',
+ url: 'https://cozy.io',
+ metadata: {
+ extractor_version: 2,
+ target: {
+ app: 'notes'
}
},
meta: { rev: '1-60e1359e63fa7fa9fa000a2726d5d4c7' }
@@ -93,15 +89,12 @@ describe('useFetchShortcut', () => {
data: {
_id: 'b7470059d40c88e4bd30031d5e0109d3',
_type: 'io.cozy.files.shortcuts',
- type: 'io.cozy.files.shortcuts',
id: 'b7470059d40c88e4bd30031d5e0109d3',
- attributes: {
- _id: '',
- name: 'cozy.url',
- dir_id: '8034db0016d0548ded99b9627e003270',
- url: 'https://cozy.io',
- metadata: { extractor_version: 2 }
- },
+ type: 'io.cozy.files.shortcuts',
+ name: 'cozy.url',
+ dir_id: '8034db0016d0548ded99b9627e003270',
+ url: 'https://cozy.io',
+ metadata: { extractor_version: 2 },
meta: { rev: '1-60e1359e63fa7fa9fa000a2726d5d4c7' }
}
})
diff --git a/packages/cozy-client/src/index.js b/packages/cozy-client/src/index.js
index 741324297e..9d77701cc1 100644
--- a/packages/cozy-client/src/index.js
+++ b/packages/cozy-client/src/index.js
@@ -1,6 +1,7 @@
export { default } from './CozyClient'
export { default as CozyLink } from './CozyLink'
export { default as StackLink } from './StackLink'
+export { default as WebFlagshipLink } from './WebFlagshipLink'
export { default as compose } from 'lodash/flow'
export {
QueryDefinition,
diff --git a/packages/cozy-client/src/models/applications.js b/packages/cozy-client/src/models/applications.js
index a81c2cd2d6..ee3889178e 100644
--- a/packages/cozy-client/src/models/applications.js
+++ b/packages/cozy-client/src/models/applications.js
@@ -48,9 +48,7 @@ export const getStoreInstallationURL = (appData = [], app = {}) => {
* @returns {object} The io.cozy.app is installed or undefined if not
*/
export const isInstalled = (apps = [], wantedApp = {}) => {
- return apps.find(
- app => app.attributes && app.attributes.slug === wantedApp.slug
- )
+ return apps.find(app => app.slug === wantedApp.slug)
}
/**
diff --git a/packages/cozy-client/src/models/applications.spec.js b/packages/cozy-client/src/models/applications.spec.js
index 78e12dbe52..a27f355593 100644
--- a/packages/cozy-client/src/models/applications.spec.js
+++ b/packages/cozy-client/src/models/applications.spec.js
@@ -94,9 +94,7 @@ describe('applications model', () => {
describe('when the store app is installed', () => {
it('should return the store url for the given app', () => {
const storeApp = {
- attributes: {
- slug: 'store'
- },
+ slug: 'store',
links: {
related: 'http://store.cozy.tools:8080/'
}
@@ -120,9 +118,7 @@ describe('applications model', () => {
describe('when the store app is installed', () => {
it('should return the store installation url for the given app', () => {
const storeApp = {
- attributes: {
- slug: 'store'
- },
+ slug: 'store',
links: {
related: 'http://store.cozy.tools:8080/'
}
diff --git a/packages/cozy-client/src/models/file.js b/packages/cozy-client/src/models/file.js
index c1daed00dd..feaf972b41 100644
--- a/packages/cozy-client/src/models/file.js
+++ b/packages/cozy-client/src/models/file.js
@@ -1,3 +1,5 @@
+import { isFlagshipApp } from 'cozy-device-helper'
+
import get from 'lodash/get'
import isString from 'lodash/isString'
import has from 'lodash/has'
@@ -696,3 +698,37 @@ export const copy = async (client, file, destination) => {
throw e
}
}
+
+/**
+ * Download the requested file
+ *
+ * This method can be used in a web page context or in a WebView hosted by a Flagship app
+ *
+ * When used in a FlagshipApp WebView context, then the action is redirected to the host app
+ * that will process the download
+ *
+ * @param {object} params - The download parameters
+ * @param {CozyClient} params.client - Instance of CozyClient
+ * @param {import("../types").IOCozyFile} params.file - io.cozy.files metadata of the document to downloaded
+ * @param {string} [params.url] - Blob url that should be used to download encrypted files
+ * @param {import('cozy-intent').WebviewService} [params.webviewIntent] - webviewIntent that can be used to redirect the download to host Flagship app
+ *
+ * @returns {Promise}
+ */
+export const downloadFile = async ({ client, file, url, webviewIntent }) => {
+ const filesCollection = client.collection(DOCTYPE_FILES)
+
+ if (isFlagshipApp() && webviewIntent && !isEncrypted(file)) {
+ const isFlagshipDownloadAvailable =
+ (await webviewIntent?.call('isAvailable', 'downloadFile')) ?? false
+
+ if (isFlagshipDownloadAvailable) {
+ return await webviewIntent.call('downloadFile', file)
+ }
+ }
+
+ if (isEncrypted(file)) {
+ return filesCollection.forceFileDownload(url, file.name)
+ }
+ return filesCollection.download(file)
+}
diff --git a/packages/cozy-client/src/models/file.spec.js b/packages/cozy-client/src/models/file.spec.js
index fd96557bd3..f96e72bc89 100644
--- a/packages/cozy-client/src/models/file.spec.js
+++ b/packages/cozy-client/src/models/file.spec.js
@@ -1,3 +1,5 @@
+import { isFlagshipApp } from 'cozy-device-helper'
+
import * as fileModel from './file'
import { Qualification } from './document/qualification'
import { QueryDefinition } from '../queries/dsl'
@@ -5,6 +7,9 @@ const CozyClient = require('cozy-client/dist/CozyClient').default
const CozyStackClient = require('cozy-stack-client').default
jest.mock('cozy-stack-client')
+jest.mock('cozy-device-helper', () => ({
+ isFlagshipApp: jest.fn()
+}))
const cozyClient = new CozyClient({
stackClient: new CozyStackClient()
@@ -19,6 +24,8 @@ const fetchFileContentByIdSpy = jest.fn().mockName('fetchFileContentById')
const moveSpy = jest.fn().mockName('move')
const moveToCozySpy = jest.fn().mockName('moveToCozy')
const moveFromCozySpy = jest.fn().mockName('moveFromCozy')
+const downloadFromCozySpy = jest.fn().mockName('downloadFromCozy')
+const forceFileDownloadFromCozySpy = jest.fn().mockName('forceFileDownload')
beforeAll(() => {
cozyClient.stackClient.collection.mockReturnValue({
@@ -31,10 +38,16 @@ beforeAll(() => {
fetchFileContentById: fetchFileContentByIdSpy,
move: moveSpy,
moveToCozy: moveToCozySpy,
- moveFromCozy: moveFromCozySpy
+ moveFromCozy: moveFromCozySpy,
+ download: downloadFromCozySpy,
+ forceFileDownload: forceFileDownloadFromCozySpy
})
})
+beforeEach(() => {
+ jest.clearAllMocks()
+})
+
describe('File Model', () => {
it('should test if a file is a note or not', () => {
const fileDocument = {
@@ -856,3 +869,111 @@ describe('File qualification', () => {
})
})
})
+
+describe('downloadFile', () => {
+ it('should handle download in web page', async () => {
+ const file = {
+ _id: 'SOME_FILE_ID',
+ _type: 'io.cozy.file',
+ name: 'SOME_FILE_NAME'
+ }
+
+ await fileModel.downloadFile({
+ // @ts-ignore
+ client: cozyClient,
+ // @ts-ignore
+ file,
+ webviewIntent: null
+ })
+
+ expect(downloadFromCozySpy).toHaveBeenCalledWith(file)
+ })
+
+ it('should handle download in Flagship app', async () => {
+ isFlagshipApp.mockReturnValue(true)
+ const webviewIntent = {
+ call: jest.fn().mockResolvedValue(true)
+ }
+
+ const file = {
+ _id: 'SOME_FILE_ID',
+ _type: 'io.cozy.file',
+ name: 'SOME_FILE_NAME'
+ }
+
+ await fileModel.downloadFile({
+ // @ts-ignore
+ client: cozyClient,
+ // @ts-ignore
+ file,
+ // @ts-ignore
+ webviewIntent
+ })
+
+ expect(downloadFromCozySpy).not.toHaveBeenCalled()
+ expect(webviewIntent.call).toHaveBeenCalledWith(
+ 'isAvailable',
+ 'downloadFile'
+ )
+ expect(webviewIntent.call).toHaveBeenCalledWith('downloadFile', file)
+ })
+
+ it('should download files from web page in old Flagship app versions', async () => {
+ isFlagshipApp.mockReturnValue(true)
+ const webviewIntent = {
+ call: jest.fn().mockResolvedValue(false) // `isAvailable` returns `false` when not implemented
+ }
+
+ const file = {
+ _id: 'SOME_FILE_ID',
+ _type: 'io.cozy.file',
+ name: 'SOME_FILE_NAME'
+ }
+
+ await fileModel.downloadFile({
+ // @ts-ignore
+ client: cozyClient,
+ // @ts-ignore
+ file,
+ // @ts-ignore
+ webviewIntent
+ })
+
+ expect(downloadFromCozySpy).toHaveBeenCalled()
+ expect(webviewIntent.call).toHaveBeenCalledWith(
+ 'isAvailable',
+ 'downloadFile'
+ )
+ expect(webviewIntent.call).not.toHaveBeenCalledWith('downloadFile', file)
+ })
+
+ it('should download encrypted files from web page as this is not supported yet by Flagship app', async () => {
+ isFlagshipApp.mockReturnValue(true)
+ const webviewIntent = {
+ call: jest.fn()
+ }
+
+ const file = {
+ _id: 'SOME_FILE_ID',
+ _type: 'io.cozy.file',
+ name: 'SOME_FILE_NAME',
+ encrypted: true
+ }
+
+ await fileModel.downloadFile({
+ // @ts-ignore
+ client: cozyClient,
+ // @ts-ignore
+ file,
+ url: 'SOME_URL',
+ // @ts-ignore
+ webviewIntent
+ })
+
+ expect(forceFileDownloadFromCozySpy).toHaveBeenCalledWith(
+ 'SOME_URL',
+ 'SOME_FILE_NAME'
+ )
+ expect(webviewIntent.call).not.toHaveBeenCalled()
+ })
+})
diff --git a/packages/cozy-client/src/models/instance.js b/packages/cozy-client/src/models/instance.js
index fa8dcaadc6..1238ad8034 100644
--- a/packages/cozy-client/src/models/instance.js
+++ b/packages/cozy-client/src/models/instance.js
@@ -20,19 +20,17 @@ const PREMIUM_QUOTA = 50 * GB
// If manager URL is present, then the instance is not self-hosted
export const isSelfHosted = instanceInfo => {
- return get(instanceInfo, 'context.data.attributes.manager_url') ? false : true
+ return get(instanceInfo, 'context.data.manager_url') ? false : true
}
export const arePremiumLinksEnabled = instanceInfo => {
- return get(instanceInfo, 'context.data.attributes.enable_premium_links')
- ? true
- : false
+ return get(instanceInfo, 'context.data.enable_premium_links') ? true : false
}
export const isFreemiumUser = instanceInfo => {
- const quota = get(instanceInfo, 'diskUsage.data.attributes.quota', false)
+ const quota = get(instanceInfo, 'diskUsage.data.quota', false)
return parseInt(quota) <= PREMIUM_QUOTA
}
export const getUuid = instanceInfo => {
- return get(instanceInfo, 'instance.data.attributes.uuid')
+ return get(instanceInfo, 'instance.data.uuid')
}
/**
@@ -70,11 +68,7 @@ export const hasAnOffer = data => {
* @param {InstanceInfo} instanceInfo - Instance information
*/
export const buildPremiumLink = instanceInfo => {
- const managerUrl = get(
- instanceInfo,
- 'context.data.attributes.manager_url',
- false
- )
+ const managerUrl = get(instanceInfo, 'context.data.manager_url', false)
const uuid = getUuid(instanceInfo)
if (managerUrl && uuid) {
return `${managerUrl}/cozy/instances/${uuid}/premium`
@@ -92,9 +86,7 @@ export const buildPremiumLink = instanceInfo => {
export const hasPasswordDefinedAttribute = async client => {
try {
const {
- data: {
- attributes: { password_defined }
- }
+ data: { password_defined }
} = await client.fetchQueryAndGetFromState({
definition: Q('io.cozy.settings').getById('io.cozy.settings.instance'),
options: {
diff --git a/packages/cozy-client/src/models/instance.spec.js b/packages/cozy-client/src/models/instance.spec.js
index d3957d3d4a..3fd3e3027e 100644
--- a/packages/cozy-client/src/models/instance.spec.js
+++ b/packages/cozy-client/src/models/instance.spec.js
@@ -3,39 +3,29 @@ import { instance } from './'
const noSelfHostedInstance = {
context: {
data: {
- attributes: {
- manager_url: 'https://manager.cozy.cc',
- enable_premium_links: true
- }
+ manager_url: 'https://manager.cozy.cc',
+ enable_premium_links: true
}
},
instance: {
data: {
- attributes: {
- uuid: '1234'
- }
+ uuid: '1234'
}
},
diskUsage: {
data: {
- attributes: {
- quota: '400000000'
- }
+ quota: '400000000'
}
}
}
const selftHostedInstance = {
context: {
- data: {
- attributes: {}
- }
+ data: {}
},
diskUsage: {
data: {
- attributes: {
- quota: '6000000000000'
- }
+ quota: '6000000000000'
}
}
}
@@ -43,24 +33,18 @@ const selftHostedInstance = {
const hadAnOfferInstance = {
context: {
data: {
- attributes: {
- manager_url: 'https://manager.cozy.cc',
- enable_premium_links: true
- }
+ manager_url: 'https://manager.cozy.cc',
+ enable_premium_links: true
}
},
instance: {
data: {
- attributes: {
- uuid: '1234'
- }
+ uuid: '1234'
}
},
diskUsage: {
data: {
- attributes: {
- quota: '60000000000'
- }
+ quota: '60000000000'
}
}
}
@@ -163,17 +147,17 @@ describe('instance', () => {
})
it('should return false if attribute password_defined is undefined', async () => {
- const res = await setup({ attributes: { password_defined: undefined } })
+ const res = await setup({ password_defined: undefined })
expect(res).toBe(false)
})
it('should return false if attribute password_defined is false', async () => {
- const res = await setup({ attributes: { password_defined: false } })
+ const res = await setup({ password_defined: false })
expect(res).toBe(false)
})
it('should return true if attribute password_defined is true', async () => {
- const res = await setup({ attributes: { password_defined: true } })
+ const res = await setup({ password_defined: true })
expect(res).toBe(true)
})
})
diff --git a/packages/cozy-client/src/types.js b/packages/cozy-client/src/types.js
index bf3a2c4c29..6c6a8a7a6b 100644
--- a/packages/cozy-client/src/types.js
+++ b/packages/cozy-client/src/types.js
@@ -419,6 +419,11 @@ import { QueryDefinition } from './queries/dsl'
* @property {boolean} [favorite] - Whether the document is marked as favorite
*/
+/**
+ * @typedef {object} CozyClientDocumentMeta - Meta object as specified by JSON-API (https://jsonapi.org/format/#document-meta)
+ * @property {string} [rev] - Current revision of the document
+ */
+
/**
* @typedef {object} CozyClientDocument - A document
* @property {string} [_id] - Id of the document
@@ -429,6 +434,8 @@ import { QueryDefinition } from './queries/dsl'
* @property {ReferencedByRelationship} [relationships] - Relationships of the document
* @property {Reference[]} [referenced_by] - referenced by of another document
* @property {CozyMetadata} [cozyMetadata] - Cozy Metadata
+ * @property {CozyClientDocumentMeta} [meta] - Pouch Metadata
+ * @property {boolean} [cozyLocalOnly] - When true the document should NOT be replicated to the remote database
*/
/**
@@ -463,6 +470,7 @@ import { QueryDefinition } from './queries/dsl'
/**
* @typedef {object} FileDocument - An io.cozy.files document
* @property {string} _id - Id of the file
+ * @property {string} _rev - Rev of the file
* @property {FilesDoctype} _type - Doctype of the file
* @property {string} dir_id - Id of the parent folder
* @property {string} [path] - Path of the file
diff --git a/packages/cozy-client/src/utils.js b/packages/cozy-client/src/utils.js
index 973ac5cf37..8ac4fe55b3 100644
--- a/packages/cozy-client/src/utils.js
+++ b/packages/cozy-client/src/utils.js
@@ -68,3 +68,15 @@ export const hasQueriesBeenLoaded = queriesResults => {
hasQueryBeenLoaded(queryResult)
)
}
+
+/**
+ * Check is the error is about ReactNative not having access to internet
+ *
+ * @param {Error} err - The error to check
+ * @returns {boolean} True if the error is a network error, otherwise false
+ */
+export const isReactNativeOfflineError = err => {
+ // This error message is specific to ReactNative
+ // Network errors on a browser would produce another error.message
+ return err.message === 'Network request failed'
+}
diff --git a/packages/cozy-client/types/CozyClient.d.ts b/packages/cozy-client/types/CozyClient.d.ts
index 9c45fe859a..89845ac3b0 100644
--- a/packages/cozy-client/types/CozyClient.d.ts
+++ b/packages/cozy-client/types/CozyClient.d.ts
@@ -1,4 +1,46 @@
export default CozyClient;
+export type CozyClientDocument = {
+ /**
+ * - Id of the document
+ */
+ _id?: string;
+ /**
+ * - Id of the document
+ */
+ id?: string;
+ /**
+ * - Type of the document
+ */
+ _type?: string;
+ /**
+ * - Current revision of the document
+ */
+ _rev?: string;
+ /**
+ * - When the document has been deleted
+ */
+ _deleted?: boolean;
+ /**
+ * - Relationships of the document
+ */
+ relationships?: import("./types").ReferencedByRelationship;
+ /**
+ * - referenced by of another document
+ */
+ referenced_by?: import("./types").Reference[];
+ /**
+ * - Cozy Metadata
+ */
+ cozyMetadata?: import("./types").CozyMetadata;
+ /**
+ * - Pouch Metadata
+ */
+ meta?: import("./types").CozyClientDocumentMeta;
+ /**
+ * - When true the document should NOT be replicated to the remote database
+ */
+ cozyLocalOnly?: boolean;
+};
export type ClientOptions = {
client?: object;
link?: object;
@@ -36,6 +78,8 @@ export type ClientOptions = {
store?: boolean;
};
/**
+ * @typedef {import("./types").CozyClientDocument} CozyClientDocument
+ *
* @typedef {object} ClientOptions
* @property {object} [client]
* @property {object} [link]
@@ -458,6 +502,23 @@ declare class CozyClient {
* @returns {Promise}
*/
private requestQuery;
+ /**
+ * Save the document or array of documents into the persisted storage (if any)
+ *
+ * @private
+ * @param {CozyClientDocument | Array} data - Document or array of documents to be saved
+ * @returns {Promise}
+ */
+ private persistVirtualDocuments;
+ /**
+ * Save the document or array of documents into the persisted storage (if any)
+ *
+ * @private
+ * @param {CozyClientDocument} document - Document to be saved
+ * @param {boolean} enforce - When true, save the document even if `meta.rev` or `_rev` exist
+ * @returns {Promise}
+ */
+ private persistVirtualDocument;
/**
* Fetch relationships for a response (can be several docs).
* Fills the `relationships` attribute of each documents.
diff --git a/packages/cozy-client/types/CozyLink.d.ts b/packages/cozy-client/types/CozyLink.d.ts
index fb2974b285..27dda27f57 100644
--- a/packages/cozy-client/types/CozyLink.d.ts
+++ b/packages/cozy-client/types/CozyLink.d.ts
@@ -1,5 +1,27 @@
export default class CozyLink {
- constructor(requestHandler: any);
- request(operation: any, result: any, forward: any): void;
+ constructor(requestHandler: any, persistHandler: any);
+ /**
+ * Request the given operation from the link
+ *
+ * @param {any} operation - The operation to request
+ * @param {any} result - The result from the previous request of the chain
+ * @param {any} forward - The next request of the chain
+ * @returns {Promise}
+ */
+ request(operation: any, result: any, forward: any): Promise;
+ /**
+ * Persist the given data into the links storage
+ *
+ * @param {any} data - The document to persist
+ * @param {any} forward - The next persistCozyData of the chain
+ * @returns {Promise}
+ */
+ persistCozyData(data: any, forward: any): Promise;
+ /**
+ * Reset the link data
+ *
+ * @returns {Promise}
+ */
+ reset(): Promise;
}
export function chain(links: any): any;
diff --git a/packages/cozy-client/types/StackLink.d.ts b/packages/cozy-client/types/StackLink.d.ts
index 80a4d6d97e..2c353a3c65 100644
--- a/packages/cozy-client/types/StackLink.d.ts
+++ b/packages/cozy-client/types/StackLink.d.ts
@@ -1,22 +1,23 @@
export function transformBulkDocsResponse(bulkResponse: import("./types").CouchDBBulkResult[], originalDocuments: import("./types").CozyClientDocument[]): {
data: import("./types").CozyClientDocument[];
};
+/**
+ * @typedef {object} StackLinkOptions
+ * @property {object} [stackClient] - A StackClient
+ * @property {object} [client] - A StackClient (deprecated)
+ * @property {import('cozy-pouch-link/dist/types').LinkPlatform} [platform] - Platform specific adapters and methods
+ */
/**
* Transfers queries and mutations to a remote stack
*/
export default class StackLink extends CozyLink {
/**
- * @param {object} [options] - Options
- * @param {object} [options.stackClient] - A StackClient
- * @param {object} [options.client] - A StackClient (deprecated)
+ * @param {StackLinkOptions} [options] - Options
*/
- constructor({ client, stackClient }?: {
- stackClient: object;
- client: object;
- });
+ constructor({ client, stackClient, platform }?: StackLinkOptions);
stackClient: any;
+ isOnline: any;
registerClient(client: any): void;
- reset(): void;
/**
*
* @param {QueryDefinition} query - Query to execute
@@ -25,5 +26,19 @@ export default class StackLink extends CozyLink {
executeQuery(query: QueryDefinition): Promise;
executeMutation(mutation: any, result: any, forward: any): Promise;
}
+export type StackLinkOptions = {
+ /**
+ * - A StackClient
+ */
+ stackClient?: object;
+ /**
+ * - A StackClient (deprecated)
+ */
+ client?: object;
+ /**
+ * - Platform specific adapters and methods
+ */
+ platform?: any;
+};
import CozyLink from "./CozyLink";
import { QueryDefinition } from "./queries/dsl";
diff --git a/packages/cozy-client/types/WebFlagshipLink.d.ts b/packages/cozy-client/types/WebFlagshipLink.d.ts
new file mode 100644
index 0000000000..f69ec81d10
--- /dev/null
+++ b/packages/cozy-client/types/WebFlagshipLink.d.ts
@@ -0,0 +1,12 @@
+export default class WebFlagshipLink extends CozyLink {
+ /**
+ * @param {object} [options] - Options
+ * @param {import('cozy-intent').WebviewService} [options.webviewIntent] - The webview's intent reference
+ */
+ constructor({ webviewIntent }?: {
+ webviewIntent: import('cozy-intent').WebviewService;
+ });
+ webviewIntent: import("cozy-intent").WebviewService;
+ registerClient(client: any): void;
+}
+import CozyLink from "./CozyLink";
diff --git a/packages/cozy-client/types/devtools/Flags.d.ts b/packages/cozy-client/types/devtools/Flags.d.ts
deleted file mode 100644
index 976ab4b7de..0000000000
--- a/packages/cozy-client/types/devtools/Flags.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export default Flags;
-declare function Flags(): JSX.Element;
diff --git a/packages/cozy-client/types/index.d.ts b/packages/cozy-client/types/index.d.ts
index 3d88dd768d..763bef556c 100644
--- a/packages/cozy-client/types/index.d.ts
+++ b/packages/cozy-client/types/index.d.ts
@@ -1,6 +1,7 @@
export { default } from "./CozyClient";
export { default as CozyLink } from "./CozyLink";
export { default as StackLink } from "./StackLink";
+export { default as WebFlagshipLink } from "./WebFlagshipLink";
export { default as compose } from "lodash/flow";
export { default as Registry } from "./registry";
export { default as RealTimeQueries } from "./RealTimeQueries";
diff --git a/packages/cozy-client/types/models/doctypes/index.d.ts b/packages/cozy-client/types/models/doctypes/index.d.ts
deleted file mode 100644
index 2b36a603cd..0000000000
--- a/packages/cozy-client/types/models/doctypes/index.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { locales };
-import * as locales from "./locales";
diff --git a/packages/cozy-client/types/models/doctypes/locales/index.d.ts b/packages/cozy-client/types/models/doctypes/locales/index.d.ts
deleted file mode 100644
index 617393d197..0000000000
--- a/packages/cozy-client/types/models/doctypes/locales/index.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import fr from "./fr.json";
-import en from "./en.json";
-export { fr, en };
diff --git a/packages/cozy-client/types/models/document/emojiCountry.d.ts b/packages/cozy-client/types/models/document/emojiCountry.d.ts
deleted file mode 100644
index b9365092ea..0000000000
--- a/packages/cozy-client/types/models/document/emojiCountry.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-export function getEmojiByCountry(countryCode: string): string;
diff --git a/packages/cozy-client/types/models/file.d.ts b/packages/cozy-client/types/models/file.d.ts
index f4bfb55e15..4cb039ad4c 100644
--- a/packages/cozy-client/types/models/file.d.ts
+++ b/packages/cozy-client/types/models/file.d.ts
@@ -68,6 +68,12 @@ export function hasCertifications(file: import("../types").IOCozyFile): boolean;
export function isFromKonnector(file: import("../types").IOCozyFile): boolean;
export function fetchBlobFileById(client: CozyClient, fileId: string): Promise;
export function copy(client: object, file: object, destination: object): Promise;
+export function downloadFile({ client, file, url, webviewIntent }: {
+ client: CozyClient;
+ file: import("../types").IOCozyFile;
+ url: string;
+ webviewIntent: import('cozy-intent').WebviewService;
+}): Promise;
export type FileUploadOptions = {
/**
* - The file name to upload
diff --git a/packages/cozy-client/types/node.d.ts b/packages/cozy-client/types/node.d.ts
deleted file mode 100644
index d168480c4f..0000000000
--- a/packages/cozy-client/types/node.d.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export { default } from "./CozyClient";
-export { default as CozyLink } from "./CozyLink";
-export { default as StackLink } from "./StackLink";
-export { default as compose } from "lodash/flow";
-export { cancelable } from "./utils";
-export { getQueryFromState } from "./store";
-export { default as Registry } from "./registry";
-export * from "./mock";
-export * from "./cli";
-import * as manifest from "./manifest";
-import * as models from "./models";
-export { manifest, models };
-export { QueryDefinition, Mutations, MutationTypes, getDoctypeFromOperation, Q } from "./queries/dsl";
-export { Association, HasMany, HasOne, HasOneInPlace, HasManyInPlace, HasManyTriggers } from "./associations";
-export { dehydrate, generateWebLink } from "./helpers";
diff --git a/packages/cozy-client/types/queries/referencedBy.d.ts b/packages/cozy-client/types/queries/referencedBy.d.ts
deleted file mode 100644
index 543e83598f..0000000000
--- a/packages/cozy-client/types/queries/referencedBy.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export function isReferencedBy(file: IOCozyFile, referencedBy: Doctype): boolean;
-export function isReferencedById(file: IOCozyFile, referencedBy: Doctype, referencedId: string): boolean;
-export function getReferencedBy(file: IOCozyFile, referencedBy: Doctype): Reference[];
-export function getReferencedById(file: IOCozyFile, referencedBy: Doctype, referencedId: string): Reference[];
-import { IOCozyFile } from "../types";
-import { Doctype } from "../types";
-import { Reference } from "../types";
diff --git a/packages/cozy-client/types/types.d.ts b/packages/cozy-client/types/types.d.ts
index cb8e3c01d4..103a91fe6b 100644
--- a/packages/cozy-client/types/types.d.ts
+++ b/packages/cozy-client/types/types.d.ts
@@ -705,6 +705,15 @@ export type CozyMetadata = {
*/
favorite?: boolean;
};
+/**
+ * - Meta object as specified by JSON-API (https://jsonapi.org/format/#document-meta)
+ */
+export type CozyClientDocumentMeta = {
+ /**
+ * - Current revision of the document
+ */
+ rev?: string;
+};
/**
* - A document
*/
@@ -741,6 +750,14 @@ export type CozyClientDocument = {
* - Cozy Metadata
*/
cozyMetadata?: CozyMetadata;
+ /**
+ * - Pouch Metadata
+ */
+ meta?: CozyClientDocumentMeta;
+ /**
+ * - When true the document should NOT be replicated to the remote database
+ */
+ cozyLocalOnly?: boolean;
};
/**
* - A io.cozy.files document's metadata
@@ -813,6 +830,10 @@ export type FileDocument = {
* - Id of the file
*/
_id: string;
+ /**
+ * - Rev of the file
+ */
+ _rev: string;
/**
* - Doctype of the file
*/
diff --git a/packages/cozy-client/types/utils.d.ts b/packages/cozy-client/types/utils.d.ts
index 031006d18c..603dfc0ac6 100644
--- a/packages/cozy-client/types/utils.d.ts
+++ b/packages/cozy-client/types/utils.d.ts
@@ -2,6 +2,7 @@ export function isQueryLoading(col: any): boolean;
export function hasQueryBeenLoaded(col: any): any;
export function isQueriesLoading(queriesResults: any): boolean;
export function hasQueriesBeenLoaded(queriesResults: any): boolean;
+export function isReactNativeOfflineError(err: Error): boolean;
export type CancelablePromise = Promise;
/**
* @typedef {Promise} CancelablePromise
diff --git a/packages/cozy-pouch-link/examples/periodic-sync/index.js b/packages/cozy-pouch-link/examples/periodic-sync/index.js
index ecfa6b5990..9e84eb7257 100755
--- a/packages/cozy-pouch-link/examples/periodic-sync/index.js
+++ b/packages/cozy-pouch-link/examples/periodic-sync/index.js
@@ -62,8 +62,9 @@ class App extends React.Component {
}
componentDidMount() {
- this.createManager()
- this.displayDocs()
+ this.createManager().then(() => {
+ this.displayDocs()
+ })
}
componentWillUnmount() {
@@ -83,7 +84,7 @@ class App extends React.Component {
})
}
- createManager() {
+ async createManager() {
this.manager = new PouchManager([DOCTYPE], {
replicationDelay: 2 * 1000,
getReplicationURL: this.getReplicationURL,
@@ -101,6 +102,7 @@ class App extends React.Component {
this.displayDocs()
}
})
+ await this.manager.init()
}
async displayDocs() {
diff --git a/packages/cozy-pouch-link/package.json b/packages/cozy-pouch-link/package.json
index 143801e3bc..c482624f1b 100644
--- a/packages/cozy-pouch-link/package.json
+++ b/packages/cozy-pouch-link/package.json
@@ -3,7 +3,9 @@
"version": "48.25.0",
"license": "MIT",
"main": "dist/index.js",
+ "types": "types/index.d.ts",
"files": [
+ "types",
"dist"
],
"repository": {
@@ -23,7 +25,8 @@
"parcel": "1.12.4",
"pouchdb-adapter-memory": "7.2.2",
"react": "16.14.0",
- "react-dom": "16.14.0"
+ "react-dom": "16.14.0",
+ "typescript": "4.1.5"
},
"peerDependencies": {
"@cozy/minilog": "1.0.0",
@@ -32,7 +35,8 @@
"scripts": {
"build": "../../bin/build",
"watch": "yarn run build --watch",
- "prepublishOnly": "yarn run build"
+ "prepublishOnly": "yarn run build",
+ "typecheck": "tsc -p tsconfig.json"
},
"sideEffects": false
}
diff --git a/packages/cozy-pouch-link/src/CozyPouchLink.js b/packages/cozy-pouch-link/src/CozyPouchLink.js
index 373ff776f9..c369144198 100644
--- a/packages/cozy-pouch-link/src/CozyPouchLink.js
+++ b/packages/cozy-pouch-link/src/CozyPouchLink.js
@@ -16,15 +16,11 @@ import { default as helpers } from './helpers'
import { getIndexNameFromFields, getIndexFields } from './mango'
import * as jsonapi from './jsonapi'
import PouchManager from './PouchManager'
+import { PouchLocalStorage } from './localStorage'
import logger from './logger'
import { migratePouch } from './migrations/adapter'
+import { platformWeb } from './platformWeb'
import { getDatabaseName, getPrefix } from './utils'
-import {
- getPersistedSyncedDoctypes,
- persistAdapterName,
- getAdapterName,
- destroyWarmedUpQueries
-} from './localStorage'
PouchDB.plugin(PouchDBFind)
@@ -52,18 +48,28 @@ export const getReplicationURL = (uri, token, doctype) => {
return `${authenticatedURL}/data/${doctype}`
}
-const doNothing = () => {}
+const doNothing = (operation, result = null) => {}
const expiredTokenError = /Expired token/
export const isExpiredTokenError = pouchError => {
return expiredTokenError.test(pouchError.error)
}
-const normalizeAll = (docs, doctype) => {
- return docs.map(doc => jsonapi.normalizeDoc(doc, doctype))
+const normalizeAll = client => (docs, doctype) => {
+ return docs.map(doc => jsonapi.normalizeDoc(doc, doctype, client))
}
/**
- * @typedef {"idle"|"replicating"} SyncStatus
+ * @typedef {import('cozy-client/src/types').CozyClientDocument} CozyClientDocument
+ *
+ * @typedef {"idle"|"replicating"} ReplicationStatus
+ */
+
+/**
+ * @typedef {object} PouchLinkOptions
+ * @property {number} [replicationInterval] Milliseconds between replications
+ * @property {string[]} doctypes Doctypes to replicate
+ * @property {Record} doctypesReplicationOptions A mapping from doctypes to replication options. All pouch replication options can be used, as well as the "strategy" option that determines which way the replication is done (can be "sync", "fromRemote" or "toRemote")
+ * @property {import('./types').LinkPlatform} platform Platform specific adapters and methods
*/
/**
@@ -75,14 +81,10 @@ class PouchLink extends CozyLink {
/**
* constructor - Initializes a new PouchLink
*
- * @param {object} [opts={}]
- * @param {number} [opts.replicationInterval] Milliseconds between replications
- * @param {string[]} opts.doctypes Doctypes to replicate
- * @param {object[]} opts.doctypesReplicationOptions A mapping from doctypes to replication options. All pouch replication options can be used, as well as the "strategy" option that determines which way the replication is done (can be "sync", "fromRemote" or "toRemote")
- * @returns {object} The PouchLink instance
+ * @param {PouchLinkOptions} [opts={}]
*/
- constructor(opts = {}) {
+ constructor(opts) {
const options = defaults({}, opts, DEFAULT_OPTIONS)
super(options)
const { doctypes, doctypesReplicationOptions } = options
@@ -95,8 +97,11 @@ class PouchLink extends CozyLink {
this.doctypes = doctypes
this.doctypesReplicationOptions = doctypesReplicationOptions
this.indexes = {}
+ this.storage = new PouchLocalStorage(
+ options.platform?.storage || platformWeb.storage
+ )
- /** @type {Record} - Stores replication states per doctype */
+ /** @type {Record} - Stores replication states per doctype */
this.replicationStatus = this.replicationStatus || {}
}
@@ -104,10 +109,12 @@ class PouchLink extends CozyLink {
* Return the PouchDB adapter name.
* Should be IndexedDB for newest adapters.
*
- * @returns {string} The adapter name
+ * @param {import('./types').LocalStorage} localStorage Methods to access local storage
+ * @returns {Promise} The adapter name
*/
- static getPouchAdapterName = () => {
- return getAdapterName()
+ static getPouchAdapterName = localStorage => {
+ const storage = new PouchLocalStorage(localStorage || platformWeb.storage)
+ return storage.getAdapterName()
}
getReplicationURL(doctype) {
@@ -149,15 +156,15 @@ class PouchLink extends CozyLink {
for (const plugin of plugins) {
PouchDB.plugin(plugin)
}
- const doctypes = getPersistedSyncedDoctypes()
+ const doctypes = await this.storage.getPersistedSyncedDoctypes()
for (const doctype of Object.keys(doctypes)) {
const prefix = getPrefix(url)
const dbName = getDatabaseName(prefix, doctype)
await migratePouch({ dbName, fromAdapter, toAdapter })
- destroyWarmedUpQueries() // force recomputing indexes
+ await this.storage.destroyWarmedUpQueries() // force recomputing indexes
}
- persistAdapterName('indexeddb')
+ await this.storage.persistAdapterName('indexeddb')
} catch (err) {
console.error('PouchLink: PouchDB migration failed. ', err)
}
@@ -195,9 +202,9 @@ class PouchLink extends CozyLink {
logger.log('Create pouches with ' + prefix + ' prefix')
}
- if (!getAdapterName()) {
+ if (!(await this.storage.getAdapterName())) {
const adapter = get(this.options, 'pouch.options.adapter')
- persistAdapterName(adapter)
+ await this.storage.persistAdapterName(adapter)
}
this.pouches = new PouchManager(this.doctypes, {
@@ -209,8 +216,10 @@ class PouchLink extends CozyLink {
onDoctypeSyncStart: this.handleDoctypeSyncStart.bind(this),
onDoctypeSyncEnd: this.handleDoctypeSyncEnd.bind(this),
prefix,
- executeQuery: this.executeQuery.bind(this)
+ executeQuery: this.executeQuery.bind(this),
+ platform: this.options.platform
})
+ await this.pouches.init()
if (this.client && this.options.initialSync) {
this.startReplication()
@@ -234,7 +243,7 @@ class PouchLink extends CozyLink {
* Emits an event (pouchlink:sync:end) when the sync (all doctypes) is done
*/
handleOnSync(doctypeUpdates) {
- const normalizedData = mapValues(doctypeUpdates, normalizeAll)
+ const normalizedData = mapValues(doctypeUpdates, normalizeAll(this.client))
if (this.client) {
this.client.setData(normalizedData)
}
@@ -334,7 +343,7 @@ class PouchLink extends CozyLink {
return !!this.getPouch(impactedDoctype)
}
- request(operation, result = null, forward = doNothing) {
+ async request(operation, result = null, forward = doNothing) {
const doctype = getDoctypeFromOperation(operation)
if (!this.pouches) {
@@ -347,7 +356,7 @@ class PouchLink extends CozyLink {
return forward(operation)
}
- if (!this.pouches.isSynced(doctype)) {
+ if (this.pouches.getSyncStatus(doctype) === 'not_synced') {
if (process.env.NODE_ENV !== 'production') {
logger.info(
`Tried to access local ${doctype} but Cozy Pouch is not synced yet. Forwarding the operation to next link`
@@ -356,7 +365,7 @@ class PouchLink extends CozyLink {
return forward(operation)
}
- if (this.needsToWaitWarmup(doctype)) {
+ if (await this.needsToWaitWarmup(doctype)) {
if (process.env.NODE_ENV !== 'production') {
logger.info(
`Tried to access local ${doctype} but not warmuped yet. Forwarding the operation to next link`
@@ -381,24 +390,89 @@ class PouchLink extends CozyLink {
return this.executeQuery(operation)
}
}
+
+ sanitizeJsonApi(data) {
+ const docWithoutType = sanitized(data)
+
+ /*
+ We persist in the local Pouch database all the documents that do not
+ exist on the remote Couch database
+
+ Those documents are computed by the cozy-stack then are sent to the
+ client using JSON-API format containing `attributes` and `meta`
+ attributes
+
+ Then the cozy-stack-client would normalize those documents by spreading
+ `attributes` and `meta` content into the document's root
+
+ So we don't need to store `attributes` and `meta` data into the Pouch
+ database as their data already exists in the document's root
+
+ Note that this is also the case for `links` and `relationships`
+ attributes, but we don't remove them for now. They are also part of the
+ JSON-API, but the normalization do not spread them in the document's
+ root, so we have to check their usefulnes first
+ */
+ const sanitizedDoc = omit(docWithoutType, ['attributes', 'meta'])
+
+ return sanitizedDoc
+ }
+
+ async persistCozyData(data, forward = doNothing) {
+ const sanitizedDoc = this.sanitizeJsonApi(data)
+ sanitizedDoc.cozyLocalOnly = true
+
+ const oldDoc = await this.getExistingDocument(data._id, data._type)
+ if (oldDoc) {
+ sanitizedDoc._rev = oldDoc._rev
+ }
+
+ const db = this.pouches.getPouch(data._type)
+ await db.put(sanitizedDoc)
+ }
+
+ /**
+ * Retrieve the existing document from Pouch
+ *
+ * @private
+ * @param {*} id - ID of the document to retrieve
+ * @param {*} type - Doctype of the document to retrieve
+ * @param {*} throwIfNotFound - If true the method will throw when the document is not found. Otherwise it will return null
+ * @returns {Promise}
+ */
+ async getExistingDocument(id, type, throwIfNotFound = false) {
+ try {
+ const db = this.pouches.getPouch(type)
+ const existingDoc = await db.get(id)
+
+ return existingDoc
+ } catch (err) {
+ if (err.name === 'not_found' && !throwIfNotFound) {
+ return null
+ } else {
+ throw err
+ }
+ }
+ }
+
/**
*
* Check if there is warmup queries for this doctype
* and return if those queries are already warmed up or not
*
* @param {string} doctype - Doctype to check
- * @returns {boolean} the need to wait for the warmup
+ * @returns {Promise} the need to wait for the warmup
*/
- needsToWaitWarmup(doctype) {
+ async needsToWaitWarmup(doctype) {
if (
this.doctypesReplicationOptions &&
this.doctypesReplicationOptions[doctype] &&
this.doctypesReplicationOptions[doctype].warmupQueries
) {
- return !this.pouches.areQueriesWarmedUp(
+ return !(await this.pouches.areQueriesWarmedUp(
doctype,
this.doctypesReplicationOptions[doctype].warmupQueries
- )
+ ))
}
return false
}
@@ -407,35 +481,77 @@ class PouchLink extends CozyLink {
return Boolean(this.indexes[name])
}
- // This merge is necessary because PouchDB does not support partial indexes
- mergePartialIndexInSelector(selector, partialFilter) {
- if (partialFilter) {
- logger.info(
- `PouchLink: The query contains a partial index but PouchDB does not support it. ` +
- `Hence, the partial index definition is used in the selector for in-memory evaluation, ` +
- `which might impact expected performances. If this support is important in your use-case, ` +
- `please let us know or help us contribute to PouchDB!`
- )
- return { ...selector, ...partialFilter }
- }
- return selector
+ /**
+ * Create the PouchDB index if not existing
+ *
+ * @param {Array} fields - Fields to index
+ * @param {object} indexOption - Options for the index
+ * @param {object} [indexOption.partialFilter] - partialFilter
+ * @param {string} [indexOption.indexName] - indexName
+ * @param {string} [indexOption.doctype] - doctype
+ * @returns {Promise}
+ */
+ async createIndex(fields, { partialFilter, indexName, doctype } = {}) {
+ const absName = `${doctype}/${indexName}`
+ const db = this.pouches.getPouch(doctype)
+
+ const index = await db.createIndex({
+ index: {
+ fields,
+ ddoc: indexName,
+ indexName,
+ partial_filter_selector: partialFilter
+ }
+ })
+ this.indexes[absName] = index
+ return index
}
- async ensureIndex(doctype, query) {
- const fields = query.indexedFields || getIndexFields(query)
- const name = getIndexNameFromFields(fields)
- const absName = `${doctype}/${name}`
- const db = this.pouches.getPouch(doctype)
- if (this.indexes[absName]) {
- return this.indexes[absName]
- } else {
- const index = await db.createIndex({
- index: {
- fields
- }
+ /**
+ * Retrieve the PouchDB index if exist, undefined otherwise
+ *
+ * @param {string} doctype - The query's doctype
+ * @param {import('./types').MangoQueryOptions} options - The find options
+ * @param {string} indexName - The index name
+ * @returns {import('./types').PouchDbIndex | undefined}
+ */
+ findExistingIndex(doctype, options, indexName) {
+ const absName = `${doctype}/${indexName}`
+ return this.indexes[absName]
+ }
+
+ /**
+ * Handle index creation if it is missing.
+ *
+ * When an index is missing, we first check if there is one with a different
+ * name but the same definition. If there is none, we create the new index.
+ *
+ * /!\ Warning: this method is similar to DocumentCollection.handleMissingIndex()
+ * If you edit this method, please check if the change is also needed in DocumentCollection
+ *
+ * @param {string} doctype The mango selector
+ * @param {import('./types').MangoQueryOptions} options The find options
+ * @returns {Promise} index
+ * @private
+ */
+ async ensureIndex(doctype, options) {
+ let { indexedFields, partialFilter } = options
+
+ if (!indexedFields) {
+ indexedFields = getIndexFields(options)
+ }
+
+ const indexName = getIndexNameFromFields(indexedFields, partialFilter)
+
+ const existingIndex = this.findExistingIndex(doctype, options, indexName)
+ if (!existingIndex) {
+ return await this.createIndex(indexedFields, {
+ partialFilter,
+ indexName,
+ doctype
})
- this.indexes[absName] = index
- return index
+ } else {
+ return existingIndex
}
}
@@ -452,11 +568,6 @@ class PouchLink extends CozyLink {
partialFilter
}) {
const db = this.getPouch(doctype)
- // The partial index is not supported by PouchDB, so we ensure the selector includes it
- const mergedSelector = this.mergePartialIndexInSelector(
- selector,
- partialFilter
- )
let res, withRows
if (id) {
res = await db.get(id)
@@ -466,23 +577,31 @@ class PouchLink extends CozyLink {
res = withoutDesignDocuments(res)
res.total_rows = null // pouch indicates the total number of docs in res.total_rows, even though we use "keys". Setting it to null avoids cozy-client thinking there are more docs to fetch.
withRows = true
- } else if (!mergedSelector && !fields && !sort) {
+ } else if (!selector && !partialFilter && !fields && !sort) {
res = await allDocs(db, { include_docs: true, limit })
res = withoutDesignDocuments(res)
withRows = true
} else {
+ const findSelector = helpers.normalizeFindSelector({
+ selector,
+ sort,
+ indexedFields,
+ partialFilter
+ })
+
const findOpts = {
sort,
- selector: mergedSelector,
- // same selector as Document Collection. We force _id.
- // Fix https://github.com/cozy/cozy-client/issues/985
- fields: fields ? [...fields, '_id', '_type', 'class'] : undefined,
+ selector: findSelector,
+ // same selector as Document Collection.
+ // _id is necessary for the store, and _rev is required for offline. See https://github.com/cozy/cozy-client/blob/95978d39546023920b0c01d689fed5dd41577a02/packages/cozy-client/src/CozyClient.js#L1153
+ fields: fields ? [...fields, '_id', '_rev'] : undefined,
limit,
skip
}
const index = await this.ensureIndex(doctype, {
...findOpts,
- indexedFields
+ indexedFields,
+ partialFilter
})
findOpts.use_index = index.id
res = await find(db, findOpts)
@@ -490,7 +609,12 @@ class PouchLink extends CozyLink {
res.limit = limit
withRows = true
}
- return jsonapi.fromPouchResult(res, withRows, doctype)
+ return jsonapi.fromPouchResult({
+ res,
+ withRows,
+ doctype,
+ client: this.client
+ })
}
async executeMutation(mutation, result, forward) {
@@ -515,11 +639,12 @@ class PouchLink extends CozyLink {
return forward(mutation, result)
}
- return jsonapi.fromPouchResult(
- pouchRes,
- false,
- getDoctypeFromOperation(mutation)
- )
+ return jsonapi.fromPouchResult({
+ res: pouchRes,
+ withRows: false,
+ doctype: getDoctypeFromOperation(mutation),
+ client: this.client
+ })
}
async createDocument(mutation) {
@@ -561,6 +686,10 @@ class PouchLink extends CozyLink {
return parseMutationResult(document, res)
}
+ async addReferencesTo(mutation) {
+ throw new Error('addReferencesTo is not implemented in CozyPouchLink')
+ }
+
async dbMethod(method, mutation) {
const doctype = getDoctypeFromOperation(mutation)
const { document: doc, documents: docs } = mutation
diff --git a/packages/cozy-pouch-link/src/CozyPouchLink.spec.js b/packages/cozy-pouch-link/src/CozyPouchLink.spec.js
index 1ce0a82ec9..f08c82b70d 100644
--- a/packages/cozy-pouch-link/src/CozyPouchLink.spec.js
+++ b/packages/cozy-pouch-link/src/CozyPouchLink.spec.js
@@ -4,6 +4,8 @@ import { find, allDocs, withoutDesignDocuments } from './helpers'
jest.mock('./helpers', () => ({
find: jest.fn(),
allDocs: jest.fn(),
+ normalizeFindSelector: jest.requireActual('./helpers').default
+ .normalizeFindSelector,
withoutDesignDocuments: jest.fn()
}))
@@ -120,7 +122,7 @@ describe('CozyPouchLink', () => {
'io.cozy.files': { warmupQueries: [query1(), query2()] }
}
})
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const query = Q(TODO_DOCTYPE)
expect.assertions(0)
@@ -174,7 +176,7 @@ describe('CozyPouchLink', () => {
'io.cozy.todos': { strategy: 'fromRemote' }
}
})
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
await link.request(
{
doctype: TODO_DOCTYPE,
@@ -194,7 +196,7 @@ describe('CozyPouchLink', () => {
'io.cozy.todos': { strategy: 'fromRemote' }
}
})
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const mock = jest.fn()
await link.request(Q(TODO_DOCTYPE), null, mock)
expect(mock).not.toHaveBeenCalled()
@@ -210,7 +212,7 @@ describe('CozyPouchLink', () => {
const docs = [TODO_1, TODO_2, TODO_3, TODO_4]
it('should be able to execute a query', async () => {
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const db = link.getPouch(TODO_DOCTYPE)
db.post({
label: 'Make PouchDB link work',
@@ -223,7 +225,7 @@ describe('CozyPouchLink', () => {
it('should be possible to query only one doc', async () => {
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const db = link.getPouch(TODO_DOCTYPE)
db.post({
_id: 'deadbeef',
@@ -238,7 +240,7 @@ describe('CozyPouchLink', () => {
it('should be possible to explicitly index fields', async () => {
find.mockReturnValue({ docs: [TODO_3, TODO_4] })
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const db = link.getPouch(TODO_DOCTYPE)
await db.bulkDocs(docs.map(x => omit(x, '_type')))
const query = Q(TODO_DOCTYPE)
@@ -253,7 +255,7 @@ describe('CozyPouchLink', () => {
it('should be possible to query multiple docs', async () => {
withoutDesignDocuments.mockReturnValue({ docs: [TODO_1, TODO_3] })
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const db = link.getPouch(TODO_DOCTYPE)
await db.bulkDocs(docs.map(x => omit(x, '_type')))
const ids = [TODO_1._id, TODO_3._id]
@@ -268,7 +270,7 @@ describe('CozyPouchLink', () => {
it('should be possible to select', async () => {
find.mockReturnValue({ docs: [TODO_3, TODO_4] })
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const db = link.getPouch(TODO_DOCTYPE)
await db.bulkDocs(docs.map(x => omit(x, '_type')))
const query = Q(TODO_DOCTYPE)
@@ -296,32 +298,10 @@ describe('CozyPouchLink', () => {
})
})
- it('should merge selector and partial filter definitions', () => {
- const selector = { _id: { $gt: null } }
- expect(link.mergePartialIndexInSelector(selector, {})).toEqual(selector)
-
- const partialFilter = {
- trashed: {
- $exists: false
- }
- }
- const expectedMergedSelector = {
- _id: {
- $gt: null
- },
- trashed: {
- $exists: false
- }
- }
- expect(link.mergePartialIndexInSelector(selector, partialFilter)).toEqual(
- expectedMergedSelector
- )
- })
-
it("should add _id in the selected fields since CozyClient' store needs it", async () => {
find.mockReturnValue({ docs: [TODO_3, TODO_4] })
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const db = link.getPouch(TODO_DOCTYPE)
await db.bulkDocs(docs.map(x => omit(x, '_type')))
const query = Q(TODO_DOCTYPE)
@@ -333,7 +313,7 @@ describe('CozyPouchLink', () => {
expect(find).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
- fields: ['label', 'done', '_id', '_type', 'class']
+ fields: ['label', 'done', '_id', '_rev']
})
)
})
@@ -342,7 +322,7 @@ describe('CozyPouchLink', () => {
describe('mutations', () => {
it('should be possible to save a new document', async () => {
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const { _id, ...NEW_TODO } = TODO_3
const mutation = client.getDocumentSavePlan(NEW_TODO)
const res = await link.request(mutation)
@@ -360,7 +340,7 @@ describe('CozyPouchLink', () => {
it('should be possible to save multiple documents', async () => {
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const { _id, ...NEW_TODO } = TODO_3
const res = await client.saveAll([TODO_3, TODO_4, NEW_TODO])
expect(link.executeMutation).toHaveBeenCalled()
@@ -394,7 +374,7 @@ describe('CozyPouchLink', () => {
{ ok: true, id: '3', rev: '1-cffeebabe' }
]
}
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const { _id, ...NEW_TODO } = TODO_3
let err
try {
@@ -412,7 +392,7 @@ describe('CozyPouchLink', () => {
it('should be possible to update a document', async () => {
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const { _id, ...NEW_TODO } = TODO_3
const saveMutation = client.getDocumentSavePlan(NEW_TODO)
const saved = (await link.request(saveMutation)).data
@@ -475,7 +455,10 @@ describe('CozyPouchLink', () => {
_type: 'io.cozy.todos',
done: false,
id: '1',
- label: 'Buy bread'
+ label: 'Buy bread',
+ relationships: {
+ referenced_by: undefined
+ }
}
]
})
@@ -583,21 +566,58 @@ describe('CozyPouchLink', () => {
it('uses the default index, the one from the sort', async () => {
spy = jest.spyOn(PouchDB.prototype, 'createIndex')
await setup()
- link.pouches.isSynced = jest.fn().mockReturnValue(true)
+ link.pouches.getSyncStatus = jest.fn().mockReturnValue('synced')
const query = Q(TODO_DOCTYPE)
.where({})
.sortBy([{ name: 'asc' }])
await link.request(query)
- expect(spy).toHaveBeenCalledWith({ index: { fields: ['name'] } })
+ expect(spy).toHaveBeenCalledWith({
+ index: {
+ ddoc: 'by_name',
+ fields: ['name'],
+ indexName: 'by_name',
+ partial_filter_selector: undefined
+ }
+ })
})
it('uses indexFields if provided', async () => {
spy = jest.spyOn(PouchDB.prototype, 'createIndex').mockReturnValue({})
await setup()
- link.ensureIndex(TODO_DOCTYPE, {
+ await link.ensureIndex(TODO_DOCTYPE, {
indexedFields: ['myIndex']
})
- expect(spy).toHaveBeenCalledWith({ index: { fields: ['myIndex'] } })
+ expect(spy).toHaveBeenCalled()
+ expect(spy).toHaveBeenCalledWith({
+ index: {
+ ddoc: 'by_myIndex',
+ fields: ['myIndex'],
+ indexName: 'by_myIndex',
+ partial_filter_selector: undefined
+ }
+ })
+ })
+
+ it('should handle partial filters', async () => {
+ spy = jest.spyOn(PouchDB.prototype, 'createIndex').mockReturnValue({})
+ await setup()
+ await link.ensureIndex(TODO_DOCTYPE, {
+ indexedFields: ['myIndex'],
+ partialFilter: { SOME_FIELD: { $exists: true } }
+ })
+ expect(spy).toHaveBeenCalled()
+ expect(spy).toHaveBeenCalledWith({
+ index: {
+ ddoc: 'by_myIndex_filter_(SOME_FIELD_$exists_true)',
+ fields: ['myIndex'],
+ indexName: 'by_myIndex_filter_(SOME_FIELD_$exists_true)',
+ partial_filter_selector: {
+ SOME_FIELD: {
+ $exists: true
+ }
+ }
+ }
+ })
})
it('uses the specified index', async () => {
@@ -614,9 +634,14 @@ describe('CozyPouchLink', () => {
})
const params = {
sort: undefined,
- selector: {},
+ selector: {
+ myIndex2: {
+ $gt: null
+ }
+ },
fields: undefined,
limit: undefined,
+ partialFilter: undefined,
skip: undefined
}
diff --git a/packages/cozy-pouch-link/src/PouchManager.js b/packages/cozy-pouch-link/src/PouchManager.js
index 4c04ae4e29..fbba66d78c 100644
--- a/packages/cozy-pouch-link/src/PouchManager.js
+++ b/packages/cozy-pouch-link/src/PouchManager.js
@@ -1,24 +1,19 @@
-import PouchDB from 'pouchdb-browser'
import fromPairs from 'lodash/fromPairs'
import forEach from 'lodash/forEach'
import get from 'lodash/get'
-import map from 'lodash/map'
-import zip from 'lodash/zip'
-import startsWith from 'lodash/startsWith'
import { isMobileApp } from 'cozy-device-helper'
-import { QueryDefinition } from 'cozy-client'
+import { PouchLocalStorage } from './localStorage'
import Loop from './loop'
import logger from './logger'
-import { fetchRemoteLastSequence } from './remote'
-import { startReplication } from './startReplication'
-import * as localStorage from './localStorage'
-import { getDatabaseName } from './utils'
+import { platformWeb } from './platformWeb'
+import { replicateOnce } from './replicateOnce'
+import { formatAggregatedError, getDatabaseName } from './utils'
const DEFAULT_DELAY = 30 * 1000
/**
- * @param {QueryDefinition} query The query definition whose name we're getting
+ * @param {import('cozy-client/types/types').Query} query The query definition whose name we're getting
*
* @returns {string} alias
*/
@@ -35,20 +30,36 @@ const getQueryAlias = query => {
class PouchManager {
constructor(doctypes, options) {
this.options = options
- const pouchPlugins = get(options, 'pouch.plugins', [])
- const pouchOptions = get(options, 'pouch.options', {})
+ this.doctypes = doctypes
- forEach(pouchPlugins, plugin => PouchDB.plugin(plugin))
+ this.storage = new PouchLocalStorage(
+ options.platform?.storage || platformWeb.storage
+ )
+ this.PouchDB = options.platform?.pouchAdapter || platformWeb.pouchAdapter
+ this.isOnline = options.platform?.isOnline || platformWeb.isOnline
+ this.events = options.platform?.events || platformWeb.events
+ }
+
+ async init() {
+ const pouchPlugins = get(this.options, 'pouch.plugins', [])
+ const pouchOptions = get(this.options, 'pouch.options', {})
+
+ forEach(pouchPlugins, plugin => this.PouchDB.plugin(plugin))
this.pouches = fromPairs(
- doctypes.map(doctype => [
+ this.doctypes.map(doctype => [
doctype,
- new PouchDB(getDatabaseName(options.prefix, doctype), pouchOptions)
+ new this.PouchDB(
+ getDatabaseName(this.options.prefix, doctype),
+ pouchOptions
+ )
])
)
- this.syncedDoctypes = localStorage.getPersistedSyncedDoctypes()
- this.warmedUpQueries = localStorage.getPersistedWarmedUpQueries()
- this.getReplicationURL = options.getReplicationURL
- this.doctypesReplicationOptions = options.doctypesReplicationOptions || {}
+ /** @type {Record} - Stores synchronization info per doctype */
+ this.syncedDoctypes = await this.storage.getPersistedSyncedDoctypes()
+ this.warmedUpQueries = await this.storage.getPersistedWarmedUpQueries()
+ this.getReplicationURL = this.options.getReplicationURL
+ this.doctypesReplicationOptions =
+ this.options.doctypesReplicationOptions || {}
this.listenerLaunched = false
// We must ensure databases exist on the remote before
@@ -59,16 +70,19 @@ class PouchManager {
this.stopReplicationLoop = this.stopReplicationLoop.bind(this)
this.replicateOnce = this.replicateOnce.bind(this)
this.executeQuery = this.options.executeQuery
+
+ /** @type {import('./types').CancelablePromise[]} - Stores replication promises */
+ this.replications = undefined
}
addListeners() {
if (!this.listenerLaunched) {
if (isMobileApp()) {
- document.addEventListener('pause', this.stopReplicationLoop)
- document.addEventListener('resume', this.startReplicationLoop)
+ this.events.addEventListener('pause', this.stopReplicationLoop)
+ this.events.addEventListener('resume', this.startReplicationLoop)
}
- document.addEventListener('online', this.startReplicationLoop)
- document.addEventListener('offline', this.stopReplicationLoop)
+ this.events.addEventListener('online', this.startReplicationLoop)
+ this.events.addEventListener('offline', this.stopReplicationLoop)
this.listenerLaunched = true
}
}
@@ -76,22 +90,22 @@ class PouchManager {
removeListeners() {
if (this.listenerLaunched) {
if (isMobileApp()) {
- document.removeEventListener('pause', this.stopReplicationLoop)
- document.removeEventListener('resume', this.startReplicationLoop)
+ this.events.removeEventListener('pause', this.stopReplicationLoop)
+ this.events.removeEventListener('resume', this.startReplicationLoop)
}
- document.removeEventListener('online', this.startReplicationLoop)
- document.removeEventListener('offline', this.stopReplicationLoop)
+ this.events.removeEventListener('online', this.startReplicationLoop)
+ this.events.removeEventListener('offline', this.stopReplicationLoop)
this.listenerLaunched = false
}
}
- destroy() {
+ async destroy() {
this.stopReplicationLoop()
this.removeListeners()
- this.clearSyncedDoctypes()
- this.clearWarmedUpQueries()
- localStorage.destroyAllDoctypeLastSequence()
- localStorage.destroyAllLastReplicatedDocID()
+ await this.clearSyncedDoctypes()
+ await this.clearWarmedUpQueries()
+ await this.storage.destroyAllDoctypeLastSequence()
+ await this.storage.destroyAllLastReplicatedDocID()
return Promise.all(
Object.values(this.pouches).map(pouch => pouch.destroy())
@@ -115,7 +129,11 @@ class PouchManager {
})
}
- /** Starts periodic syncing of the pouches */
+ /**
+ * Starts periodic syncing of the pouches
+ *
+ * @returns {Promise}
+ */
async startReplicationLoop() {
await this.ensureDatabasesExist()
@@ -158,92 +176,19 @@ class PouchManager {
/** Starts replication */
async replicateOnce() {
- if (!window.navigator.onLine) {
- logger.info(
- 'PouchManager: The device is offline so the replication has been skipped'
- )
- return Promise.resolve()
- }
-
- logger.info('PouchManager: Starting replication iteration')
-
- // Creating each replication
- this.replications = map(this.pouches, async (pouch, doctype) => {
- logger.info('PouchManager: Starting replication for ' + doctype)
-
- const getReplicationURL = () => this.getReplicationURL(doctype)
-
- const initialReplication = !this.isSynced(doctype)
- const replicationFilter = doc => {
- return !startsWith(doc._id, '_design')
- }
- let seq = ''
- if (initialReplication) {
- // Before the first replication, get the last remote sequence,
- // which will be used as a checkpoint for the next replication
- const lastSeq = await fetchRemoteLastSequence(getReplicationURL())
- localStorage.persistDoctypeLastSequence(doctype, lastSeq)
- } else {
- seq = localStorage.getDoctypeLastSequence(doctype)
- }
-
- const replicationOptions = get(
- this.doctypesReplicationOptions,
- doctype,
- {}
- )
- replicationOptions.initialReplication = initialReplication
- replicationOptions.filter = replicationFilter
- replicationOptions.since = seq
- replicationOptions.doctype = doctype
-
- if (this.options.onDoctypeSyncStart) {
- this.options.onDoctypeSyncStart(doctype)
- }
- const res = await startReplication(
- pouch,
- replicationOptions,
- getReplicationURL
- )
- if (seq) {
- // We only need the sequence for the second replication, as PouchDB
- // will use a local checkpoint for the next runs.
- localStorage.destroyDoctypeLastSequence(doctype)
- }
-
- this.updateSyncInfo(doctype)
- this.checkToWarmupDoctype(doctype, replicationOptions)
- if (this.options.onDoctypeSyncEnd) {
- this.options.onDoctypeSyncEnd(doctype)
- }
- return res
- })
-
- // Waiting on each replication
- const doctypes = Object.keys(this.pouches)
- const promises = Object.values(this.replications)
- try {
- const res = await Promise.all(promises)
-
- if (process.env.NODE_ENV !== 'production') {
- logger.info('PouchManager: Replication ended')
- }
-
- if (this.options.onSync) {
- const doctypeUpdates = fromPairs(zip(doctypes, res))
- this.options.onSync(doctypeUpdates)
- }
-
- res.cancel = this.cancelCurrentReplications
-
- return res
- } catch (err) {
- this.handleReplicationError(err)
- }
+ return replicateOnce(this)
}
handleReplicationError(err) {
- logger.warn('PouchManager: Error during replication', err)
+ let aggregatedMessage = ''
+ // @ts-ignore
+ // eslint-disable-next-line no-undef
+ if (err instanceof AggregateError) {
+ aggregatedMessage = formatAggregatedError(err)
+ }
+ logger.warn(
+ `PouchManager: Error during replication - ${err.message}${aggregatedMessage}`
+ )
// On error, replication stops, it needs to be started
// again manually by the owner of PouchManager
this.stopReplicationLoop()
@@ -273,23 +218,41 @@ class PouchManager {
return this.pouches[doctype]
}
- updateSyncInfo(doctype) {
- this.syncedDoctypes[doctype] = { date: new Date().toISOString() }
- localStorage.persistSyncedDoctypes(this.syncedDoctypes)
+ /**
+ * Update the Sync info for the specifed doctype
+ *
+ * @param {string} doctype - The doctype to update
+ * @param {import('./types').SyncStatus} status - The new Sync status for the doctype
+ */
+ async updateSyncInfo(doctype, status = 'synced') {
+ this.syncedDoctypes[doctype] = { date: new Date().toISOString(), status }
+ await this.storage.persistSyncedDoctypes(this.syncedDoctypes)
}
+ /**
+ * Get the Sync info for the specified doctype
+ *
+ * @param {string} doctype - The doctype to check
+ * @returns {import('./types').SyncInfo}
+ */
getSyncInfo(doctype) {
return this.syncedDoctypes && this.syncedDoctypes[doctype]
}
- isSynced(doctype) {
+ /**
+ * Get the Sync status for the specified doctype
+ *
+ * @param {string} doctype - The doctype to check
+ * @returns {import('./types').SyncStatus}
+ */
+ getSyncStatus(doctype) {
const info = this.getSyncInfo(doctype)
- return info ? !!info.date : false
+ return info?.status || 'not_synced'
}
- clearSyncedDoctypes() {
+ async clearSyncedDoctypes() {
this.syncedDoctypes = {}
- localStorage.destroySyncedDoctypes()
+ await this.storage.destroySyncedDoctypes()
}
async warmupQueries(doctype, queries) {
@@ -304,7 +267,7 @@ class PouchManager {
}
})
)
- localStorage.persistWarmedUpQueries(this.warmedUpQueries)
+ await this.storage.persistWarmedUpQueries(this.warmedUpQueries)
logger.log('PouchManager: warmupQueries for ' + doctype + ' are done')
} catch (err) {
logger.error(
@@ -324,8 +287,8 @@ class PouchManager {
}
}
- areQueriesWarmedUp(doctype, queries) {
- const persistWarmedUpQueries = localStorage.getPersistedWarmedUpQueries()
+ async areQueriesWarmedUp(doctype, queries) {
+ const persistWarmedUpQueries = await this.storage.getPersistedWarmedUpQueries()
return queries.every(
query =>
persistWarmedUpQueries[doctype] &&
@@ -333,9 +296,9 @@ class PouchManager {
)
}
- clearWarmedUpQueries() {
+ async clearWarmedUpQueries() {
this.warmedUpQueries = {}
- localStorage.destroyWarmedUpQueries()
+ await this.storage.destroyWarmedUpQueries()
}
}
diff --git a/packages/cozy-pouch-link/src/PouchManager.spec.js b/packages/cozy-pouch-link/src/PouchManager.spec.js
index 0fa00a477d..4147c0341f 100644
--- a/packages/cozy-pouch-link/src/PouchManager.spec.js
+++ b/packages/cozy-pouch-link/src/PouchManager.spec.js
@@ -19,10 +19,17 @@ jest.mock('./remote', () => ({
import * as rep from './startReplication'
import PouchDB from 'pouchdb-browser'
-import * as ls from './localStorage'
+import {
+ LOCALSTORAGE_SYNCED_KEY,
+ LOCALSTORAGE_WARMUPEDQUERIES_KEY,
+ PouchLocalStorage
+} from './localStorage'
+import { platformWeb } from './platformWeb'
import { fetchRemoteLastSequence, fetchRemoteInstance } from './remote'
+const ls = new PouchLocalStorage(platformWeb.storage)
+
const sleep = delay => {
return new Promise(resolve => {
setTimeout(resolve, delay)
@@ -63,7 +70,7 @@ describe('PouchManager', () => {
getReplicationURL,
onSync = jest.fn()
- beforeEach(() => {
+ beforeEach(async () => {
getReplicationURL = () => 'http://replicationURL.local'
managerOptions = {
replicationDelay: 16,
@@ -72,6 +79,7 @@ describe('PouchManager', () => {
prefix: 'cozy.tools'
}
manager = new PouchManager(['io.cozy.todos'], managerOptions)
+ await manager.init()
const pouch = manager.getPouch('io.cozy.todos')
const replication = mocks.pouchReplication({
direction: 'pull',
@@ -133,6 +141,7 @@ describe('PouchManager', () => {
'io.cozy.readonly': { strategy: 'fromRemote' }
}
})
+ await manager.init()
const normalPouch = manager.getPouch('io.cozy.todos')
const readOnlyPouch = manager.getPouch('io.cozy.readonly')
readOnlyPouch.replicate = {}
@@ -155,6 +164,7 @@ describe('PouchManager', () => {
}
}
)
+ await manager.init()
const normalPouch = manager.getPouch('io.cozy.todos')
const readOnlyPouch = manager.getPouch('io.cozy.readonly')
readOnlyPouch.replicate = {}
@@ -162,6 +172,9 @@ describe('PouchManager', () => {
const writeOnlyPouch = manager.getPouch('io.cozy.writeonly')
writeOnlyPouch.replicate = {}
writeOnlyPouch.replicate.to = jest.fn()
+ manager.updateSyncInfo('io.cozy.todos')
+ manager.updateSyncInfo('io.cozy.readonly')
+ manager.updateSyncInfo('io.cozy.writeonly')
manager.startReplicationLoop()
await sleep(1000)
expect(readOnlyPouch.replicate.from).toHaveBeenCalled()
@@ -214,6 +227,7 @@ describe('PouchManager', () => {
it('should call on sync with doctype updates', async () => {
jest.spyOn(manager, 'replicateOnce')
onSync.mockReset()
+ manager.updateSyncInfo('io.cozy.todos')
await manager.replicateOnce()
expect(onSync).toHaveBeenCalledWith({
'io.cozy.todos': [
@@ -231,14 +245,16 @@ describe('PouchManager', () => {
it('should add pouch plugin', async () => {
const options = { ...managerOptions, pouch: { plugins: ['myPlugin'] } }
- new PouchManager(['io.cozy.todos'], options)
+ const manager = new PouchManager(['io.cozy.todos'], options)
+ await manager.init()
expect(PouchDB.plugin).toHaveBeenCalledTimes(1)
})
it('should instanciate pouch with options', async () => {
const pouchOptions = { adapter: 'cordova-sqlite', location: 'default' }
const options = { ...managerOptions, pouch: { options: pouchOptions } }
- new PouchManager(['io.cozy.todos'], options)
+ const manager = new PouchManager(['io.cozy.todos'], options)
+ await manager.init()
expect(PouchDB).toHaveBeenCalledWith(
'cozy.tools_io.cozy.todos',
pouchOptions
@@ -246,33 +262,36 @@ describe('PouchManager', () => {
})
describe('getPersistedSyncedDoctypes', () => {
- it('should return an empty array if local storage is empty', () => {
- expect(ls.getPersistedSyncedDoctypes()).toEqual({})
+ it('should return an empty array if local storage is empty', async () => {
+ expect(await ls.getPersistedSyncedDoctypes()).toEqual({})
})
- it('should return an empty array if local storage contains something that is not an array', () => {
- localStorage.__STORE__[ls.LOCALSTORAGE_SYNCED_KEY] = 'true'
- expect(ls.getPersistedSyncedDoctypes()).toEqual({})
+ it('should return an empty array if local storage contains something that is not an array', async () => {
+ localStorage.__STORE__[LOCALSTORAGE_SYNCED_KEY] = 'true'
+ expect(await ls.getPersistedSyncedDoctypes()).toEqual({})
})
- it('should return the list of doctypes if local storage contains one', () => {
+ it('should return the list of doctypes if local storage contains one', async () => {
const persistedSyncedDoctypes = {
'io.cozy.todos': { date: '2021-08-11T13:48:06.085Z' }
}
- localStorage.__STORE__[ls.LOCALSTORAGE_SYNCED_KEY] = JSON.stringify(
+ localStorage.__STORE__[LOCALSTORAGE_SYNCED_KEY] = JSON.stringify(
+ persistedSyncedDoctypes
+ )
+ expect(await ls.getPersistedSyncedDoctypes()).toEqual(
persistedSyncedDoctypes
)
- expect(ls.getPersistedSyncedDoctypes()).toEqual(persistedSyncedDoctypes)
})
})
describe('persistSyncedDoctypes', () => {
- it('should put the list of synced doctypes in localStorage', () => {
+ it('should put the list of synced doctypes in localStorage', async () => {
const manager = new PouchManager(['io.cozy.todos'], managerOptions)
+ await manager.init()
manager.syncedDoctypes = ['io.cozy.todos']
ls.persistSyncedDoctypes(manager.syncedDoctypes)
- expect(localStorage.__STORE__[ls.LOCALSTORAGE_SYNCED_KEY]).toEqual(
+ expect(localStorage.__STORE__[LOCALSTORAGE_SYNCED_KEY]).toEqual(
JSON.stringify(manager.syncedDoctypes)
)
})
@@ -287,126 +306,139 @@ describe('PouchManager', () => {
MockDate.reset()
})
- it('should add the doctype to synced doctypes', () => {
+ it('should add the doctype to synced doctypes', async () => {
const manager = new PouchManager(['io.cozy.todos'], managerOptions)
- manager.updateSyncInfo('io.cozy.todos')
+ await manager.init()
+ await manager.updateSyncInfo('io.cozy.todos')
expect(Object.keys(manager.syncedDoctypes)).toEqual(['io.cozy.todos'])
})
- it('should persist the new synced doctypes list', () => {
+ it('should persist the new synced doctypes list', async () => {
const manager = new PouchManager(['io.cozy.todos'], managerOptions)
+ await manager.init()
- manager.updateSyncInfo('io.cozy.todos')
- expect(localStorage.__STORE__[ls.LOCALSTORAGE_SYNCED_KEY]).toEqual(
+ await manager.updateSyncInfo('io.cozy.todos')
+ expect(localStorage.__STORE__[LOCALSTORAGE_SYNCED_KEY]).toEqual(
JSON.stringify({
- 'io.cozy.todos': { date: '2021-08-01T00:00:00.000Z' }
+ 'io.cozy.todos': {
+ date: '2021-08-01T00:00:00.000Z',
+ status: 'synced'
+ }
})
)
})
})
- describe('isSynced', () => {
+ describe('getSyncStatus', () => {
let manager
- beforeEach(() => {
+ beforeEach(async () => {
manager = new PouchManager(['io.cozy.todos'], managerOptions)
+ await manager.init()
})
- it('should return true if the doctype is synced', () => {
- manager.updateSyncInfo('io.cozy.todos')
- expect(manager.isSynced('io.cozy.todos')).toBe(true)
+ it(`should return 'synced' if the doctype is synced`, async () => {
+ await manager.updateSyncInfo('io.cozy.todos')
+ expect(manager.getSyncStatus('io.cozy.todos')).toBe('synced')
})
- it('should return false if the doctype is not synced', () => {
- expect(manager.isSynced('io.cozy.todos')).toBe(false)
+ it(`should return 'not_synced' if the doctype is not synced`, () => {
+ expect(manager.getSyncStatus('io.cozy.todos')).toBe('not_synced')
+ })
+
+ it('should return status if updateSyncInfo was called with custom status', async () => {
+ await manager.updateSyncInfo('io.cozy.todos', 'not_complete')
+ expect(manager.getSyncStatus('io.cozy.todos')).toBe('not_complete')
})
})
describe('destroySyncedDoctypes', () => {
- it('should destroy the local storage item', () => {
- ls.destroySyncedDoctypes()
+ it('should destroy the local storage item', async () => {
+ await ls.destroySyncedDoctypes()
expect(localStorage.removeItem).toHaveBeenLastCalledWith(
- ls.LOCALSTORAGE_SYNCED_KEY
+ LOCALSTORAGE_SYNCED_KEY
)
})
- it('should reset syncedDoctypes', () => {
+ it('should reset syncedDoctypes', async () => {
manager.syncedDoctypes = {
'io.cozy.todos': { date: '2021-08-11T13:48:06.085Z' }
}
- manager.clearSyncedDoctypes()
+ await manager.clearSyncedDoctypes()
expect(manager.syncedDoctypes).toEqual({})
})
})
describe('getPersistedWarmedUpQueriess', () => {
- it('should return an empty object if local storage is empty', () => {
- expect(ls.getPersistedWarmedUpQueries()).toEqual({})
+ it('should return an empty object if local storage is empty', async () => {
+ expect(await ls.getPersistedWarmedUpQueries()).toEqual({})
})
- it('should return the list of queries if local storage contains ones', () => {
+ it('should return the list of queries if local storage contains ones', async () => {
const persistedQueries = [query().options.as]
- localStorage.__STORE__[
- ls.LOCALSTORAGE_WARMUPEDQUERIES_KEY
- ] = JSON.stringify(persistedQueries)
- expect(ls.getPersistedWarmedUpQueries()).toEqual(persistedQueries)
+ localStorage.__STORE__[LOCALSTORAGE_WARMUPEDQUERIES_KEY] = JSON.stringify(
+ persistedQueries
+ )
+ expect(await ls.getPersistedWarmedUpQueries()).toEqual(persistedQueries)
})
})
describe('persistWarmedUpQueries', () => {
- it('should put the list of warmedUpQueries in localStorage', () => {
+ it('should put the list of warmedUpQueries in localStorage', async () => {
const manager = new PouchManager(['io.cozy.todos'], managerOptions)
+ await manager.init()
manager.warmedUpQueries = { 'io.cozy.todos': ['query1', 'query2'] }
- ls.persistWarmedUpQueries(manager.warmedUpQueries)
+ await ls.persistWarmedUpQueries(manager.warmedUpQueries)
- expect(
- localStorage.__STORE__[ls.LOCALSTORAGE_WARMUPEDQUERIES_KEY]
- ).toEqual(JSON.stringify(manager.warmedUpQueries))
+ expect(localStorage.__STORE__[LOCALSTORAGE_WARMUPEDQUERIES_KEY]).toEqual(
+ JSON.stringify(manager.warmedUpQueries)
+ )
})
})
describe('areQueriesWarmedUp', () => {
let manager
- beforeEach(() => {
+ beforeEach(async () => {
manager = new PouchManager(['io.cozy.todos'], managerOptions)
+ await manager.init()
})
- it('should return true if all the queries are warmuped', () => {
+ it('should return true if all the queries are warmuped', async () => {
manager.warmedUpQueries = {
'io.cozy.todos': [query1().options.as, query2().options.as]
}
- ls.persistWarmedUpQueries(manager.warmedUpQueries)
+ await ls.persistWarmedUpQueries(manager.warmedUpQueries)
expect(
- manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()])
+ await manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()])
).toBe(true)
})
- it('should return false if at least one query is not warmuped', () => {
+ it('should return false if at least one query is not warmuped', async () => {
manager.warmedUpQueries = {
'io.cozy.todos': [query2().options.as]
}
- ls.persistWarmedUpQueries()
+ await ls.persistWarmedUpQueries()
expect(
- manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()])
+ await manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()])
).toBe(false)
})
- it('should return false if the queries are not been done', () => {
+ it('should return false if the queries are not been done', async () => {
expect(
- manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()])
+ await manager.areQueriesWarmedUp('io.cozy.todos', [query1(), query2()])
).toBe(false)
})
})
describe('clearWarmedupQueries', () => {
- it('should clear the local storage item', () => {
+ it('should clear the local storage item', async () => {
manager.clearWarmedUpQueries()
expect(localStorage.removeItem).toHaveBeenLastCalledWith(
- ls.LOCALSTORAGE_WARMUPEDQUERIES_KEY
+ LOCALSTORAGE_WARMUPEDQUERIES_KEY
)
})
it('should reset warmedupQueries', () => {
@@ -490,7 +522,7 @@ describe('PouchManager', () => {
describe('warmupQueries', () => {
let manager
const executeMock = jest.fn()
- beforeEach(() => {
+ beforeEach(async () => {
let newManagerOptions = {
...managerOptions,
executeQuery: executeMock,
@@ -502,6 +534,7 @@ describe('PouchManager', () => {
}
}
manager = new PouchManager(['io.cozy.todos'], newManagerOptions)
+ await manager.init()
})
it('should executes warmeupQueries on the first replicationLoop only', async () => {
@@ -524,7 +557,7 @@ describe('PouchManager', () => {
.definition()
.toDefinition()
)
- expect(ls.getPersistedWarmedUpQueries()).toEqual({
+ expect(await ls.getPersistedWarmedUpQueries()).toEqual({
'io.cozy.todos': ['query1', 'query2']
})
//Simulation of a loop. Let's replicate again
@@ -541,7 +574,7 @@ describe('PouchManager', () => {
await manager.replicateOnce()
await sleep(10)
- expect(ls.getPersistedWarmedUpQueries()).toEqual({})
+ expect(await ls.getPersistedWarmedUpQueries()).toEqual({})
expect(manager.warmedUpQueries['io.cozy.todos']).toBeUndefined()
})
})
diff --git a/packages/cozy-pouch-link/src/helpers.js b/packages/cozy-pouch-link/src/helpers.js
index e71ec98a17..f630efbe05 100644
--- a/packages/cozy-pouch-link/src/helpers.js
+++ b/packages/cozy-pouch-link/src/helpers.js
@@ -1,5 +1,8 @@
+import merge from 'lodash/merge'
import startsWith from 'lodash/startsWith'
+import logger from './logger'
+
const helpers = {}
// https://github.com/pouchdb/pouchdb/issues/7011
@@ -61,4 +64,49 @@ helpers.insertBulkDocs = async (db, docs) => {
return db.bulkDocs(docs, { new_edits: false })
}
+helpers.normalizeFindSelector = ({
+ selector,
+ sort,
+ indexedFields,
+ partialFilter
+}) => {
+ let findSelector = selector || {}
+ if (indexedFields) {
+ for (const indexedField of indexedFields) {
+ if (!Object.keys(findSelector).includes(indexedField)) {
+ const selectorJson = JSON.stringify(selector)
+ logger.warn(
+ `${indexedField} was missing in selector, it has been automatically added from indexed fields. Please consider adding this field to your query's selector as required by PouchDB. The query's selector is: ${selectorJson}`
+ )
+ findSelector[indexedField] = {
+ $gt: null
+ }
+ }
+ }
+ }
+
+ if (sort) {
+ const sortedFields = sort.flatMap(s => Object.keys(s))
+ for (const sortedField of sortedFields) {
+ if (!Object.keys(findSelector).includes(sortedField)) {
+ const selectorJson = JSON.stringify(selector)
+ logger.warn(
+ `${sortedField} was missing in selector, it has been automatically added from sorted fields. Please consider adding this field to your query's selector as required by PouchDB. The query's selector is: ${selectorJson}`
+ )
+ findSelector[sortedField] = {
+ $gt: null
+ }
+ }
+ }
+ }
+
+ const mergedSelector = partialFilter
+ ? merge({ ...findSelector }, partialFilter)
+ : findSelector
+
+ return Object.keys(mergedSelector).length > 0
+ ? mergedSelector
+ : { _id: { $gt: null } } // PouchDB does not accept empty selector
+}
+
export default helpers
diff --git a/packages/cozy-pouch-link/src/helpers.spec.js b/packages/cozy-pouch-link/src/helpers.spec.js
index 5f57ed557f..14eaec0b5a 100644
--- a/packages/cozy-pouch-link/src/helpers.spec.js
+++ b/packages/cozy-pouch-link/src/helpers.spec.js
@@ -1,5 +1,10 @@
import helpers from './helpers'
-const { withoutDesignDocuments, isDeletedDocument, isDesignDocument } = helpers
+const {
+ withoutDesignDocuments,
+ isDeletedDocument,
+ isDesignDocument,
+ normalizeFindSelector
+} = helpers
import PouchDB from 'pouchdb-browser'
import PouchDBFind from 'pouchdb-find'
@@ -113,4 +118,83 @@ describe('Helpers', () => {
expect(isDeletedDocument({ _id: 'notdeleted' })).toBeFalsy()
})
})
+
+ describe('normalizeFindSelector', () => {
+ it('should add indexed fields in the selector if they are missing', () => {
+ const selector = {
+ SOME_FIELD: { $gt: null }
+ }
+ const sort = undefined
+ const indexedFields = ['SOME_INDEXED_FIELD']
+
+ const findSelector = normalizeFindSelector({
+ selector,
+ sort,
+ indexedFields
+ })
+ expect(findSelector).toStrictEqual({
+ SOME_FIELD: { $gt: null },
+ SOME_INDEXED_FIELD: { $gt: null }
+ })
+ })
+
+ it('should add sorted fields in the selector if they are missing', () => {
+ const selector = {}
+ const sort = [{ SOME_SORTED_FIELD: 'asc' }]
+ const indexedFields = undefined
+
+ const findSelector = normalizeFindSelector({
+ selector,
+ sort,
+ indexedFields
+ })
+ expect(findSelector).toStrictEqual({
+ SOME_SORTED_FIELD: { $gt: null }
+ })
+ })
+
+ it('should add indexed fields AND sorted fields in the selector if they are missing', () => {
+ const selector = undefined
+ const sort = [{ SOME_SORTED_FIELD: 'asc' }]
+ const indexedFields = ['SOME_INDEXED_FIELD']
+
+ const findSelector = normalizeFindSelector({
+ selector,
+ sort,
+ indexedFields
+ })
+ expect(findSelector).toStrictEqual({
+ SOME_INDEXED_FIELD: { $gt: null },
+ SOME_SORTED_FIELD: { $gt: null }
+ })
+ })
+
+ it('should prevent empty selector by adding a selector on _id when no selector is provided', () => {
+ const selector = undefined
+ const sort = undefined
+ const indexedFields = undefined
+
+ const findSelector = normalizeFindSelector({
+ selector,
+ sort,
+ indexedFields
+ })
+ expect(findSelector).toStrictEqual({ _id: { $gt: null } })
+ })
+
+ it('should not add selector on _id when no selector is provided but there are some indexed fields', () => {
+ const selector = undefined
+ const sort = undefined
+ const indexedFields = ['SOME_INDEXED_FIELD']
+
+ const findSelector = normalizeFindSelector({
+ selector,
+ sort,
+ indexedFields
+ })
+ expect(findSelector).toStrictEqual({
+ SOME_INDEXED_FIELD: { $gt: null }
+ })
+ })
+ })
})
diff --git a/packages/cozy-pouch-link/src/jsonapi.js b/packages/cozy-pouch-link/src/jsonapi.js
index 84901340cc..a54223ed94 100644
--- a/packages/cozy-pouch-link/src/jsonapi.js
+++ b/packages/cozy-pouch-link/src/jsonapi.js
@@ -1,6 +1,10 @@
-export const normalizeDoc = (doc, doctype) => {
+import { generateWebLink } from 'cozy-client'
+
+export const normalizeDoc = (doc, doctype, client) => {
const id = doc._id || doc.id
+ const { relationships, referenced_by } = doc
+
// PouchDB sends back .rev attribute but we do not want to
// keep it on the server. It is potentially higher than the
// _rev.
@@ -10,17 +14,59 @@ export const normalizeDoc = (doc, doctype) => {
id,
_id: id,
_rev,
- _type: doctype
+ _type: doctype,
+ relationships: {
+ ...relationships,
+ referenced_by
+ }
}
if (normalizedDoc.rev) {
delete normalizedDoc.rev
}
+
+ normalizeAppsLinks(normalizedDoc, doctype, client)
+
return normalizedDoc
}
+const normalizeAppsLinks = (docRef, doctype, client) => {
+ if (doctype !== 'io.cozy.apps') {
+ return
+ }
+
+ const webLink = generateWebLink({
+ cozyUrl: client.getStackClient().uri,
+ slug: docRef.slug,
+ subDomainType: client.capabilities.flat_subdomains ? 'flat' : 'nested',
+ pathname: '',
+ hash: '',
+ searchParams: []
+ })
+
+ docRef.links = {
+ self: `/apps/${docRef.slug}`,
+ related: webLink,
+ icon: `/apps/${docRef.slug}/icon/${docRef.version}`
+ }
+}
+
const filterDeletedDocumentsFromRows = doc => !!doc
-export const fromPouchResult = (res, withRows, doctype) => {
+export const fromPouchResult = ({ res, withRows, doctype, client }) => {
+ // Sometimes, queries are transformed by Collections and they call a dedicated
+ // cozy-stack route. When this is the case, we want to be able to replicate the same
+ // query from cozy-pouch-link. It is not possible as-is because the received data
+ // is not the same as the one stored in the Couch database
+ // To handle this, we store the received data in the Pouch with a dedicated id and
+ // we store the query result in a `cozyPouchData` attribute
+ // So when `cozyPouchData` attribute exists, we know that we want to return its content
+ // as the result of the query
+ if (res.cozyPouchData) {
+ return {
+ data: res.cozyPouchData
+ }
+ }
+
if (withRows) {
const docs = res.rows
? res.rows.map(row => row.doc).filter(filterDeletedDocumentsFromRows)
@@ -28,7 +74,7 @@ export const fromPouchResult = (res, withRows, doctype) => {
const offset = res.offset || 0
return {
- data: docs.map(doc => normalizeDoc(doc, doctype)),
+ data: docs.map(doc => normalizeDoc(doc, doctype, client)),
meta: { count: docs.length },
skip: offset,
next: offset + docs.length < res.total_rows || docs.length >= res.limit
@@ -36,8 +82,8 @@ export const fromPouchResult = (res, withRows, doctype) => {
} else {
return {
data: Array.isArray(res)
- ? res.map(doc => normalizeDoc(doc, doctype))
- : normalizeDoc(res, doctype)
+ ? res.map(doc => normalizeDoc(doc, doctype, client))
+ : normalizeDoc(res, doctype, client)
}
}
}
diff --git a/packages/cozy-pouch-link/src/jsonapi.spec.js b/packages/cozy-pouch-link/src/jsonapi.spec.js
index acfb280972..f507c1034b 100644
--- a/packages/cozy-pouch-link/src/jsonapi.spec.js
+++ b/packages/cozy-pouch-link/src/jsonapi.spec.js
@@ -1,3 +1,5 @@
+import CozyClient from 'cozy-client'
+
import { fromPouchResult, normalizeDoc } from './jsonapi'
const BART_FIXTURE = {
@@ -26,21 +28,32 @@ const DELETED_DOC_FIXTURE = {
delete: true
}
+const token = 'fake_token'
+const uri = 'https://claude.mycozy.cloud'
+const client = new CozyClient({ token, uri })
+
describe('doc normalization', () => {
it('keeps the highest between rev and _rev and removes the rev attribute', () => {
- const normalized = normalizeDoc({
- _id: 1234,
- _rev: '3-deadbeef',
- rev: '4-cffee',
- firstName: 'Bobba',
- lastName: 'Fett'
- })
+ const normalized = normalizeDoc(
+ {
+ _id: 1234,
+ _rev: '3-deadbeef',
+ rev: '4-cffee',
+ firstName: 'Bobba',
+ lastName: 'Fett'
+ },
+ 'io.cozy.contacts'
+ )
expect(normalized).toEqual({
_id: 1234,
id: 1234,
_rev: '4-cffee',
+ _type: 'io.cozy.contacts',
firstName: 'Bobba',
- lastName: 'Fett'
+ lastName: 'Fett',
+ relationships: {
+ referenced_by: undefined
+ }
})
})
})
@@ -50,7 +63,12 @@ describe('jsonapi', () => {
const res = {
rows: [BART_FIXTURE, LISA_FIXTURE, MARGE_FIXTURE, DELETED_DOC_FIXTURE]
}
- const normalized = fromPouchResult(res, true, 'io.cozy.simpsons')
+ const normalized = fromPouchResult({
+ res,
+ withRows: true,
+ doctype: 'io.cozy.simpsons',
+ client
+ })
expect(normalized.data[0].name).toBe('Bart')
expect(normalized.data[0].id).toBe(1)
expect(normalized.data[0]._id).toBe(1)
@@ -68,13 +86,23 @@ describe('jsonapi', () => {
describe('pagination', () => {
it('has no next when there is no pagination information', () => {
const res = { rows: [BART_FIXTURE] }
- const normalized = fromPouchResult(res, true, 'io.cozy.simpsons')
+ const normalized = fromPouchResult({
+ res,
+ withRows: true,
+ doctype: 'io.cozy.simpsons',
+ client
+ })
expect(normalized.next).toBe(false)
})
it('paginates when there is a total_rows field greater than the rows number', () => {
const res = { rows: [BART_FIXTURE], total_rows: 3 }
- const normalized = fromPouchResult(res, true, 'io.cozy.simpsons')
+ const normalized = fromPouchResult({
+ res,
+ withRows: true,
+ doctype: 'io.cozy.simpsons',
+ client
+ })
expect(normalized.next).toBe(true)
})
@@ -83,17 +111,32 @@ describe('jsonapi', () => {
rows: [BART_FIXTURE, MARGE_FIXTURE, LISA_FIXTURE],
total_rows: 3
}
- const normalized = fromPouchResult(res, true, 'io.cozy.simpsons')
+ const normalized = fromPouchResult({
+ res,
+ withRows: true,
+ doctype: 'io.cozy.simpsons',
+ client
+ })
expect(normalized.next).toBe(false)
})
it('paginates when there is a limit field', () => {
const res = { rows: [BART_FIXTURE, LISA_FIXTURE], limit: 2 }
- const normalized = fromPouchResult(res, true, 'io.cozy.simpsons')
+ const normalized = fromPouchResult({
+ res,
+ withRows: true,
+ doctype: 'io.cozy.simpsons',
+ client
+ })
expect(normalized.next).toBe(true)
const lastRes = { rows: [MARGE_FIXTURE], limit: 2 }
- const lastNormalized = fromPouchResult(lastRes, true, 'io.cozy.simpsons')
+ const lastNormalized = fromPouchResult({
+ res: lastRes,
+ withRows: true,
+ doctype: 'io.cozy.simpsons',
+ client
+ })
expect(lastNormalized.next).toBe(false)
})
})
diff --git a/packages/cozy-pouch-link/src/localStorage.js b/packages/cozy-pouch-link/src/localStorage.js
index ad05e48d6a..0e213604a3 100644
--- a/packages/cozy-pouch-link/src/localStorage.js
+++ b/packages/cozy-pouch-link/src/localStorage.js
@@ -7,182 +7,237 @@ export const LOCALSTORAGE_LASTREPLICATEDDOCID_KEY =
'cozy-client-pouch-link-lastreplicateddocid'
export const LOCALSTORAGE_ADAPTERNAME = 'cozy-client-pouch-link-adaptername'
-/**
- * Persist the last replicated doc id for a doctype
- *
- * @param {string} doctype - The replicated doctype
- * @param {string} id - The docid
- */
-export const persistLastReplicatedDocID = (doctype, id) => {
- const docids = getAllLastReplicatedDocID()
- docids[doctype] = id
+export class PouchLocalStorage {
+ constructor(storageEngine) {
+ checkStorageEngine(storageEngine)
+ this.storageEngine = storageEngine
+ }
- window.localStorage.setItem(
- LOCALSTORAGE_LASTREPLICATEDDOCID_KEY,
- JSON.stringify(docids)
- )
-}
+ /**
+ * Persist the last replicated doc id for a doctype
+ *
+ * @param {string} doctype - The replicated doctype
+ * @param {string} id - The docid
+ *
+ * @returns {Promise}
+ */
+ async persistLastReplicatedDocID(doctype, id) {
+ const docids = await this.getAllLastReplicatedDocID()
+ docids[doctype] = id
+
+ await this.storageEngine.setItem(
+ LOCALSTORAGE_LASTREPLICATEDDOCID_KEY,
+ JSON.stringify(docids)
+ )
+ }
-export const getAllLastReplicatedDocID = () => {
- const item = window.localStorage.getItem(LOCALSTORAGE_LASTREPLICATEDDOCID_KEY)
- return item ? JSON.parse(item) : {}
-}
+ /**
+ * @returns {Promise>}
+ */
+ async getAllLastReplicatedDocID() {
+ const item = await this.storageEngine.getItem(
+ LOCALSTORAGE_LASTREPLICATEDDOCID_KEY
+ )
+ return item ? JSON.parse(item) : {}
+ }
-/**
- * Get the last replicated doc id for a doctype
- *
- * @param {string} doctype - The doctype
- * @returns {string} The last replicated docid
- */
-export const getLastReplicatedDocID = doctype => {
- const docids = getAllLastSequences()
- return docids[doctype]
-}
+ /**
+ * Get the last replicated doc id for a doctype
+ *
+ * @param {string} doctype - The doctype
+ * @returns {Promise} The last replicated docid
+ */
+ async getLastReplicatedDocID(doctype) {
+ const docids = await this.getAllLastReplicatedDocID()
+ return docids[doctype]
+ }
-/**
- * Destroy all the replicated doc id
- */
-export const destroyAllLastReplicatedDocID = () => {
- window.localStorage.removeItem(LOCALSTORAGE_LASTREPLICATEDDOCID_KEY)
-}
+ /**
+ * Destroy all the replicated doc id
+ *
+ * @returns {Promise}
+ */
+ async destroyAllLastReplicatedDocID() {
+ await this.storageEngine.removeItem(LOCALSTORAGE_LASTREPLICATEDDOCID_KEY)
+ }
-/**
- * Persist the synchronized doctypes
- *
- * @typedef {object} SyncInfo
- * @property {string} Date
- *
- * @param {Record} syncedDoctypes - The sync doctypes
- */
-export const persistSyncedDoctypes = syncedDoctypes => {
- window.localStorage.setItem(
- LOCALSTORAGE_SYNCED_KEY,
- JSON.stringify(syncedDoctypes)
- )
-}
+ /**
+ * Persist the synchronized doctypes
+ *
+ * @param {Record} syncedDoctypes - The sync doctypes
+ *
+ * @returns {Promise}
+ */
+ async persistSyncedDoctypes(syncedDoctypes) {
+ await this.storageEngine.setItem(
+ LOCALSTORAGE_SYNCED_KEY,
+ JSON.stringify(syncedDoctypes)
+ )
+ }
-/**
- * Get the persisted doctypes
- *
- * @returns {object} The synced doctypes
- */
-export const getPersistedSyncedDoctypes = () => {
- const item = window.localStorage.getItem(LOCALSTORAGE_SYNCED_KEY)
- const parsed = item ? JSON.parse(item) : {}
- if (typeof parsed !== 'object') {
- return {}
+ /**
+ * Get the persisted doctypes
+ *
+ * @returns {Promise} The synced doctypes
+ */
+ async getPersistedSyncedDoctypes() {
+ const item = await this.storageEngine.getItem(LOCALSTORAGE_SYNCED_KEY)
+ const parsed = item ? JSON.parse(item) : {}
+ if (typeof parsed !== 'object') {
+ return {}
+ }
+ return parsed
}
- return parsed
-}
-/**
- * Destroy the synced doctypes
- *
- */
-export const destroySyncedDoctypes = () => {
- window.localStorage.removeItem(LOCALSTORAGE_SYNCED_KEY)
-}
+ /**
+ * Destroy the synced doctypes
+ *
+ * @returns {Promise}
+ */
+ async destroySyncedDoctypes() {
+ await this.storageEngine.removeItem(LOCALSTORAGE_SYNCED_KEY)
+ }
-/**
- * Persist the last CouchDB sequence for a synced doctype
- *
- * @param {string} doctype - The synced doctype
- * @param {string} sequence - The sequence hash
- */
-export const persistDoctypeLastSequence = (doctype, sequence) => {
- const seqs = getAllLastSequences()
- seqs[doctype] = sequence
+ /**
+ * Persist the last CouchDB sequence for a synced doctype
+ *
+ * @param {string} doctype - The synced doctype
+ * @param {string} sequence - The sequence hash
+ *
+ * @returns {Promise}
+ */
+ async persistDoctypeLastSequence(doctype, sequence) {
+ const seqs = await this.getAllLastSequences()
+ seqs[doctype] = sequence
+
+ await this.storageEngine.setItem(
+ LOCALSTORAGE_LASTSEQUENCES_KEY,
+ JSON.stringify(seqs)
+ )
+ }
- window.localStorage.setItem(
- LOCALSTORAGE_LASTSEQUENCES_KEY,
- JSON.stringify(seqs)
- )
-}
+ /**
+ * @returns {Promise}
+ */
+ async getAllLastSequences() {
+ const item = await this.storageEngine.getItem(
+ LOCALSTORAGE_LASTSEQUENCES_KEY
+ )
+ return item ? JSON.parse(item) : {}
+ }
-export const getAllLastSequences = () => {
- const item = window.localStorage.getItem(LOCALSTORAGE_LASTSEQUENCES_KEY)
- return item ? JSON.parse(item) : {}
-}
+ /**
+ * Get the last CouchDB sequence for a doctype
+ *
+ * @param {string} doctype - The doctype
+ *
+ * @returns {Promise} the last sequence
+ */
+ async getDoctypeLastSequence(doctype) {
+ const seqs = await this.getAllLastSequences()
+ return seqs[doctype]
+ }
-/**
- * Get the last CouchDB sequence for a doctype
- *
- * @param {string} doctype - The doctype
- * @returns {string} the last sequence
- */
-export const getDoctypeLastSequence = doctype => {
- const seqs = getAllLastSequences()
- return seqs[doctype]
-}
+ /**
+ * Destroy all the last sequence
+ *
+ * @returns {Promise}
+ */
+ async destroyAllDoctypeLastSequence() {
+ await this.storageEngine.removeItem(LOCALSTORAGE_LASTSEQUENCES_KEY)
+ }
-/**
- * Destroy all the last sequence
- */
-export const destroyAllDoctypeLastSequence = () => {
- window.localStorage.removeItem(LOCALSTORAGE_LASTSEQUENCES_KEY)
-}
+ /**
+ * Destroy the last sequence for a doctype
+ *
+ * @param {string} doctype - The doctype
+ *
+ * @returns {Promise}
+ */
+ async destroyDoctypeLastSequence(doctype) {
+ const seqs = await this.getAllLastSequences()
+ delete seqs[doctype]
+ await this.storageEngine.setItem(
+ LOCALSTORAGE_LASTSEQUENCES_KEY,
+ JSON.stringify(seqs)
+ )
+ }
-/**
- * Destroy the last sequence for a doctype
- *
- * @param {string} doctype - The doctype
- */
-export const destroyDoctypeLastSequence = doctype => {
- const seqs = getAllLastSequences()
- delete seqs[doctype]
- window.localStorage.setItem(
- LOCALSTORAGE_LASTSEQUENCES_KEY,
- JSON.stringify(seqs)
- )
-}
+ /**
+ * Persist the warmed up queries
+ *
+ * @param {object} warmedUpQueries - The warmedup queries
+ *
+ * @returns {Promise}
+ */
+ async persistWarmedUpQueries(warmedUpQueries) {
+ await this.storageEngine.setItem(
+ LOCALSTORAGE_WARMUPEDQUERIES_KEY,
+ JSON.stringify(warmedUpQueries)
+ )
+ }
-/**
- * Persist the warmed up queries
- *
- * @param {object} warmedUpQueries - The warmedup queries
- */
-export const persistWarmedUpQueries = warmedUpQueries => {
- window.localStorage.setItem(
- LOCALSTORAGE_WARMUPEDQUERIES_KEY,
- JSON.stringify(warmedUpQueries)
- )
-}
+ /**
+ * Get the warmed up queries
+ *
+ * @returns {Promise} the warmed up queries
+ */
+ async getPersistedWarmedUpQueries() {
+ const item = await this.storageEngine.getItem(
+ LOCALSTORAGE_WARMUPEDQUERIES_KEY
+ )
+ if (!item) {
+ return {}
+ }
+ return JSON.parse(item)
+ }
-/**
- * Get the warmed up queries
- *
- * @returns {object} the warmed up queries
- */
-export const getPersistedWarmedUpQueries = () => {
- const item = window.localStorage.getItem(LOCALSTORAGE_WARMUPEDQUERIES_KEY)
- if (!item) {
- return {}
+ /**
+ * Destroy the warmed queries
+ *
+ * @returns {Promise}
+ */
+ async destroyWarmedUpQueries() {
+ await this.storageEngine.removeItem(LOCALSTORAGE_WARMUPEDQUERIES_KEY)
}
- return JSON.parse(item)
-}
-/**
- * Destroy the warmed queries
- *
- */
-export const destroyWarmedUpQueries = () => {
- window.localStorage.removeItem(LOCALSTORAGE_WARMUPEDQUERIES_KEY)
-}
+ /**
+ * Get the adapter name
+ *
+ * @returns {Promise} The adapter name
+ */
+ async getAdapterName() {
+ return await this.storageEngine.getItem(LOCALSTORAGE_ADAPTERNAME)
+ }
-/**
- * Get the adapter name
- *
- * @returns {string} The adapter name
- */
-export const getAdapterName = () => {
- return window.localStorage.getItem(LOCALSTORAGE_ADAPTERNAME)
+ /**
+ * Persist the adapter name
+ *
+ * @param {string} adapter - The adapter name
+ *
+ * @returns {Promise}
+ */
+ async persistAdapterName(adapter) {
+ await this.storageEngine.setItem(LOCALSTORAGE_ADAPTERNAME, adapter)
+ }
}
/**
- * Persist the adapter name
+ * Throw if the given storage engine does not implement the expected Interface
*
- * @param {string} adapter - The adapter name
+ * @param {*} storageEngine - Object containing storage access methods
*/
-export const persistAdapterName = adapter => {
- window.localStorage.setItem(LOCALSTORAGE_ADAPTERNAME, adapter)
+const checkStorageEngine = storageEngine => {
+ const requiredMethods = ['setItem', 'getItem', 'removeItem']
+
+ const missingMethods = requiredMethods.filter(
+ requiredMethod => !storageEngine[requiredMethod]
+ )
+
+ if (missingMethods.length > 0) {
+ const missingMethodsString = missingMethods.join(', ')
+ throw new Error(
+ `Provided storageEngine is missing the following methods: ${missingMethodsString}`
+ )
+ }
}
diff --git a/packages/cozy-pouch-link/src/localStorage.spec.js b/packages/cozy-pouch-link/src/localStorage.spec.js
new file mode 100644
index 0000000000..7d7c4ee225
--- /dev/null
+++ b/packages/cozy-pouch-link/src/localStorage.spec.js
@@ -0,0 +1,48 @@
+import { PouchLocalStorage } from './localStorage'
+
+describe('LocalStorage', () => {
+ describe('Type assertion', () => {
+ it('should throw if setItem method is missing', () => {
+ expect(() => {
+ new PouchLocalStorage({
+ getItem: jest.fn(),
+ removeItem: jest.fn()
+ })
+ }).toThrow(
+ 'Provided storageEngine is missing the following methods: setItem'
+ )
+ })
+
+ it('should throw if getItem method is missing', () => {
+ expect(() => {
+ new PouchLocalStorage({
+ setItem: jest.fn(),
+ removeItem: jest.fn()
+ })
+ }).toThrow(
+ 'Provided storageEngine is missing the following methods: getItem'
+ )
+ })
+
+ it('should throw if removeItem method is missing', () => {
+ expect(() => {
+ new PouchLocalStorage({
+ getItem: jest.fn(),
+ setItem: jest.fn()
+ })
+ }).toThrow(
+ 'Provided storageEngine is missing the following methods: removeItem'
+ )
+ })
+
+ it('should throw if multiple methods are missing', () => {
+ expect(() => {
+ new PouchLocalStorage({
+ getItem: jest.fn()
+ })
+ }).toThrow(
+ 'Provided storageEngine is missing the following methods: setItem, removeItem'
+ )
+ })
+ })
+})
diff --git a/packages/cozy-pouch-link/src/mango.js b/packages/cozy-pouch-link/src/mango.js
index 046a5f2971..e38fc7cb21 100644
--- a/packages/cozy-pouch-link/src/mango.js
+++ b/packages/cozy-pouch-link/src/mango.js
@@ -1,16 +1,60 @@
-import flatten from 'lodash/flatten'
-import isObject from 'lodash/isObject'
+import head from 'lodash/head'
-export const getIndexNameFromFields = fields => {
- return `by_${fields.join('_and_')}`
+/**
+ * Process a partial filter to generate a string key
+ *
+ * /!\ Warning: this method is similar to cozy-stack-client mangoIndex.makeKeyFromPartialFilter()
+ * If you edit this method, please check if the change is also needed in mangoIndex file
+ *
+ * @param {object} condition - An object representing the partial filter or a sub-condition of the partial filter
+ * @returns {string} - The string key of the processed partial filter
+ */
+export const makeKeyFromPartialFilter = condition => {
+ if (typeof condition !== 'object' || condition === null) {
+ return String(condition)
+ }
+
+ const conditions = Object.entries(condition).map(([key, value]) => {
+ if (
+ Array.isArray(value) &&
+ value.every(subObj => typeof subObj === 'string')
+ ) {
+ return `${key}_(${value.join('_')})`
+ } else if (Array.isArray(value)) {
+ return `(${value
+ .map(subCondition => `${makeKeyFromPartialFilter(subCondition)}`)
+ .join(`)_${key}_(`)})`
+ } else if (typeof value === 'object') {
+ return `${key}_${makeKeyFromPartialFilter(value)}`
+ } else {
+ return `${key}_${value}`
+ }
+ })
+
+ return conditions.join(')_and_(')
}
-const getSortKeys = sort => {
- if (Array.isArray(sort)) {
- return flatten(sort.map(x => Object.keys(x)))
- } else if (isObject(sort)) {
- return Object.keys(sort)
+/**
+ * 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.key1}_{partial_filter.value1})_and_({partial_filter.key2}_{partial_filter.value2})`
+ *
+ * /!\ Warning: this method is similar to cozy-stack-client mangoIndex.getIndexNameFromFields()
+ * If you edit this method, please check if the change is also needed in mangoIndex file
+ *
+ * @param {Array} fields - The indexed fields
+ * @param {object} [partialFilter] - The partial filter
+ * @returns {string} The index name, built from the fields
+ */
+export const getIndexNameFromFields = (fields, partialFilter) => {
+ const indexName = `by_${fields.join('_and_')}`
+
+ if (partialFilter) {
+ return `${indexName}_filter_(${makeKeyFromPartialFilter(partialFilter)})`
}
+
+ return indexName
}
/**
@@ -19,10 +63,22 @@ const getSortKeys = sort => {
* query to work
*
* @private
- * @param {object} options - Mango query options
+ * @param {import('./types').MangoQueryOptions} options - Mango query options
* @returns {Array} - Fields to index
*/
const defaultSelector = { _id: { $gt: null } }
-export const getIndexFields = ({ selector = defaultSelector, sort = {} }) => {
- return Array.from(new Set([...Object.keys(selector), ...getSortKeys(sort)]))
+export const getIndexFields = (
+ /** @type {import('./types').MangoQueryOptions} */ {
+ selector = defaultSelector,
+ sort = [],
+ partialFilter
+ }
+) => {
+ return Array.from(
+ new Set([
+ ...sort.map(sortOption => head(Object.keys(sortOption))),
+ ...(selector ? Object.keys(selector) : []),
+ ...(partialFilter ? Object.keys(partialFilter) : [])
+ ])
+ )
}
diff --git a/packages/cozy-pouch-link/src/mango.spec.js b/packages/cozy-pouch-link/src/mango.spec.js
index 86f778f919..8579ff1010 100644
--- a/packages/cozy-pouch-link/src/mango.spec.js
+++ b/packages/cozy-pouch-link/src/mango.spec.js
@@ -6,6 +6,6 @@ describe('mango utils', () => {
it('should be able to get the fields from the selector', () => {
const query = Q('io.cozy.rockets').sortBy([{ label: true }, { _id: true }])
const fields = getIndexFields(query)
- expect(fields).toEqual(['_id', 'label'])
+ expect(fields).toEqual(['label', '_id'])
})
})
diff --git a/packages/cozy-pouch-link/src/migrations/adapter.js b/packages/cozy-pouch-link/src/migrations/adapter.js
index 491f36cc7f..4d2a95d16e 100644
--- a/packages/cozy-pouch-link/src/migrations/adapter.js
+++ b/packages/cozy-pouch-link/src/migrations/adapter.js
@@ -13,7 +13,7 @@ const getNewIndexedDBDatabaseName = dbName => {
* @property {string} [toAdapter] - The new adapter type, e.g. 'indexeddb'
*
* @param {MigrationParams} params - The migration params
- * @returns {object} - The migrated pouch
+ * @returns {Promise} - The migrated pouch
*/
export const migratePouch = async ({ dbName, fromAdapter, toAdapter }) => {
let oldPouch = new PouchDB(dbName, {
diff --git a/packages/cozy-pouch-link/src/platformWeb.js b/packages/cozy-pouch-link/src/platformWeb.js
new file mode 100644
index 0000000000..9f96d78c38
--- /dev/null
+++ b/packages/cozy-pouch-link/src/platformWeb.js
@@ -0,0 +1,33 @@
+import PouchDB from 'pouchdb-browser'
+
+const events = {
+ addEventListener: (eventName, handler) => {
+ document.addEventListener(eventName, handler)
+ },
+ removeEventListener: (eventName, handler) => {
+ document.removeEventListener(eventName, handler)
+ }
+}
+
+const storage = {
+ getItem: async key => {
+ return window.localStorage.getItem(key)
+ },
+ setItem: async (key, value) => {
+ return window.localStorage.setItem(key, value)
+ },
+ removeItem: async key => {
+ return window.localStorage.removeItem(key)
+ }
+}
+
+const isOnline = async () => {
+ return window.navigator.onLine
+}
+
+export const platformWeb = {
+ storage,
+ events,
+ pouchAdapter: PouchDB,
+ isOnline
+}
diff --git a/packages/cozy-pouch-link/src/remote.js b/packages/cozy-pouch-link/src/remote.js
index b4995239c8..2bb52fd712 100644
--- a/packages/cozy-pouch-link/src/remote.js
+++ b/packages/cozy-pouch-link/src/remote.js
@@ -1,11 +1,22 @@
import AccessToken from './AccessToken'
+export const DATABASE_NOT_FOUND_ERROR = 'Database does not exist'
+export const DATABASE_RESERVED_DOCTYPE_ERROR = 'Reserved doctype'
+
+export const isDatabaseNotFoundError = error => {
+ return error.message === DATABASE_NOT_FOUND_ERROR
+}
+
+export const isDatabaseUnradableError = error => {
+ return error.message === DATABASE_RESERVED_DOCTYPE_ERROR
+}
+
/**
* Fetch remote instance
*
* @param {URL} url - The remote instance URL, including the credentials
* @param {object} params - The params to query the remote instance
- * @returns {object} The instance response
+ * @returns {Promise} The instance response
*/
export const fetchRemoteInstance = async (url, params = {}) => {
const access = new AccessToken({ accessToken: url.password })
@@ -25,14 +36,27 @@ export const fetchRemoteInstance = async (url, params = {}) => {
if (resp.ok) {
return data
}
- return null
+
+ if (resp.status === 404) {
+ throw new Error(DATABASE_NOT_FOUND_ERROR)
+ }
+
+ if (resp.status === 403 && data.error.includes('message=reserved doctype')) {
+ throw new Error(DATABASE_RESERVED_DOCTYPE_ERROR)
+ }
+
+ throw new Error(
+ `Error (${resp.status}) while fetching remote instance: ${JSON.stringify(
+ data
+ )}`
+ )
}
/**
* Fetch last sequence from remote instance
*
* @param {string} baseUrl - The base URL of the remote instance
- * @returns {string} The last sequence
+ * @returns {Promise} The last sequence
*/
export const fetchRemoteLastSequence = async baseUrl => {
const remoteUrl = new URL(`${baseUrl}/_changes`)
diff --git a/packages/cozy-pouch-link/src/remote.spec.js b/packages/cozy-pouch-link/src/remote.spec.js
new file mode 100644
index 0000000000..bb59000e80
--- /dev/null
+++ b/packages/cozy-pouch-link/src/remote.spec.js
@@ -0,0 +1,214 @@
+import { enableFetchMocks, disableFetchMocks } from 'jest-fetch-mock'
+
+import {
+ DATABASE_NOT_FOUND_ERROR,
+ DATABASE_RESERVED_DOCTYPE_ERROR,
+ fetchRemoteInstance,
+ fetchRemoteLastSequence
+} from './remote'
+
+describe('remote', () => {
+ beforeAll(() => {
+ enableFetchMocks()
+ })
+
+ beforeEach(() => {
+ fetch.resetMocks()
+ })
+
+ afterAll(() => {
+ disableFetchMocks()
+ })
+
+ describe('fetchRemoteInstance', () => {
+ it(`Should add Authorization header based on URL's password`, async () => {
+ const remoteUrl =
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts/_changes'
+
+ mockDatabaseOn(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes'
+ )
+
+ await fetchRemoteInstance(new URL(remoteUrl))
+
+ const expectedHeaders = new Headers()
+ expectedHeaders.append('Accept', 'application/json')
+ expectedHeaders.append('Content-Type', 'application/json')
+ expectedHeaders.append('Authorization', 'Bearer SOME_TOKEN')
+
+ expect(fetch).toHaveBeenCalledWith(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes',
+ {
+ headers: expectedHeaders
+ }
+ )
+ })
+
+ it('Should return data when found', async () => {
+ const remoteUrl =
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
+ mockDatabaseOn(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
+ )
+
+ const result = await fetchRemoteInstance(new URL(remoteUrl))
+
+ expect(result).toStrictEqual({
+ last_seq: '97-SOME_SEQ_VALUE',
+ pending: -1,
+ results: [
+ {
+ id: 'SOME_ID',
+ seq: '97-SOME_SEQ_VALUE',
+ doc: null,
+ changes: [{ rev: '3-SOME_REV' }]
+ }
+ ]
+ })
+ })
+
+ it('Should add parameters when given', async () => {
+ const remoteUrl =
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts/_changes'
+
+ mockDatabaseOn(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
+ )
+
+ await fetchRemoteInstance(new URL(remoteUrl), {
+ limit: 1,
+ descending: true
+ })
+
+ const expectedHeaders = new Headers()
+ expectedHeaders.append('Accept', 'application/json')
+ expectedHeaders.append('Content-Type', 'application/json')
+ expectedHeaders.append('Authorization', 'Bearer SOME_TOKEN')
+
+ expect(fetch).toHaveBeenCalledWith(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true',
+ expect.anything()
+ )
+ })
+
+ it('Should throw when 404 error', async () => {
+ const remoteUrl =
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
+
+ mockDatabaseNotFoundOn(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
+ )
+
+ await expect(fetchRemoteInstance(new URL(remoteUrl))).rejects.toThrow(
+ DATABASE_NOT_FOUND_ERROR
+ )
+ })
+ })
+
+ describe('fetchRemoteLastSequence', () => {
+ it('Should return data when found', async () => {
+ const remoteUrl =
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts'
+ mockDatabaseOn(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
+ )
+
+ const result = await fetchRemoteLastSequence(remoteUrl)
+
+ expect(result).toBe('97-SOME_SEQ_VALUE')
+ })
+
+ it('Shoud throw when HTTP error', async () => {
+ const remoteUrl =
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts'
+ mockUnknownErrorOn(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
+ )
+
+ await expect(fetchRemoteLastSequence(remoteUrl)).rejects.toThrow(
+ 'Error (503) while fetching remote instance: {"error":"code=503, message=SOME UNKNOWN ERROR"}'
+ )
+ })
+
+ it('Shoud throw dedicated error when 404 error', async () => {
+ const remoteUrl =
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts'
+ mockDatabaseNotFoundOn(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
+ )
+
+ await expect(fetchRemoteLastSequence(remoteUrl)).rejects.toThrow(
+ DATABASE_NOT_FOUND_ERROR
+ )
+ })
+
+ it('Shoud throw dedicated error when Reserved Doctype error', async () => {
+ const remoteUrl =
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.accounts'
+ mockDatabaseReservedDoctypeOn(
+ 'https://claude.mycozy.cloud/data/io.cozy.accounts/_changes?limit=1&descending=true'
+ )
+
+ await expect(fetchRemoteLastSequence(remoteUrl)).rejects.toThrow(
+ DATABASE_RESERVED_DOCTYPE_ERROR
+ )
+ })
+ })
+})
+
+const mockDatabaseNotFoundOn = url => {
+ fetch.mockOnceIf(url, JSON.stringify({}), {
+ error: 'not_found',
+ ok: false,
+ reason: 'Database does not exist.',
+ status: 404
+ })
+}
+
+const mockDatabaseReservedDoctypeOn = url => {
+ fetch.mockOnceIf(
+ url,
+ JSON.stringify({
+ error: 'code=403, message=reserved doctype io.cozy.sharings unreadable'
+ }),
+ {
+ ok: false,
+ status: 403
+ }
+ )
+}
+
+const mockUnknownErrorOn = url => {
+ fetch.mockOnceIf(
+ url,
+ JSON.stringify({
+ error: 'code=503, message=SOME UNKNOWN ERROR'
+ }),
+ {
+ ok: false,
+ status: 503
+ }
+ )
+}
+
+const mockDatabaseOn = url => {
+ fetch.mockOnceIf(
+ url,
+ JSON.stringify({
+ last_seq: '97-SOME_SEQ_VALUE',
+ pending: -1,
+ results: [
+ {
+ id: 'SOME_ID',
+ seq: '97-SOME_SEQ_VALUE',
+ doc: null,
+ changes: [{ rev: '3-SOME_REV' }]
+ }
+ ]
+ }),
+ {
+ ok: true,
+ status: 200
+ }
+ )
+}
diff --git a/packages/cozy-pouch-link/src/replicateOnce.js b/packages/cozy-pouch-link/src/replicateOnce.js
new file mode 100644
index 0000000000..ab85037d32
--- /dev/null
+++ b/packages/cozy-pouch-link/src/replicateOnce.js
@@ -0,0 +1,203 @@
+import fromPairs from 'lodash/fromPairs'
+import get from 'lodash/get'
+import map from 'lodash/map'
+import startsWith from 'lodash/startsWith'
+import zip from 'lodash/zip'
+
+import logger from './logger'
+import {
+ fetchRemoteLastSequence,
+ isDatabaseNotFoundError,
+ isDatabaseUnradableError
+} from './remote'
+import { startReplication } from './startReplication'
+
+/**
+ * Process replication once for given PouchManager
+ *
+ * @param {import('./PouchManager').default} pouchManager - PouchManager that handle the replication
+ * @returns {Promise} res
+ */
+export const replicateOnce = async pouchManager => {
+ if (!(await pouchManager.isOnline())) {
+ logger.info(
+ 'PouchManager: The device is offline so the replication has been skipped'
+ )
+ return Promise.resolve()
+ }
+
+ logger.info('PouchManager: Starting replication iteration')
+
+ // Creating each replication
+ pouchManager.replications = map(
+ pouchManager.pouches,
+ async (pouch, doctype) => {
+ logger.info('PouchManager: Starting replication for ' + doctype)
+
+ const getReplicationURL = () => pouchManager.getReplicationURL(doctype)
+
+ const initialReplication =
+ pouchManager.getSyncStatus(doctype) !== 'synced'
+ const replicationFilter = doc => {
+ return !startsWith(doc._id, '_design')
+ }
+ let seq = ''
+ if (initialReplication) {
+ // Before the first replication, get the last remote sequence,
+ // which will be used as a checkpoint for the next replication
+ const lastSeq = await fetchRemoteLastSequence(getReplicationURL())
+ await pouchManager.storage.persistDoctypeLastSequence(doctype, lastSeq)
+ } else {
+ seq = await pouchManager.storage.getDoctypeLastSequence(doctype)
+ }
+
+ const replicationOptions = get(
+ pouchManager.doctypesReplicationOptions,
+ doctype,
+ {}
+ )
+ replicationOptions.initialReplication = initialReplication
+ replicationOptions.filter = replicationFilter
+ replicationOptions.since = seq
+ replicationOptions.doctype = doctype
+
+ if (pouchManager.options.onDoctypeSyncStart) {
+ pouchManager.options.onDoctypeSyncStart(doctype)
+ }
+ const res = await startReplication(
+ pouch,
+ replicationOptions,
+ getReplicationURL,
+ pouchManager.storage
+ )
+ if (seq) {
+ // We only need the sequence for the second replication, as PouchDB
+ // will use a local checkpoint for the next runs.
+ await pouchManager.storage.destroyDoctypeLastSequence(doctype)
+ }
+
+ await pouchManager.updateSyncInfo(doctype)
+ pouchManager.checkToWarmupDoctype(doctype, replicationOptions)
+ if (pouchManager.options.onDoctypeSyncEnd) {
+ pouchManager.options.onDoctypeSyncEnd(doctype)
+ }
+ return res
+ }
+ )
+
+ // Waiting on each replication
+ const doctypes = Object.keys(pouchManager.pouches)
+ const promises = Object.values(pouchManager.replications)
+ try {
+ const res = await allSettled(promises)
+
+ if (process.env.NODE_ENV !== 'production') {
+ logger.info('PouchManager: Replication ended')
+ }
+
+ if (pouchManager.options.onSync) {
+ const zippedDoctypes = zip(doctypes, res)
+ const successZippedDoctypes = zippedDoctypes
+ .filter(d => d[1].status === 'fulfilled')
+ .map(d => {
+ return [d[0], d[1].value]
+ })
+ const failedZippedDoctypes = zippedDoctypes
+ .filter(d => d[1].status === 'rejected')
+ .map(d => {
+ return [d[0], d[1].reason]
+ })
+
+ const blockingErrors = res.filter(
+ r =>
+ r.status === 'rejected' &&
+ !isDatabaseNotFoundError(r.reason) &&
+ !isDatabaseUnradableError(r.reason)
+ )
+
+ const unblockingErrors = failedZippedDoctypes.filter(
+ r => isDatabaseNotFoundError(r[1]) || isDatabaseUnradableError(r[1])
+ )
+
+ for (const unblockingError of unblockingErrors) {
+ const doctype = unblockingError[0]
+ // @ts-ignore
+ await pouchManager.updateSyncInfo(doctype, 'not_complete')
+ }
+
+ if (blockingErrors.length > 0) {
+ const errors = blockingErrors.map(err => err.reason)
+ const reasons = errors.join('\n')
+ logger.debug(
+ `ReplicateOnce's promises failed with the following errors`,
+ reasons
+ )
+ // @ts-ignore
+ // eslint-disable-next-line no-undef
+ throw new AggregateError(errors, 'Failed with blocking errors')
+ } else {
+ logger.debug(`ReplicateOnce's promises succeed with no blocking errors`)
+ }
+
+ const doctypeUpdated = fromPairs(successZippedDoctypes)
+ const doctypeFailed = fromPairs(failedZippedDoctypes)
+ logger.debug(
+ 'Doctypes replications in error: ',
+ Object.keys(doctypeFailed)
+ )
+ logger.debug(
+ 'Doctypes replications in success: ',
+ Object.keys(doctypeUpdated)
+ )
+ pouchManager.options.onSync(doctypeUpdated)
+ }
+
+ // @ts-ignore
+ res.cancel = pouchManager.cancelCurrentReplications
+
+ return res
+ } catch (err) {
+ pouchManager.handleReplicationError(err)
+ }
+}
+
+/**
+ * @typedef {object} FulfilledPromise
+ * @property {'fulfilled'} status - The status of the promise
+ * @property {undefined} reason - The Error rejected by the promise (undefined when fulfilled)
+ * @property {any} value - The resolved value of the promise
+ */
+
+/**
+ * @typedef {object} RejectedPromise
+ * @property {'rejected'} status - The status of the promise
+ * @property {Error} reason - The Error rejected by the promise
+ * @property {undefined} value - The resolved value of the promise (undefined when rejected)
+ */
+
+/**
+ * Takes an iterable of promises as input and returns a single Promise.
+ * This returned promise fulfills when all of the input's promises settle (including
+ * when an empty iterable is passed), with an array of objects that describe the
+ * outcome of each promise.
+ *
+ * @param {Promise[]} promises - Promise to be awaited
+ * @returns {Promise<(FulfilledPromise|RejectedPromise)[]>}
+ */
+const allSettled = promises => {
+ return Promise.all(
+ promises.map(promise =>
+ promise
+ .then(value => /** @type {FulfilledPromise} */ ({
+ status: 'fulfilled',
+ value
+ }))
+ .catch((
+ /** @type {Error} */ reason
+ ) => /** @type {RejectedPromise} */ ({
+ status: 'rejected',
+ reason
+ }))
+ )
+ )
+}
diff --git a/packages/cozy-pouch-link/src/startReplication.js b/packages/cozy-pouch-link/src/startReplication.js
index 7867cf83cb..88731b8a40 100644
--- a/packages/cozy-pouch-link/src/startReplication.js
+++ b/packages/cozy-pouch-link/src/startReplication.js
@@ -2,10 +2,7 @@ import { default as helpers } from './helpers'
import startsWith from 'lodash/startsWith'
import logger from './logger'
import { fetchRemoteInstance } from './remote'
-import {
- getLastReplicatedDocID,
- persistLastReplicatedDocID
-} from './localStorage'
+
const { isDesignDocument, isDeletedDocument } = helpers
const BATCH_SIZE = 1000 // we have mostly small documents
@@ -26,6 +23,7 @@ const humanTimeDelta = timeMs => {
str = `${cur}${lastUnit[0]}` + str
return str
}
+/** @type {[string, number][]} */
const TIME_UNITS = [['ms', 1000], ['s', 60], ['m', 60], ['h', 24]]
/**
@@ -37,18 +35,22 @@ const TIME_UNITS = [['ms', 1000], ['s', 60], ['m', 60], ['h', 24]]
* @param {string} replicationOptions.strategy The direction of the replication. Can be "fromRemote", "toRemote" or "sync"
* @param {boolean} replicationOptions.initialReplication Whether or not this is an initial replication
* @param {string} replicationOptions.doctype The doctype to replicate
+ * @param {import('cozy-client/types/types').Query[]} replicationOptions.warmupQueries The queries to warmup
* @param {Function} getReplicationURL A function that should return the remote replication URL
+ * @param {import('./localStorage').PouchLocalStorage} storage Methods to access local storage
*
- * @returns {Promise} A cancelable promise that resolves at the end of the replication
+ * @returns {import('./types').CancelablePromise} A cancelable promise that resolves at the end of the replication
*/
export const startReplication = (
pouch,
replicationOptions,
- getReplicationURL
+ getReplicationURL,
+ storage
) => {
let replication
let docs = {}
const start = new Date()
+ /** @type {import('./types').CancelablePromise} */
const promise = new Promise((resolve, reject) => {
const url = getReplicationURL()
const {
@@ -60,25 +62,36 @@ export const startReplication = (
} = replicationOptions
const options = {
batch_size: BATCH_SIZE,
- ...customReplicationOptions
+ ...customReplicationOptions,
+ selector: {
+ cozyLocalOnly: {
+ $exists: false
+ }
+ }
}
- let replication
+
if (initialReplication && strategy !== 'toRemote') {
;(async () => {
// For the first remote->local replication, we manually replicate all docs
// as it avoids to replicate all revs history, which can lead to
// performances issues
- docs = await replicateAllDocs(pouch, url, doctype)
+ docs = await replicateAllDocs({
+ db: pouch,
+ baseUrl: url,
+ doctype,
+ storage
+ })
const end = new Date()
if (process.env.NODE_ENV !== 'production') {
logger.info(
`PouchManager: initial replication with all_docs for ${url} took ${humanTimeDelta(
- end - start
+ end.getTime() - start.getTime()
)}`
)
}
return resolve(docs)
})()
+ return
}
if (strategy === 'fromRemote') {
replication = pouch.replicate.from(url, options)
@@ -108,7 +121,7 @@ export const startReplication = (
if (process.env.NODE_ENV !== 'production') {
logger.info(
`PouchManager: replication for ${url} took ${humanTimeDelta(
- end - start
+ end.getTime() - start.getTime()
)}`
)
}
@@ -138,16 +151,18 @@ const filterDocs = docs => {
* Note it saves the last replicated _id for each run and
* starts from there in case the process stops before the end.
*
- * @param {object} db - Pouch instance
- * @param {string} baseUrl - The remote instance
- * @param {string} doctype - The doctype to replicate
- * @returns {Array} The retrieved documents
+ * @param {object} params - The replications parameters
+ * @param {object} params.db - Pouch instance
+ * @param {string} params.baseUrl - The remote instance
+ * @param {string} params.doctype - The doctype to replicate
+ * @param {import('./localStorage').PouchLocalStorage} params.storage - Methods to access local storage
+ * @returns {Promise} The retrieved documents
*/
-export const replicateAllDocs = async (db, baseUrl, doctype) => {
+export const replicateAllDocs = async ({ db, baseUrl, doctype, storage }) => {
const remoteUrlAllDocs = new URL(`${baseUrl}/_all_docs`)
const batchSize = BATCH_SIZE
let hasMore = true
- let startDocId = getLastReplicatedDocID(doctype) // Get last replicated _id in localStorage
+ let startDocId = await storage.getLastReplicatedDocID(doctype) // Get last replicated _id in localStorage
let docs = []
while (hasMore) {
@@ -166,7 +181,7 @@ export const replicateAllDocs = async (db, baseUrl, doctype) => {
hasMore = false
}
await helpers.insertBulkDocs(db, docs)
- persistLastReplicatedDocID(doctype, startDocId)
+ await storage.persistLastReplicatedDocID(doctype, startDocId)
}
} else {
const res = await fetchRemoteInstance(remoteUrlAllDocs, {
@@ -181,7 +196,7 @@ export const replicateAllDocs = async (db, baseUrl, doctype) => {
filteredDocs.shift() // Remove first element, already included in previous request
startDocId = filteredDocs[filteredDocs.length - 1]._id
await helpers.insertBulkDocs(db, filteredDocs)
- persistLastReplicatedDocID(doctype, startDocId)
+ await storage.persistLastReplicatedDocID(doctype, startDocId)
docs = docs.concat(filteredDocs)
if (res.rows.length < batchSize) {
diff --git a/packages/cozy-pouch-link/src/startReplication.spec.js b/packages/cozy-pouch-link/src/startReplication.spec.js
index 41acd9a503..e6bc72a7d5 100644
--- a/packages/cozy-pouch-link/src/startReplication.spec.js
+++ b/packages/cozy-pouch-link/src/startReplication.spec.js
@@ -1,12 +1,8 @@
+import MicroEE from 'microee'
import { fetchRemoteLastSequence, fetchRemoteInstance } from './remote'
-import { getLastReplicatedDocID } from './localStorage'
-import { replicateAllDocs } from './startReplication'
-
-jest.mock('./localStorage', () => ({
- getLastReplicatedDocID: jest.fn(),
- persistLastReplicatedDocID: jest.fn()
-}))
+import { replicateAllDocs, startReplication } from './startReplication'
+import { insertBulkDocs } from './helpers'
jest.mock('./remote', () => ({
fetchRemoteLastSequence: jest.fn(),
@@ -14,6 +10,7 @@ jest.mock('./remote', () => ({
}))
jest.mock('./helpers', () => ({
+ ...jest.requireActual('./helpers').default,
insertBulkDocs: jest.fn()
}))
@@ -27,49 +24,387 @@ const generateDocs = nDocs => {
return docs
}
-describe('replication through _all_docs', () => {
+const storage = {
+ getLastReplicatedDocID: jest.fn(),
+ persistLastReplicatedDocID: jest.fn()
+}
+
+function ReplicationOnMock() {}
+MicroEE.mixin(ReplicationOnMock)
+const mockReplicationOn = new ReplicationOnMock()
+mockReplicationOn.cancel = () => {
+ mockReplicationOn.emit('complete')
+}
+
+describe('startReplication', () => {
beforeEach(() => {
+ jest.resetAllMocks()
fetchRemoteLastSequence.mockResolvedValue('10-xyz')
})
- it('should replicate all docs', async () => {
- getLastReplicatedDocID.mockReturnValue(null)
- const dummyDocs = generateDocs(2)
- fetchRemoteInstance.mockResolvedValue({ rows: dummyDocs })
-
- const rep = await replicateAllDocs(null, url)
- const expectedDocs = dummyDocs.map(doc => doc.doc)
- expect(rep).toEqual(expectedDocs)
- })
- it('should replicate all docs when it gets more docs than the batch limit', async () => {
- getLastReplicatedDocID.mockReturnValue(null)
- const dummyDocs = generateDocs(1002)
- fetchRemoteInstance.mockResolvedValueOnce({
- rows: dummyDocs.slice(0, 1001)
+ describe('replication through _all_docs', () => {
+ it('should replicate all docs', async () => {
+ storage.getLastReplicatedDocID.mockReturnValue(null)
+ const dummyDocs = generateDocs(2)
+ fetchRemoteInstance.mockResolvedValue({ rows: dummyDocs })
+
+ const rep = await replicateAllDocs({
+ db: null,
+ baseUrl: url,
+ doctype: undefined,
+ storage
+ })
+ const expectedDocs = dummyDocs.map(doc => doc.doc)
+ expect(rep).toEqual(expectedDocs)
+ expect(fetchRemoteInstance).toHaveBeenCalledTimes(1)
+ expect(insertBulkDocs).toHaveBeenCalledTimes(1)
})
- fetchRemoteInstance.mockResolvedValueOnce({
- rows: dummyDocs.slice(1000, 1002)
+
+ it('should replicate all docs when it gets more docs than the batch limit', async () => {
+ storage.getLastReplicatedDocID.mockReturnValue(null)
+ const dummyDocs = generateDocs(1002)
+ fetchRemoteInstance.mockResolvedValueOnce({
+ rows: dummyDocs.slice(0, 1001)
+ })
+ fetchRemoteInstance.mockResolvedValueOnce({
+ rows: dummyDocs.slice(1000, 1002)
+ })
+
+ const rep = await replicateAllDocs({
+ db: null,
+ baseUrl: url,
+ doctype: undefined,
+ storage
+ })
+ const expectedDocs = dummyDocs.map(doc => doc.doc)
+ expect(rep).toEqual(expectedDocs)
+ expect(fetchRemoteInstance).toHaveBeenCalledTimes(2)
+ expect(insertBulkDocs).toHaveBeenCalledTimes(2)
})
- const rep = await replicateAllDocs(null, url)
- const expectedDocs = dummyDocs.map(doc => doc.doc)
- expect(rep).toEqual(expectedDocs)
+ it('should replicate from the last saved doc id', async () => {
+ storage.getLastReplicatedDocID.mockReturnValue('5')
+ const dummyDocs = generateDocs(10)
+ fetchRemoteInstance.mockResolvedValue({ rows: dummyDocs.slice(5, 11) })
+
+ const rep = await replicateAllDocs({
+ db: null,
+ baseUrl: url,
+ doctype: undefined,
+ storage
+ })
+
+ const calledUrl = new URL(`${url}/_all_docs`)
+ expect(fetchRemoteInstance).toHaveBeenCalledWith(calledUrl, {
+ include_docs: true,
+ limit: 1000,
+ startkey_docid: '5'
+ })
+ expect(fetchRemoteInstance).toHaveBeenCalledTimes(1)
+ expect(insertBulkDocs).toHaveBeenCalledTimes(1)
+ const expectedDocs = dummyDocs.map(doc => doc.doc).slice(6, 11)
+ expect(rep).toEqual(expectedDocs)
+ })
})
- it('should replicate from the last saved doc id', async () => {
- getLastReplicatedDocID.mockReturnValue('5')
- const dummyDocs = generateDocs(10)
- fetchRemoteInstance.mockResolvedValue({ rows: dummyDocs.slice(5, 11) })
+ describe('startReplication', () => {
+ it('should call replicateAllDocs on initial replication', async () => {
+ const replicationOptions = getReplicationOptionsMock()
+ replicationOptions.initialReplication = true
+
+ const getReplicationURL = () =>
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files'
- const rep = await replicateAllDocs(null, url)
+ const dummyDocs = generateDocs(2)
+ fetchRemoteInstance.mockResolvedValue({ rows: dummyDocs })
- const calledUrl = new URL(`${url}/_all_docs`)
- expect(fetchRemoteInstance).toHaveBeenCalledWith(calledUrl, {
- include_docs: true,
- limit: 1000,
- startkey_docid: '5'
+ const pouch = getPouchMock()
+
+ await startReplication(
+ pouch,
+ replicationOptions,
+ getReplicationURL,
+ storage
+ )
+
+ expect(fetchRemoteInstance).toHaveBeenCalledWith(
+ new URL(
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files/_all_docs'
+ ),
+ { include_docs: true, limit: 1000 }
+ )
+ expect(pouch.replicate.from).not.toHaveBeenCalled()
+ expect(pouch.replicate.to).not.toHaveBeenCalled()
+ expect(pouch.sync).not.toHaveBeenCalled()
+ })
+
+ it('should call Pouch replication on non-initial replications', async () => {
+ const replicationOptions = getReplicationOptionsMock()
+ replicationOptions.initialReplication = false
+
+ const getReplicationURL = () =>
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files'
+
+ const pouch = getPouchMock()
+
+ const promise = startReplication(
+ pouch,
+ replicationOptions,
+ getReplicationURL,
+ storage
+ )
+ mockReplicationOn.emit('complete')
+ await promise
+
+ expect(fetchRemoteInstance).not.toHaveBeenCalled()
+ expect(pouch.replicate.to).not.toHaveBeenCalled()
+ expect(pouch.sync).not.toHaveBeenCalled()
+ expect(pouch.replicate.from).toHaveBeenCalledWith(
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files',
+ { batch_size: 1000, selector: { cozyLocalOnly: { $exists: false } } }
+ )
+ })
+
+ it(`should call Pouch replication on initial replications AND strategy is 'toRemote'`, async () => {
+ const replicationOptions = getReplicationOptionsMock()
+ replicationOptions.initialReplication = true
+ replicationOptions.strategy = 'toRemote'
+
+ const getReplicationURL = () =>
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files'
+
+ const pouch = getPouchMock()
+
+ const promise = startReplication(
+ pouch,
+ replicationOptions,
+ getReplicationURL,
+ storage
+ )
+ mockReplicationOn.emit('complete')
+ await promise
+
+ expect(fetchRemoteInstance).not.toHaveBeenCalled()
+ expect(pouch.replicate.from).not.toHaveBeenCalled()
+ expect(pouch.sync).not.toHaveBeenCalled()
+ expect(pouch.replicate.to).toHaveBeenCalledWith(
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files',
+ { batch_size: 1000, selector: { cozyLocalOnly: { $exists: false } } }
+ )
+ })
+
+ it(`should handle error result when Pouch replication`, async () => {
+ const replicationOptions = getReplicationOptionsMock()
+ replicationOptions.initialReplication = false
+
+ const getReplicationURL = () =>
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files'
+
+ const pouch = getPouchMock()
+
+ const promise = startReplication(
+ pouch,
+ replicationOptions,
+ getReplicationURL,
+ storage
+ )
+ mockReplicationOn.emit('error', 'some_error_message')
+ await expect(promise).rejects.toEqual('some_error_message')
+ })
+
+ it(`should handle change event with Sync format and Replication format when Pouch replication`, async () => {
+ const replicationOptions = getReplicationOptionsMock()
+ replicationOptions.initialReplication = false
+
+ const getReplicationURL = () =>
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files'
+
+ const pouch = getPouchMock()
+
+ const promise = startReplication(
+ pouch,
+ replicationOptions,
+ getReplicationURL,
+ storage
+ )
+ // Sync format
+ mockReplicationOn.emit('change', {
+ change: {
+ docs: [
+ {
+ _id: 'SOME_DOCUMENT_ID_1',
+ some_property: 'some_value'
+ }
+ ]
+ }
+ })
+ // Replicaiton format
+ mockReplicationOn.emit('change', {
+ docs: [
+ {
+ _id: 'SOME_DOCUMENT_ID_2',
+ some_property: 'some_value'
+ }
+ ]
+ })
+ mockReplicationOn.emit('complete')
+ const result = await promise
+
+ expect(result).toStrictEqual([
+ {
+ _id: 'SOME_DOCUMENT_ID_1',
+ some_property: 'some_value'
+ },
+ {
+ _id: 'SOME_DOCUMENT_ID_2',
+ some_property: 'some_value'
+ }
+ ])
+ })
+
+ it(`should filter design document from change event when Pouch replication`, async () => {
+ const replicationOptions = getReplicationOptionsMock()
+ replicationOptions.initialReplication = false
+
+ const getReplicationURL = () =>
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files'
+
+ const pouch = getPouchMock()
+
+ const promise = startReplication(
+ pouch,
+ replicationOptions,
+ getReplicationURL,
+ storage
+ )
+ mockReplicationOn.emit('change', {
+ change: {
+ docs: [
+ {
+ _id: 'SOME_DOCUMENT_ID_1',
+ some_property: 'some_value'
+ },
+ {
+ _id: '_design_SOME_DOCUMENT_ID_2',
+ some_property: 'some_value'
+ }
+ ]
+ }
+ })
+ mockReplicationOn.emit('complete')
+ const result = await promise
+
+ expect(result).toStrictEqual([
+ {
+ _id: 'SOME_DOCUMENT_ID_1',
+ some_property: 'some_value'
+ }
+ ])
+ })
+
+ it(`should filter deleted document from change event when Pouch replication`, async () => {
+ const replicationOptions = getReplicationOptionsMock()
+ replicationOptions.initialReplication = false
+
+ const getReplicationURL = () =>
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files'
+
+ const pouch = getPouchMock()
+
+ const promise = startReplication(
+ pouch,
+ replicationOptions,
+ getReplicationURL,
+ storage
+ )
+ mockReplicationOn.emit('change', {
+ change: {
+ docs: [
+ {
+ _id: 'SOME_DOCUMENT_ID_1',
+ some_property: 'some_value'
+ },
+ {
+ _id: 'SOME_DOCUMENT_ID_2',
+ some_property: 'some_value',
+ _deleted: true
+ }
+ ]
+ }
+ })
+ mockReplicationOn.emit('complete')
+ const result = await promise
+
+ expect(result).toStrictEqual([
+ {
+ _id: 'SOME_DOCUMENT_ID_1',
+ some_property: 'some_value'
+ }
+ ])
+ })
+
+ it(`should allow to cancel promise when Pouch replication`, async () => {
+ const replicationOptions = getReplicationOptionsMock()
+ replicationOptions.initialReplication = false
+
+ const getReplicationURL = () =>
+ 'https://user:SOME_TOKEN@claude.mycozy.cloud/data/io.cozy.files'
+
+ const pouch = getPouchMock()
+
+ const promise = startReplication(
+ pouch,
+ replicationOptions,
+ getReplicationURL,
+ storage
+ )
+
+ expect(promise.cancel).toBeDefined()
+
+ promise.cancel()
+
+ // this change should be ignored
+ mockReplicationOn.emit('change', {
+ change: {
+ docs: [
+ {
+ _id: 'SOME_DOCUMENT_ID_1',
+ some_property: 'some_value'
+ },
+ {
+ _id: 'SOME_DOCUMENT_ID_2',
+ some_property: 'some_value',
+ _deleted: true
+ }
+ ]
+ }
+ })
+
+ const result = await promise
+
+ expect(result).toStrictEqual([])
})
- const expectedDocs = dummyDocs.map(doc => doc.doc).slice(6, 11)
- expect(rep).toEqual(expectedDocs)
})
})
+
+const getPouchMock = () => {
+ const pouch = {
+ replicate: {
+ from: jest.fn(),
+ to: jest.fn()
+ },
+ sync: jest.fn()
+ }
+ pouch.replicate.from.mockReturnValue(mockReplicationOn)
+ pouch.replicate.to.mockReturnValue(mockReplicationOn)
+ pouch.sync.mockReturnValue(mockReplicationOn)
+
+ return pouch
+}
+
+const getReplicationOptionsMock = () => ({
+ strategy: 'fromRemote',
+ initialReplication: false,
+ warmupQueries: {},
+ doctype: 'io.cozy.files'
+})
diff --git a/packages/cozy-pouch-link/src/types.js b/packages/cozy-pouch-link/src/types.js
new file mode 100644
index 0000000000..7f324e805f
--- /dev/null
+++ b/packages/cozy-pouch-link/src/types.js
@@ -0,0 +1,73 @@
+/**
+ * @typedef {Object} Cancelable
+ * @property {Function} [cancel] - Cancel the promise
+ */
+
+/**
+ * @typedef {Promise & Cancelable} CancelablePromise
+ */
+
+/**
+ * @typedef {CancelablePromise[] & Cancelable} CancelablePromises
+ */
+
+/**
+ * @typedef {"synced"|"not_synced"|"not_complete"} SyncStatus
+ */
+
+/**
+ * @typedef {object} SyncInfo
+ * @property {string} date - The date of the last synchronization
+ * @property {SyncStatus} status - The current synchronization status
+ */
+
+/**
+ * @typedef {object} LocalStorage
+ * @property {function(string): Promise} getItem
+ * @property {function(string, string): Promise} setItem
+ * @property {function(string): Promise} removeItem
+ */
+
+/**
+ * @typedef {object} LinkPlatform
+ * @property {LocalStorage} storage Methods to access local storage
+ * @property {any} pouchAdapter PouchDB class (can be pouchdb-core or pouchdb-browser)
+ * @property {function(): Promise} isOnline Method that check if the app is connected to internet
+ */
+
+/**
+ * @typedef {Object} MangoPartialFilter
+ */
+
+/**
+ * @typedef {object} MangoSelector
+ */
+
+/**
+ * @typedef {Array} MangoSort
+ */
+
+/**
+ * @typedef {object} MangoQueryOptions
+ * @property {MangoSelector} [selector] Selector
+ * @property {MangoSort} [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
+ * @property {string|null} [bookmark] For bookmark-based pagination, the document _id to start from
+ * @property {Array} [indexedFields]
+ * @property {string} [use_index] Name of the index to use
+ * @property {boolean} [execution_stats] If true, we request the stats from Couch
+ * @property {MangoPartialFilter|null} [partialFilter] An optional partial filter
+ */
+
+/**
+ * @typedef {object} PouchDbIndex
+ * @property {string} id - The ddoc's id
+ * @property {string} name - The ddoc's name
+ * @property {'exists'|'created'} result - If the index has been created or if it already exists
+ */
+
+export default {}
diff --git a/packages/cozy-pouch-link/src/utils.js b/packages/cozy-pouch-link/src/utils.js
index abf1ceb2b4..33d0de4ad8 100644
--- a/packages/cozy-pouch-link/src/utils.js
+++ b/packages/cozy-pouch-link/src/utils.js
@@ -19,3 +19,11 @@ export const getDatabaseName = (prefix, doctype) => {
export const getPrefix = uri => {
return uri.replace(/^https?:\/\//, '')
}
+
+export const formatAggregatedError = aggregatedError => {
+ const strings = aggregatedError.errors.map((e, index) => {
+ return '\n[' + index + ']: ' + e.message + '\n' + e.stack
+ })
+
+ return strings.join('\n')
+}
diff --git a/packages/cozy-pouch-link/tsconfig.json b/packages/cozy-pouch-link/tsconfig.json
new file mode 100644
index 0000000000..0f1d72a867
--- /dev/null
+++ b/packages/cozy-pouch-link/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "include": ["src/**/*"],
+ "exclude": ["**/*.spec.*"],
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "outDir": "types",
+ "emitDeclarationOnly": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "jsx": "react",
+ "declaration": true,
+ "target": "es6",
+ "moduleResolution": "node",
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "types": ["jest"]
+}
diff --git a/packages/cozy-pouch-link/types/AccessToken.d.ts b/packages/cozy-pouch-link/types/AccessToken.d.ts
new file mode 100644
index 0000000000..9e417b54db
--- /dev/null
+++ b/packages/cozy-pouch-link/types/AccessToken.d.ts
@@ -0,0 +1,16 @@
+export default class AccessToken {
+ static fromJSON(data: any): AccessToken;
+ constructor(opts: any);
+ tokenType: any;
+ accessToken: any;
+ refreshToken: any;
+ scope: any;
+ toAuthHeader(): string;
+ toBasicAuth(): string;
+ toJSON(): {
+ tokenType: any;
+ accessToken: any;
+ refreshToken: any;
+ scope: any;
+ };
+}
diff --git a/packages/cozy-pouch-link/types/CozyPouchLink.d.ts b/packages/cozy-pouch-link/types/CozyPouchLink.d.ts
new file mode 100644
index 0000000000..49b9fce8f0
--- /dev/null
+++ b/packages/cozy-pouch-link/types/CozyPouchLink.d.ts
@@ -0,0 +1,228 @@
+export function getReplicationURL(uri: any, token: any, doctype: any): string;
+export function isExpiredTokenError(pouchError: any): boolean;
+export default PouchLink;
+export type CozyClientDocument = any;
+export type ReplicationStatus = "idle" | "replicating";
+export type PouchLinkOptions = {
+ /**
+ * Milliseconds between replications
+ */
+ replicationInterval?: number;
+ /**
+ * Doctypes to replicate
+ */
+ doctypes: string[];
+ /**
+ * A mapping from doctypes to replication options. All pouch replication options can be used, as well as the "strategy" option that determines which way the replication is done (can be "sync", "fromRemote" or "toRemote")
+ */
+ doctypesReplicationOptions: Record;
+ /**
+ * Platform specific adapters and methods
+ */
+ platform: import('./types').LinkPlatform;
+};
+/**
+ * @typedef {import('cozy-client/src/types').CozyClientDocument} CozyClientDocument
+ *
+ * @typedef {"idle"|"replicating"} ReplicationStatus
+ */
+/**
+ * @typedef {object} PouchLinkOptions
+ * @property {number} [replicationInterval] Milliseconds between replications
+ * @property {string[]} doctypes Doctypes to replicate
+ * @property {Record} doctypesReplicationOptions A mapping from doctypes to replication options. All pouch replication options can be used, as well as the "strategy" option that determines which way the replication is done (can be "sync", "fromRemote" or "toRemote")
+ * @property {import('./types').LinkPlatform} platform Platform specific adapters and methods
+ */
+/**
+ * Link to be passed to a `CozyClient` instance to support CouchDB. It instantiates
+ * PouchDB collections for each doctype that it supports and knows how
+ * to respond to queries and mutations.
+ */
+declare class PouchLink extends CozyLink {
+ /**
+ * Return the PouchDB adapter name.
+ * Should be IndexedDB for newest adapters.
+ *
+ * @param {import('./types').LocalStorage} localStorage Methods to access local storage
+ * @returns {Promise} The adapter name
+ */
+ static getPouchAdapterName: (localStorage: import('./types').LocalStorage) => Promise;
+ /**
+ * constructor - Initializes a new PouchLink
+ *
+ * @param {PouchLinkOptions} [opts={}]
+ */
+ constructor(opts?: PouchLinkOptions);
+ options: {
+ replicationInterval: number;
+ } & PouchLinkOptions;
+ doctypes: string[];
+ doctypesReplicationOptions: Record;
+ indexes: {};
+ storage: PouchLocalStorage;
+ /** @type {Record} - Stores replication states per doctype */
+ replicationStatus: Record;
+ getReplicationURL(doctype: any): string;
+ registerClient(client: any): Promise;
+ client: any;
+ /**
+ * Migrate the current adapter
+ *
+ * @typedef {object} MigrationParams
+ * @property {string} [fromAdapter] - The current adapter type, e.g. 'idb'
+ * @property {string} [toAdapter] - The new adapter type, e.g. 'indexeddb'
+ * @property {string} [url] - The Cozy URL
+ * @property {Array} [plugins] - The PouchDB plugins
+ *
+ * @param {MigrationParams} params - Migration params
+ */
+ migrateAdapter({ fromAdapter, toAdapter, url, plugins }: {
+ /**
+ * - The current adapter type, e.g. 'idb'
+ */
+ fromAdapter?: string;
+ /**
+ * - The new adapter type, e.g. 'indexeddb'
+ */
+ toAdapter?: string;
+ /**
+ * - The Cozy URL
+ */
+ url?: string;
+ /**
+ * - The PouchDB plugins
+ */
+ plugins?: Array;
+ }): Promise;
+ onLogin(): Promise;
+ pouches: PouchManager;
+ /**
+ * Receives PouchDB updates (documents grouped by doctype).
+ * Normalizes the data (.id -> ._id, .rev -> _rev).
+ * Passes the data to the client and to the onSync handler.
+ *
+ * Emits an event (pouchlink:sync:end) when the sync (all doctypes) is done
+ */
+ handleOnSync(doctypeUpdates: any): void;
+ handleDoctypeSyncStart(doctype: any): void;
+ handleDoctypeSyncEnd(doctype: any): void;
+ /**
+ * User of the link can call this to start ongoing replications.
+ * Typically, it can be used when the application regains focus.
+ *
+ * Emits pouchlink:sync:start event when the replication begins
+ *
+ * @public
+ * @returns {void}
+ */
+ public startReplication(): void;
+ /**
+ * User of the link can call this to stop ongoing replications.
+ * Typically, it can be used when the applications loses focus.
+ *
+ * Emits pouchlink:sync:stop event
+ *
+ * @public
+ * @returns {void}
+ */
+ public stopReplication(): void;
+ onSyncError(error: any): Promise;
+ getSyncInfo(doctype: any): import("./types").SyncInfo;
+ getPouch(doctype: any): any;
+ supportsOperation(operation: any): boolean;
+ sanitizeJsonApi(data: any): Pick, string | number | symbol>;
+ /**
+ * Retrieve the existing document from Pouch
+ *
+ * @private
+ * @param {*} id - ID of the document to retrieve
+ * @param {*} type - Doctype of the document to retrieve
+ * @param {*} throwIfNotFound - If true the method will throw when the document is not found. Otherwise it will return null
+ * @returns {Promise}
+ */
+ private getExistingDocument;
+ /**
+ *
+ * Check if there is warmup queries for this doctype
+ * and return if those queries are already warmed up or not
+ *
+ * @param {string} doctype - Doctype to check
+ * @returns {Promise} the need to wait for the warmup
+ */
+ needsToWaitWarmup(doctype: string): Promise;
+ hasIndex(name: any): boolean;
+ /**
+ * Create the PouchDB index if not existing
+ *
+ * @param {Array} fields - Fields to index
+ * @param {object} indexOption - Options for the index
+ * @param {object} [indexOption.partialFilter] - partialFilter
+ * @param {string} [indexOption.indexName] - indexName
+ * @param {string} [indexOption.doctype] - doctype
+ * @returns {Promise}
+ */
+ createIndex(fields: any[], { partialFilter, indexName, doctype }?: {
+ partialFilter: object;
+ indexName: string;
+ doctype: string;
+ }): Promise;
+ /**
+ * Retrieve the PouchDB index if exist, undefined otherwise
+ *
+ * @param {string} doctype - The query's doctype
+ * @param {import('./types').MangoQueryOptions} options - The find options
+ * @param {string} indexName - The index name
+ * @returns {import('./types').PouchDbIndex | undefined}
+ */
+ findExistingIndex(doctype: string, options: import('./types').MangoQueryOptions, indexName: string): import('./types').PouchDbIndex | undefined;
+ /**
+ * Handle index creation if it is missing.
+ *
+ * When an index is missing, we first check if there is one with a different
+ * name but the same definition. If there is none, we create the new index.
+ *
+ * /!\ Warning: this method is similar to DocumentCollection.handleMissingIndex()
+ * If you edit this method, please check if the change is also needed in DocumentCollection
+ *
+ * @param {string} doctype The mango selector
+ * @param {import('./types').MangoQueryOptions} options The find options
+ * @returns {Promise} index
+ * @private
+ */
+ private ensureIndex;
+ executeQuery({ doctype, selector, sort, fields, limit, id, ids, skip, indexedFields, partialFilter }: {
+ doctype: any;
+ selector: any;
+ sort: any;
+ fields: any;
+ limit: any;
+ id: any;
+ ids: any;
+ skip: any;
+ indexedFields: any;
+ partialFilter: any;
+ }): Promise<{
+ data: any;
+ meta?: undefined;
+ skip?: undefined;
+ next?: undefined;
+ } | {
+ data: any;
+ meta: {
+ count: any;
+ };
+ skip: any;
+ next: boolean;
+ }>;
+ executeMutation(mutation: any, result: any, forward: any): Promise;
+ createDocument(mutation: any): Promise;
+ updateDocument(mutation: any): Promise;
+ updateDocuments(mutation: any): Promise;
+ deleteDocument(mutation: any): Promise;
+ addReferencesTo(mutation: any): Promise;
+ dbMethod(method: any, mutation: any): Promise;
+ syncImmediately(): Promise;
+}
+import { CozyLink } from "cozy-client";
+import { PouchLocalStorage } from "./localStorage";
+import PouchManager from "./PouchManager";
diff --git a/packages/cozy-pouch-link/types/PouchManager.d.ts b/packages/cozy-pouch-link/types/PouchManager.d.ts
new file mode 100644
index 0000000000..f654355a7c
--- /dev/null
+++ b/packages/cozy-pouch-link/types/PouchManager.d.ts
@@ -0,0 +1,86 @@
+export default PouchManager;
+/**
+ * Handles the lifecycle of several pouches
+ *
+ * - Creates/Destroys the pouches
+ * - Replicates periodically
+ */
+declare class PouchManager {
+ constructor(doctypes: any, options: any);
+ options: any;
+ doctypes: any;
+ storage: PouchLocalStorage;
+ PouchDB: any;
+ isOnline: any;
+ events: any;
+ init(): Promise;
+ pouches: import("lodash").Dictionary;
+ /** @type {Record} - Stores synchronization info per doctype */
+ syncedDoctypes: Record;
+ warmedUpQueries: any;
+ getReplicationURL: any;
+ doctypesReplicationOptions: any;
+ listenerLaunched: boolean;
+ ensureDatabasesExistDone: boolean;
+ /**
+ * Starts periodic syncing of the pouches
+ *
+ * @returns {Promise}
+ */
+ startReplicationLoop(): Promise;
+ /** Stop periodic syncing of the pouches */
+ stopReplicationLoop(): void;
+ /** Starts replication */
+ replicateOnce(): Promise;
+ executeQuery: any;
+ /** @type {import('./types').CancelablePromise[]} - Stores replication promises */
+ replications: import('./types').CancelablePromise[];
+ addListeners(): void;
+ removeListeners(): void;
+ destroy(): Promise;
+ /**
+ * Via a call to info() we ensure the database exist on the
+ * remote side. This is done only once since after the first
+ * call, we are sure that the databases have been created.
+ */
+ ensureDatabasesExist(): Promise;
+ replicationLoop: Loop;
+ /**
+ * If a replication is currently ongoing, will start a replication
+ * just after it has finished. Otherwise it will start a replication
+ * immediately
+ */
+ syncImmediately(): void;
+ handleReplicationError(err: any): void;
+ cancelCurrentReplications(): void;
+ waitForCurrentReplications(): Promise | Promise;
+ getPouch(doctype: any): any;
+ /**
+ * Update the Sync info for the specifed doctype
+ *
+ * @param {string} doctype - The doctype to update
+ * @param {import('./types').SyncStatus} status - The new Sync status for the doctype
+ */
+ updateSyncInfo(doctype: string, status?: import('./types').SyncStatus): Promise;
+ /**
+ * Get the Sync info for the specified doctype
+ *
+ * @param {string} doctype - The doctype to check
+ * @returns {import('./types').SyncInfo}
+ */
+ getSyncInfo(doctype: string): import('./types').SyncInfo;
+ /**
+ * Get the Sync status for the specified doctype
+ *
+ * @param {string} doctype - The doctype to check
+ * @returns {import('./types').SyncStatus}
+ */
+ getSyncStatus(doctype: string): import('./types').SyncStatus;
+ clearSyncedDoctypes(): Promise;
+ warmupQueries(doctype: any, queries: any): Promise;
+ checkToWarmupDoctype(doctype: any, replicationOptions: any): void;
+ areQueriesWarmedUp(doctype: any, queries: any): Promise;
+ clearWarmedUpQueries(): Promise;
+}
+import { PouchLocalStorage } from "./localStorage";
+import Loop from "./loop";
diff --git a/packages/cozy-pouch-link/types/__tests__/fixtures.d.ts b/packages/cozy-pouch-link/types/__tests__/fixtures.d.ts
new file mode 100644
index 0000000000..578a3ee772
--- /dev/null
+++ b/packages/cozy-pouch-link/types/__tests__/fixtures.d.ts
@@ -0,0 +1,48 @@
+export namespace TODO_1 {
+ const _id: string;
+ const _type: string;
+ const label: string;
+ const done: boolean;
+}
+export namespace TODO_2 {
+ const _id_1: string;
+ export { _id_1 as _id };
+ const _type_1: string;
+ export { _type_1 as _type };
+ const label_1: string;
+ export { label_1 as label };
+ const done_1: boolean;
+ export { done_1 as done };
+}
+export namespace TODO_3 {
+ const _id_2: string;
+ export { _id_2 as _id };
+ const _type_2: string;
+ export { _type_2 as _type };
+ const label_2: string;
+ export { label_2 as label };
+ const done_2: boolean;
+ export { done_2 as done };
+}
+export namespace TODO_4 {
+ const _id_3: string;
+ export { _id_3 as _id };
+ const _type_3: string;
+ export { _type_3 as _type };
+ const label_3: string;
+ export { label_3 as label };
+ const done_3: boolean;
+ export { done_3 as done };
+}
+export namespace SCHEMA {
+ namespace todos {
+ const doctype: string;
+ namespace relationships {
+ namespace attachments {
+ export const type: string;
+ const doctype_1: string;
+ export { doctype_1 as doctype };
+ }
+ }
+ }
+}
diff --git a/packages/cozy-pouch-link/types/__tests__/mocks.d.ts b/packages/cozy-pouch-link/types/__tests__/mocks.d.ts
new file mode 100644
index 0000000000..70ad0c452a
--- /dev/null
+++ b/packages/cozy-pouch-link/types/__tests__/mocks.d.ts
@@ -0,0 +1,4 @@
+export function pouchReplication(mockOptions: any): (url: any, options: any) => {
+ on: (event: any, fn: any) => any;
+ cancel: () => void;
+};
diff --git a/packages/cozy-pouch-link/types/helpers.d.ts b/packages/cozy-pouch-link/types/helpers.d.ts
new file mode 100644
index 0000000000..a4e88a8034
--- /dev/null
+++ b/packages/cozy-pouch-link/types/helpers.d.ts
@@ -0,0 +1,17 @@
+export default helpers;
+declare namespace helpers {
+ function isAdapterBugged(adapterName: any): boolean;
+ function withoutDesignDocuments(res: any): any;
+ function getDocs(db: any, fct: any, options?: {}): any;
+ function allDocs(db: any, options?: {}): Promise;
+ function find(db: any, options?: {}): Promise;
+ function isDesignDocument(doc: any): boolean;
+ function isDeletedDocument(doc: any): any;
+ function insertBulkDocs(db: any, docs: any): Promise;
+ function normalizeFindSelector({ selector, sort, indexedFields, partialFilter }: {
+ selector: any;
+ sort: any;
+ indexedFields: any;
+ partialFilter: any;
+ }): any;
+}
diff --git a/packages/cozy-pouch-link/types/index.d.ts b/packages/cozy-pouch-link/types/index.d.ts
new file mode 100644
index 0000000000..23d7a258d6
--- /dev/null
+++ b/packages/cozy-pouch-link/types/index.d.ts
@@ -0,0 +1 @@
+export { default } from "./CozyPouchLink";
diff --git a/packages/cozy-pouch-link/types/jsonapi.d.ts b/packages/cozy-pouch-link/types/jsonapi.d.ts
new file mode 100644
index 0000000000..886261a900
--- /dev/null
+++ b/packages/cozy-pouch-link/types/jsonapi.d.ts
@@ -0,0 +1,19 @@
+export function normalizeDoc(doc: any, doctype: any, client: any): any;
+export function fromPouchResult({ res, withRows, doctype, client }: {
+ res: any;
+ withRows: any;
+ doctype: any;
+ client: any;
+}): {
+ data: any;
+ meta?: undefined;
+ skip?: undefined;
+ next?: undefined;
+} | {
+ data: any;
+ meta: {
+ count: any;
+ };
+ skip: any;
+ next: boolean;
+};
diff --git a/packages/cozy-pouch-link/types/localStorage.d.ts b/packages/cozy-pouch-link/types/localStorage.d.ts
new file mode 100644
index 0000000000..c60f0f36be
--- /dev/null
+++ b/packages/cozy-pouch-link/types/localStorage.d.ts
@@ -0,0 +1,124 @@
+export const LOCALSTORAGE_SYNCED_KEY: "cozy-client-pouch-link-synced";
+export const LOCALSTORAGE_WARMUPEDQUERIES_KEY: "cozy-client-pouch-link-warmupedqueries";
+export const LOCALSTORAGE_LASTSEQUENCES_KEY: "cozy-client-pouch-link-lastreplicationsequence";
+export const LOCALSTORAGE_LASTREPLICATEDDOCID_KEY: "cozy-client-pouch-link-lastreplicateddocid";
+export const LOCALSTORAGE_ADAPTERNAME: "cozy-client-pouch-link-adaptername";
+export class PouchLocalStorage {
+ constructor(storageEngine: any);
+ storageEngine: any;
+ /**
+ * Persist the last replicated doc id for a doctype
+ *
+ * @param {string} doctype - The replicated doctype
+ * @param {string} id - The docid
+ *
+ * @returns {Promise}
+ */
+ persistLastReplicatedDocID(doctype: string, id: string): Promise;
+ /**
+ * @returns {Promise>}
+ */
+ getAllLastReplicatedDocID(): Promise>;
+ /**
+ * Get the last replicated doc id for a doctype
+ *
+ * @param {string} doctype - The doctype
+ * @returns {Promise} The last replicated docid
+ */
+ getLastReplicatedDocID(doctype: string): Promise;
+ /**
+ * Destroy all the replicated doc id
+ *
+ * @returns {Promise}
+ */
+ destroyAllLastReplicatedDocID(): Promise;
+ /**
+ * Persist the synchronized doctypes
+ *
+ * @param {Record} syncedDoctypes - The sync doctypes
+ *
+ * @returns {Promise}
+ */
+ persistSyncedDoctypes(syncedDoctypes: Record): Promise;
+ /**
+ * Get the persisted doctypes
+ *
+ * @returns {Promise} The synced doctypes
+ */
+ getPersistedSyncedDoctypes(): Promise;
+ /**
+ * Destroy the synced doctypes
+ *
+ * @returns {Promise}
+ */
+ destroySyncedDoctypes(): Promise;
+ /**
+ * Persist the last CouchDB sequence for a synced doctype
+ *
+ * @param {string} doctype - The synced doctype
+ * @param {string} sequence - The sequence hash
+ *
+ * @returns {Promise}
+ */
+ persistDoctypeLastSequence(doctype: string, sequence: string): Promise;
+ /**
+ * @returns {Promise}
+ */
+ getAllLastSequences(): Promise;
+ /**
+ * Get the last CouchDB sequence for a doctype
+ *
+ * @param {string} doctype - The doctype
+ *
+ * @returns {Promise} the last sequence
+ */
+ getDoctypeLastSequence(doctype: string): Promise;
+ /**
+ * Destroy all the last sequence
+ *
+ * @returns {Promise}
+ */
+ destroyAllDoctypeLastSequence(): Promise;
+ /**
+ * Destroy the last sequence for a doctype
+ *
+ * @param {string} doctype - The doctype
+ *
+ * @returns {Promise}
+ */
+ destroyDoctypeLastSequence(doctype: string): Promise;
+ /**
+ * Persist the warmed up queries
+ *
+ * @param {object} warmedUpQueries - The warmedup queries
+ *
+ * @returns {Promise}
+ */
+ persistWarmedUpQueries(warmedUpQueries: object): Promise;
+ /**
+ * Get the warmed up queries
+ *
+ * @returns {Promise} the warmed up queries
+ */
+ getPersistedWarmedUpQueries(): Promise;
+ /**
+ * Destroy the warmed queries
+ *
+ * @returns {Promise}
+ */
+ destroyWarmedUpQueries(): Promise;
+ /**
+ * Get the adapter name
+ *
+ * @returns {Promise} The adapter name
+ */
+ getAdapterName(): Promise;
+ /**
+ * Persist the adapter name
+ *
+ * @param {string} adapter - The adapter name
+ *
+ * @returns {Promise}
+ */
+ persistAdapterName(adapter: string): Promise;
+}
diff --git a/packages/cozy-pouch-link/types/logger.d.ts b/packages/cozy-pouch-link/types/logger.d.ts
new file mode 100644
index 0000000000..e4d82eb243
--- /dev/null
+++ b/packages/cozy-pouch-link/types/logger.d.ts
@@ -0,0 +1,2 @@
+export default logger;
+declare const logger: any;
diff --git a/packages/cozy-pouch-link/types/loop.d.ts b/packages/cozy-pouch-link/types/loop.d.ts
new file mode 100644
index 0000000000..cfea491dc3
--- /dev/null
+++ b/packages/cozy-pouch-link/types/loop.d.ts
@@ -0,0 +1,60 @@
+export default Loop;
+/**
+ * Utility to call a function (task) periodically
+ * and on demand immediately.
+ *
+ * Public API
+ *
+ * - start
+ * - stop
+ * - scheduleImmediateTask
+ * - waitForCurrentTask
+ */
+declare class Loop {
+ constructor(task: any, delay: any, _afterRound: any, _sleep: any);
+ task: any;
+ delay: any;
+ /**
+ * Runs immediate tasks and then schedule the next round.
+ * Immediate tasks are called sequentially without delay
+ * There is a delay between immediate tasks and normal periodic tasks.
+ */
+ round(): Promise;
+ immediateTasks: any[];
+ started: boolean;
+ afterRound: any;
+ sleep: any;
+ /**
+ * Starts the loop. Will run the task periodically each `this.delay` ms.
+ * Ignores multiple starts.
+ */
+ start(): void;
+ /**
+ * Stops the loop, clears immediate tasks.
+ * Cancels current task if possible
+ */
+ stop(): void;
+ waitForCurrent(): Promise;
+ /**
+ * Flushes the immediate tasks list and calls each task.
+ * Each task is awaited before the next is started.
+ */
+ runImmediateTasks(): Promise;
+ /**
+ * Schedules a task to be run immediately at next round.
+ * Ignored if loop is not started.
+ * If not task is passed, the default task from the loop is used.
+ *
+ * @param {Function} task - Optional custom function to be run immediately
+ */
+ scheduleImmediateTask(task?: Function): Promise;
+ clearImmediateTasks(): void;
+ /**
+ * Calls and saves current task.
+ * Stops loop in case of error of the task.
+ */
+ runTask(task: any): Promise;
+ currentTask: any;
+ _rounding: boolean;
+ timeout: NodeJS.Timeout;
+}
diff --git a/packages/cozy-pouch-link/types/mango.d.ts b/packages/cozy-pouch-link/types/mango.d.ts
new file mode 100644
index 0000000000..8c44479026
--- /dev/null
+++ b/packages/cozy-pouch-link/types/mango.d.ts
@@ -0,0 +1,3 @@
+export function makeKeyFromPartialFilter(condition: object): string;
+export function getIndexNameFromFields(fields: Array, partialFilter?: object): string;
+export function getIndexFields({ selector, sort, partialFilter }: import('./types').MangoQueryOptions): string[];
diff --git a/packages/cozy-pouch-link/types/migrations/adapter.d.ts b/packages/cozy-pouch-link/types/migrations/adapter.d.ts
new file mode 100644
index 0000000000..df5f04d238
--- /dev/null
+++ b/packages/cozy-pouch-link/types/migrations/adapter.d.ts
@@ -0,0 +1,18 @@
+export function migratePouch({ dbName, fromAdapter, toAdapter }: MigrationParams): Promise;
+/**
+ * Migrate a PouchDB database to a new adapter.
+ */
+export type MigrationParams = {
+ /**
+ * - The database name
+ */
+ dbName?: string;
+ /**
+ * - The current adapter type, e.g. 'idb'
+ */
+ fromAdapter?: string;
+ /**
+ * - The new adapter type, e.g. 'indexeddb'
+ */
+ toAdapter?: string;
+};
diff --git a/packages/cozy-pouch-link/types/platformWeb.d.ts b/packages/cozy-pouch-link/types/platformWeb.d.ts
new file mode 100644
index 0000000000..9b8b8fdf71
--- /dev/null
+++ b/packages/cozy-pouch-link/types/platformWeb.d.ts
@@ -0,0 +1,17 @@
+export namespace platformWeb {
+ export { storage };
+ export { events };
+ export { PouchDB as pouchAdapter };
+ export { isOnline };
+}
+declare namespace storage {
+ function getItem(key: any): Promise;
+ function setItem(key: any, value: any): Promise;
+ function removeItem(key: any): Promise;
+}
+declare namespace events {
+ function addEventListener(eventName: any, handler: any): void;
+ function removeEventListener(eventName: any, handler: any): void;
+}
+declare function isOnline(): Promise;
+export {};
diff --git a/packages/cozy-pouch-link/types/remote.d.ts b/packages/cozy-pouch-link/types/remote.d.ts
new file mode 100644
index 0000000000..864c5dcf98
--- /dev/null
+++ b/packages/cozy-pouch-link/types/remote.d.ts
@@ -0,0 +1,6 @@
+export const DATABASE_NOT_FOUND_ERROR: "Database does not exist";
+export const DATABASE_RESERVED_DOCTYPE_ERROR: "Reserved doctype";
+export function isDatabaseNotFoundError(error: any): boolean;
+export function isDatabaseUnradableError(error: any): boolean;
+export function fetchRemoteInstance(url: URL, params?: object): Promise;
+export function fetchRemoteLastSequence(baseUrl: string): Promise;
diff --git a/packages/cozy-pouch-link/types/replicateOnce.d.ts b/packages/cozy-pouch-link/types/replicateOnce.d.ts
new file mode 100644
index 0000000000..9da41aaac2
--- /dev/null
+++ b/packages/cozy-pouch-link/types/replicateOnce.d.ts
@@ -0,0 +1,29 @@
+export function replicateOnce(pouchManager: import('./PouchManager').default): Promise;
+export type FulfilledPromise = {
+ /**
+ * - The status of the promise
+ */
+ status: 'fulfilled';
+ /**
+ * - The Error rejected by the promise (undefined when fulfilled)
+ */
+ reason: undefined;
+ /**
+ * - The resolved value of the promise
+ */
+ value: any;
+};
+export type RejectedPromise = {
+ /**
+ * - The status of the promise
+ */
+ status: 'rejected';
+ /**
+ * - The Error rejected by the promise
+ */
+ reason: Error;
+ /**
+ * - The resolved value of the promise (undefined when rejected)
+ */
+ value: undefined;
+};
diff --git a/packages/cozy-pouch-link/types/startReplication.d.ts b/packages/cozy-pouch-link/types/startReplication.d.ts
new file mode 100644
index 0000000000..5b769edb96
--- /dev/null
+++ b/packages/cozy-pouch-link/types/startReplication.d.ts
@@ -0,0 +1,12 @@
+export function startReplication(pouch: object, replicationOptions: {
+ strategy: string;
+ initialReplication: boolean;
+ doctype: string;
+ warmupQueries: import('cozy-client/types/types').Query[];
+}, getReplicationURL: Function, storage: import('./localStorage').PouchLocalStorage): import('./types').CancelablePromise;
+export function replicateAllDocs({ db, baseUrl, doctype, storage }: {
+ db: object;
+ baseUrl: string;
+ doctype: string;
+ storage: import('./localStorage').PouchLocalStorage;
+}): Promise;
diff --git a/packages/cozy-pouch-link/types/types.d.ts b/packages/cozy-pouch-link/types/types.d.ts
new file mode 100644
index 0000000000..3322d56501
--- /dev/null
+++ b/packages/cozy-pouch-link/types/types.d.ts
@@ -0,0 +1,104 @@
+declare var _default: {};
+export default _default;
+export type Cancelable = {
+ /**
+ * - Cancel the promise
+ */
+ cancel?: Function;
+};
+export type CancelablePromise = Promise & Cancelable;
+export type CancelablePromises = CancelablePromise[] & Cancelable;
+export type SyncStatus = "synced" | "not_synced" | "not_complete";
+export type SyncInfo = {
+ /**
+ * - The date of the last synchronization
+ */
+ date: string;
+ /**
+ * - The current synchronization status
+ */
+ status: SyncStatus;
+};
+export type LocalStorage = {
+ getItem: (arg0: string) => Promise;
+ setItem: (arg0: string, arg1: string) => Promise;
+ removeItem: (arg0: string) => Promise;
+};
+export type LinkPlatform = {
+ /**
+ * Methods to access local storage
+ */
+ storage: LocalStorage;
+ /**
+ * PouchDB class (can be pouchdb-core or pouchdb-browser)
+ */
+ pouchAdapter: any;
+ /**
+ * Method that check if the app is connected to internet
+ */
+ isOnline: () => Promise;
+};
+export type MangoPartialFilter = any;
+export type MangoSelector = any;
+export type MangoSort = any[];
+export type MangoQueryOptions = {
+ /**
+ * Selector
+ */
+ selector?: MangoSelector;
+ /**
+ * The sorting parameters
+ */
+ sort?: MangoSort;
+ /**
+ * The fields to return
+ */
+ fields?: Array;
+ /**
+ * The partial filter fields
+ */
+ partialFilterFields?: Array;
+ /**
+ * For pagination, the number of results to return
+ */
+ limit?: number | null;
+ /**
+ * For skip-based pagination, the number of referenced files to skip
+ */
+ skip?: number | null;
+ /**
+ * The _id of the CouchDB index to use for this request
+ */
+ indexId?: string | null;
+ /**
+ * For bookmark-based pagination, the document _id to start from
+ */
+ bookmark?: string | null;
+ indexedFields?: Array;
+ /**
+ * Name of the index to use
+ */
+ use_index?: string;
+ /**
+ * If true, we request the stats from Couch
+ */
+ execution_stats?: boolean;
+ /**
+ * An optional partial filter
+ */
+ partialFilter?: MangoPartialFilter | null;
+};
+export type PouchDbIndex = {
+ /**
+ * - The ddoc's id
+ */
+ id: string;
+ /**
+ * - The ddoc's name
+ */
+ name: string;
+ /**
+ * - If the index has been created or if it already exists
+ */
+ result: 'exists' | 'created';
+};
diff --git a/packages/cozy-pouch-link/types/utils.d.ts b/packages/cozy-pouch-link/types/utils.d.ts
new file mode 100644
index 0000000000..de3dca2514
--- /dev/null
+++ b/packages/cozy-pouch-link/types/utils.d.ts
@@ -0,0 +1,3 @@
+export function getDatabaseName(prefix: string, doctype: string): string;
+export function getPrefix(uri: string): string;
+export function formatAggregatedError(aggregatedError: any): any;
diff --git a/packages/cozy-stack-client/src/AppsRegistryCollection.js b/packages/cozy-stack-client/src/AppsRegistryCollection.js
index 1967a14854..a34da894b9 100644
--- a/packages/cozy-stack-client/src/AppsRegistryCollection.js
+++ b/packages/cozy-stack-client/src/AppsRegistryCollection.js
@@ -27,6 +27,16 @@ export const normalizeAppFromRegistry = (data, doctype) => {
}
}
+const fetchKonnectorsByChannel = async (channel, doctype, stackClient) => {
+ const resp = await stackClient.fetchJSON(
+ 'GET',
+ `/registry?versionsChannel=${channel}&filter[type]=konnector&limit=500`
+ )
+ return {
+ data: resp.data.map(data => normalizeAppFromRegistry(data, doctype))
+ }
+}
+
/**
* Extends `DocumentCollection` API along with specific methods for `io.cozy.apps_registry`.
*/
@@ -44,6 +54,12 @@ class AppsRegistryCollection extends DocumentCollection {
* @throws {FetchError}
*/
async get(slug) {
+ if (slug.startsWith('konnectors/')) {
+ const channel = slug.split('/')[1]
+
+ return fetchKonnectorsByChannel(channel, this.doctype, this.stackClient)
+ }
+
const resp = await this.stackClient.fetchJSON(
'GET',
`${this.endpoint}${slug}`
diff --git a/packages/cozy-stack-client/src/DocumentCollection.js b/packages/cozy-stack-client/src/DocumentCollection.js
index 645b05e4bd..ac1e2ac889 100644
--- a/packages/cozy-stack-client/src/DocumentCollection.js
+++ b/packages/cozy-stack-client/src/DocumentCollection.js
@@ -227,6 +227,9 @@ class DocumentCollection {
* name but the same definition. If yes, it means we found an old unamed
* index, so we migrate it. If there is none, we create the new index.
*
+ * /!\ Warning: this method is similar to CozyPouchLink.ensureIndex()
+ * If you edit this method, please check if the change is also needed in CozyPouchLink
+ *
* @param {object} selector The mango selector
* @param {MangoQueryOptions} options The find options
* @private
@@ -238,10 +241,9 @@ class DocumentCollection {
indexedFields = getIndexFields({ sort: options.sort, selector })
}
- const existingIndex = await this.findExistingIndex(selector, options)
-
const indexName = getIndexNameFromFields(indexedFields, partialFilter)
+ const existingIndex = await this.findExistingIndex(selector, options)
if (!existingIndex) {
await this.createIndex(indexedFields, {
partialFilter,
diff --git a/packages/cozy-stack-client/src/FileCollection.js b/packages/cozy-stack-client/src/FileCollection.js
index 520dd5091d..69c73e83de 100644
--- a/packages/cozy-stack-client/src/FileCollection.js
+++ b/packages/cozy-stack-client/src/FileCollection.js
@@ -57,6 +57,7 @@ import logger from './logger'
*
* @typedef {object} FileDocument
* @property {string} _id - Id of the file
+ * @property {string} _rev - Rev of the file
* @property {FileAttributes} attributes - Attributes of the file
* @property {object} meta - Meta
* @property {object} relationships - Relationships
diff --git a/packages/cozy-stack-client/src/mangoIndex.js b/packages/cozy-stack-client/src/mangoIndex.js
index 38d662c6cb..2963915dd9 100644
--- a/packages/cozy-stack-client/src/mangoIndex.js
+++ b/packages/cozy-stack-client/src/mangoIndex.js
@@ -50,6 +50,9 @@ export const normalizeDesignDoc = designDoc => {
/**
* Process a partial filter to generate a string key
*
+ * /!\ Warning: this method is similar to cozy-pouch-link mango.makeKeyFromPartialFilter()
+ * If you edit this method, please check if the change is also needed in mango file
+ *
* @param {object} condition - An object representing the partial filter or a sub-condition of the partial filter
* @returns {string} - The string key of the processed partial filter
*/
@@ -84,6 +87,9 @@ export const makeKeyFromPartialFilter = condition => {
* It follows this naming convention:
* `by_{indexed_field1}_and_{indexed_field2}_filter_({partial_filter.key1}_{partial_filter.value1})_and_({partial_filter.key2}_{partial_filter.value2})`
*
+ * /!\ Warning: this method is similar to cozy-pouch-link mango.getIndexNameFromFields()
+ * If you edit this method, please check if the change is also needed in mango file
+ *
* @param {Array} fields - The indexed fields
* @param {object} [partialFilter] - The partial filter
* @returns {string} The index name, built from the fields
diff --git a/scripts/docs.js b/scripts/docs.js
index e59c9641d3..ddc74de769 100644
--- a/scripts/docs.js
+++ b/scripts/docs.js
@@ -20,7 +20,7 @@ const main = async () => {
const packages = await globPromise('packages/*')
await fs.mkdirp(path.resolve(docsFolder, 'api'))
for (let pkg of packages) {
- if (pkg === 'packages/cozy-client') {
+ if (pkg === 'packages/cozy-client' || pkg === 'packages/cozy-pouch-link') {
continue // documentation for cozy-client is made via typedoc
}
const files = await globPromise(`${pkg}/src/**/*.js*`, {
diff --git a/scripts/travis.sh b/scripts/travis.sh
index 32d619d0f9..562f596d86 100755
--- a/scripts/travis.sh
+++ b/scripts/travis.sh
@@ -21,8 +21,7 @@ fi
set -e
set +e # The following command relies on exit 1
-cd packages/cozy-client
-yarn typecheck
+yarn types
[ $? -eq 0 ] || exit 1
git diff --exit-code
types_status=$?
diff --git a/yarn.lock b/yarn.lock
index a7e438dc6b..26330ff4c7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5037,11 +5037,12 @@ cozy-flags@2.10.2:
dependencies:
microee "^0.0.6"
-cozy-intent@1.17.3:
- version "1.17.3"
- resolved "https://registry.yarnpkg.com/cozy-intent/-/cozy-intent-1.17.3.tgz#dcf8085a9c561ce56ab0c7afc69474243e4f9e9c"
- integrity sha512-Qko/tUJlXWh5wYLfw+CknbIm+KeAW4F3lAk/n1CA+uKwcseua+LCoNIypC/04ttm9g6ntbEogb/u4h6d5+H6lg==
+cozy-intent@2.23.0:
+ version "2.23.0"
+ resolved "https://registry.yarnpkg.com/cozy-intent/-/cozy-intent-2.23.0.tgz#b6f3a407413df05c108e848b9dcb074b8780824b"
+ integrity sha512-DFn0ny4B4HpOE+3PYuZTTa074gRnFHqID+XaJ3gY2OrPL2xUQKEZmmFLp2bPVWThi5FvgvsU3EQeWPHZNQPbaQ==
dependencies:
+ cozy-minilog "^3.3.1"
post-me "0.4.5"
cozy-interapp@^0.5.4:
@@ -5057,6 +5058,13 @@ cozy-logger@1.7.0:
chalk "^2.4.2"
json-stringify-safe "5.0.1"
+cozy-minilog@^3.3.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/cozy-minilog/-/cozy-minilog-3.3.1.tgz#472dccf4a9391c479120a83d26b435cf9d609c72"
+ integrity sha512-NLQNQ1Q/bvJrqNv9w5bLjfAxYKv+pESobJgUKXondxP616kx7k0mpiRrCZBaJRbEbpKryT/eJ0JJwLdVaIP5NA==
+ dependencies:
+ microee "0.0.6"
+
cozy-ui@93.1.1:
version "93.1.1"
resolved "https://registry.yarnpkg.com/cozy-ui/-/cozy-ui-93.1.1.tgz#a09512a53a55a0b8ecab21e0572bbd12678ad128"