document-drive 0.0.26 → 0.0.28
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 +18 -10
- package/src/server/error.ts +18 -0
- package/src/server/index.ts +678 -82
- package/src/server/listener/index.ts +2 -0
- package/src/server/listener/manager.ts +382 -0
- package/src/server/listener/transmitter/index.ts +3 -0
- package/src/server/listener/transmitter/pull-responder.ts +308 -0
- package/src/server/listener/transmitter/switchboard-push.ts +63 -0
- package/src/server/listener/transmitter/types.ts +18 -0
- package/src/server/types.ts +209 -23
- package/src/storage/filesystem.ts +2 -2
- package/src/storage/index.ts +0 -4
- package/src/storage/memory.ts +5 -2
- package/src/storage/prisma.ts +65 -18
- package/src/utils/graphql.ts +46 -0
- package/src/{utils.ts → utils/index.ts} +8 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import stringify from 'json-stringify-deterministic';
|
|
2
|
+
import { gql, requestGraphql } from '../../../utils/graphql';
|
|
3
|
+
import {
|
|
4
|
+
BaseDocumentDriveServer,
|
|
5
|
+
Listener,
|
|
6
|
+
ListenerRevision,
|
|
7
|
+
StrandUpdate
|
|
8
|
+
} from '../../types';
|
|
9
|
+
import { ITransmitter } from './types';
|
|
10
|
+
|
|
11
|
+
export class SwitchboardPushTransmitter implements ITransmitter {
|
|
12
|
+
private drive: BaseDocumentDriveServer;
|
|
13
|
+
private listener: Listener;
|
|
14
|
+
private targetURL: string;
|
|
15
|
+
|
|
16
|
+
constructor(listener: Listener, drive: BaseDocumentDriveServer) {
|
|
17
|
+
this.listener = listener;
|
|
18
|
+
this.drive = drive;
|
|
19
|
+
this.targetURL = listener.callInfo!.data!;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async transmit(strands: StrandUpdate[]): Promise<ListenerRevision[]> {
|
|
23
|
+
// Send Graphql mutation to switchboard
|
|
24
|
+
try {
|
|
25
|
+
const { pushUpdates } = await requestGraphql<{
|
|
26
|
+
pushUpdates: ListenerRevision[];
|
|
27
|
+
}>(
|
|
28
|
+
this.targetURL,
|
|
29
|
+
gql`
|
|
30
|
+
mutation pushUpdates($strands: [InputStrandUpdate!]) {
|
|
31
|
+
pushUpdates(strands: $strands) {
|
|
32
|
+
driveId
|
|
33
|
+
documentId
|
|
34
|
+
scope
|
|
35
|
+
branch
|
|
36
|
+
status
|
|
37
|
+
revision
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
`,
|
|
41
|
+
{
|
|
42
|
+
strands: strands.map(strand => ({
|
|
43
|
+
...strand,
|
|
44
|
+
operations: strand.operations.map(op => ({
|
|
45
|
+
...op,
|
|
46
|
+
input: stringify(op.input)
|
|
47
|
+
}))
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (!pushUpdates) {
|
|
53
|
+
throw new Error("Couldn't update listener revision");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return pushUpdates;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error(e);
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PullResponderTriggerData,
|
|
3
|
+
Trigger
|
|
4
|
+
} from 'document-model-libs/document-drive';
|
|
5
|
+
import { ListenerRevision, StrandUpdate } from '../..';
|
|
6
|
+
|
|
7
|
+
export interface ITransmitter {
|
|
8
|
+
transmit(strands: StrandUpdate[]): Promise<ListenerRevision[]>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface InternalTransmitterService extends ITransmitter {
|
|
12
|
+
getName(): string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PullResponderTrigger = Omit<Trigger, 'data' | 'type'> & {
|
|
16
|
+
data: PullResponderTriggerData;
|
|
17
|
+
type: 'PullResponder';
|
|
18
|
+
};
|
package/src/server/types.ts
CHANGED
|
@@ -2,68 +2,254 @@ import type {
|
|
|
2
2
|
DocumentDriveAction,
|
|
3
3
|
DocumentDriveDocument,
|
|
4
4
|
DocumentDriveLocalState,
|
|
5
|
-
DocumentDriveState
|
|
5
|
+
DocumentDriveState,
|
|
6
|
+
ListenerCallInfo,
|
|
7
|
+
ListenerFilter
|
|
6
8
|
} from 'document-model-libs/document-drive';
|
|
7
9
|
import type {
|
|
8
10
|
BaseAction,
|
|
11
|
+
CreateChildDocumentInput,
|
|
9
12
|
Document,
|
|
10
13
|
Operation,
|
|
14
|
+
OperationScope,
|
|
11
15
|
Signal,
|
|
12
16
|
State
|
|
13
17
|
} from 'document-model/document';
|
|
18
|
+
import { OperationError } from './error';
|
|
19
|
+
import { ITransmitter } from './listener/transmitter/types';
|
|
14
20
|
|
|
15
21
|
export type DriveInput = State<
|
|
16
|
-
Omit<DocumentDriveState, '__typename' | 'nodes'
|
|
22
|
+
Omit<DocumentDriveState, '__typename' | 'id' | 'nodes'> & { id?: string },
|
|
17
23
|
DocumentDriveLocalState
|
|
18
24
|
>;
|
|
19
25
|
|
|
20
|
-
export type
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
export type RemoteDriveOptions = DocumentDriveLocalState & {
|
|
27
|
+
// TODO make local state optional
|
|
28
|
+
pullFilter?: ListenerFilter;
|
|
29
|
+
pullInterval?: number;
|
|
24
30
|
};
|
|
25
31
|
|
|
32
|
+
export type CreateDocumentInput = CreateChildDocumentInput;
|
|
33
|
+
|
|
26
34
|
export type SignalResult = {
|
|
27
35
|
signal: Signal;
|
|
28
36
|
result: unknown; // infer from return types on document-model
|
|
29
37
|
};
|
|
30
38
|
|
|
31
39
|
export type IOperationResult<T extends Document = Document> = {
|
|
32
|
-
|
|
33
|
-
error?:
|
|
40
|
+
status: UpdateStatus;
|
|
41
|
+
error?: OperationError;
|
|
34
42
|
operations: Operation[];
|
|
35
43
|
document: T | undefined;
|
|
36
44
|
signals: SignalResult[];
|
|
37
45
|
};
|
|
38
46
|
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
export type SynchronizationUnit = {
|
|
48
|
+
syncId: string;
|
|
49
|
+
driveId: string;
|
|
50
|
+
documentId: string;
|
|
51
|
+
documentType: string;
|
|
52
|
+
scope: string;
|
|
53
|
+
branch: string;
|
|
54
|
+
lastUpdated: string;
|
|
55
|
+
revision: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type Listener = {
|
|
59
|
+
driveId: string;
|
|
60
|
+
listenerId: string;
|
|
61
|
+
label?: string;
|
|
62
|
+
block: boolean;
|
|
63
|
+
system: boolean;
|
|
64
|
+
filter: ListenerFilter;
|
|
65
|
+
callInfo?: ListenerCallInfo;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type CreateListenerInput = {
|
|
69
|
+
driveId: string;
|
|
70
|
+
label?: string;
|
|
71
|
+
block: boolean;
|
|
72
|
+
system: boolean;
|
|
73
|
+
filter: ListenerFilter;
|
|
74
|
+
callInfo?: ListenerCallInfo;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export enum TransmitterType {
|
|
78
|
+
Internal,
|
|
79
|
+
SwitchboardPush,
|
|
80
|
+
PullResponder,
|
|
81
|
+
SecureConnect,
|
|
82
|
+
MatrixConnect,
|
|
83
|
+
RESTWebhook
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ListenerRevision = {
|
|
87
|
+
driveId: string;
|
|
88
|
+
documentId: string;
|
|
89
|
+
scope: string;
|
|
90
|
+
branch: string;
|
|
91
|
+
status: UpdateStatus;
|
|
92
|
+
revision: number;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type UpdateStatus = 'SUCCESS' | 'CONFLICT' | 'MISSING' | 'ERROR';
|
|
96
|
+
export type ErrorStatus = Exclude<UpdateStatus, 'SUCCESS'>;
|
|
97
|
+
|
|
98
|
+
export type OperationUpdate = {
|
|
99
|
+
timestamp: string;
|
|
100
|
+
index: number;
|
|
101
|
+
skip: number;
|
|
102
|
+
type: string;
|
|
103
|
+
input: object;
|
|
104
|
+
hash: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type StrandUpdate = {
|
|
108
|
+
driveId: string;
|
|
109
|
+
documentId: string;
|
|
110
|
+
scope: OperationScope;
|
|
111
|
+
branch: string;
|
|
112
|
+
operations: OperationUpdate[];
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type SyncStatus = 'SYNCING' | UpdateStatus;
|
|
116
|
+
|
|
117
|
+
export abstract class BaseDocumentDriveServer {
|
|
118
|
+
/** Public methods **/
|
|
119
|
+
abstract getDrives(): Promise<string[]>;
|
|
120
|
+
abstract addDrive(drive: DriveInput): Promise<void>;
|
|
121
|
+
abstract addRemoteDrive(
|
|
122
|
+
url: string,
|
|
123
|
+
options: RemoteDriveOptions
|
|
124
|
+
): Promise<void>;
|
|
125
|
+
abstract deleteDrive(id: string): Promise<void>;
|
|
126
|
+
abstract getDrive(id: string): Promise<DocumentDriveDocument>;
|
|
44
127
|
|
|
45
|
-
getDocuments
|
|
46
|
-
getDocument
|
|
47
|
-
createDocument(drive: string, document: CreateDocumentInput): Promise<void>;
|
|
48
|
-
deleteDocument(drive: string, id: string): Promise<void>;
|
|
128
|
+
abstract getDocuments(drive: string): Promise<string[]>;
|
|
129
|
+
abstract getDocument(drive: string, id: string): Promise<Document>;
|
|
49
130
|
|
|
50
|
-
addOperation(
|
|
131
|
+
abstract addOperation(
|
|
51
132
|
drive: string,
|
|
52
133
|
id: string,
|
|
53
134
|
operation: Operation
|
|
54
|
-
): Promise<IOperationResult
|
|
55
|
-
addOperations(
|
|
135
|
+
): Promise<IOperationResult<Document>>;
|
|
136
|
+
abstract addOperations(
|
|
56
137
|
drive: string,
|
|
57
138
|
id: string,
|
|
58
139
|
operations: Operation[]
|
|
59
|
-
): Promise<IOperationResult
|
|
140
|
+
): Promise<IOperationResult<Document>>;
|
|
60
141
|
|
|
61
|
-
addDriveOperation(
|
|
142
|
+
abstract addDriveOperation(
|
|
62
143
|
drive: string,
|
|
63
144
|
operation: Operation<DocumentDriveAction | BaseAction>
|
|
64
145
|
): Promise<IOperationResult<DocumentDriveDocument>>;
|
|
65
|
-
addDriveOperations(
|
|
146
|
+
abstract addDriveOperations(
|
|
66
147
|
drive: string,
|
|
67
148
|
operations: Operation<DocumentDriveAction | BaseAction>[]
|
|
68
149
|
): Promise<IOperationResult<DocumentDriveDocument>>;
|
|
150
|
+
|
|
151
|
+
abstract getSyncStatus(drive: string): SyncStatus;
|
|
152
|
+
|
|
153
|
+
/** Synchronization methods */
|
|
154
|
+
abstract getSynchronizationUnits(
|
|
155
|
+
driveId: string,
|
|
156
|
+
documentId?: string[],
|
|
157
|
+
scope?: string[],
|
|
158
|
+
branch?: string[]
|
|
159
|
+
): Promise<SynchronizationUnit[]>;
|
|
160
|
+
|
|
161
|
+
abstract getSynchronizationUnit(
|
|
162
|
+
driveId: string,
|
|
163
|
+
syncId: string
|
|
164
|
+
): Promise<SynchronizationUnit>;
|
|
165
|
+
|
|
166
|
+
abstract getOperationData(
|
|
167
|
+
driveId: string,
|
|
168
|
+
syncId: string,
|
|
169
|
+
filter: {
|
|
170
|
+
since?: string;
|
|
171
|
+
fromRevision?: number;
|
|
172
|
+
}
|
|
173
|
+
): Promise<OperationUpdate[]>;
|
|
174
|
+
|
|
175
|
+
/** Internal methods **/
|
|
176
|
+
protected abstract createDocument(
|
|
177
|
+
drive: string,
|
|
178
|
+
document: CreateDocumentInput
|
|
179
|
+
): Promise<Document>;
|
|
180
|
+
protected abstract deleteDocument(drive: string, id: string): Promise<void>;
|
|
181
|
+
|
|
182
|
+
abstract getTransmitter(
|
|
183
|
+
driveId: string,
|
|
184
|
+
listenerId: string
|
|
185
|
+
): Promise<ITransmitter | undefined>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export abstract class BaseListenerManager {
|
|
189
|
+
protected drive: BaseDocumentDriveServer;
|
|
190
|
+
protected listenerState: Map<string, Map<string, ListenerState>> =
|
|
191
|
+
new Map();
|
|
192
|
+
protected transmitters: Record<
|
|
193
|
+
DocumentDriveState['id'],
|
|
194
|
+
Record<Listener['listenerId'], ITransmitter>
|
|
195
|
+
> = {};
|
|
196
|
+
|
|
197
|
+
constructor(
|
|
198
|
+
drive: BaseDocumentDriveServer,
|
|
199
|
+
listenerState: Map<string, Map<string, ListenerState>> = new Map()
|
|
200
|
+
) {
|
|
201
|
+
this.drive = drive;
|
|
202
|
+
this.listenerState = listenerState;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
abstract init(): Promise<void>;
|
|
206
|
+
abstract addListener(listener: Listener): Promise<ITransmitter>;
|
|
207
|
+
abstract removeListener(
|
|
208
|
+
driveId: string,
|
|
209
|
+
listenerId: string
|
|
210
|
+
): Promise<boolean>;
|
|
211
|
+
abstract getTransmitter(
|
|
212
|
+
driveId: string,
|
|
213
|
+
listenerId: string
|
|
214
|
+
): Promise<ITransmitter | undefined>;
|
|
215
|
+
abstract updateSynchronizationRevision(
|
|
216
|
+
driveId: string,
|
|
217
|
+
syncId: string,
|
|
218
|
+
syncRev: number,
|
|
219
|
+
lastUpdated: string
|
|
220
|
+
): Promise<void>;
|
|
221
|
+
|
|
222
|
+
abstract updateListenerRevision(
|
|
223
|
+
listenerId: string,
|
|
224
|
+
driveId: string,
|
|
225
|
+
syncId: string,
|
|
226
|
+
listenerRev: number
|
|
227
|
+
): Promise<void>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export type IDocumentDriveServer = Pick<
|
|
231
|
+
BaseDocumentDriveServer,
|
|
232
|
+
keyof BaseDocumentDriveServer
|
|
233
|
+
>;
|
|
234
|
+
|
|
235
|
+
export type ListenerStatus =
|
|
236
|
+
| 'CREATED'
|
|
237
|
+
| 'PENDING'
|
|
238
|
+
| 'SUCCESS'
|
|
239
|
+
| 'MISSING'
|
|
240
|
+
| 'CONFLICT'
|
|
241
|
+
| 'ERROR';
|
|
242
|
+
|
|
243
|
+
export interface ListenerState {
|
|
244
|
+
driveId: string;
|
|
245
|
+
block: boolean;
|
|
246
|
+
pendingTimeout: string;
|
|
247
|
+
listener: Listener;
|
|
248
|
+
syncUnits: SyncronizationUnitState[];
|
|
249
|
+
listenerStatus: ListenerStatus;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface SyncronizationUnitState extends SynchronizationUnit {
|
|
253
|
+
listenerRev: number;
|
|
254
|
+
syncRev: number;
|
|
69
255
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
writeFileSync
|
|
10
10
|
} from 'fs';
|
|
11
11
|
import fs from 'fs/promises';
|
|
12
|
+
import stringify from 'json-stringify-deterministic';
|
|
12
13
|
import path from 'path';
|
|
13
14
|
import sanitize from 'sanitize-filename';
|
|
14
15
|
import { mergeOperations } from '..';
|
|
@@ -83,7 +84,6 @@ export class FilesystemStorage implements IDriveStorage {
|
|
|
83
84
|
});
|
|
84
85
|
return JSON.parse(content) as Promise<DocumentStorage>;
|
|
85
86
|
} catch (error) {
|
|
86
|
-
console.error(error);
|
|
87
87
|
throw new Error(`Document with id ${id} not found`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -91,7 +91,7 @@ export class FilesystemStorage implements IDriveStorage {
|
|
|
91
91
|
async createDocument(drive: string, id: string, document: DocumentStorage) {
|
|
92
92
|
const documentPath = this._buildDocumentPath(drive, id);
|
|
93
93
|
await ensureDir(path.dirname(documentPath));
|
|
94
|
-
await writeFileSync(documentPath,
|
|
94
|
+
await writeFileSync(documentPath, stringify(document), {
|
|
95
95
|
encoding: 'utf-8'
|
|
96
96
|
});
|
|
97
97
|
}
|
package/src/storage/index.ts
CHANGED
package/src/storage/memory.ts
CHANGED
|
@@ -30,6 +30,7 @@ export class MemoryStorage implements IDriveStorage {
|
|
|
30
30
|
if (!document) {
|
|
31
31
|
throw new Error(`Document with id ${id} not found`);
|
|
32
32
|
}
|
|
33
|
+
|
|
33
34
|
return document;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -47,7 +48,8 @@ export class MemoryStorage implements IDriveStorage {
|
|
|
47
48
|
revision,
|
|
48
49
|
documentType,
|
|
49
50
|
created,
|
|
50
|
-
lastModified
|
|
51
|
+
lastModified,
|
|
52
|
+
clipboard
|
|
51
53
|
} = document;
|
|
52
54
|
this.documents[drive]![id] = {
|
|
53
55
|
operations,
|
|
@@ -56,7 +58,8 @@ export class MemoryStorage implements IDriveStorage {
|
|
|
56
58
|
revision,
|
|
57
59
|
documentType,
|
|
58
60
|
created,
|
|
59
|
-
lastModified
|
|
61
|
+
lastModified,
|
|
62
|
+
clipboard
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
65
|
|
package/src/storage/prisma.ts
CHANGED
|
@@ -17,9 +17,11 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
17
17
|
constructor(db: Prisma.TransactionClient) {
|
|
18
18
|
this.db = db;
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
|
|
21
22
|
await this.createDocument('drives', id, drive as DocumentStorage);
|
|
22
23
|
}
|
|
24
|
+
|
|
23
25
|
async addDriveOperations(
|
|
24
26
|
id: string,
|
|
25
27
|
operations: Operation[],
|
|
@@ -27,6 +29,7 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
27
29
|
): Promise<void> {
|
|
28
30
|
await this.addDocumentOperations('drives', id, operations, header);
|
|
29
31
|
}
|
|
32
|
+
|
|
30
33
|
async createDocument(
|
|
31
34
|
drive: string,
|
|
32
35
|
id: string,
|
|
@@ -84,7 +87,8 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
84
87
|
timestamp: op.timestamp,
|
|
85
88
|
type: op.type,
|
|
86
89
|
scope: op.scope,
|
|
87
|
-
branch: 'main'
|
|
90
|
+
branch: 'main',
|
|
91
|
+
skip: op.skip
|
|
88
92
|
},
|
|
89
93
|
update: {
|
|
90
94
|
driveId: drive,
|
|
@@ -95,18 +99,17 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
95
99
|
timestamp: op.timestamp,
|
|
96
100
|
type: op.type,
|
|
97
101
|
scope: op.scope,
|
|
98
|
-
branch: 'main'
|
|
102
|
+
branch: 'main',
|
|
103
|
+
skip: op.skip
|
|
99
104
|
}
|
|
100
105
|
});
|
|
101
106
|
})
|
|
102
107
|
);
|
|
103
108
|
|
|
104
|
-
await this.db.document.
|
|
109
|
+
await this.db.document.updateMany({
|
|
105
110
|
where: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
driveId: 'drives'
|
|
109
|
-
}
|
|
111
|
+
id,
|
|
112
|
+
driveId: drive
|
|
110
113
|
},
|
|
111
114
|
data: {
|
|
112
115
|
lastModified: header.lastModified,
|
|
@@ -175,7 +178,6 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
const dbDoc = result;
|
|
178
|
-
|
|
179
181
|
const doc = {
|
|
180
182
|
created: dbDoc.created.toISOString(),
|
|
181
183
|
name: dbDoc.name ? dbDoc.name : '',
|
|
@@ -184,11 +186,12 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
184
186
|
DocumentDriveState,
|
|
185
187
|
DocumentDriveLocalState
|
|
186
188
|
>,
|
|
187
|
-
lastModified: dbDoc.lastModified.toISOString(),
|
|
189
|
+
lastModified: new Date(dbDoc.lastModified).toISOString(),
|
|
188
190
|
operations: {
|
|
189
191
|
global: dbDoc.operations
|
|
190
|
-
.filter(op => op.scope === 'global')
|
|
192
|
+
.filter(op => op.scope === 'global' && !op.clipboard)
|
|
191
193
|
.map(op => ({
|
|
194
|
+
skip: op.skip,
|
|
192
195
|
hash: op.hash,
|
|
193
196
|
index: op.index,
|
|
194
197
|
timestamp: new Date(op.timestamp).toISOString(),
|
|
@@ -198,8 +201,9 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
198
201
|
// attachments: fileRegistry
|
|
199
202
|
})),
|
|
200
203
|
local: dbDoc.operations
|
|
201
|
-
.filter(op => op.scope === 'local')
|
|
204
|
+
.filter(op => op.scope === 'local' && !op.clipboard)
|
|
202
205
|
.map(op => ({
|
|
206
|
+
skip: op.skip,
|
|
203
207
|
hash: op.hash,
|
|
204
208
|
index: op.index,
|
|
205
209
|
timestamp: new Date(op.timestamp).toISOString(),
|
|
@@ -209,6 +213,18 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
209
213
|
// attachments: fileRegistry
|
|
210
214
|
}))
|
|
211
215
|
},
|
|
216
|
+
clipboard: dbDoc.operations
|
|
217
|
+
.filter(op => op.clipboard)
|
|
218
|
+
.map(op => ({
|
|
219
|
+
skip: op.skip,
|
|
220
|
+
hash: op.hash,
|
|
221
|
+
index: op.index,
|
|
222
|
+
timestamp: new Date(op.timestamp).toISOString(),
|
|
223
|
+
input: op.input,
|
|
224
|
+
type: op.type,
|
|
225
|
+
scope: op.scope as OperationScope
|
|
226
|
+
// attachments: fileRegistry
|
|
227
|
+
})),
|
|
212
228
|
revision: dbDoc.revision as Required<Record<OperationScope, number>>
|
|
213
229
|
};
|
|
214
230
|
|
|
@@ -216,12 +232,48 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
async deleteDocument(drive: string, id: string) {
|
|
219
|
-
await this.db.
|
|
235
|
+
await this.db.attachment.deleteMany({
|
|
220
236
|
where: {
|
|
221
237
|
driveId: drive,
|
|
222
|
-
|
|
238
|
+
documentId: id
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await this.db.operation.deleteMany({
|
|
243
|
+
where: {
|
|
244
|
+
driveId: drive,
|
|
245
|
+
documentId: id
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await this.db.document.delete({
|
|
250
|
+
where: {
|
|
251
|
+
id_driveId: {
|
|
252
|
+
driveId: drive,
|
|
253
|
+
id: id
|
|
254
|
+
}
|
|
223
255
|
}
|
|
224
256
|
});
|
|
257
|
+
|
|
258
|
+
if (drive === 'drives') {
|
|
259
|
+
await this.db.attachment.deleteMany({
|
|
260
|
+
where: {
|
|
261
|
+
driveId: id
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await this.db.operation.deleteMany({
|
|
266
|
+
where: {
|
|
267
|
+
driveId: id
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
await this.db.document.deleteMany({
|
|
272
|
+
where: {
|
|
273
|
+
driveId: id
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
225
277
|
}
|
|
226
278
|
|
|
227
279
|
async getDrives() {
|
|
@@ -234,10 +286,5 @@ export class PrismaStorage implements IDriveStorage {
|
|
|
234
286
|
|
|
235
287
|
async deleteDrive(id: string) {
|
|
236
288
|
await this.deleteDocument('drives', id);
|
|
237
|
-
await this.db.document.deleteMany({
|
|
238
|
-
where: {
|
|
239
|
-
driveId: id
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
289
|
}
|
|
243
290
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import request, { GraphQLClient, gql } from 'graphql-request';
|
|
2
|
+
|
|
3
|
+
export { gql } from 'graphql-request';
|
|
4
|
+
|
|
5
|
+
export type DriveInfo = {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
icon?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// replaces fetch so it can be used in Node and Browser envs
|
|
13
|
+
export async function requestGraphql<T>(...args: Parameters<typeof request>) {
|
|
14
|
+
const [url, ...requestArgs] = args;
|
|
15
|
+
const client = new GraphQLClient(url, { fetch });
|
|
16
|
+
return client.request<T>(...requestArgs);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function requestPublicDrive(url: string): Promise<DriveInfo> {
|
|
20
|
+
let drive: DriveInfo;
|
|
21
|
+
try {
|
|
22
|
+
const result = await requestGraphql<{ drive: DriveInfo }>(
|
|
23
|
+
url,
|
|
24
|
+
gql`
|
|
25
|
+
query getDrive {
|
|
26
|
+
drive {
|
|
27
|
+
id
|
|
28
|
+
name
|
|
29
|
+
icon
|
|
30
|
+
slug
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
);
|
|
35
|
+
drive = result.drive;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error(e);
|
|
38
|
+
throw new Error("Couldn't find drive info");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!drive) {
|
|
42
|
+
throw new Error('Drive not found');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return drive;
|
|
46
|
+
}
|
|
@@ -30,3 +30,11 @@ export function mergeOperations<A extends Action = Action>(
|
|
|
30
30
|
return acc;
|
|
31
31
|
}, currentOperations);
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
export function generateUUID(): string {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
36
|
+
const crypto =
|
|
37
|
+
typeof window !== 'undefined' ? window.crypto : require('crypto');
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
39
|
+
return crypto.randomUUID() as string;
|
|
40
|
+
}
|