document-drive 1.0.0-alpha.74 → 1.0.0-alpha.77

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.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # Document Drive
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "1.0.0-alpha.74",
3
+ "version": "1.0.0-alpha.77",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -18,18 +18,27 @@ import {
18
18
  Document,
19
19
  DocumentHeader,
20
20
  DocumentModel,
21
+ utils as DocumentUtils,
21
22
  Operation,
22
- OperationScope,
23
- utils as DocumentUtils
23
+ OperationScope
24
24
  } from 'document-model/document';
25
25
  import { createNanoEvents, Unsubscribe } from 'nanoevents';
26
26
  import { ICache } from '../cache';
27
27
  import InMemoryCache from '../cache/memory';
28
+ import { BaseQueueManager } from '../queue/base';
29
+ import {
30
+ ActionJob,
31
+ IQueueManager,
32
+ isActionJob,
33
+ isOperationJob,
34
+ Job,
35
+ OperationJob
36
+ } from '../queue/types';
28
37
  import { MemoryStorage } from '../storage/memory';
29
38
  import type {
30
39
  DocumentDriveStorage,
31
40
  DocumentStorage,
32
- IDriveStorage,
41
+ IDriveStorage
33
42
  } from '../storage/types';
34
43
  import { generateUUID, isBefore, isDocumentDrive } from '../utils';
35
44
  import {
@@ -70,8 +79,6 @@ import {
70
79
  type SynchronizationUnit
71
80
  } from './types';
72
81
  import { filterOperationsByRevision } from './utils';
73
- import { BaseQueueManager } from '../queue/base';
74
- import { ActionJob, IQueueManager, isActionJob, isOperationJob, Job, OperationJob } from '../queue/types';
75
82
 
76
83
  export * from './listener';
77
84
  export type * from './types';
@@ -96,7 +103,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
96
103
  documentModels: DocumentModel[],
97
104
  storage: IDriveStorage = new MemoryStorage(),
98
105
  cache: ICache = new InMemoryCache(),
99
- queueManager: IQueueManager = new BaseQueueManager(),
106
+ queueManager: IQueueManager = new BaseQueueManager()
100
107
  ) {
101
108
  super();
102
109
  this.listenerStateManager = new ListenerManager(this);
@@ -115,7 +122,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
115
122
  return undefined;
116
123
  }
117
124
  }
118
- })
125
+ });
119
126
  }
120
127
 
121
128
  private updateSyncStatus(
@@ -132,26 +139,24 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
132
139
  }
133
140
 
134
141
  private async saveStrand(strand: StrandUpdate) {
135
- const operations: Operation[] = strand.operations.map(
136
- (op) => ({
137
- ...op,
138
- scope: strand.scope,
139
- branch: strand.branch
140
- })
141
- );
142
+ const operations: Operation[] = strand.operations.map(op => ({
143
+ ...op,
144
+ scope: strand.scope,
145
+ branch: strand.branch
146
+ }));
142
147
 
143
148
  const result = await (!strand.documentId
144
149
  ? this.queueDriveOperations(
145
- strand.driveId,
146
- operations as Operation<DocumentDriveAction | BaseAction>[],
147
- false
148
- )
150
+ strand.driveId,
151
+ operations as Operation<DocumentDriveAction | BaseAction>[],
152
+ false
153
+ )
149
154
  : this.queueOperations(
150
- strand.driveId,
151
- strand.documentId,
152
- operations,
153
- false
154
- ));
155
+ strand.driveId,
156
+ strand.documentId,
157
+ operations,
158
+ false
159
+ ));
155
160
 
156
161
  if (result.status === 'ERROR') {
157
162
  this.updateSyncStatus(strand.driveId, result.status, result.error);
@@ -269,12 +274,40 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
269
274
  }
270
275
 
271
276
  private queueDelegate = {
272
- checkDocumentExists: (driveId: string, documentId: string): Promise<boolean> => this.storage.checkDocumentExists(driveId, documentId),
273
- processOperationJob: async ({ driveId, documentId, operations, forceSync }: OperationJob) => {
274
- return documentId ? this.addOperations(driveId, documentId, operations, forceSync) : this.addDriveOperations(driveId, operations as (Operation<DocumentDriveAction | BaseAction>)[], forceSync)
277
+ checkDocumentExists: (
278
+ driveId: string,
279
+ documentId: string
280
+ ): Promise<boolean> =>
281
+ this.storage.checkDocumentExists(driveId, documentId),
282
+ processOperationJob: async ({
283
+ driveId,
284
+ documentId,
285
+ operations,
286
+ forceSync
287
+ }: OperationJob) => {
288
+ return documentId
289
+ ? this.addOperations(driveId, documentId, operations, forceSync)
290
+ : this.addDriveOperations(
291
+ driveId,
292
+ operations as Operation<
293
+ DocumentDriveAction | BaseAction
294
+ >[],
295
+ forceSync
296
+ );
275
297
  },
276
- processActionJob: async ({ driveId, documentId, actions, forceSync }: ActionJob) => {
277
- return documentId ? this.addActions(driveId, documentId, actions, forceSync) : this.addDriveActions(driveId, actions as (Operation<DocumentDriveAction | BaseAction>)[], forceSync)
298
+ processActionJob: async ({
299
+ driveId,
300
+ documentId,
301
+ actions,
302
+ forceSync
303
+ }: ActionJob) => {
304
+ return documentId
305
+ ? this.addActions(driveId, documentId, actions, forceSync)
306
+ : this.addDriveActions(
307
+ driveId,
308
+ actions as Operation<DocumentDriveAction | BaseAction>[],
309
+ forceSync
310
+ );
278
311
  },
279
312
  processJob: async (job: Job) => {
280
313
  if (isOperationJob(job)) {
@@ -282,7 +315,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
282
315
  } else if (isActionJob(job)) {
283
316
  return this.queueDelegate.processActionJob(job);
284
317
  } else {
285
- throw new Error("Unknown job type", job);
318
+ throw new Error('Unknown job type', job);
286
319
  }
287
320
  }
288
321
  };
@@ -300,7 +333,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
300
333
  await this.queueManager.init(this.queueDelegate, error => {
301
334
  logger.error(`Error initializing queue manager`, error);
302
335
  errors.push(error);
303
- })
336
+ });
304
337
 
