cozy-pouch-link 60.15.2 → 60.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,12 @@
1
1
  import MicroEE from 'microee'
2
2
  import { fetchRemoteLastSequence, fetchRemoteInstance } from './remote'
3
3
 
4
- import { replicateAllDocs, startReplication } from './startReplication'
5
- import { insertBulkDocs } from './helpers'
4
+ import {
5
+ replicateAllDocs,
6
+ startReplication,
7
+ sharedDriveReplicateAllDocs
8
+ } from './startReplication'
9
+ import helpers from './helpers'
6
10
 
7
11
  jest.mock('./remote', () => ({
8
12
  fetchRemoteLastSequence: jest.fn(),
@@ -14,6 +18,8 @@ jest.mock('./helpers', () => ({
14
18
  insertBulkDocs: jest.fn()
15
19
  }))
16
20
 
21
+ const { insertBulkDocs } = helpers
22
+
17
23
  const url = 'http://test.local'
18
24
 
19
25
  const generateDocs = nDocs => {
@@ -124,7 +130,8 @@ describe('startReplication', () => {
124
130
  pouch,
125
131
  replicationOptions,
126
132
  getReplicationURL,
127
- storage
133
+ storage,
134
+ null
128
135
  )
129
136
 
130
137
  expect(fetchRemoteInstance).toHaveBeenCalledWith(
@@ -151,7 +158,8 @@ describe('startReplication', () => {
151
158
  pouch,
152
159
  replicationOptions,
153
160
  getReplicationURL,
154
- storage
161
+ storage,
162
+ null
155
163
  )
156
164
  mockReplicationOn.emit('complete')
157
165
  await promise
@@ -179,7 +187,8 @@ describe('startReplication', () => {
179
187
  pouch,
180
188
  replicationOptions,
181
189
  getReplicationURL,
182
- storage
190
+ storage,
191
+ null
183
192
  )
184
193
  mockReplicationOn.emit('complete')
185
194
  await promise
@@ -206,7 +215,8 @@ describe('startReplication', () => {
206
215
  pouch,
207
216
  replicationOptions,
208
217
  getReplicationURL,
209
- storage
218
+ storage,
219
+ null
210
220
  )
211
221
  mockReplicationOn.emit('error', 'some_error_message')
212
222
  await expect(promise).rejects.toEqual('some_error_message')
@@ -225,7 +235,8 @@ describe('startReplication', () => {
225
235
  pouch,
226
236
  replicationOptions,
227
237
  getReplicationURL,
228
- storage
238
+ storage,
239
+ null
229
240
  )
230
241
  // Sync format
231
242
  mockReplicationOn.emit('change', {
@@ -275,7 +286,8 @@ describe('startReplication', () => {
275
286
  pouch,
276
287
  replicationOptions,
277
288
  getReplicationURL,
278
- storage
289
+ storage,
290
+ null
279
291
  )
280
292
  mockReplicationOn.emit('change', {
281
293
  change: {
@@ -315,7 +327,8 @@ describe('startReplication', () => {
315
327
  pouch,
316
328
  replicationOptions,
317
329
  getReplicationURL,
318
- storage
330
+ storage,
331
+ null
319
332
  )
320
333
  mockReplicationOn.emit('change', {
321
334
  change: {
@@ -356,7 +369,8 @@ describe('startReplication', () => {
356
369
  pouch,
357
370
  replicationOptions,
358
371
  getReplicationURL,
359
- storage
372
+ storage,
373
+ null
360
374
  )
361
375
 
362
376
  expect(promise.cancel).toBeDefined()
@@ -385,6 +399,366 @@ describe('startReplication', () => {
385
399
  expect(result).toStrictEqual([])
386
400
  })
387
401
  })
402
+
403
+ describe('sharedDriveReplicateAllDocs', () => {
404
+ const driveId = 'test-drive-123'
405
+ const doctype = 'io.cozy.files'
406
+
407
+ const mockClient = {
408
+ collection: jest.fn()
409
+ }
410
+
411
+ const mockCollection = {
412
+ fetchChanges: jest.fn()
413
+ }
414
+
415
+ const mockPouch = {
416
+ get: jest.fn(),
417
+ remove: jest.fn(),
418
+ bulkGet: jest.fn(),
419
+ bulkDocs: jest.fn()
420
+ }
421
+
422
+ const mockStorage = {
423
+ getLastReplicatedDocID: jest.fn(),
424
+ persistLastReplicatedDocID: jest.fn(),
425
+ getDoctypeLastSequence: jest.fn(),
426
+ persistDoctypeLastSequence: jest.fn()
427
+ }
428
+
429
+ beforeEach(() => {
430
+ jest.resetAllMocks()
431
+ mockClient.collection.mockReturnValue(mockCollection)
432
+ insertBulkDocs.mockResolvedValue()
433
+ })
434
+
435
+ it('should throw error when driveId is not provided', async () => {
436
+ await expect(
437
+ sharedDriveReplicateAllDocs({
438
+ pouch: mockPouch,
439
+ storage: mockStorage,
440
+ doctype,
441
+ client: mockClient,
442
+ initialReplication: false
443
+ })
444
+ ).rejects.toThrow('sharedDriveReplicateAllDocs: driveId is required')
445
+ })
446
+
447
+ it('should handle initial replication with empty results', async () => {
448
+ mockStorage.getDoctypeLastSequence.mockResolvedValue(null)
449
+ mockCollection.fetchChanges.mockResolvedValue({
450
+ newLastSeq: 'seq-1',
451
+ results: [],
452
+ pending: false
453
+ })
454
+
455
+ const result = await sharedDriveReplicateAllDocs({
456
+ driveId,
457
+ pouch: mockPouch,
458
+ storage: mockStorage,
459
+ doctype,
460
+ client: mockClient,
461
+ initialReplication: true
462
+ })
463
+
464
+ expect(result).toEqual([])
465
+ expect(mockCollection.fetchChanges).toHaveBeenCalledWith(
466
+ { include_docs: true },
467
+ {
468
+ includeFilePath: false,
469
+ skipDeleted: true,
470
+ skipTrashed: true,
471
+ limit: 1000
472
+ }
473
+ )
474
+ expect(insertBulkDocs).toHaveBeenCalledWith(mockPouch, [])
475
+ expect(mockStorage.persistDoctypeLastSequence).toHaveBeenCalledWith(
476
+ doctype,
477
+ 'seq-1'
478
+ )
479
+ })
480
+
481
+ it('should handle initial replication with single batch of documents', async () => {
482
+ mockStorage.getDoctypeLastSequence.mockResolvedValue(null)
483
+ const mockDocs = [
484
+ { doc: { _id: 'doc-1', name: 'file1.txt' } },
485
+ { doc: { _id: 'doc-2', name: 'file2.txt' } }
486
+ ]
487
+ mockCollection.fetchChanges.mockResolvedValue({
488
+ newLastSeq: 'seq-2',
489
+ results: mockDocs,
490
+ pending: false
491
+ })
492
+
493
+ const result = await sharedDriveReplicateAllDocs({
494
+ driveId,
495
+ pouch: mockPouch,
496
+ storage: mockStorage,
497
+ doctype,
498
+ client: mockClient,
499
+ initialReplication: true
500
+ })
501
+
502
+ const expectedDocs = [
503
+ { _id: 'doc-1', name: 'file1.txt', driveId },
504
+ { _id: 'doc-2', name: 'file2.txt', driveId }
505
+ ]
506
+
507
+ expect(result).toEqual(expectedDocs)
508
+ expect(mockCollection.fetchChanges).toHaveBeenCalledWith(
509
+ { include_docs: true },
510
+ {
511
+ includeFilePath: false,
512
+ skipDeleted: true,
513
+ skipTrashed: true,
514
+ limit: 1000
515
+ }
516
+ )
517
+ expect(insertBulkDocs).toHaveBeenCalledWith(mockPouch, expectedDocs)
518
+ expect(mockStorage.persistDoctypeLastSequence).toHaveBeenCalledWith(
519
+ doctype,
520
+ 'seq-2'
521
+ )
522
+ })
523
+
524
+ it('should handle incremental replication from last saved doc ID', async () => {
525
+ mockStorage.getDoctypeLastSequence.mockResolvedValue('seq-1')
526
+ const mockDocs = [{ doc: { _id: 'doc-3', name: 'file3.txt' } }]
527
+ mockCollection.fetchChanges.mockResolvedValue({
528
+ newLastSeq: 'seq-3',
529
+ results: mockDocs,
530
+ pending: false
531
+ })
532
+
533
+ const result = await sharedDriveReplicateAllDocs({
534
+ driveId,
535
+ pouch: mockPouch,
536
+ storage: mockStorage,
537
+ doctype,
538
+ client: mockClient,
539
+ initialReplication: false
540
+ })
541
+
542
+ const expectedDocs = [{ _id: 'doc-3', name: 'file3.txt', driveId }]
543
+
544
+ expect(result).toEqual(expectedDocs)
545
+ expect(mockCollection.fetchChanges).toHaveBeenCalledWith(
546
+ { include_docs: true, since: 'seq-1' },
547
+ {
548
+ includeFilePath: false,
549
+ skipDeleted: false,
550
+ skipTrashed: false,
551
+ limit: 1000
552
+ }
553
+ )
554
+ expect(insertBulkDocs).toHaveBeenCalledWith(mockPouch, expectedDocs)
555
+ expect(mockStorage.persistDoctypeLastSequence).toHaveBeenCalledWith(
556
+ doctype,
557
+ 'seq-3'
558
+ )
559
+ })
560
+
561
+ it('should handle replication with multiple batches (pagination)', async () => {
562
+ mockStorage.getDoctypeLastSequence.mockResolvedValue(null)
563
+
564
+ // First batch
565
+ mockCollection.fetchChanges.mockResolvedValueOnce({
566
+ newLastSeq: 'seq-1',
567
+ results: [{ doc: { _id: 'doc-1', name: 'file1.txt' } }],
568
+ pending: true
569
+ })
570
+
571
+ // Second batch
572
+ mockCollection.fetchChanges.mockResolvedValueOnce({
573
+ newLastSeq: 'seq-2',
574
+ results: [{ doc: { _id: 'doc-2', name: 'file2.txt' } }],
575
+ pending: false
576
+ })
577
+
578
+ const result = await sharedDriveReplicateAllDocs({
579
+ driveId,
580
+ pouch: mockPouch,
581
+ storage: mockStorage,
582
+ doctype,
583
+ client: mockClient,
584
+ initialReplication: true
585
+ })
586
+
587
+ const expectedDocs = [
588
+ { _id: 'doc-1', name: 'file1.txt', driveId },
589
+ { _id: 'doc-2', name: 'file2.txt', driveId }
590
+ ]
591
+
592
+ expect(result).toEqual(expectedDocs)
593
+ expect(mockCollection.fetchChanges).toHaveBeenCalledTimes(2)
594
+ expect(insertBulkDocs).toHaveBeenCalledTimes(2)
595
+ expect(mockStorage.persistDoctypeLastSequence).toHaveBeenCalledTimes(2)
596
+ })
597
+
598
+ it('should handle deleted documents by removing them from local pouch', async () => {
599
+ mockStorage.getDoctypeLastSequence.mockResolvedValue(null)
600
+ const mockDocs = [
601
+ { doc: { _id: 'doc-1', name: 'file1.txt' } },
602
+ { doc: { _id: 'doc-2', name: 'file2.txt', _deleted: true } }
603
+ ]
604
+ mockCollection.fetchChanges.mockResolvedValue({
605
+ newLastSeq: 'seq-1',
606
+ results: mockDocs,
607
+ pending: false
608
+ })
609
+
610
+ // Mock that doc-2 exists in local pouch
611
+ mockPouch.bulkGet.mockResolvedValue({
612
+ results: [
613
+ {
614
+ docs: [
615
+ {
616
+ ok: { _id: 'doc-2', _rev: '1-abc', name: 'file2.txt' }
617
+ }
618
+ ]
619
+ }
620
+ ]
621
+ })
622
+ mockPouch.bulkDocs.mockResolvedValue([{ ok: true, id: 'doc-2' }])
623
+
624
+ const result = await sharedDriveReplicateAllDocs({
625
+ driveId,
626
+ pouch: mockPouch,
627
+ storage: mockStorage,
628
+ doctype,
629
+ client: mockClient,
630
+ initialReplication: true
631
+ })
632
+
633
+ const expectedDocs = [
634
+ { _id: 'doc-1', name: 'file1.txt', driveId },
635
+ { _id: 'doc-2', name: 'file2.txt', _deleted: true, driveId }
636
+ ]
637
+
638
+ expect(result).toEqual(expectedDocs)
639
+ expect(mockPouch.bulkGet).toHaveBeenCalledWith({
640
+ docs: [{ id: 'doc-2' }]
641
+ })
642
+ expect(mockPouch.bulkDocs).toHaveBeenCalledWith([
643
+ {
644
+ _id: 'doc-2',
645
+ _rev: '1-abc',
646
+ name: 'file2.txt',
647
+ _deleted: true
648
+ }
649
+ ])
650
+ expect(insertBulkDocs).toHaveBeenCalledWith(
651
+ mockPouch,
652
+ expectedDocs.filter(doc => !doc._deleted)
653
+ )
654
+ })
655
+
656
+ it('should handle deleted documents when they do not exist in local pouch', async () => {
657
+ mockStorage.getDoctypeLastSequence.mockResolvedValue(null)
658
+ const mockDocs = [{ doc: { _id: 'doc-1', _deleted: true } }]
659
+ mockCollection.fetchChanges.mockResolvedValue({
660
+ newLastSeq: 'seq-1',
661
+ results: mockDocs,
662
+ pending: false
663
+ })
664
+
665
+ // Mock that doc-1 does not exist in local pouch (bulkGet returns error)
666
+ mockPouch.bulkGet.mockResolvedValue({
667
+ results: [
668
+ {
669
+ docs: [
670
+ {
671
+ error: {
672
+ status: 404,
673
+ name: 'not_found',
674
+ message: 'missing'
675
+ }
676
+ }
677
+ ]
678
+ }
679
+ ]
680
+ })
681
+ mockPouch.bulkDocs.mockResolvedValue([])
682
+
683
+ const result = await sharedDriveReplicateAllDocs({
684
+ driveId,
685
+ pouch: mockPouch,
686
+ storage: mockStorage,
687
+ doctype,
688
+ client: mockClient,
689
+ initialReplication: true
690
+ })
691
+
692
+ const expectedDocs = [{ _id: 'doc-1', _deleted: true, driveId }]
693
+
694
+ expect(result).toEqual(expectedDocs)
695
+ expect(mockPouch.bulkGet).toHaveBeenCalledWith({
696
+ docs: [{ id: 'doc-1' }]
697
+ })
698
+ expect(mockPouch.bulkDocs).not.toHaveBeenCalled()
699
+ expect(insertBulkDocs).toHaveBeenCalledWith(
700
+ mockPouch,
701
+ expectedDocs.filter(doc => !doc._deleted)
702
+ )
703
+ })
704
+
705
+ it('should use correct options for initial vs incremental replication', async () => {
706
+ mockStorage.getDoctypeLastSequence.mockResolvedValue('seq-1')
707
+ mockCollection.fetchChanges.mockResolvedValue({
708
+ newLastSeq: 'seq-2',
709
+ results: [],
710
+ pending: false
711
+ })
712
+
713
+ // Test initial replication
714
+ await sharedDriveReplicateAllDocs({
715
+ driveId,
716
+ pouch: mockPouch,
717
+ storage: mockStorage,
718
+ doctype,
719
+ client: mockClient,
720
+ initialReplication: true
721
+ })
722
+
723
+ expect(mockCollection.fetchChanges).toHaveBeenCalledWith(
724
+ { include_docs: true, since: 'seq-1' },
725
+ {
726
+ includeFilePath: false,
727
+ skipDeleted: true,
728
+ skipTrashed: true,
729
+ limit: 1000
730
+ }
731
+ )
732
+
733
+ jest.clearAllMocks()
734
+ mockStorage.getDoctypeLastSequence.mockResolvedValue('seq-1')
735
+ mockCollection.fetchChanges.mockResolvedValue({
736
+ newLastSeq: 'seq-3',
737
+ results: [],
738
+ pending: false
739
+ })
740
+
741
+ // Test incremental replication
742
+ await sharedDriveReplicateAllDocs({
743
+ driveId,
744
+ pouch: mockPouch,
745
+ storage: mockStorage,
746
+ doctype,
747
+ client: mockClient,
748
+ initialReplication: false
749
+ })
750
+
751
+ expect(mockCollection.fetchChanges).toHaveBeenCalledWith(
752
+ { include_docs: true, since: 'seq-1' },
753
+ {
754
+ includeFilePath: false,
755
+ skipDeleted: false,
756
+ skipTrashed: false,
757
+ limit: 1000
758
+ }
759
+ )
760
+ })
761
+ })
388
762
  })
389
763
 
390
764
  const getPouchMock = () => {
@@ -405,6 +779,6 @@ const getPouchMock = () => {
405
779
  const getReplicationOptionsMock = () => ({
406
780
  strategy: 'fromRemote',
407
781
  initialReplication: false,
408
- warmupQueries: {},
782
+ warmupQueries: [],
409
783
  doctype: 'io.cozy.files'
410
784
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-pouch-link",
3
- "version": "60.15.2",
3
+ "version": "60.16.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "types/index.d.ts",
@@ -41,5 +41,5 @@
41
41
  "typecheck": "tsc -p tsconfig.json"
42
42
  },
43
43
  "sideEffects": false,
44
- "gitHead": "34f198a7cb619bdfba442fe94c3b41beec424e6a"
44
+ "gitHead": "fef043f6cf4ee1bb86f73388a33a7afce8d66b38"
45
45
  }
@@ -1,4 +1,6 @@
1
- export function getReplicationURL(uri: any, token: any, doctype: any): string;
1
+ export function getReplicationURL(uri: string, token: any, doctype: string, replicationOptions?: {
2
+ driveId: string;
3
+ }): string;
2
4
  export default PouchLink;
3
5
  export type CozyPouchDocument = any;
4
6
  export type ReplicationStatus = "idle" | "replicating";
@@ -96,7 +98,17 @@ declare class PouchLink extends CozyLink {
96
98
  private startReplicationDebounced;
97
99
  /** @type {import('cozy-client/src/performances/types').PerformanceAPI} */
98
100
  performanceApi: any;
99
- getReplicationURL(doctype: any): string;
101
+ /**
102
+ * Get the authenticated replication URL for a specific doctype
103
+ *
104
+ * @param {string} doctype - The document type to replicate (e.g., 'io.cozy.files')
105
+ * @param {object} [replicationOptions={}] - Replication options
106
+ * @param {string} [replicationOptions.driveId] - The ID of the shared drive to replicate (for shared drives)
107
+ * @returns {string} The authenticated replication URL
108
+ */
109
+ getReplicationURL(doctype: string, replicationOptions?: {
110
+ driveId: string;
111
+ }): string;
100
112
  registerClient(client: any): Promise<void>;
101
113
  client: any;
102
114
  /**
@@ -239,6 +251,26 @@ declare class PouchLink extends CozyLink {
239
251
  addReferencesTo(mutation: any): Promise<void>;
240
252
  dbMethod(method: any, mutation: any): Promise<any>;
241
253
  syncImmediately(): Promise<void>;
254
+ /**
255
+ * Adds a new doctype to the list of managed doctypes, sets its replication options,
256
+ * adds it to the pouches, and starts replication.
257
+ *
258
+ * @param {string} doctype - The name of the doctype to add.
259
+ * @param {Object} replicationOptions - The replication options for the doctype.
260
+ * @param {Object} options - The replication options for the doctype.
261
+ * @param {boolean} [options.shouldStartReplication=true] - Whether the replication should be started.
262
+ */
263
+ addDoctype(doctype: string, replicationOptions: any, options: {
264
+ shouldStartReplication: boolean;
265
+ }): Promise<void>;
266
+ /**
267
+ * Removes a doctype from the list of managed doctypes, deletes its replication options,
268
+ * and removes it from the pouches.
269
+ *
270
+ * @param {string} doctype - The name of the doctype to remove.
271
+ */
272
+ removeDoctype(doctype: string): Promise<void>;
273
+ getSharedDriveDoctypes(): string[];
242
274
  }
243
275
  import { CozyLink } from "cozy-client";
244
276
  import { PouchLocalStorage } from "./localStorage";
@@ -17,12 +17,12 @@ declare class PouchManager {
17
17
  events: any;
18
18
  dbQueryEngines: Map<any, any>;
19
19
  init(): Promise<void>;
20
- pouches: import("lodash").Dictionary<any>;
20
+ pouches: {};
21
+ doctypesReplicationOptions: any;
21
22
  /** @type {Record<string, import('./types').SyncInfo>} - Stores synchronization info per doctype */
22
23
  syncedDoctypes: Record<string, import('./types').SyncInfo>;
23
24
  warmedUpQueries: any;
24
25
  getReplicationURL: any;
25
- doctypesReplicationOptions: any;
26
26
  listenerLaunched: boolean;
27
27
  ensureDatabasesExistDone: boolean;
28
28
  /**
@@ -94,6 +94,32 @@ declare class PouchManager {
94
94
  checkToWarmupDoctype(doctype: any, replicationOptions: any): void;
95
95
  areQueriesWarmedUp(doctype: any, queries: any): Promise<any>;
96
96
  clearWarmedUpQueries(): Promise<void>;
97
+ /**
98
+ * Adds a new doctype to the list of managed doctypes, sets its replication options,
99
+ * creates a new PouchDB instance for it, and sets up the query engine.
100
+ *
101
+ * @param {string} doctype - The name of the doctype to add.
102
+ * @param {Object} replicationOptions - The replication options for the doctype.
103
+ */
104
+ addDoctype(doctype: string, replicationOptions: any): Promise<void>;
105
+ /**
106
+ * Removes a doctype from the list of managed doctypes, deletes its replication options,
107
+ * destroys its PouchDB instance, and removes it from the pouches.
108
+ *
109
+ * @param {string} doctype - The name of the doctype to remove.
110
+ */
111
+ removeDoctype(doctype: string): Promise<void>;
112
+ /**
113
+ * Persists the names of the PouchDB databases.
114
+ *
115
+ * This method is primarily used to ensure that database names are saved for
116
+ * old browsers that do not support `indexeddb.databases()`. This persistence
117
+ * facilitates cleanup processes. Note that PouchDB automatically adds the
118
+ * `_pouch_` prefix to database names.
119
+ *
120
+ * @returns {Promise<void>}
121
+ */
122
+ persistDatabasesNames(): Promise<void>;
97
123
  }
98
124
  import { PouchLocalStorage } from "./localStorage";
99
125
  import Loop from "./loop";
@@ -1,12 +1,22 @@
1
1
  export function startReplication(pouch: object, replicationOptions: {
2
2
  strategy: string;
3
3
  initialReplication: boolean;
4
+ driveId: string;
4
5
  doctype: string;
5
6
  warmupQueries: import('cozy-client/types/types').Query[];
6
- }, getReplicationURL: Function, storage: import('./localStorage').PouchLocalStorage): import('./types').CancelablePromise;
7
+ }, getReplicationURL: Function, storage: import('./localStorage').PouchLocalStorage, client: CozyClient): import('./types').CancelablePromise;
7
8
  export function replicateAllDocs({ db, baseUrl, doctype, storage }: {
8
9
  db: object;
9
10
  baseUrl: string;
10
11
  doctype: string;
11
12
  storage: import('./localStorage').PouchLocalStorage;
12
13
  }): Promise<any[]>;
14
+ export function sharedDriveReplicateAllDocs({ driveId, pouch, storage, initialReplication, doctype, client }: {
15
+ driveId: string;
16
+ pouch: any;
17
+ storage: any;
18
+ doctype: string;
19
+ initialReplication: boolean;
20
+ client: CozyClient;
21
+ }): Promise<any[]>;
22
+ import CozyClient from "cozy-client";