document-drive 1.0.0-experimental.8 → 1.0.0-websockets
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/CHANGELOG.md +148 -0
- package/package.json +15 -10
- package/src/queue/base.ts +19 -34
- package/src/queue/redis.ts +15 -25
- package/src/queue/types.ts +32 -16
- package/src/server/index.ts +773 -197
- package/src/server/listener/manager.ts +60 -22
- package/src/server/listener/transmitter/index.ts +1 -0
- package/src/server/listener/transmitter/internal.ts +5 -0
- package/src/server/listener/transmitter/pull-responder.ts +11 -15
- package/src/server/listener/transmitter/subscription.ts +364 -0
- package/src/server/listener/transmitter/types.ts +15 -0
- package/src/server/types.ts +55 -6
- package/src/storage/browser.ts +69 -5
- package/src/storage/filesystem.ts +60 -2
- package/src/storage/memory.ts +67 -4
- package/src/storage/prisma.ts +316 -81
- package/src/storage/sequelize.ts +69 -7
- package/src/storage/types.ts +19 -5
- package/src/queue/manager.ts +0 -22
package/src/storage/prisma.ts
CHANGED
|
@@ -1,32 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Prisma, PrismaClient } from '@prisma/client';
|
|
2
2
|
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
|
3
|
-
import { backOff, IBackOffOptions } from "exponential-backoff";
|
|
4
3
|
import {
|
|
5
4
|
DocumentDriveAction,
|
|
6
5
|
DocumentDriveLocalState,
|
|
7
6
|
DocumentDriveState
|
|
8
7
|
} from 'document-model-libs/document-drive';
|
|
9
8
|
import type {
|
|
9
|
+
Action,
|
|
10
|
+
AttachmentInput,
|
|
10
11
|
BaseAction,
|
|
12
|
+
Document,
|
|
11
13
|
DocumentHeader,
|
|
14
|
+
DocumentOperations,
|
|
12
15
|
ExtendedState,
|
|
16
|
+
FileRegistry,
|
|
13
17
|
Operation,
|
|
14
18
|
OperationScope,
|
|
15
19
|
State
|
|
16
20
|
} from 'document-model/document';
|
|
21
|
+
import { IBackOffOptions, backOff } from 'exponential-backoff';
|
|
17
22
|
import { ConflictOperationError } from '../server/error';
|
|
23
|
+
import type { SynchronizationUnitQuery } from '../server/types';
|
|
18
24
|
import { logger } from '../utils/logger';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
import {
|
|
26
|
+
DocumentDriveStorage,
|
|
27
|
+
DocumentStorage,
|
|
28
|
+
IDriveStorage,
|
|
29
|
+
IStorageDelegate
|
|
30
|
+
} from './types';
|
|
31
|
+
|
|
32
|
+
type Transaction =
|
|
33
|
+
| Omit<
|
|
34
|
+
PrismaClient<Prisma.PrismaClientOptions, never>,
|
|
35
|
+
| '$connect'
|
|
36
|
+
| '$disconnect'
|
|
37
|
+
| '$on'
|
|
38
|
+
| '$transaction'
|
|
39
|
+
| '$use'
|
|
40
|
+
| '$extends'
|
|
41
|
+
>
|
|
42
|
+
| ExtendedPrismaClient;
|
|
25
43
|
|
|
26
44
|
function storageToOperation(
|
|
27
|
-
op: Prisma.$OperationPayload['scalars']
|
|
45
|
+
op: Prisma.$OperationPayload['scalars'] & {
|
|
46
|
+
attachments?: AttachmentInput[];
|
|
47
|
+
}
|
|
28
48
|
): Operation {
|
|
29
49
|
const operation: Operation = {
|
|
50
|
+
id: op.opId || undefined,
|
|
30
51
|
skip: op.skip,
|
|
31
52
|
hash: op.hash,
|
|
32
53
|
index: op.index,
|
|
@@ -34,7 +55,10 @@ function storageToOperation(
|
|
|
34
55
|
input: JSON.parse(op.input),
|
|
35
56
|
type: op.type,
|
|
36
57
|
scope: op.scope as OperationScope,
|
|
37
|
-
|
|
58
|
+
resultingState: op.resultingState
|
|
59
|
+
? op.resultingState.toString()
|
|
60
|
+
: undefined,
|
|
61
|
+
attachments: op.attachments
|
|
38
62
|
};
|
|
39
63
|
if (op.context) {
|
|
40
64
|
operation.context = op.context as Prisma.JsonObject;
|
|
@@ -44,38 +68,47 @@ function storageToOperation(
|
|
|
44
68
|
|
|
45
69
|
export type PrismaStorageOptions = {
|
|
46
70
|
transactionRetryBackoff?: IBackOffOptions;
|
|
47
|
-
}
|
|
71
|
+
};
|
|
48
72
|
|
|
49
|
-
function getRetryTransactionsClient<T extends PrismaClient>(
|
|
73
|
+
function getRetryTransactionsClient<T extends PrismaClient>(
|
|
74
|
+
prisma: T,
|
|
75
|
+
backOffOptions?: Partial<IBackOffOptions>
|
|
76
|
+
) {
|
|
50
77
|
return prisma.$extends({
|
|
51
78
|
client: {
|
|
52
|
-
$transaction: (...args: Parameters<T[
|
|
79
|
+
$transaction: (...args: Parameters<T['$transaction']>) => {
|
|
53
80
|
// eslint-disable-next-line prefer-spread
|
|
54
81
|
return backOff(() => prisma.$transaction.apply(prisma, args), {
|
|
55
|
-
retry:
|
|
82
|
+
retry: e => {
|
|
56
83
|
// Retry the transaction only if the error was due to a write conflict or deadlock
|
|
57
84
|
// See: https://www.prisma.io/docs/reference/api-reference/error-reference#p2034
|
|
58
|
-
return (e as { code: string }).code ===
|
|
85
|
+
return (e as { code: string }).code === 'P2034';
|
|
59
86
|
},
|
|
60
|
-
...backOffOptions
|
|
87
|
+
...backOffOptions
|
|
61
88
|
});
|
|
62
89
|
}
|
|
63
90
|
}
|
|
64
91
|
});
|
|
65
92
|
}
|
|
66
93
|
|
|
67
|
-
type ExtendedPrismaClient = ReturnType<
|
|
94
|
+
type ExtendedPrismaClient = ReturnType<
|
|
95
|
+
typeof getRetryTransactionsClient<PrismaClient>
|
|
96
|
+
>;
|
|
68
97
|
|
|
69
98
|
export class PrismaStorage implements IDriveStorage {
|
|
70
99
|
private db: ExtendedPrismaClient;
|
|
100
|
+
private delegate: IStorageDelegate | undefined;
|
|
71
101
|
|
|
72
102
|
constructor(db: PrismaClient, options?: PrismaStorageOptions) {
|
|
73
103
|
const backOffOptions = options?.transactionRetryBackoff;
|
|
74
104
|
this.db = getRetryTransactionsClient(db, {
|
|
75
105
|
...backOffOptions,
|
|
76
|
-
jitter: backOffOptions?.jitter ??
|
|
106
|
+
jitter: backOffOptions?.jitter ?? 'full'
|
|
77
107
|
});
|
|
108
|
+
}
|
|
78
109
|
|
|
110
|
+
setStorageDelegate(delegate: IStorageDelegate): void {
|
|
111
|
+
this.delegate = delegate;
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
|
|
@@ -133,11 +166,10 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
133
166
|
name: document.name,
|
|
134
167
|
documentType: document.documentType,
|
|
135
168
|
driveId: drive,
|
|
136
|
-
initialState: document.initialState
|
|
169
|
+
initialState: JSON.stringify(document.initialState),
|
|
137
170
|
lastModified: document.lastModified,
|
|
138
|
-
revision: document.revision,
|
|
139
|
-
id
|
|
140
|
-
state: document.initialState
|
|
171
|
+
revision: JSON.stringify(document.revision),
|
|
172
|
+
id
|
|
141
173
|
}
|
|
142
174
|
});
|
|
143
175
|
}
|
|
@@ -147,14 +179,8 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
147
179
|
drive: string,
|
|
148
180
|
id: string,
|
|
149
181
|
operations: Operation[],
|
|
150
|
-
header: DocumentHeader
|
|
151
|
-
newState: State<any, any> | undefined = undefined
|
|
182
|
+
header: DocumentHeader
|
|
152
183
|
): Promise<void> {
|
|
153
|
-
const document = await this.getDocument(drive, id, tx);
|
|
154
|
-
if (!document) {
|
|
155
|
-
throw new Error(`Document with id ${id} not found`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
184
|
try {
|
|
159
185
|
await tx.operation.createMany({
|
|
160
186
|
data: operations.map(op => ({
|
|
@@ -168,7 +194,10 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
168
194
|
scope: op.scope,
|
|
169
195
|
branch: 'main',
|
|
170
196
|
skip: op.skip,
|
|
171
|
-
context: op.context
|
|
197
|
+
context: op.context,
|
|
198
|
+
resultingState: op.resultingState
|
|
199
|
+
? Buffer.from(JSON.stringify(op.resultingState))
|
|
200
|
+
: undefined
|
|
172
201
|
}))
|
|
173
202
|
});
|
|
174
203
|
|
|
@@ -179,10 +208,34 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
179
208
|
},
|
|
180
209
|
data: {
|
|
181
210
|
lastModified: header.lastModified,
|
|
182
|
-
revision: header.revision
|
|
183
|
-
state: newState
|
|
211
|
+
revision: JSON.stringify(header.revision)
|
|
184
212
|
}
|
|
185
213
|
});
|
|
214
|
+
|
|
215
|
+
await Promise.all(
|
|
216
|
+
operations
|
|
217
|
+
.filter(o => o.attachments?.length)
|
|
218
|
+
.map(op => {
|
|
219
|
+
return tx.operation.update({
|
|
220
|
+
where: {
|
|
221
|
+
unique_operation: {
|
|
222
|
+
driveId: drive,
|
|
223
|
+
documentId: id,
|
|
224
|
+
index: op.index,
|
|
225
|
+
scope: op.scope,
|
|
226
|
+
branch: 'main'
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
data: {
|
|
230
|
+
attachments: {
|
|
231
|
+
createMany: {
|
|
232
|
+
data: op.attachments ?? []
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
})
|
|
238
|
+
);
|
|
186
239
|
} catch (e) {
|
|
187
240
|
// P2002: Unique constraint failed
|
|
188
241
|
// Operation with existing index
|
|
@@ -209,6 +262,7 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
209
262
|
);
|
|
210
263
|
|
|
211
264
|
if (!existingOperation || !conflictOp) {
|
|
265
|
+
console.error(e);
|
|
212
266
|
throw e;
|
|
213
267
|
} else {
|
|
214
268
|
throw new ConflictOperationError(
|
|
@@ -228,7 +282,7 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
228
282
|
callback: (document: DocumentStorage) => Promise<{
|
|
229
283
|
operations: Operation[];
|
|
230
284
|
header: DocumentHeader;
|
|
231
|
-
newState?: State<any, any> | undefined
|
|
285
|
+
newState?: State<any, any> | undefined;
|
|
232
286
|
}>
|
|
233
287
|
) {
|
|
234
288
|
let result: {
|
|
@@ -237,24 +291,25 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
237
291
|
newState?: State<any, any> | undefined;
|
|
238
292
|
} | null = null;
|
|
239
293
|
|
|
240
|
-
await this.db.$transaction(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const { operations, header, newState } = result;
|
|
248
|
-
return this._addDocumentOperations(
|
|
249
|
-
tx,
|
|
250
|
-
drive,
|
|
251
|
-
id,
|
|
252
|
-
operations,
|
|
253
|
-
header,
|
|
254
|
-
newState
|
|
255
|
-
);
|
|
256
|
-
}, { isolationLevel: "Serializable" });
|
|
294
|
+
await this.db.$transaction(
|
|
295
|
+
async tx => {
|
|
296
|
+
const document = await this.getDocument(drive, id, tx);
|
|
297
|
+
if (!document) {
|
|
298
|
+
throw new Error(`Document with id ${id} not found`);
|
|
299
|
+
}
|
|
300
|
+
result = await callback(document);
|
|
257
301
|
|
|
302
|
+
const { operations, header, newState } = result;
|
|
303
|
+
return this._addDocumentOperations(
|
|
304
|
+
tx,
|
|
305
|
+
drive,
|
|
306
|
+
id,
|
|
307
|
+
operations,
|
|
308
|
+
header
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
{ isolationLevel: 'Serializable' }
|
|
312
|
+
);
|
|
258
313
|
|
|
259
314
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
260
315
|
if (!result) {
|
|
@@ -268,19 +323,22 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
268
323
|
drive: string,
|
|
269
324
|
id: string,
|
|
270
325
|
operations: Operation[],
|
|
271
|
-
header: DocumentHeader
|
|
326
|
+
header: DocumentHeader
|
|
272
327
|
): Promise<void> {
|
|
273
328
|
return this._addDocumentOperations(
|
|
274
329
|
this.db,
|
|
275
330
|
drive,
|
|
276
331
|
id,
|
|
277
332
|
operations,
|
|
278
|
-
header
|
|
333
|
+
header
|
|
279
334
|
);
|
|
280
335
|
}
|
|
281
336
|
|
|
282
337
|
async getDocuments(drive: string) {
|
|
283
338
|
const docs = await this.db.document.findMany({
|
|
339
|
+
select: {
|
|
340
|
+
id: true
|
|
341
|
+
},
|
|
284
342
|
where: {
|
|
285
343
|
AND: {
|
|
286
344
|
driveId: drive,
|
|
@@ -299,25 +357,18 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
299
357
|
where: {
|
|
300
358
|
id: id,
|
|
301
359
|
driveId: driveId
|
|
302
|
-
}
|
|
360
|
+
}
|
|
303
361
|
});
|
|
304
362
|
return count > 0;
|
|
305
363
|
}
|
|
306
364
|
|
|
307
365
|
async getDocument(driveId: string, id: string, tx?: Transaction) {
|
|
308
|
-
const
|
|
366
|
+
const prisma = tx ?? this.db;
|
|
367
|
+
const result = await prisma.document.findUnique({
|
|
309
368
|
where: {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
include: {
|
|
314
|
-
operations: {
|
|
315
|
-
orderBy: {
|
|
316
|
-
index: 'asc'
|
|
317
|
-
},
|
|
318
|
-
include: {
|
|
319
|
-
attachments: true
|
|
320
|
-
}
|
|
369
|
+
id_driveId: {
|
|
370
|
+
driveId,
|
|
371
|
+
id
|
|
321
372
|
}
|
|
322
373
|
}
|
|
323
374
|
});
|
|
@@ -326,31 +377,119 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
326
377
|
throw new Error(`Document with id ${id} not found`);
|
|
327
378
|
}
|
|
328
379
|
|
|
380
|
+
const cachedOperations = (await this.delegate?.getCachedOperations(
|
|
381
|
+
driveId,
|
|
382
|
+
id
|
|
383
|
+
)) ?? {
|
|
384
|
+
global: [],
|
|
385
|
+
local: []
|
|
386
|
+
};
|
|
387
|
+
const scopeIndex = Object.keys(cachedOperations).reduceRight<
|
|
388
|
+
Record<OperationScope, number>
|
|
389
|
+
>(
|
|
390
|
+
(acc, value) => {
|
|
391
|
+
const scope = value as OperationScope;
|
|
392
|
+
const lastIndex = cachedOperations[scope]?.at(-1)?.index ?? -1;
|
|
393
|
+
acc[scope] = lastIndex;
|
|
394
|
+
return acc;
|
|
395
|
+
},
|
|
396
|
+
{ global: -1, local: -1 }
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const conditions = Object.entries(scopeIndex).map(
|
|
400
|
+
([scope, index]) => `("scope" = '${scope}' AND "index" > ${index})`
|
|
401
|
+
);
|
|
402
|
+
conditions.push(
|
|
403
|
+
`("scope" NOT IN (${Object.keys(cachedOperations)
|
|
404
|
+
.map(s => `'${s}'`)
|
|
405
|
+
.join(', ')}))`
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// retrieves operations with resulting state
|
|
409
|
+
// for the last operation of each scope
|
|
410
|
+
// TODO prevent SQL injection
|
|
411
|
+
const queryOperations = await prisma.$queryRawUnsafe<
|
|
412
|
+
Prisma.$OperationPayload['scalars'][]
|
|
413
|
+
>(
|
|
414
|
+
`WITH ranked_operations AS (
|
|
415
|
+
SELECT
|
|
416
|
+
*,
|
|
417
|
+
ROW_NUMBER() OVER (PARTITION BY scope ORDER BY index DESC) AS rn
|
|
418
|
+
FROM "Operation"
|
|
419
|
+
)
|
|
420
|
+
SELECT
|
|
421
|
+
"id",
|
|
422
|
+
"opId",
|
|
423
|
+
"scope",
|
|
424
|
+
"branch",
|
|
425
|
+
"index",
|
|
426
|
+
"skip",
|
|
427
|
+
"hash",
|
|
428
|
+
"timestamp",
|
|
429
|
+
"input",
|
|
430
|
+
"type",
|
|
431
|
+
"context",
|
|
432
|
+
CASE
|
|
433
|
+
WHEN rn = 1 THEN "resultingState"
|
|
434
|
+
ELSE NULL
|
|
435
|
+
END AS "resultingState"
|
|
436
|
+
FROM ranked_operations
|
|
437
|
+
WHERE "driveId" = $1 AND "documentId" = $2
|
|
438
|
+
AND (${conditions.join(' OR ')})
|
|
439
|
+
ORDER BY scope, index;
|
|
440
|
+
`,
|
|
441
|
+
driveId,
|
|
442
|
+
id
|
|
443
|
+
);
|
|
444
|
+
const operationIds = queryOperations.map(o => o.id);
|
|
445
|
+
const attachments = await prisma.attachment.findMany({
|
|
446
|
+
where: {
|
|
447
|
+
operationId: {
|
|
448
|
+
in: operationIds
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// TODO add attachments from cached operations
|
|
454
|
+
const fileRegistry: FileRegistry = {};
|
|
455
|
+
|
|
456
|
+
const operationsByScope = queryOperations.reduce<
|
|
457
|
+
DocumentOperations<Action>
|
|
458
|
+
>((acc, operation) => {
|
|
459
|
+
const scope = operation.scope as OperationScope;
|
|
460
|
+
if (!acc[scope]) {
|
|
461
|
+
acc[scope] = [];
|
|
462
|
+
}
|
|
463
|
+
const result = storageToOperation(operation);
|
|
464
|
+
result.attachments = attachments.filter(
|
|
465
|
+
a => a.operationId === operation.id
|
|
466
|
+
);
|
|
467
|
+
result.attachments.forEach(({ hash, ...file }) => {
|
|
468
|
+
fileRegistry[hash] = file;
|
|
469
|
+
});
|
|
470
|
+
acc[scope].push(result);
|
|
471
|
+
return acc;
|
|
472
|
+
}, cachedOperations);
|
|
473
|
+
|
|
329
474
|
const dbDoc = result;
|
|
330
|
-
const doc = {
|
|
475
|
+
const doc: Document = {
|
|
331
476
|
created: dbDoc.created.toISOString(),
|
|
332
477
|
name: dbDoc.name ? dbDoc.name : '',
|
|
333
478
|
documentType: dbDoc.documentType,
|
|
334
|
-
initialState: dbDoc.initialState as ExtendedState<
|
|
479
|
+
initialState: JSON.parse(dbDoc.initialState) as ExtendedState<
|
|
335
480
|
DocumentDriveState,
|
|
336
481
|
DocumentDriveLocalState
|
|
337
482
|
>,
|
|
338
|
-
state:
|
|
483
|
+
state: undefined,
|
|
339
484
|
lastModified: new Date(dbDoc.lastModified).toISOString(),
|
|
340
|
-
operations:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
},
|
|
348
|
-
clipboard: dbDoc.operations
|
|
349
|
-
.filter(op => op.clipboard)
|
|
350
|
-
.map(storageToOperation),
|
|
351
|
-
revision: dbDoc.revision as Record<OperationScope, number>
|
|
485
|
+
operations: operationsByScope,
|
|
486
|
+
clipboard: [],
|
|
487
|
+
revision: JSON.parse(dbDoc.revision) as Record<
|
|
488
|
+
OperationScope,
|
|
489
|
+
number
|
|
490
|
+
>,
|
|
491
|
+
attachments: {}
|
|
352
492
|
};
|
|
353
|
-
|
|
354
493
|
return doc;
|
|
355
494
|
}
|
|
356
495
|
|
|
@@ -401,11 +540,107 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
401
540
|
}
|
|
402
541
|
|
|
403
542
|
async deleteDrive(id: string) {
|
|
543
|
+
// delete drive documents and operations
|
|
404
544
|
await this.db.document.deleteMany({
|
|
405
545
|
where: {
|
|
406
546
|
driveId: id
|
|
407
547
|
}
|
|
408
548
|
});
|
|
549
|
+
|
|
550
|
+
// delete drive and associated slug
|
|
551
|
+
await this.db.drive.deleteMany({
|
|
552
|
+
where: {
|
|
553
|
+
id
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// delete drive itself
|
|
409
558
|
await this.deleteDocument('drives', id);
|
|
410
559
|
}
|
|
560
|
+
|
|
561
|
+
async getOperationResultingState(
|
|
562
|
+
driveId: string,
|
|
563
|
+
documentId: string,
|
|
564
|
+
index: number,
|
|
565
|
+
scope: string,
|
|
566
|
+
branch: string
|
|
567
|
+
): Promise<unknown> {
|
|
568
|
+
const operation = await this.db.operation.findUnique({
|
|
569
|
+
where: {
|
|
570
|
+
unique_operation: {
|
|
571
|
+
driveId,
|
|
572
|
+
documentId,
|
|
573
|
+
index,
|
|
574
|
+
scope,
|
|
575
|
+
branch
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
return operation?.resultingState?.toString();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
getDriveOperationResultingState(
|
|
583
|
+
drive: string,
|
|
584
|
+
index: number,
|
|
585
|
+
scope: string,
|
|
586
|
+
branch: string
|
|
587
|
+
): Promise<unknown> {
|
|
588
|
+
return this.getOperationResultingState(
|
|
589
|
+
'drives',
|
|
590
|
+
drive,
|
|
591
|
+
index,
|
|
592
|
+
scope,
|
|
593
|
+
branch
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async getSynchronizationUnitsRevision(
|
|
598
|
+
units: SynchronizationUnitQuery[]
|
|
599
|
+
): Promise<
|
|
600
|
+
{
|
|
601
|
+
driveId: string;
|
|
602
|
+
documentId: string;
|
|
603
|
+
scope: string;
|
|
604
|
+
branch: string;
|
|
605
|
+
lastUpdated: string;
|
|
606
|
+
revision: number;
|
|
607
|
+
}[]
|
|
608
|
+
> {
|
|
609
|
+
// TODO add branch condition
|
|
610
|
+
const whereClauses = units
|
|
611
|
+
.map((_, index) => {
|
|
612
|
+
return `("driveId" = $${index * 3 + 1} AND "documentId" = $${index * 3 + 2} AND "scope" = $${index * 3 + 3})`;
|
|
613
|
+
})
|
|
614
|
+
.join(' OR ');
|
|
615
|
+
|
|
616
|
+
const query = `
|
|
617
|
+
SELECT "driveId", "documentId", "scope", "branch", MAX("timestamp") as "lastUpdated", MAX("index") as revision FROM "Operation"
|
|
618
|
+
WHERE ${whereClauses}
|
|
619
|
+
GROUP BY "driveId", "documentId", "scope", "branch"
|
|
620
|
+
`;
|
|
621
|
+
|
|
622
|
+
const params = units
|
|
623
|
+
.map(unit => [
|
|
624
|
+
unit.documentId ? unit.driveId : 'drives',
|
|
625
|
+
unit.documentId || unit.driveId,
|
|
626
|
+
unit.scope
|
|
627
|
+
])
|
|
628
|
+
.flat();
|
|
629
|
+
const results = await this.db.$queryRawUnsafe<
|
|
630
|
+
{
|
|
631
|
+
driveId: string;
|
|
632
|
+
documentId: string;
|
|
633
|
+
lastUpdated: string;
|
|
634
|
+
scope: OperationScope;
|
|
635
|
+
branch: string;
|
|
636
|
+
revision: number;
|
|
637
|
+
}[]
|
|
638
|
+
>(query, ...params);
|
|
639
|
+
return results.map(row => ({
|
|
640
|
+
...row,
|
|
641
|
+
driveId: row.driveId === 'drives' ? row.documentId : row.driveId,
|
|
642
|
+
documentId: row.driveId === 'drives' ? '' : row.documentId,
|
|
643
|
+
lastUpdated: new Date(row.lastUpdated).toISOString()
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
411
646
|
}
|
package/src/storage/sequelize.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from 'document-model/document';
|
|
12
12
|
import { DataTypes, Options, Sequelize } from 'sequelize';
|
|
13
13
|
import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
|
|
14
|
+
import type { SynchronizationUnitQuery } from '../server/types';
|
|
14
15
|
|
|
15
16
|
export class SequelizeStorage implements IDriveStorage {
|
|
16
17
|
private db: Sequelize;
|
|
@@ -25,8 +26,8 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
25
26
|
type: DataTypes.STRING,
|
|
26
27
|
primaryKey: true
|
|
27
28
|
},
|
|
28
|
-
id: DataTypes.STRING
|
|
29
|
-
})
|
|
29
|
+
id: DataTypes.STRING
|
|
30
|
+
});
|
|
30
31
|
const Document = this.db.define('document', {
|
|
31
32
|
id: {
|
|
32
33
|
type: DataTypes.STRING,
|
|
@@ -155,7 +156,7 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
155
156
|
drive: string,
|
|
156
157
|
id: string,
|
|
157
158
|
operations: Operation[],
|
|
158
|
-
header: DocumentHeader
|
|
159
|
+
header: DocumentHeader
|
|
159
160
|
): Promise<void> {
|
|
160
161
|
const document = await this.getDocument(drive, id);
|
|
161
162
|
if (!document) {
|
|
@@ -177,7 +178,8 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
177
178
|
timestamp: op.timestamp,
|
|
178
179
|
type: op.type,
|
|
179
180
|
scope: op.scope,
|
|
180
|
-
branch: 'main'
|
|
181
|
+
branch: 'main',
|
|
182
|
+
opId: op.id
|
|
181
183
|
}))
|
|
182
184
|
);
|
|
183
185
|
|
|
@@ -284,8 +286,8 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
284
286
|
where: {
|
|
285
287
|
id: id,
|
|
286
288
|
driveId: driveId
|
|
287
|
-
}
|
|
288
|
-
})
|
|
289
|
+
}
|
|
290
|
+
});
|
|
289
291
|
|
|
290
292
|
return count > 0;
|
|
291
293
|
}
|
|
@@ -323,6 +325,7 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
323
325
|
input: JSON;
|
|
324
326
|
type: string;
|
|
325
327
|
scope: string;
|
|
328
|
+
opId?: string;
|
|
326
329
|
}
|
|
327
330
|
];
|
|
328
331
|
revision: Required<Record<OperationScope, number>>;
|
|
@@ -348,13 +351,15 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
348
351
|
input: JSON;
|
|
349
352
|
type: string;
|
|
350
353
|
scope: string;
|
|
354
|
+
opId?: string;
|
|
351
355
|
}) => ({
|
|
352
356
|
hash: op.hash,
|
|
353
357
|
index: op.index,
|
|
354
358
|
timestamp: new Date(op.timestamp).toISOString(),
|
|
355
359
|
input: op.input,
|
|
356
360
|
type: op.type,
|
|
357
|
-
scope: op.scope as OperationScope
|
|
361
|
+
scope: op.scope as OperationScope,
|
|
362
|
+
id: op.opId
|
|
358
363
|
// attachments: fileRegistry
|
|
359
364
|
})
|
|
360
365
|
);
|
|
@@ -444,4 +449,61 @@ export class SequelizeStorage implements IDriveStorage {
|
|
|
444
449
|
});
|
|
445
450
|
}
|
|
446
451
|
}
|
|
452
|
+
|
|
453
|
+
async getSynchronizationUnitsRevision(
|
|
454
|
+
units: SynchronizationUnitQuery[]
|
|
455
|
+
): Promise<
|
|
456
|
+
{
|
|
457
|
+
driveId: string;
|
|
458
|
+
documentId: string;
|
|
459
|
+
scope: string;
|
|
460
|
+
branch: string;
|
|
461
|
+
lastUpdated: string;
|
|
462
|
+
revision: number;
|
|
463
|
+
}[]
|
|
464
|
+
> {
|
|
465
|
+
const results = await Promise.allSettled(
|
|
466
|
+
units.map(async unit => {
|
|
467
|
+
try {
|
|
468
|
+
const document = await (unit.documentId
|
|
469
|
+
? this.getDocument(unit.driveId, unit.documentId)
|
|
470
|
+
: this.getDrive(unit.driveId));
|
|
471
|
+
if (!document) {
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
const operation =
|
|
475
|
+
document.operations[unit.scope as OperationScope]?.at(
|
|
476
|
+
-1
|
|
477
|
+
);
|
|
478
|
+
if (operation) {
|
|
479
|
+
return {
|
|
480
|
+
driveId: unit.driveId,
|
|
481
|
+
documentId: unit.documentId,
|
|
482
|
+
scope: unit.scope,
|
|
483
|
+
branch: unit.branch,
|
|
484
|
+
lastUpdated: operation.timestamp,
|
|
485
|
+
revision: operation.index
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
);
|
|
493
|
+
return results.reduce<
|
|
494
|
+
{
|
|
495
|
+
driveId: string;
|
|
496
|
+
documentId: string;
|
|
497
|
+
scope: string;
|
|
498
|
+
branch: string;
|
|
499
|
+
lastUpdated: string;
|
|
500
|
+
revision: number;
|
|
501
|
+
}[]
|
|
502
|
+
>((acc, curr) => {
|
|
503
|
+
if (curr.status === 'fulfilled' && curr.value !== undefined) {
|
|
504
|
+
acc.push(curr.value);
|
|
505
|
+
}
|
|
506
|
+
return acc;
|
|
507
|
+
}, []);
|
|
508
|
+
}
|
|
447
509
|
}
|