305
338
  // if network connect comes online then
306
339
  // triggers the listeners update
@@ -339,20 +372,35 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
339
372
  ) {
340
373
  const drive = await this.getDrive(driveId);
341
374
 
342
- const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(driveId, documentId, scope, branch, documentType, drive);
343
- const revisions = await this.storage.getSynchronizationUnitsRevision(synchronizationUnitsQuery);
375
+ const synchronizationUnitsQuery = await this.getSynchronizationUnitsIds(
376
+ driveId,
377
+ documentId,
378
+ scope,
379
+ branch,
380
+ documentType,
381
+ drive
382
+ );
383
+ const revisions = await this.storage.getSynchronizationUnitsRevision(
384
+ synchronizationUnitsQuery
385
+ );
344
386
 
345
- const synchronizationUnits: SynchronizationUnit[] = synchronizationUnitsQuery.map(s => ({ ...s, lastUpdated: drive.created, revision: -1 }));
387
+ const synchronizationUnits: SynchronizationUnit[] =
388
+ synchronizationUnitsQuery.map(s => ({
389
+ ...s,
390
+ lastUpdated: drive.created,
391
+ revision: -1
392
+ }));
346
393
  for (const revision of revisions) {
347
- const syncUnit = synchronizationUnits.find(s =>
348
- revision.driveId === s.driveId &&
349
- revision.documentId === s.documentId &&
350
- revision.scope === s.scope &&
351
- revision.branch === s.branch
394
+ const syncUnit = synchronizationUnits.find(
395
+ s =>
396
+ revision.driveId === s.driveId &&
397
+ revision.documentId === s.documentId &&
398
+ revision.scope === s.scope &&
399
+ revision.branch === s.branch
352
400
  );
353
401
  if (syncUnit) {
354
402
  syncUnit.revision = revision.revision;
355
- syncUnit.lastUpdated = revision.lastUpdated
403
+ syncUnit.lastUpdated = revision.lastUpdated;
356
404
  }
357
405
  }
358
406
  return synchronizationUnits;
@@ -366,7 +414,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
366
414
  documentType?: string[],
367
415
  loadedDrive?: DocumentDriveDocument
368
416
  ): Promise<SynchronizationUnitQuery[]> {
369
- const drive = loadedDrive ?? await this.getDrive(driveId);
417
+ const drive = loadedDrive ?? (await this.getDrive(driveId));
370
418
  const nodes = drive.state.global.nodes.filter(
371
419
  node =>
372
420
  isFileNode(node) &&
@@ -400,31 +448,36 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
400
448
  });
401
449
  }
402
450
 
403
- const synchronizationUnitsQuery: Omit<SynchronizationUnit, "revision" | "lastUpdated">[] = [];
451
+ const synchronizationUnitsQuery: Omit<
452
+ SynchronizationUnit,
453
+ 'revision' | 'lastUpdated'
454
+ >[] = [];
404
455
  for (const node of nodes) {
405
456
  const nodeUnits =
406
457
  scope?.length || branch?.length
407
458
  ? node.synchronizationUnits.filter(
408
- unit =>
409
- (!scope?.length ||
410
- scope.includes(unit.scope) ||
411
- scope.includes('*')) &&
412
- (!branch?.length ||
413
- branch.includes(unit.branch) ||
414
- branch.includes('*'))
415
- )
459
+ unit =>
460
+ (!scope?.length ||
461
+ scope.includes(unit.scope) ||
462
+ scope.includes('*')) &&
463
+ (!branch?.length ||
464
+ branch.includes(unit.branch) ||
465
+ branch.includes('*'))
466
+ )
416
467
  : node.synchronizationUnits;
417
468
  if (!nodeUnits.length) {
418
469
  continue;
419
470
  }
420
- synchronizationUnitsQuery.push(...nodeUnits.map(n => ({
421
- driveId,
422
- documentId: node.id,
423
- syncId: n.syncId,
424
- documentType: node.documentType,
425
- scope: n.scope,
426
- branch: n.branch
427
- })));
471
+ synchronizationUnitsQuery.push(
472
+ ...nodeUnits.map(n => ({
473
+ driveId,
474
+ documentId: node.id,
475
+ syncId: n.syncId,
476
+ documentType: node.documentType,
477
+ scope: n.scope,
478
+ branch: n.branch
479
+ }))
480
+ );
428
481
  }
429
482
  return synchronizationUnitsQuery;
430
483
  }
@@ -458,7 +511,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
458
511
  branch: syncUnit.branch,
459
512
  driveId,
460
513
  documentId: node.id,
461
- documentType: node.documentType,
514
+ documentType: node.documentType
462
515
  };
463
516
  }
464
517
 
@@ -466,7 +519,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
466
519
  driveId: string,
467
520
  syncId: string
