document-drive 1.12.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -7
- package/src/storage/drizzle/queries.ts +0 -51
- package/src/storage/drizzle.ts +0 -627
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -31,10 +31,11 @@
|
|
|
31
31
|
"graphql-request": "^6.1.0",
|
|
32
32
|
"json-stringify-deterministic": "^1.0.12",
|
|
33
33
|
"nanoevents": "^9.0.0",
|
|
34
|
+
"prisma": "^5.18.0",
|
|
34
35
|
"sanitize-filename": "^1.6.3",
|
|
35
|
-
"@powerhousedao/scalars": "1.
|
|
36
|
-
"document-model": "
|
|
37
|
-
"document-model
|
|
36
|
+
"@powerhousedao/scalars": "1.16.0",
|
|
37
|
+
"document-model-libs": "1.125.0",
|
|
38
|
+
"document-model": "2.15.0"
|
|
38
39
|
},
|
|
39
40
|
"optionalDependencies": {
|
|
40
41
|
"@prisma/client": "^5.18.0",
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"redis": "^4.6.15",
|
|
44
45
|
"sequelize": "^6.37.3",
|
|
45
46
|
"sqlite3": "^5.1.7",
|
|
46
|
-
"@powerhousedao/scalars": "1.
|
|
47
|
+
"@powerhousedao/scalars": "1.16.0"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@prisma/client": "5.17.0",
|
|
@@ -64,8 +65,8 @@
|
|
|
64
65
|
"sqlite3": "^5.1.7",
|
|
65
66
|
"vitest-fetch-mock": "^0.3.0",
|
|
66
67
|
"webdriverio": "^9.0.9",
|
|
67
|
-
"document-model": "2.
|
|
68
|
-
"document-model-libs": "1.
|
|
68
|
+
"document-model": "2.15.0",
|
|
69
|
+
"document-model-libs": "1.125.0"
|
|
69
70
|
},
|
|
70
71
|
"scripts": {
|
|
71
72
|
"check-types": "tsc --build",
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { DocumentDriveStorage, DocumentStorage } from "../types";
|
|
2
|
-
import { documentsTable, drivesTable } from "./schema";
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
4
|
-
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
5
|
-
export const getDriveBySlug = async (db: NodePgDatabase, slug: string) => {
|
|
6
|
-
const result = await db
|
|
7
|
-
.select()
|
|
8
|
-
.from(drivesTable)
|
|
9
|
-
.where(eq(drivesTable.slug, slug));
|
|
10
|
-
return result.length > 0 ? result[0] : null;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const upsertDrive = async (
|
|
14
|
-
db: NodePgDatabase,
|
|
15
|
-
id: string,
|
|
16
|
-
drive: DocumentDriveStorage,
|
|
17
|
-
) => {
|
|
18
|
-
const [result] = await db
|
|
19
|
-
.update(drivesTable)
|
|
20
|
-
.set({
|
|
21
|
-
id,
|
|
22
|
-
})
|
|
23
|
-
.where(eq(drivesTable.slug, drive.initialState.state.global.slug ?? id))
|
|
24
|
-
.returning();
|
|
25
|
-
|
|
26
|
-
if (result) {
|
|
27
|
-
return result;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return db.insert(drivesTable).values({
|
|
31
|
-
id,
|
|
32
|
-
slug: drive.initialState.state.global.slug ?? id,
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const createDocumentQuery = async (
|
|
37
|
-
db: NodePgDatabase,
|
|
38
|
-
driveId: string,
|
|
39
|
-
documentId: string,
|
|
40
|
-
document: DocumentStorage,
|
|
41
|
-
) => {
|
|
42
|
-
return db.insert(documentsTable).values({
|
|
43
|
-
name: document.name,
|
|
44
|
-
documentType: document.documentType,
|
|
45
|
-
driveId,
|
|
46
|
-
initialState: JSON.stringify(document.initialState),
|
|
47
|
-
lastModified: document.lastModified,
|
|
48
|
-
revision: JSON.stringify(document.revision),
|
|
49
|
-
id: documentId,
|
|
50
|
-
});
|
|
51
|
-
};
|
package/src/storage/drizzle.ts
DELETED
|
@@ -1,627 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DocumentDriveAction,
|
|
3
|
-
DocumentDriveLocalState,
|
|
4
|
-
DocumentDriveState,
|
|
5
|
-
} from "document-model-libs/document-drive";
|
|
6
|
-
import type {
|
|
7
|
-
Action,
|
|
8
|
-
AttachmentInput,
|
|
9
|
-
BaseAction,
|
|
10
|
-
Document,
|
|
11
|
-
DocumentHeader,
|
|
12
|
-
DocumentOperations,
|
|
13
|
-
ExtendedState,
|
|
14
|
-
FileRegistry,
|
|
15
|
-
Operation,
|
|
16
|
-
OperationScope,
|
|
17
|
-
State,
|
|
18
|
-
} from "document-model/document";
|
|
19
|
-
import { IBackOffOptions } from "exponential-backoff";
|
|
20
|
-
import { DriveNotFoundError } from "../server/error";
|
|
21
|
-
import type { SynchronizationUnitQuery } from "../server/types";
|
|
22
|
-
import { logger } from "../utils/logger";
|
|
23
|
-
import {
|
|
24
|
-
DocumentDriveStorage,
|
|
25
|
-
DocumentStorage,
|
|
26
|
-
IDriveStorage,
|
|
27
|
-
IStorageDelegate,
|
|
28
|
-
} from "./types";
|
|
29
|
-
|
|
30
|
-
import {
|
|
31
|
-
and,
|
|
32
|
-
count,
|
|
33
|
-
eq,
|
|
34
|
-
ExtractTablesWithRelations,
|
|
35
|
-
inArray,
|
|
36
|
-
sql,
|
|
37
|
-
} from "drizzle-orm";
|
|
38
|
-
import {
|
|
39
|
-
NodePgDatabase,
|
|
40
|
-
NodePgQueryResultHKT,
|
|
41
|
-
NodePgTransaction,
|
|
42
|
-
} from "drizzle-orm/node-postgres";
|
|
43
|
-
import {
|
|
44
|
-
createDocumentQuery,
|
|
45
|
-
getDriveBySlug,
|
|
46
|
-
upsertDrive,
|
|
47
|
-
} from "./drizzle/queries";
|
|
48
|
-
import {
|
|
49
|
-
attachmentsTable,
|
|
50
|
-
documentsTable,
|
|
51
|
-
drivesTable,
|
|
52
|
-
operationsTable,
|
|
53
|
-
} from "./drizzle/schema";
|
|
54
|
-
import { randomUUID } from "crypto";
|
|
55
|
-
import { PgQueryResultHKT, PgTransaction } from "drizzle-orm/pg-core";
|
|
56
|
-
|
|
57
|
-
// type Transaction =
|
|
58
|
-
// | Omit<
|
|
59
|
-
// PrismaClient<Prisma.PrismaClientOptions, never>,
|
|
60
|
-
// "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends"
|
|
61
|
-
// >
|
|
62
|
-
// | ExtendedPrismaClient;
|
|
63
|
-
|
|
64
|
-
function storageToOperation(
|
|
65
|
-
op: typeof operationsTable.$inferSelect & {
|
|
66
|
-
attachments?: AttachmentInput[];
|
|
67
|
-
},
|
|
68
|
-
): Operation {
|
|
69
|
-
const operation: Operation = {
|
|
70
|
-
id: op.opId || undefined,
|
|
71
|
-
skip: op.skip,
|
|
72
|
-
hash: op.hash,
|
|
73
|
-
index: op.index,
|
|
74
|
-
timestamp: new Date(op.timestamp).toISOString(),
|
|
75
|
-
input: JSON.parse(op.input),
|
|
76
|
-
type: op.type,
|
|
77
|
-
scope: op.scope as OperationScope,
|
|
78
|
-
resultingState: op.resultingState
|
|
79
|
-
? op.resultingState.toString()
|
|
80
|
-
: undefined,
|
|
81
|
-
attachments: op.attachments,
|
|
82
|
-
};
|
|
83
|
-
if (op.context) {
|
|
84
|
-
operation.context = op.context;
|
|
85
|
-
}
|
|
86
|
-
return operation;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export type DrizzleStorageOptions = {
|
|
90
|
-
transactionRetryBackoff?: IBackOffOptions;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
function getRetryTransactionsClient<T extends NodePgDatabase>(
|
|
94
|
-
db: T,
|
|
95
|
-
backOffOptions?: Partial<IBackOffOptions>,
|
|
96
|
-
) {
|
|
97
|
-
return db;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
type ExtendedDrizzleClient = ReturnType<
|
|
101
|
-
typeof getRetryTransactionsClient<NodePgDatabase>
|
|
102
|
-
>;
|
|
103
|
-
|
|
104
|
-
export class DrizzleStorage implements IDriveStorage {
|
|
105
|
-
private db: NodePgDatabase;
|
|
106
|
-
private delegate: IStorageDelegate | undefined;
|
|
107
|
-
|
|
108
|
-
constructor(db: NodePgDatabase, options?: DrizzleStorageOptions) {
|
|
109
|
-
const backOffOptions = options?.transactionRetryBackoff;
|
|
110
|
-
this.db = getRetryTransactionsClient(db, {
|
|
111
|
-
...backOffOptions,
|
|
112
|
-
jitter: backOffOptions?.jitter ?? "full",
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
setStorageDelegate(delegate: IStorageDelegate): void {
|
|
117
|
-
this.delegate = delegate;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
|
|
121
|
-
// drive for all drive documents
|
|
122
|
-
await this.createDocument("drives", id, drive as DocumentStorage);
|
|
123
|
-
await upsertDrive(this.db, id, drive);
|
|
124
|
-
}
|
|
125
|
-
async addDriveOperations(
|
|
126
|
-
id: string,
|
|
127
|
-
operations: Operation[],
|
|
128
|
-
header: DocumentHeader,
|
|
129
|
-
): Promise<void> {
|
|
130
|
-
await this.addDocumentOperations("drives", id, operations, header);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async addDriveOperationsWithTransaction(
|
|
134
|
-
drive: string,
|
|
135
|
-
callback: (document: DocumentDriveStorage) => Promise<{
|
|
136
|
-
operations: Operation<DocumentDriveAction | BaseAction>[];
|
|
137
|
-
header: DocumentHeader;
|
|
138
|
-
}>,
|
|
139
|
-
) {
|
|
140
|
-
return this.addDocumentOperationsWithTransaction(
|
|
141
|
-
"drives",
|
|
142
|
-
drive,
|
|
143
|
-
(document) => callback(document as DocumentDriveStorage),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async createDocument(
|
|
148
|
-
drive: string,
|
|
149
|
-
id: string,
|
|
150
|
-
document: DocumentStorage,
|
|
151
|
-
): Promise<void> {
|
|
152
|
-
await createDocumentQuery(this.db, drive, id, document);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async _addDocumentOperations(
|
|
156
|
-
tx: PgTransaction<
|
|
157
|
-
NodePgQueryResultHKT,
|
|
158
|
-
Record<string, never>,
|
|
159
|
-
ExtractTablesWithRelations<Record<string, never>>
|
|
160
|
-
>,
|
|
161
|
-
drive: string,
|
|
162
|
-
id: string,
|
|
163
|
-
operations: Operation[],
|
|
164
|
-
header: DocumentHeader,
|
|
165
|
-
): Promise<void> {
|
|
166
|
-
try {
|
|
167
|
-
await tx.insert(operationsTable).values(
|
|
168
|
-
operations.map((op) => ({
|
|
169
|
-
driveId: drive,
|
|
170
|
-
id: randomUUID(),
|
|
171
|
-
documentId: id,
|
|
172
|
-
hash: op.hash,
|
|
173
|
-
index: op.index,
|
|
174
|
-
input: JSON.stringify(op.input),
|
|
175
|
-
timestamp: op.timestamp,
|
|
176
|
-
type: op.type,
|
|
177
|
-
scope: op.scope,
|
|
178
|
-
branch: "main",
|
|
179
|
-
opId: op.id,
|
|
180
|
-
skip: op.skip,
|
|
181
|
-
context: op.context,
|
|
182
|
-
resultingState: op.resultingState
|
|
183
|
-
? Buffer.from(JSON.stringify(op.resultingState))
|
|
184
|
-
: undefined,
|
|
185
|
-
})),
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
await tx
|
|
189
|
-
.update(documentsTable)
|
|
190
|
-
.set({
|
|
191
|
-
lastModified: header.lastModified,
|
|
192
|
-
revision: JSON.stringify(header.revision),
|
|
193
|
-
})
|
|
194
|
-
.where(
|
|
195
|
-
and(eq(documentsTable.id, id), eq(documentsTable.driveId, drive)),
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
await Promise.all(
|
|
199
|
-
operations
|
|
200
|
-
.filter((o) => o.attachments?.length)
|
|
201
|
-
.map((op) => {
|
|
202
|
-
return tx
|
|
203
|
-
.update(operationsTable)
|
|
204
|
-
.set({
|
|
205
|
-
driveId: drive,
|
|
206
|
-
documentId: id,
|
|
207
|
-
index: op.index,
|
|
208
|
-
scope: op.scope,
|
|
209
|
-
branch: "main",
|
|
210
|
-
})
|
|
211
|
-
.where(
|
|
212
|
-
and(
|
|
213
|
-
eq(operationsTable.documentId, id),
|
|
214
|
-
eq(operationsTable.driveId, drive),
|
|
215
|
-
),
|
|
216
|
-
);
|
|
217
|
-
}),
|
|
218
|
-
);
|
|
219
|
-
} catch (e) {
|
|
220
|
-
// P2002: Unique constraint failed
|
|
221
|
-
// Operation with existing index
|
|
222
|
-
// if (e instanceof PrismaClientKnownRequestError && e.code === "P2002") {
|
|
223
|
-
// const existingOperation = await this.db.operation.findFirst({
|
|
224
|
-
// where: {
|
|
225
|
-
// AND: operations.map((op) => ({
|
|
226
|
-
// driveId: drive,
|
|
227
|
-
// documentId: id,
|
|
228
|
-
// scope: op.scope,
|
|
229
|
-
// branch: "main",
|
|
230
|
-
// index: op.index,
|
|
231
|
-
// })),
|
|
232
|
-
// },
|
|
233
|
-
// });
|
|
234
|
-
// const conflictOp = operations.find(
|
|
235
|
-
// (op) =>
|
|
236
|
-
// existingOperation?.index === op.index &&
|
|
237
|
-
// existingOperation.scope === op.scope
|
|
238
|
-
// );
|
|
239
|
-
// if (!existingOperation || !conflictOp) {
|
|
240
|
-
// console.error(e);
|
|
241
|
-
// throw e;
|
|
242
|
-
// } else {
|
|
243
|
-
// throw new ConflictOperationError(
|
|
244
|
-
// storageToOperation(existingOperation),
|
|
245
|
-
// conflictOp
|
|
246
|
-
// );
|
|
247
|
-
// }
|
|
248
|
-
// } else {
|
|
249
|
-
// throw e;
|
|
250
|
-
// }
|
|
251
|
-
console.error(e);
|
|
252
|
-
throw e;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async addDocumentOperationsWithTransaction(
|
|
257
|
-
drive: string,
|
|
258
|
-
id: string,
|
|
259
|
-
callback: (document: DocumentStorage) => Promise<{
|
|
260
|
-
operations: Operation[];
|
|
261
|
-
header: DocumentHeader;
|
|
262
|
-
newState?: State<any, any> | undefined;
|
|
263
|
-
}>,
|
|
264
|
-
) {
|
|
265
|
-
let result: {
|
|
266
|
-
operations: Operation[];
|
|
267
|
-
header: DocumentHeader;
|
|
268
|
-
newState?: State<any, any> | undefined;
|
|
269
|
-
} | null = null;
|
|
270
|
-
|
|
271
|
-
await this.db.transaction(
|
|
272
|
-
async (tx) => {
|
|
273
|
-
const document = await this.getDocument(
|
|
274
|
-
drive,
|
|
275
|
-
id,
|
|
276
|
-
tx as unknown as NodePgDatabase<Record<string, never>>,
|
|
277
|
-
);
|
|
278
|
-
if (!document) {
|
|
279
|
-
throw new Error(`Document with id ${id} not found`);
|
|
280
|
-
}
|
|
281
|
-
result = await callback(document);
|
|
282
|
-
|
|
283
|
-
const { operations, header, newState } = result;
|
|
284
|
-
return this._addDocumentOperations(tx, drive, id, operations, header);
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
accessMode: "read write",
|
|
288
|
-
isolationLevel: "serializable",
|
|
289
|
-
},
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
293
|
-
if (!result) {
|
|
294
|
-
throw new Error("No operations were provided");
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return result;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async addDocumentOperations(
|
|
301
|
-
drive: string,
|
|
302
|
-
id: string,
|
|
303
|
-
operations: Operation[],
|
|
304
|
-
header: DocumentHeader,
|
|
305
|
-
): Promise<void> {
|
|
306
|
-
return this._addDocumentOperations(
|
|
307
|
-
this.db as PgTransaction<
|
|
308
|
-
NodePgQueryResultHKT,
|
|
309
|
-
Record<string, never>,
|
|
310
|
-
ExtractTablesWithRelations<Record<string, never>>
|
|
311
|
-
>,
|
|
312
|
-
drive,
|
|
313
|
-
id,
|
|
314
|
-
operations,
|
|
315
|
-
header,
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
async getDocuments(drive: string): Promise<string[]> {
|
|
319
|
-
const docs: { id: string }[] = await this.db
|
|
320
|
-
.select({ id: documentsTable.id })
|
|
321
|
-
.from(documentsTable)
|
|
322
|
-
.where(eq(documentsTable.driveId, drive));
|
|
323
|
-
|
|
324
|
-
return docs.map((d) => d.id);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async checkDocumentExists(driveId: string, id: string) {
|
|
328
|
-
const [result] = await this.db
|
|
329
|
-
.select({ count: count() })
|
|
330
|
-
.from(documentsTable)
|
|
331
|
-
.where(
|
|
332
|
-
and(eq(documentsTable.id, id), eq(documentsTable.driveId, driveId)),
|
|
333
|
-
);
|
|
334
|
-
if (!result) {
|
|
335
|
-
return false;
|
|
336
|
-
}
|
|
337
|
-
return result.count > 0;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async getDocument(
|
|
341
|
-
driveId: string,
|
|
342
|
-
id: string,
|
|
343
|
-
tx?: NodePgDatabase<Record<string, never>>,
|
|
344
|
-
) {
|
|
345
|
-
const db = tx ?? this.db;
|
|
346
|
-
const [result] = await db
|
|
347
|
-
.select()
|
|
348
|
-
.from(documentsTable)
|
|
349
|
-
.where(
|
|
350
|
-
and(eq(documentsTable.id, id), eq(documentsTable.driveId, driveId)),
|
|
351
|
-
)
|
|
352
|
-
.limit(1);
|
|
353
|
-
|
|
354
|
-
if (result === null) {
|
|
355
|
-
throw new Error(`Document with id ${id} not found`);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const cachedOperations = (await this.delegate?.getCachedOperations(
|
|
359
|
-
driveId,
|
|
360
|
-
id,
|
|
361
|
-
)) ?? {
|
|
362
|
-
global: [],
|
|
363
|
-
local: [],
|
|
364
|
-
};
|
|
365
|
-
const scopeIndex = Object.keys(cachedOperations).reduceRight<
|
|
366
|
-
Record<OperationScope, number>
|
|
367
|
-
>(
|
|
368
|
-
(acc, value) => {
|
|
369
|
-
const scope = value as OperationScope;
|
|
370
|
-
const lastIndex = cachedOperations[scope]?.at(-1)?.index ?? -1;
|
|
371
|
-
acc[scope] = lastIndex;
|
|
372
|
-
return acc;
|
|
373
|
-
},
|
|
374
|
-
{ global: -1, local: -1 },
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
const conditions = Object.entries(scopeIndex).map(
|
|
378
|
-
([scope, index]) => `("scope" = '${scope}' AND "index" > ${index})`,
|
|
379
|
-
);
|
|
380
|
-
conditions.push(
|
|
381
|
-
`("scope" NOT IN (${Object.keys(cachedOperations)
|
|
382
|
-
.map((s) => `'${s}'`)
|
|
383
|
-
.join(", ")}))`,
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
// retrieves operations with resulting state
|
|
387
|
-
// for the last operation of each scope
|
|
388
|
-
// TODO prevent SQL injection
|
|
389
|
-
const queryOperations = await this.db.execute(
|
|
390
|
-
sql`WITH ranked_operations AS (
|
|
391
|
-
SELECT
|
|
392
|
-
*,
|
|
393
|
-
ROW_NUMBER() OVER (PARTITION BY scope ORDER BY index DESC) AS rn
|
|
394
|
-
FROM "Operation"
|
|
395
|
-
)
|
|
396
|
-
SELECT
|
|
397
|
-
"id",
|
|
398
|
-
"opId",
|
|
399
|
-
"scope",
|
|
400
|
-
"branch",
|
|
401
|
-
"index",
|
|
402
|
-
"skip",
|
|
403
|
-
"hash",
|
|
404
|
-
"timestamp",
|
|
405
|
-
"input",
|
|
406
|
-
"type",
|
|
407
|
-
"context",
|
|
408
|
-
CASE
|
|
409
|
-
WHEN rn = 1 THEN "resultingState"
|
|
410
|
-
ELSE NULL
|
|
411
|
-
END AS "resultingState"
|
|
412
|
-
FROM ranked_operations
|
|
413
|
-
WHERE "driveId" = ${driveId} AND "documentId" = ${id}
|
|
414
|
-
AND (${conditions.join(" OR ")})
|
|
415
|
-
ORDER BY scope, index;
|
|
416
|
-
`,
|
|
417
|
-
);
|
|
418
|
-
const operationIds = queryOperations.map((o: Operation) => o.id);
|
|
419
|
-
const attachments = await this.db
|
|
420
|
-
.select()
|
|
421
|
-
.from(attachmentsTable)
|
|
422
|
-
.where(inArray(attachmentsTable.operationId, operationIds));
|
|
423
|
-
|
|
424
|
-
// TODO add attachments from cached operations
|
|
425
|
-
const fileRegistry: FileRegistry = {};
|
|
426
|
-
|
|
427
|
-
const operationsByScope = queryOperations.reduce<
|
|
428
|
-
DocumentOperations<Action>
|
|
429
|
-
>(
|
|
430
|
-
(
|
|
431
|
-
acc: Record<string, Operation[]>,
|
|
432
|
-
operation: typeof operationsTable.$inferSelect,
|
|
433
|
-
) => {
|
|
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
|
-
);
|
|
450
|
-
|
|
451
|
-
const dbDoc = result;
|
|
452
|
-
if (!dbDoc) {
|
|
453
|
-
throw new Error("Document not found");
|
|
454
|
-
}
|
|
455
|
-
const doc: Document = {
|
|
456
|
-
created: dbDoc.created,
|
|
457
|
-
name: dbDoc.name ? dbDoc.name : "",
|
|
458
|
-
documentType: dbDoc.documentType,
|
|
459
|
-
initialState: JSON.parse(dbDoc.initialState) as ExtendedState<
|
|
460
|
-
DocumentDriveState,
|
|
461
|
-
DocumentDriveLocalState
|
|
462
|
-
>,
|
|
463
|
-
// @ts-expect-error TODO: fix as this should not be undefined
|
|
464
|
-
state: undefined,
|
|
465
|
-
lastModified: new Date(dbDoc.lastModified).toISOString(),
|
|
466
|
-
operations: operationsByScope,
|
|
467
|
-
clipboard: [],
|
|
468
|
-
revision: JSON.parse(dbDoc.revision) as Record<OperationScope, number>,
|
|
469
|
-
attachments: {},
|
|
470
|
-
};
|
|
471
|
-
return doc;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
async deleteDocument(drive: string, id: string) {
|
|
475
|
-
try {
|
|
476
|
-
await this.db
|
|
477
|
-
.delete(documentsTable)
|
|
478
|
-
.where(
|
|
479
|
-
and(eq(documentsTable.driveId, drive), eq(documentsTable.id, id)),
|
|
480
|
-
);
|
|
481
|
-
} catch (e: unknown) {
|
|
482
|
-
console.error(e);
|
|
483
|
-
throw e;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
async getDrives() {
|
|
488
|
-
return this.getDocuments("drives");
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async getDrive(id: string) {
|
|
492
|
-
try {
|
|
493
|
-
const doc = await this.getDocument("drives", id);
|
|
494
|
-
return doc as DocumentDriveStorage;
|
|
495
|
-
} catch (e) {
|
|
496
|
-
logger.error(e);
|
|
497
|
-
throw new DriveNotFoundError(id);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
async getDriveBySlug(slug: string) {
|
|
502
|
-
const driveEntity = await getDriveBySlug(this.db, slug);
|
|
503
|
-
|
|
504
|
-
if (!driveEntity) {
|
|
505
|
-
throw new Error(`Drive with slug ${slug} not found`);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return this.getDrive(driveEntity.id);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
async deleteDrive(id: string) {
|
|
512
|
-
// delete drive and associated slug
|
|
513
|
-
await this.db.delete(drivesTable).where(eq(drivesTable.id, id));
|
|
514
|
-
|
|
515
|
-
// delete drive document and its operations
|
|
516
|
-
await this.deleteDocument("drives", id);
|
|
517
|
-
|
|
518
|
-
// deletes all documents of the drive
|
|
519
|
-
await this.db.delete(documentsTable).where(eq(documentsTable.driveId, id));
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
async getOperationResultingState(
|
|
523
|
-
driveId: string,
|
|
524
|
-
documentId: string,
|
|
525
|
-
index: number,
|
|
526
|
-
scope: string,
|
|
527
|
-
branch: string,
|
|
528
|
-
): Promise<unknown> {
|
|
529
|
-
const [operation] = await this.db
|
|
530
|
-
.select()
|
|
531
|
-
.from(operationsTable)
|
|
532
|
-
.where(
|
|
533
|
-
and(
|
|
534
|
-
eq(operationsTable.driveId, driveId),
|
|
535
|
-
eq(operationsTable.documentId, documentId),
|
|
536
|
-
eq(operationsTable.index, index),
|
|
537
|
-
eq(operationsTable.scope, scope),
|
|
538
|
-
eq(operationsTable.branch, branch),
|
|
539
|
-
),
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
return operation?.resultingState?.toString();
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
getDriveOperationResultingState(
|
|
546
|
-
drive: string,
|
|
547
|
-
index: number,
|
|
548
|
-
scope: string,
|
|
549
|
-
branch: string,
|
|
550
|
-
): Promise<unknown> {
|
|
551
|
-
return this.getOperationResultingState(
|
|
552
|
-
"drives",
|
|
553
|
-
drive,
|
|
554
|
-
index,
|
|
555
|
-
scope,
|
|
556
|
-
branch,
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
async getSynchronizationUnitsRevision(
|
|
561
|
-
units: SynchronizationUnitQuery[],
|
|
562
|
-
): Promise<
|
|
563
|
-
{
|
|
564
|
-
driveId: string;
|
|
565
|
-
documentId: string;
|
|
566
|
-
scope: string;
|
|
567
|
-
branch: string;
|
|
568
|
-
lastUpdated: string;
|
|
569
|
-
revision: number;
|
|
570
|
-
}[]
|
|
571
|
-
> {
|
|
572
|
-
// TODO add branch condition
|
|
573
|
-
const whereClauses = units
|
|
574
|
-
.map((_, index) => {
|
|
575
|
-
return `("driveId" = $${index * 3 + 1} AND "documentId" = $${index * 3 + 2} AND "scope" = $${index * 3 + 3})`;
|
|
576
|
-
})
|
|
577
|
-
.join(" OR ");
|
|
578
|
-
|
|
579
|
-
const query = `
|
|
580
|
-
SELECT "driveId", "documentId", "scope", "branch", MAX("timestamp") as "lastUpdated", MAX("index") as revision FROM "Operation"
|
|
581
|
-
WHERE ${whereClauses}
|
|
582
|
-
GROUP BY "driveId", "documentId", "scope", "branch"
|
|
583
|
-
`;
|
|
584
|
-
|
|
585
|
-
const params = units
|
|
586
|
-
.map((unit) => [
|
|
587
|
-
unit.documentId ? unit.driveId : "drives",
|
|
588
|
-
unit.documentId || unit.driveId,
|
|
589
|
-
unit.scope,
|
|
590
|
-
])
|
|
591
|
-
.flat();
|
|
592
|
-
const results = await this.db.$queryRawUnsafe<
|
|
593
|
-
{
|
|
594
|
-
driveId: string;
|
|
595
|
-
documentId: string;
|
|
596
|
-
lastUpdated: string;
|
|
597
|
-
scope: OperationScope;
|
|
598
|
-
branch: string;
|
|
599
|
-
revision: number;
|
|
600
|
-
}[]
|
|
601
|
-
>(query, ...params);
|
|
602
|
-
return results.map((row) => ({
|
|
603
|
-
...row,
|
|
604
|
-
driveId: row.driveId === "drives" ? row.documentId : row.driveId,
|
|
605
|
-
documentId: row.driveId === "drives" ? "" : row.documentId,
|
|
606
|
-
lastUpdated: new Date(row.lastUpdated).toISOString(),
|
|
607
|
-
}));
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// migrates all stored operations from legacy signature to signatures array
|
|
611
|
-
async migrateOperationSignatures() {
|
|
612
|
-
const count = await this.db.$executeRaw`
|
|
613
|
-
UPDATE "Operation"
|
|
614
|
-
SET context = jsonb_set(
|
|
615
|
-
context #- '{signer,signature}', -- Remove the old 'signature' field
|
|
616
|
-
'{signer,signatures}', -- Path to the new 'signatures' field
|
|
617
|
-
CASE
|
|
618
|
-
WHEN context->'signer'->>'signature' = '' THEN '[]'::jsonb
|
|
619
|
-
ELSE to_jsonb(array[context->'signer'->>'signature'])
|
|
620
|
-
END
|
|
621
|
-
)
|
|
622
|
-
WHERE context->'signer' ? 'signature' -- Check if the 'signature' key exists
|
|
623
|
-
`;
|
|
624
|
-
logger.info(`Migrated ${count} operations`);
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
}
|