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