468
521
  ): Promise<SynchronizationUnit | undefined> {
469
- const syncUnit = await this.getSynchronizationUnitIdInfo(driveId, syncId);
522
+ const syncUnit = await this.getSynchronizationUnitIdInfo(
523
+ driveId,
524
+ syncId
525
+ );
470
526
 
471
527
  if (!syncUnit) {
472
528
  return undefined;
@@ -513,7 +569,8 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
513
569
  ? await this.getDrive(driveId)
514
570
  : await this.getDocument(driveId, syncUnit.documentId); // TODO replace with getDocumentOperations
515
571
 
516
- const operations = document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
572
+ const operations =
573
+ document.operations[syncUnit.scope as OperationScope] ?? []; // TODO filter by branch also
517
574
  const filteredOperations = operations.filter(
518
575
  operation =>
519
576
  Object.keys(filter).length === 0 ||
@@ -563,7 +620,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
563
620
  await this.storage.createDrive(id, document);
564
621
 
565
622
  if (drive.global.slug) {
566
- await this.cache.deleteDocument("drives-slug", drive.global.slug)
623
+ await this.cache.deleteDocument('drives-slug', drive.global.slug);
567
624
  }
568
625
 
569
626
  await this._initializeDrive(id);
@@ -571,7 +628,10 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
571
628
  return document;
572
629
  }
573
630
 
574
- async addRemoteDrive(url: string, options: RemoteDriveOptions): Promise<DocumentDriveDocument> {
631
+ async addRemoteDrive(
632
+ url: string,
633
+ options: RemoteDriveOptions
634
+ ): Promise<DocumentDriveDocument> {
575
635
  const { id, name, slug, icon } = await requestPublicDrive(url);
576
636
  const {
577
637
  pullFilter,
@@ -613,7 +673,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
613
673
  ]);
614
674
 
615
675
  result.forEach(r => {
616
- if (r.status === "rejected") {
676
+ if (r.status === 'rejected') {
617
677
  throw r.reason;
618
678
  }
619
679
  });
@@ -625,7 +685,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
625
685
 
626
686
  async getDrive(drive: string, options?: GetDocumentOptions) {
627
687
  try {
628
- const document = await this.cache.getDocument('drives', drive);
688
+ const document = await this.cache.getDocument('drives', drive); // TODO support GetDocumentOptions
629
689
  if (document && isDocumentDrive(document)) {
630
690
  return document;
631
691
  }
@@ -672,16 +732,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
672
732
 
673
733
  async getDocument(drive: string, id: string, options?: GetDocumentOptions) {
674
734
  try {
675
- const document = await this.cache.getDocument(drive, id);
735
+ const document = await this.cache.getDocument(drive, id); // TODO support GetDocumentOptions
676
736
  if (document) {
677
737
  return document;
678
738
  }
679
739
  } catch (e) {
680
740
  logger.error('Error getting document from cache', e);
681
741
  }
682
- const documentStorage =
683
- await this.storage.getDocument(drive, id);
684
- const document = this._buildDocument(documentStorage, options)
742
+ const documentStorage = await this.storage.getDocument(drive, id);
743
+ const document = this._buildDocument(documentStorage, options);
685
744
 
686
745
  this.cache.setDocument(drive, id, document).catch(logger.error);
687
746
  return document;
@@ -699,14 +758,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
699
758
  let state = undefined;
700
759
  if (input.document) {
701
760
  if (input.documentType !== input.document.documentType) {
702
- throw new Error(`Provided document is not ${input.documentType}`);
761
+ throw new Error(
762
+ `Provided document is not ${input.documentType}`
763
+ );
703
764
  }
704
765
  const doc = this._buildDocument(input.document);
705
766
  state = doc.state;
706
767
  }
707
768
 
708
769
  // if no document was provided then create a new one
709
- const document = input.document ??
770
+ const document =
771
+ input.document ??
710
772
  this._getDocumentModel(input.documentType).utils.createDocument();
711
773
 
712
774
  // stores document information
@@ -728,9 +790,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
728
790
  const operations = Object.values(document.operations).flat();
729
791
  if (operations.length) {
730
792
  if (isDocumentDrive(document)) {
731
- await this.storage.addDriveOperations(driveId, operations as Operation<DocumentDriveAction>[], document);
793
+ await this.storage.addDriveOperations(
794
+ driveId,
795
+ operations as Operation<DocumentDriveAction>[],
796
+ document
797
+ );
732
798
  } else {
733
- await this.storage.addDocumentOperations(driveId, input.id, operations, document)
799
+ await this.storage.addDocumentOperations(
800
+ driveId,
801
+ input.id,
802
+ operations,
803
+ document
804
+ );
734
805
  }
735
806
  }
736
807
 
@@ -739,7 +810,9 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
739
810
 
740
811
  async deleteDocument(driveId: string, id: string) {
741
812
  try {
742
- const syncUnits = await this.getSynchronizationUnitsIds(driveId, [id]);
813
+ const syncUnits = await this.getSynchronizationUnitsIds(driveId, [
814
+ id
815
+ ]);
743
816
  await this.listenerStateManager.removeSyncUnits(driveId, syncUnits);
744
817
  } catch (error) {
745
818
  logger.warn('Error deleting document', error);
@@ -788,7 +861,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
788
861
  : merge(trunk, invertedTrunk, reshuffleByTimestamp);
789
862
 
790
863
  const newOperations = newHistory.filter(
791
- (op) => trunk.length < 1 || precedes(trunk[trunk.length - 1]!, op)
864
+ op => trunk.length < 1 || precedes(trunk[trunk.length - 1]!, op)
792
865
  );
793
866
 
794
867
  for (const nextOperation of newOperations) {
@@ -823,11 +896,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
823
896
  e instanceof OperationError
824
897
  ? e
825
898
  : new OperationError(
826
- 'ERROR',
827
- nextOperation,
828
- (e as Error).message,
829
- (e as Error).cause
830
- );
899
+ 'ERROR',
900
+ nextOperation,
901
+ (e as Error).message,
902
+ (e as Error).cause
903
+ );
831
904
 
832
905
  // TODO: don't break on errors...
833
906
  break;
@@ -839,14 +912,18 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
839
912
  document,
840
913
  operationsApplied,
841
914
  signals,
842
- error,
915
+ error
843
916
  } as const;
844
917
  }
845
918
 
846
919
  private _buildDocument<T extends Document>(
847
- documentStorage: DocumentStorage<T>, options?: GetDocumentOptions
920
+ documentStorage: DocumentStorage<T>,
921
+ options?: GetDocumentOptions
848
922
  ): T {
849
- if (documentStorage.state && (!options || options.checkHashes === false)) {
923
+ if (
924
+ documentStorage.state &&
925
+ (!options || options.checkHashes === false)
926
+ ) {
850
927
  return documentStorage as T;
851
928
  }
852
929
 
@@ -854,11 +931,17 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
854
931
  documentStorage.documentType
855
932
  );
856
933
 
857
- const revisionOperations = options?.revisions !== undefined ? filterOperationsByRevision(
858
- documentStorage.operations,
859
- options.revisions
860
- ) : documentStorage.operations;
861
- const operations = baseUtils.documentHelpers.garbageCollectDocumentOperations(revisionOperations);
934
+ const revisionOperations =
935
+ options?.revisions !== undefined
936
+ ? filterOperationsByRevision(
937
+ documentStorage.operations,
938
+ options.revisions
939
+ )
940
+ : documentStorage.operations;
941
+ const operations =
942
+ baseUtils.documentHelpers.garbageCollectDocumentOperations(
943
+ revisionOperations
944
+ );
862
945
 
863
946
  return baseUtils.replayDocument(
864
947
  documentStorage.initialState,
@@ -888,22 +971,33 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
888
971
  let newDocument = document;
889
972
 
890
973
  const scope = operation.scope;
891
- const documentOperations = DocumentUtils.documentHelpers.garbageCollectDocumentOperations(
892
- {
974
+ const documentOperations =
975
+ DocumentUtils.documentHelpers.garbageCollectDocumentOperations({
893
976
  ...document.operations,
894
977
  [scope]: DocumentUtils.documentHelpers.skipHeaderOperations(
895
978
  document.operations[scope],
896
- operation,
897
- ),
898
- },
899
- );
979
+ operation
980
+ )
981
+ });
900
982
 
901
983
  const lastRemainingOperation = documentOperations[scope].at(-1);
902
984
  // if the latest operation doesn't have a resulting state then tries
903
985
  // to retrieve it from the db to avoid rerunning all the operations
904
986
  if (lastRemainingOperation && !lastRemainingOperation.resultingState) {
905
- lastRemainingOperation.resultingState = await (id ? this.storage.getOperationResultingState?.(drive, id, lastRemainingOperation.index, lastRemainingOperation.scope, "main") :
906
- this.storage.getDriveOperationResultingState?.(drive, lastRemainingOperation.index, lastRemainingOperation.scope, "main"))
987
+ lastRemainingOperation.resultingState = await (id
988
+ ? this.storage.getOperationResultingState?.(
989
+ drive,
990
+ id,
991
+ lastRemainingOperation.index,
992
+ lastRemainingOperation.scope,
993
+ 'main'
994
+ )
995
+ : this.storage.getDriveOperationResultingState?.(
996
+ drive,
997
+ lastRemainingOperation.index,
998
+ lastRemainingOperation.scope,
999
+ 'main'
1000
+ ));
907
1001
  }
908
1002
 
909
1003
  const operationSignals: (() => Promise<SignalResult>)[] = [];
@@ -959,10 +1053,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
959
1053
  appliedOperation[0]!.hash !== operation.hash &&
960
1054
  !skipHashValidation
961
1055
  ) {
962
- throw new ConflictOperationError(
963
- operation,
964
- appliedOperation[0]!
965
- );
1056
+ throw new ConflictOperationError(operation, appliedOperation[0]!);
966
1057
  }
967
1058
 
968
1059
  for (const signalHandler of operationSignals) {
@@ -977,7 +1068,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
977
1068
  };
978
1069
  }
979
1070
 
980
- addOperation(drive: string, id: string, operation: Operation, forceSync = true): Promise<IOperationResult> {
1071
+ addOperation(
1072
+ drive: string,
1073
+ id: string,
1074
+ operation: Operation,
1075
+ forceSync = true
1076
+ ): Promise<IOperationResult> {
981
1077
  return this.addOperations(drive, id, [operation], forceSync);
982
1078
  }
983
1079
 
@@ -993,14 +1089,12 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
993
1089
  const documentStorage = await this.storage.getDocument(drive, id);
994
1090
  const result = await callback(documentStorage);
995
1091
  // saves the applied operations to storage
996
- if (
997
- result.operations.length > 0
998
- ) {
1092
+ if (result.operations.length > 0) {
999
1093
  await this.storage.addDocumentOperations(
1000
1094
  drive,
1001
1095
  id,
1002
1096
  result.operations,
1003
- result.header,
1097
+ result.header
1004
1098
  );
1005
1099
  }
1006
1100
  } else {
@@ -1012,93 +1106,196 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1012
1106
  }
1013
1107
  }
1014
1108
 
1015
- queueOperation(drive: string, id: string, operation: Operation, forceSync = true): Promise<IOperationResult> {
1109
+ queueOperation(
1110
+ drive: string,
1111
+ id: string,
1112
+ operation: Operation,
1113
+ forceSync = true
1114
+ ): Promise<IOperationResult> {
1016
1115
  return this.queueOperations(drive, id, [operation], forceSync);
1017
1116
  }
1018
1117
 
1019
- async queueOperations(drive: string,
1118
+ private async resultIfExistingOperations(
1119
+ drive: string,
1020
1120
  id: string,
1021
- operations: Operation[],
1022
- forceSync = true) {
1121
+ operations: Operation[]
1122
+ ): Promise<IOperationResult | undefined> {
1123
+ try {
1124
+ const document = await this.getDocument(drive, id);
1125
+ const newOperation = operations.find(
1126
+ op =>
1127
+ !op.id ||
1128
+ !document.operations[op.scope].find(
1129
+ existingOp =>
1130
+ existingOp.id === op.id &&
1131
+ existingOp.index === op.index &&
1132
+ existingOp.type === op.type &&
1133
+ existingOp.hash === op.hash
1134
+ )
1135
+ );
1136
+ if (!newOperation) {
1137
+ return {
1138
+ status: 'SUCCESS',
1139
+ document,
1140
+ operations,
1141
+ signals: []
1142
+ };
1143
+ } else {
1144
+ return undefined;
1145
+ }
1146
+ } catch (error) {
1147
+ console.error(error); // TODO error
1148
+ return undefined;
1149
+ }
1150
+ }
1023
1151
 
1152
+ async queueOperations(
1153
+ drive: string,
1154
+ id: string,
1155
+ operations: Operation[],
1156
+ forceSync = true
1157
+ ) {
1158
+ // if operations are already stored then returns cached document
1159
+ const result = await this.resultIfExistingOperations(
1160
+ drive,
1161
+ id,
1162
+ operations
1163
+ );
1164
+ if (result) {
1165
+ console.log('Duplicated operations!');
1166
+ return result;
1167
+ }
1024
1168
  try {
1025
- const jobId = await this.queueManager.addJob({ driveId: drive, documentId: id, operations, forceSync });
1169
+ const jobId = await this.queueManager.addJob({
1170
+ driveId: drive,
1171
+ documentId: id,
1172
+ operations,
1173
+ forceSync
1174
+ });
1026
1175
 
1027
1176
  return new Promise<IOperationResult>((resolve, reject) => {
1028
- const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
1029
- if (job.jobId === jobId) {
1030
- unsubscribe();
1031
- unsubscribeError();
1032
- resolve(result);
1177
+ const unsubscribe = this.queueManager.on(
1178
+ 'jobCompleted',
1179
+ (job, result) => {
1180
+ if (job.jobId === jobId) {
1181
+ unsubscribe();
1182
+ unsubscribeError();
1183
+ resolve(result);
1184
+ }
1033
1185
  }
1034
- });
1035
- const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
1036
- if (job.jobId === jobId) {
1037
- unsubscribe();
1038
- unsubscribeError();
1039
- reject(error);
1186
+ );
1187
+ const unsubscribeError = this.queueManager.on(
1188
+ 'jobFailed',
1189
+ (job, error) => {
1190
+ if (job.jobId === jobId) {
1191
+ unsubscribe();
1192
+ unsubscribeError();
1193
+ reject(error);
1194
+ }
1040
1195
  }
1041
- });
1042
- })
1196
+ );
1197
+ });
1043
1198
  } catch (error) {
1044
1199
  logger.error('Error adding job', error);
1045
1200
  throw error;
1046
1201
  }
1047
1202
  }
1048
1203
 
1049
- async queueAction(drive: string, id: string, action: Action, forceSync?: boolean | undefined): Promise<IOperationResult> {
1204
+ async queueAction(
1205
+ drive: string,
1206
+ id: string,
1207
+ action: Action,
1208
+ forceSync?: boolean | undefined
1209
+ ): Promise<IOperationResult> {
1050
1210
  return this.queueActions(drive, id, [action], forceSync);
1051
1211
  }
1052
1212
 
1053
- async queueActions(drive: string, id: string, actions: Action[], forceSync?: boolean | undefined): Promise<IOperationResult> {
1213
+ async queueActions(
1214
+ drive: string,
1215
+ id: string,
1216
+ actions: Action[],
1217
+ forceSync?: boolean | undefined
1218
+ ): Promise<IOperationResult> {
1054
1219
  try {
1055
- const jobId = await this.queueManager.addJob({ driveId: drive, documentId: id, actions, forceSync });
1220
+ const jobId = await this.queueManager.addJob({
1221
+ driveId: drive,
1222
+ documentId: id,
1223
+ actions,
1224
+ forceSync
1225
+ });
1056
1226
 
1057
1227
  return new Promise<IOperationResult>((resolve, reject) => {
1058
- const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
1059
- if (job.jobId === jobId) {
1060
- unsubscribe();
1061
- unsubscribeError();
1062
- resolve(result);
1228
+ const unsubscribe = this.queueManager.on(
1229
+ 'jobCompleted',
1230
+ (job, result) => {
1231
+ if (job.jobId === jobId) {
1232
+ unsubscribe();
1233
+ unsubscribeError();
1234
+ resolve(result);
1235
+ }
1063
1236
  }
1064
- });
1065
- const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
1066
- if (job.jobId === jobId) {
1067
- unsubscribe();
1068
- unsubscribeError();
1069
- reject(error);
1237
+ );
1238
+ const unsubscribeError = this.queueManager.on(
1239
+ 'jobFailed',
1240
+ (job, error) => {
1241
+ if (job.jobId === jobId) {
1242
+ unsubscribe();
1243
+ unsubscribeError();
1244
+ reject(error);
1245
+ }
1070
1246
  }
1071
- });
1072
- })
1247
+ );
1248
+ });
1073
1249
  } catch (error) {
1074
1250
  logger.error('Error adding job', error);
1075
1251
  throw error;
1076
1252
  }
1077
1253
  }
1078
1254
 
1079
- async queueDriveAction(drive: string, action: DocumentDriveAction | BaseAction, forceSync?: boolean | undefined): Promise<IOperationResult<DocumentDriveDocument>> {
1255
+ async queueDriveAction(
1256
+ drive: string,
1257
+ action: DocumentDriveAction | BaseAction,
1258
+ forceSync?: boolean | undefined
1259
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1080
1260
  return this.queueDriveActions(drive, [action], forceSync);
1081
1261
  }
1082
1262
 
1083
- async queueDriveActions(drive: string, actions: (DocumentDriveAction | BaseAction)[], forceSync?: boolean | undefined): Promise<IOperationResult<DocumentDriveDocument>> {
1084
- const jobId = await this.queueManager.addJob({ driveId: drive, actions, forceSync });
1085
- return new Promise<IOperationResult<DocumentDriveDocument>>((resolve, reject) => {
1086
- const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
1087
- if (job.jobId === jobId) {
1088
- unsubscribe();
1089
- unsubscribeError();
1090
- resolve(result as IOperationResult<DocumentDriveDocument>);
1091
- }
1092
- });
1093
- const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
1094
- if (job.jobId === jobId) {
1095
- unsubscribe();
1096
- unsubscribeError();
1097
- reject(error);
1098
- }
1099
- });
1100
-
1101
- })
1263
+ async queueDriveActions(
1264
+ drive: string,
1265
+ actions: (DocumentDriveAction | BaseAction)[],
1266
+ forceSync?: boolean | undefined
1267
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1268
+ const jobId = await this.queueManager.addJob({
1269
+ driveId: drive,
1270
+ actions,
1271
+ forceSync
1272
+ });
1273
+ return new Promise<IOperationResult<DocumentDriveDocument>>(
1274
+ (resolve, reject) => {
1275
+ const unsubscribe = this.queueManager.on(
1276
+ 'jobCompleted',
1277
+ (job, result) => {
1278
+ if (job.jobId === jobId) {
1279
+ unsubscribe();
1280
+ unsubscribeError();
1281
+ resolve(
1282
+ result as IOperationResult<DocumentDriveDocument>
1283
+ );
1284
+ }
1285
+ }
1286
+ );
1287
+ const unsubscribeError = this.queueManager.on(
1288
+ 'jobFailed',
1289
+ (job, error) => {
1290
+ if (job.jobId === jobId) {
1291
+ unsubscribe();
1292
+ unsubscribeError();
1293
+ reject(error);
1294
+ }
1295
+ }
1296
+ );
1297
+ }
1298
+ );
1102
1299
  }
1103
1300
 
1104
1301
  async addOperations(
@@ -1107,6 +1304,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1107
1304
  operations: Operation[],
1108
1305
  forceSync = true
1109
1306
  ) {
1307
+ // if operations are already stored then returns the result
1308
+ const result = await this.resultIfExistingOperations(
1309
+ drive,
1310
+ id,
1311
+ operations
1312
+ );
1313
+ if (result) {
1314
+ return result;
1315
+ }
1110
1316
  let document: Document | undefined;
1111
1317
  const operationsApplied: Operation[] = [];
1112
1318
  const signals: SignalResult[] = [];
@@ -1214,11 +1420,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1214
1420
  error instanceof OperationError
1215
1421
  ? error
1216
1422
  : new OperationError(
1217
- 'ERROR',
1218
- undefined,
1219
- (error as Error).message,
1220
- (error as Error).cause
1221
- );
1423
+ 'ERROR',
1424
+ undefined,
1425
+ (error as Error).message,
1426
+ (error as Error).cause
1427
+ );
1222
1428
 
1223
1429
  return {
1224
1430
  status: operationError.status,
@@ -1273,33 +1479,92 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1273
1479
  }
1274
1480
  }
1275
1481
 
1276
- queueDriveOperation(drive: string, operation: Operation<DocumentDriveAction | BaseAction>, forceSync = true): Promise<IOperationResult<DocumentDriveDocument>> {
1482
+ queueDriveOperation(
1483
+ drive: string,
1484
+ operation: Operation<DocumentDriveAction | BaseAction>,
1485
+ forceSync = true
1486
+ ): Promise<IOperationResult<DocumentDriveDocument>> {
1277
1487
  return this.queueDriveOperations(drive, [operation], forceSync);
1278
1488
  }
1279
1489
 
1490
+ private async resultIfExistingDriveOperations(
1491
+ driveId: string,
1492
+ operations: Operation<DocumentDriveAction | BaseAction>[]
1493
+ ): Promise<IOperationResult<DocumentDriveDocument> | undefined> {
1494
+ try {
1495
+ const drive = await this.getDrive(driveId);
1496
+ const newOperation = operations.find(
1497
+ op =>
1498
+ !op.id ||
1499
+ !drive.operations[op.scope].find(
1500
+ existingOp =>
1501
+ existingOp.id === op.id &&
1502
+ existingOp.index === op.index &&
1503
+ existingOp.type === op.type &&
1504
+ existingOp.hash === op.hash
1505
+ )
1506
+ );
1507
+ if (!newOperation) {
1508
+ return {
1509
+ status: 'SUCCESS',
1510
+ document: drive,
1511
+ operations: operations,
1512
+ signals: []
1513
+ } as IOperationResult<DocumentDriveDocument>;
1514
+ } else {
1515
+ return undefined;
1516
+ }
1517
+ } catch (error) {
1518
+ console.error(error); // TODO error
1519
+ return undefined;
1520
+ }
1521
+ }
1522
+
1280
1523
  async queueDriveOperations(
1281
1524
  drive: string,
1282
1525
  operations: Operation<DocumentDriveAction | BaseAction>[],
1283
1526
  forceSync = true
1284
1527
  ): Promise<IOperationResult<DocumentDriveDocument>> {
1285
- const jobId = await this.queueManager.addJob({ driveId: drive, operations, forceSync });
1286
- return new Promise<IOperationResult<DocumentDriveDocument>>((resolve, reject) => {
1287
- const unsubscribe = this.queueManager.on('jobCompleted', (job, result) => {
1288
- if (job.jobId === jobId) {
1289
- unsubscribe();
1290
- unsubscribeError();
1291
- resolve(result as IOperationResult<DocumentDriveDocument>);
1292
- }
1293
- });
1294
- const unsubscribeError = this.queueManager.on('jobFailed', (job, error) => {
1295
- if (job.jobId === jobId) {
1296
- unsubscribe();
1297
- unsubscribeError();
1298
- reject(error);
1299
- }
1300
- });
1528
+ // if operations are already stored then returns cached document
1529
+ const result = await this.resultIfExistingDriveOperations(
1530
+ drive,
1531
+ operations
1532
+ );
1533
+ if (result) {
1534
+ return result;
1535
+ }
1301
1536
 
1302
- })
1537
+ const jobId = await this.queueManager.addJob({
1538
+ driveId: drive,
1539
+ operations,
1540
+ forceSync
1541
+ });
1542
+ return new Promise<IOperationResult<DocumentDriveDocument>>(
1543
+ (resolve, reject) => {
1544
+ const unsubscribe = this.queueManager.on(
1545
+ 'jobCompleted',
1546
+ (job, result) => {
1547
+ if (job.jobId === jobId) {
1548
+ unsubscribe();
1549
+ unsubscribeError();
1550
+ resolve(
1551
+ result as IOperationResult<DocumentDriveDocument>
1552
+ );
1553
+ }
1554
+ }
1555
+ );
1556
+ const unsubscribeError = this.queueManager.on(
1557
+ 'jobFailed',
1558
+ (job, error) => {
1559
+ if (job.jobId === jobId) {
1560
+ unsubscribe();
1561
+ unsubscribeError();
1562
+ reject(error);
1563
+ }
1564
+ }
1565
+ );
1566
+ }
1567
+ );
1303
1568
  }
1304
1569
 
1305
1570
  async addDriveOperations(
@@ -1313,6 +1578,15 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1313
1578
  const signals: SignalResult[] = [];
1314
1579
  let error: Error | undefined;
1315
1580
 
1581
+ // if operations are already stored then returns cached drive
1582
+ const result = await this.resultIfExistingDriveOperations(
1583
+ drive,
1584
+ operations
1585
+ );
1586
+ if (result) {
1587
+ return result;
1588
+ }
1589
+
1316
1590
  const prevSyncUnits = await this.getSynchronizationUnitsIds(drive);
1317
1591
 
1318
1592
  try {
@@ -1329,7 +1603,7 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1329
1603
 
1330
1604
  return {
1331
1605
  operations: result.operationsApplied,
1332
- header: result.document,
1606
+ header: result.document
1333
1607
  };
1334
1608
  });
1335
1609
 
@@ -1457,11 +1731,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1457
1731
  error instanceof OperationError
1458
1732
  ? error
1459
1733
  : new OperationError(
1460
- 'ERROR',
1461
- undefined,
1462
- (error as Error).message,
1463
- (error as Error).cause
1464
- );
1734
+ 'ERROR',
1735
+ undefined,
1736
+ (error as Error).message,
1737
+ (error as Error).cause
1738
+ );
1465
1739
 
1466
1740
  return {
1467
1741
  status: operationError.status,
@@ -1525,7 +1799,11 @@ export class DocumentDriveServer extends BaseDocumentDriveServer {
1525
1799
  ): Promise<IOperationResult<DocumentDriveDocument>> {
1526
1800
  const document = await this.getDrive(drive);
1527
1801
  const operations = this._buildOperations(document, actions);
1528
- const result = await this.addDriveOperations(drive, operations, forceSync);
1802
+ const result = await this.addDriveOperations(
1803
+ drive,
1804
+ operations,
1805
+ forceSync
1806
+ );
1529
1807
  return result;
1530
1808
  }
1531
1809
 
@@ -8,32 +8,37 @@ import {
8
8
  import type {
9
9
  Action,
10
10
  AttachmentInput,
11
- DocumentOperations,
12
- FileRegistry,
13
11
  BaseAction,
14
12
  Document,
15
13
  DocumentHeader,
14
+ DocumentOperations,
16
15
  ExtendedState,
16
+ FileRegistry,
17
17
  Operation,
18
18
  OperationScope,
19
19
  State
20
20
  } from 'document-model/document';
21
21
  import { IBackOffOptions, backOff } from 'exponential-backoff';
22
22
  import { ConflictOperationError } from '../server/error';
23
- import { logger } from '../utils/logger';
24
- import { DocumentDriveStorage, DocumentStorage, IDriveStorage, IStorageDelegate } from './types';
25
23
  import type { SynchronizationUnitQuery } from '../server/types';
24
+ import { logger } from '../utils/logger';
25
+ import {
26
+ DocumentDriveStorage,
27
+ DocumentStorage,
28
+ IDriveStorage,
29
+ IStorageDelegate
30
+ } from './types';
26
31
 
27
32
  type Transaction =
28
33
  | Omit<
29
- PrismaClient<Prisma.PrismaClientOptions, never>,
30
- | '$connect'
31
- | '$disconnect'
32
- | '$on'
33
- | '$transaction'
34
- | '$use'
35
- | '$extends'
36
- >
34
+ PrismaClient<Prisma.PrismaClientOptions, never>,
35
+ | '$connect'
36
+ | '$disconnect'
37
+ | '$on'
38
+ | '$transaction'
39
+ | '$use'
40
+ | '$extends'
41
+ >
37
42
  | ExtendedPrismaClient;
38
43
 
39
44
  function storageToOperation(
@@ -42,6 +47,7 @@ function storageToOperation(
42
47
  }
43
48
  ): Operation {
44
49
  const operation: Operation = {
50
+ id: op.opId || undefined,
45
51
  skip: op.skip,
46
52
  hash: op.hash,
47
53
  index: op.index,
@@ -187,6 +193,7 @@ export class PrismaStorage implements IDriveStorage {
187
193
  type: op.type,
188
194
  scope: op.scope,
189
195
  branch: 'main',
196
+ opId: op.id,
190
197
  skip: op.skip,
191
198
  context: op.context,
192
199
  resultingState: op.resultingState
@@ -371,26 +378,41 @@ export class PrismaStorage implements IDriveStorage {
371
378
  throw new Error(`Document with id ${id} not found`);
372
379
  }
373
380
 
374
- const cachedOperations = await this.delegate?.getCachedOperations(driveId, id) ?? {
381
+ const cachedOperations = (await this.delegate?.getCachedOperations(
382
+ driveId,
383
+ id
384
+ )) ?? {
375
385
  global: [],
376
386
  local: []
377
387
  };
378
- const scopeIndex = Object.keys(cachedOperations).reduceRight<Record<OperationScope, number>>((acc, value) => {
379
- const scope = value as OperationScope;
380
- const lastIndex = cachedOperations[scope]?.at(-1)?.index ?? -1;
381
- acc[scope] = lastIndex;
382
- return acc;
383
- }, { global: -1, local: -1 });
388
+ const scopeIndex = Object.keys(cachedOperations).reduceRight<
389
+ Record<OperationScope, number>
390
+ >(
391
+ (acc, value) => {
392
+ const scope = value as OperationScope;
393
+ const lastIndex = cachedOperations[scope]?.at(-1)?.index ?? -1;
394
+ acc[scope] = lastIndex;
395
+ return acc;
396
+ },
397
+ { global: -1, local: -1 }
398
+ );
384
399
 
385
- const conditions = Object.entries(scopeIndex).map(([scope, index]) => `("scope" = '${scope}' AND "index" > ${index})`);
386
- conditions.push(`("scope" NOT IN (${Object.keys(cachedOperations).map(s => `'${s}'`).join(", ")}))`);
400
+ const conditions = Object.entries(scopeIndex).map(
401
+ ([scope, index]) => `("scope" = '${scope}' AND "index" > ${index})`
402
+ );
403
+ conditions.push(
404
+ `("scope" NOT IN (${Object.keys(cachedOperations)
405
+ .map(s => `'${s}'`)
406
+ .join(', ')}))`
407
+ );
387
408
 
388
409
  // retrieves operations with resulting state
389
410
  // for the last operation of each scope
390
411
  // TODO prevent SQL injection
391
412
  const queryOperations = await prisma.$queryRawUnsafe<
392
413
  Prisma.$OperationPayload['scalars'][]
393
- >(`WITH ranked_operations AS (
414
+ >(
415
+ `WITH ranked_operations AS (
394
416
  SELECT
395
417
  *,
396
418
  ROW_NUMBER() OVER (PARTITION BY scope ORDER BY index DESC) AS rn
@@ -416,37 +438,39 @@ export class PrismaStorage implements IDriveStorage {
416
438
  WHERE "driveId" = $1 AND "documentId" = $2
417
439
  AND (${conditions.join(' OR ')})
418
440
  ORDER BY scope, index;
419
- `, driveId, id);
420
- const operationIds = queryOperations.map(o => o.id)
441
+ `,
442
+ driveId,
443
+ id
444
+ );
445
+ const operationIds = queryOperations.map(o => o.id);
421
446
  const attachments = await prisma.attachment.findMany({
422
447
  where: {
423
448
  operationId: {
424
449
  in: operationIds
425
450
  }
426
- },
451
+ }
427
452
  });
428
453
 
429
454
  // TODO add attachments from cached operations
430
455
  const fileRegistry: FileRegistry = {};
431
456
 
432
- const operationsByScope = queryOperations.reduce<DocumentOperations<Action>>(
433
- (acc, operation) => {
434
- const scope = operation.scope as OperationScope;
435
- if (!acc[scope]) {
436
- acc[scope] = [];
437
- }
438
- const result = storageToOperation(operation);
439
- result.attachments = attachments.filter(
440
- a => a.operationId === operation.id
441
- );
442
- result.attachments.forEach(({ hash, ...file }) => {
443
- fileRegistry[hash] = file;
444
- });
445
- acc[scope].push(result);
446
- return acc;
447
- },
448
- cachedOperations
449
- );
457
+ const operationsByScope = queryOperations.reduce<
458
+ DocumentOperations<Action>
459
+ >((acc, operation) => {
460
+ const scope = operation.scope as OperationScope;
461
+ if (!acc[scope]) {
462
+ acc[scope] = [];
463
+ }
464
+ const result = storageToOperation(operation);
465
+ result.attachments = attachments.filter(
466
+ a => a.operationId === operation.id
467
+ );
468
+ result.attachments.forEach(({ hash, ...file }) => {
469
+ fileRegistry[hash] = file;
470
+ });
471
+ acc[scope].push(result);
472
+ return acc;
473
+ }, cachedOperations);
450
474
 
451
475
  const dbDoc = result;
452
476
  const doc: Document = {
@@ -529,13 +553,19 @@ export class PrismaStorage implements IDriveStorage {
529
553
  where: {
530
554
  id
531
555
  }
532
- })
556
+ });
533
557
 
534
558
  // delete drive itself
535
559
  await this.deleteDocument('drives', id);
536
560
  }
537
561
 
538
- async getOperationResultingState(driveId: string, documentId: string, index: number, scope: string, branch: string): Promise<unknown> {
562
+ async getOperationResultingState(
563
+ driveId: string,
564
+ documentId: string,
565
+ index: number,
566
+ scope: string,
567
+ branch: string
568
+ ): Promise<unknown> {
539
569
  const operation = await this.db.operation.findUnique({
540
570
  where: {
541
571
  unique_operation: {
@@ -550,8 +580,19 @@ export class PrismaStorage implements IDriveStorage {
550
580
  return operation?.resultingState?.toString();
551
581
  }
552
582
 
553
- getDriveOperationResultingState(drive: string, index: number, scope: string, branch: string): Promise<unknown> {
554
- return this.getOperationResultingState("drives", drive, index, scope, branch);
583
+ getDriveOperationResultingState(
584
+ drive: string,
585
+ index: number,
586
+ scope: string,
587
+ branch: string
588
+ ): Promise<unknown> {
589
+ return this.getOperationResultingState(
590
+ 'drives',
591
+ drive,
592
+ index,
593
+ scope,
594
+ branch
595
+ );
555
596
  }
556
597
 
557
598
  async getSynchronizationUnitsRevision(
@@ -567,9 +608,11 @@ export class PrismaStorage implements IDriveStorage {
567
608
  }[]
568
609
  > {
569
610
  // TODO add branch condition
570
- const whereClauses = units.map((_, index) => {
571
- return `("driveId" = $${index * 3 + 1} AND "documentId" = $${index * 3 + 2} AND "scope" = $${index * 3 + 3})`;
572
- }).join(' OR ');
611
+ const whereClauses = units
612
+ .map((_, index) => {
613
+ return `("driveId" = $${index * 3 + 1} AND "documentId" = $${index * 3 + 2} AND "scope" = $${index * 3 + 3})`;
614
+ })
615
+ .join(' OR ');
573
616
 
574
617
  const query = `
575
618
  SELECT "driveId", "documentId", "scope", "branch", MAX("timestamp") as "lastUpdated", MAX("index") as revision FROM "Operation"
@@ -577,13 +620,28 @@ export class PrismaStorage implements IDriveStorage {
577
620
  GROUP BY "driveId", "documentId", "scope", "branch"
578
621
  `;
579
622
 
580
- const params = units.map(unit => [unit.documentId ? unit.driveId : "drives", unit.documentId || unit.driveId, unit.scope]).flat();
581
- const results = await this.db.$queryRawUnsafe<{ driveId: string, documentId: string, lastUpdated: string, scope: OperationScope, branch: string, revision: number }[]>(query, ...params);
623
+ const params = units
624
+ .map(unit => [
625
+ unit.documentId ? unit.driveId : 'drives',
626
+ unit.documentId || unit.driveId,
627
+ unit.scope
628
+ ])
629
+ .flat();
630
+ const results = await this.db.$queryRawUnsafe<
631
+ {
632
+ driveId: string;
633
+ documentId: string;
634
+ lastUpdated: string;
635
+ scope: OperationScope;
636
+ branch: string;
637
+ revision: number;
638
+ }[]
639
+ >(query, ...params);
582
640
  return results.map(row => ({
583
641
  ...row,
584
- driveId: row.driveId === "drives" ? row.documentId : row.driveId,
585
- documentId: row.driveId === "drives" ? '' : row.documentId,
586
- lastUpdated: new Date(row.lastUpdated).toISOString(),
642
+ driveId: row.driveId === 'drives' ? row.documentId : row.driveId,
643
+ documentId: row.driveId === 'drives' ? '' : row.documentId,
644
+ lastUpdated: new Date(row.lastUpdated).toISOString()
587
645
  }));
588
646
  }
589
647
  }