document-drive 0.0.21 → 0.0.23
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 +14 -11
- package/src/server/index.ts +146 -55
- package/src/server/types.ts +4 -4
- package/src/storage/browser.ts +68 -38
- package/src/storage/filesystem.ts +50 -19
- package/src/storage/index.ts +1 -0
- package/src/storage/memory.ts +73 -7
- package/src/storage/prisma.ts +248 -0
- package/src/storage/types.ts +35 -6
- package/src/utils.ts +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -25,26 +25,29 @@
|
|
|
25
25
|
"test:watch": "vitest watch"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"document-model": "^1.0.
|
|
29
|
-
"document-model-libs": "^1.1.
|
|
28
|
+
"document-model": "^1.0.20",
|
|
29
|
+
"document-model-libs": "^1.1.27",
|
|
30
30
|
"localforage": "^1.10.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@prisma/client": "5.7.1",
|
|
33
34
|
"sanitize-filename": "^1.6.3"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@typescript
|
|
37
|
-
"@typescript-eslint/
|
|
37
|
+
"@total-typescript/ts-reset": "^0.5.1",
|
|
38
|
+
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
|
39
|
+
"@typescript-eslint/parser": "^6.18.1",
|
|
38
40
|
"@vitest/coverage-v8": "^0.34.6",
|
|
39
|
-
"document-model": "^1.0.
|
|
40
|
-
"document-model-libs": "^1.1.
|
|
41
|
-
"eslint": "^8.
|
|
42
|
-
"eslint-config-prettier": "^9.
|
|
41
|
+
"document-model": "^1.0.20",
|
|
42
|
+
"document-model-libs": "^1.1.27",
|
|
43
|
+
"eslint": "^8.56.0",
|
|
44
|
+
"eslint-config-prettier": "^9.1.0",
|
|
43
45
|
"fake-indexeddb": "^5.0.1",
|
|
44
46
|
"localforage": "^1.10.0",
|
|
45
|
-
"prettier": "^3.1.
|
|
47
|
+
"prettier": "^3.1.1",
|
|
46
48
|
"prettier-plugin-organize-imports": "^3.2.4",
|
|
47
|
-
"
|
|
49
|
+
"prisma": "^5.8.0",
|
|
50
|
+
"typescript": "^5.3.3",
|
|
48
51
|
"vitest": "^0.34.6"
|
|
49
52
|
}
|
|
50
53
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
+
import { DocumentDriveAction, utils } from 'document-model-libs/document-drive';
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { IDriveStorage } from '../storage';
|
|
3
|
+
BaseAction,
|
|
4
|
+
DocumentModel,
|
|
5
|
+
Operation,
|
|
6
|
+
utils as baseUtils
|
|
7
|
+
} from 'document-model/document';
|
|
8
|
+
import { DocumentStorage, IDriveStorage } from '../storage';
|
|
8
9
|
import { MemoryStorage } from '../storage/memory';
|
|
9
10
|
import { isDocumentDrive } from '../utils';
|
|
10
11
|
import {
|
|
11
12
|
CreateDocumentInput,
|
|
12
13
|
DriveInput,
|
|
13
14
|
IDocumentDriveServer,
|
|
14
|
-
IOperationResult,
|
|
15
15
|
SignalResult
|
|
16
16
|
} from './types';
|
|
17
17
|
|
|
@@ -39,11 +39,23 @@ export class DocumentDriveServer implements IDocumentDriveServer {
|
|
|
39
39
|
return documentModel;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
addDrive(drive: DriveInput) {
|
|
42
|
+
async addDrive(drive: DriveInput) {
|
|
43
|
+
const id = drive.global.id;
|
|
44
|
+
if (!id) {
|
|
45
|
+
throw new Error('Invalid Drive Id');
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const driveStorage = await this.storage.getDrive(id);
|
|
49
|
+
if (driveStorage) {
|
|
50
|
+
throw new Error('Drive already exists');
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore error has it means drive does not exist already
|
|
54
|
+
}
|
|
43
55
|
const document = utils.createDocument({
|
|
44
56
|
state: drive
|
|
45
57
|
});
|
|
46
|
-
return this.storage.
|
|
58
|
+
return this.storage.createDrive(id, document);
|
|
47
59
|
}
|
|
48
60
|
|
|
49
61
|
deleteDrive(id: string) {
|
|
@@ -54,12 +66,38 @@ export class DocumentDriveServer implements IDocumentDriveServer {
|
|
|
54
66
|
return this.storage.getDrives();
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
getDrive(drive: string) {
|
|
58
|
-
|
|
69
|
+
async getDrive(drive: string) {
|
|
70
|
+
const driveStorage = await this.storage.getDrive(drive);
|
|
71
|
+
const documentModel = this._getDocumentModel(driveStorage.documentType);
|
|
72
|
+
const document = baseUtils.replayDocument(
|
|
73
|
+
driveStorage.initialState,
|
|
74
|
+
driveStorage.operations,
|
|
75
|
+
documentModel.reducer,
|
|
76
|
+
undefined,
|
|
77
|
+
driveStorage
|
|
78
|
+
);
|
|
79
|
+
if (!isDocumentDrive(document)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Document with id ${drive} is not a Document Drive`
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
return document;
|
|
85
|
+
}
|
|
59
86
|
}
|
|
60
87
|
|
|
61
|
-
getDocument(drive: string, id: string) {
|
|
62
|
-
|
|
88
|
+
async getDocument(drive: string, id: string) {
|
|
89
|
+
const { initialState, operations, ...header } =
|
|
90
|
+
await this.storage.getDocument(drive, id);
|
|
91
|
+
|
|
92
|
+
const documentModel = this._getDocumentModel(header.documentType);
|
|
93
|
+
|
|
94
|
+
return baseUtils.replayDocument(
|
|
95
|
+
initialState,
|
|
96
|
+
operations,
|
|
97
|
+
documentModel.reducer,
|
|
98
|
+
undefined,
|
|
99
|
+
header
|
|
100
|
+
);
|
|
63
101
|
}
|
|
64
102
|
|
|
65
103
|
getDocuments(drive: string) {
|
|
@@ -72,25 +110,35 @@ export class DocumentDriveServer implements IDocumentDriveServer {
|
|
|
72
110
|
// TODO validate input.document is of documentType
|
|
73
111
|
const document = input.document ?? documentModel.utils.createDocument();
|
|
74
112
|
|
|
75
|
-
return this.storage.
|
|
113
|
+
return this.storage.createDocument(driveId, input.id, document);
|
|
76
114
|
}
|
|
77
115
|
|
|
78
|
-
async deleteDocument(driveId: string, id: string)
|
|
116
|
+
async deleteDocument(driveId: string, id: string) {
|
|
79
117
|
return this.storage.deleteDocument(driveId, id);
|
|
80
118
|
}
|
|
81
119
|
|
|
82
|
-
async
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
private async _performOperations(
|
|
121
|
+
drive: string,
|
|
122
|
+
documentStorage: DocumentStorage,
|
|
123
|
+
operations: Operation[]
|
|
124
|
+
) {
|
|
125
|
+
const documentModel = this._getDocumentModel(
|
|
126
|
+
documentStorage.documentType
|
|
127
|
+
);
|
|
128
|
+
const document = baseUtils.replayDocument(
|
|
129
|
+
documentStorage.initialState,
|
|
130
|
+
documentStorage.operations,
|
|
131
|
+
documentModel.reducer,
|
|
132
|
+
undefined,
|
|
133
|
+
documentStorage
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const signalResults: SignalResult[] = [];
|
|
137
|
+
let newDocument = document;
|
|
138
|
+
for (const operation of operations) {
|
|
139
|
+
const operationSignals: Promise<SignalResult>[] = [];
|
|
140
|
+
newDocument = documentModel.reducer(
|
|
141
|
+
newDocument,
|
|
94
142
|
operation,
|
|
95
143
|
signal => {
|
|
96
144
|
let handler: Promise<unknown> | undefined = undefined;
|
|
@@ -118,62 +166,105 @@ export class DocumentDriveServer implements IDocumentDriveServer {
|
|
|
118
166
|
break;
|
|
119
167
|
}
|
|
120
168
|
if (handler) {
|
|
121
|
-
|
|
169
|
+
operationSignals.push(
|
|
122
170
|
handler.then(result => ({ signal, result }))
|
|
123
171
|
);
|
|
124
172
|
}
|
|
125
173
|
}
|
|
126
174
|
);
|
|
127
|
-
const
|
|
175
|
+
const results = await Promise.all(operationSignals);
|
|
176
|
+
signalResults.push(...results);
|
|
177
|
+
}
|
|
178
|
+
return { document: newDocument, signals: signalResults };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
addOperation(drive: string, id: string, operation: Operation) {
|
|
182
|
+
return this.addOperations(drive, id, [operation]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async addOperations(drive: string, id: string, operations: Operation[]) {
|
|
186
|
+
// retrieves document from storage
|
|
187
|
+
const documentStorage = await this.storage.getDocument(drive, id);
|
|
188
|
+
try {
|
|
189
|
+
// retrieves the document's document model and
|
|
190
|
+
// applies the operations using its reducer
|
|
191
|
+
const { document, signals } = await this._performOperations(
|
|
192
|
+
drive,
|
|
193
|
+
documentStorage,
|
|
194
|
+
operations
|
|
195
|
+
);
|
|
128
196
|
|
|
129
197
|
// saves the updated state of the document and returns it
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
198
|
+
await this.storage.addDocumentOperations(
|
|
199
|
+
drive,
|
|
200
|
+
id,
|
|
201
|
+
operations,
|
|
202
|
+
document
|
|
203
|
+
);
|
|
204
|
+
|
|
137
205
|
return {
|
|
138
206
|
success: true,
|
|
139
|
-
document
|
|
140
|
-
|
|
207
|
+
document,
|
|
208
|
+
operations,
|
|
141
209
|
signals
|
|
142
210
|
};
|
|
143
211
|
} catch (error) {
|
|
144
212
|
return {
|
|
145
213
|
success: false,
|
|
146
214
|
error: error as Error,
|
|
147
|
-
document,
|
|
148
|
-
|
|
215
|
+
document: undefined,
|
|
216
|
+
operations,
|
|
149
217
|
signals: []
|
|
150
218
|
};
|
|
151
219
|
}
|
|
152
220
|
}
|
|
153
221
|
|
|
154
|
-
async addOperations(drive: string, id: string, operations: Operation[]) {
|
|
155
|
-
const results: IOperationResult[] = [];
|
|
156
|
-
for (const operation of operations) {
|
|
157
|
-
results.push(await this.addOperation(drive, id, operation));
|
|
158
|
-
}
|
|
159
|
-
return results;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
222
|
addDriveOperation(
|
|
163
223
|
drive: string,
|
|
164
224
|
operation: Operation<DocumentDriveAction | BaseAction>
|
|
165
225
|
) {
|
|
166
|
-
return this.
|
|
167
|
-
IOperationResult<DocumentDriveDocument>
|
|
168
|
-
>;
|
|
226
|
+
return this.addDriveOperations(drive, [operation]);
|
|
169
227
|
}
|
|
170
228
|
|
|
171
|
-
addDriveOperations(
|
|
229
|
+
async addDriveOperations(
|
|
172
230
|
drive: string,
|
|
173
231
|
operations: Operation<DocumentDriveAction | BaseAction>[]
|
|
174
232
|
) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
233
|
+
// retrieves document from storage
|
|
234
|
+
const documentStorage = await this.storage.getDrive(drive);
|
|
235
|
+
try {
|
|
236
|
+
// retrieves the document's document model and
|
|
237
|
+
// applies the operations using its reducer
|
|
238
|
+
const { document, signals } = await this._performOperations(
|
|
239
|
+
drive,
|
|
240
|
+
documentStorage,
|
|
241
|
+
operations
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (isDocumentDrive(document)) {
|
|
245
|
+
await this.storage.addDriveOperations(
|
|
246
|
+
drive,
|
|
247
|
+
operations, // TODO check?
|
|
248
|
+
document
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
throw new Error('Invalid Document Drive document');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
success: true,
|
|
256
|
+
document,
|
|
257
|
+
operations,
|
|
258
|
+
signals
|
|
259
|
+
};
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
error: error as Error,
|
|
264
|
+
document: undefined,
|
|
265
|
+
operations,
|
|
266
|
+
signals: []
|
|
267
|
+
};
|
|
268
|
+
}
|
|
178
269
|
}
|
|
179
270
|
}
|
package/src/server/types.ts
CHANGED
|
@@ -31,8 +31,8 @@ export type SignalResult = {
|
|
|
31
31
|
export type IOperationResult<T extends Document = Document> = {
|
|
32
32
|
success: boolean;
|
|
33
33
|
error?: Error;
|
|
34
|
-
|
|
35
|
-
document: T;
|
|
34
|
+
operations: Operation[];
|
|
35
|
+
document: T | undefined;
|
|
36
36
|
signals: SignalResult[];
|
|
37
37
|
};
|
|
38
38
|
|
|
@@ -56,7 +56,7 @@ export interface IDocumentDriveServer {
|
|
|
56
56
|
drive: string,
|
|
57
57
|
id: string,
|
|
58
58
|
operations: Operation[]
|
|
59
|
-
): Promise<IOperationResult
|
|
59
|
+
): Promise<IOperationResult>;
|
|
60
60
|
|
|
61
61
|
addDriveOperation(
|
|
62
62
|
drive: string,
|
|
@@ -65,5 +65,5 @@ export interface IDocumentDriveServer {
|
|
|
65
65
|
addDriveOperations(
|
|
66
66
|
drive: string,
|
|
67
67
|
operations: Operation<DocumentDriveAction | BaseAction>[]
|
|
68
|
-
): Promise<IOperationResult<DocumentDriveDocument
|
|
68
|
+
): Promise<IOperationResult<DocumentDriveDocument>>;
|
|
69
69
|
}
|
package/src/storage/browser.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { DocumentDriveAction } from 'document-model-libs/document-drive';
|
|
2
|
+
import {
|
|
3
|
+
BaseAction,
|
|
4
|
+
Document,
|
|
5
|
+
DocumentHeader,
|
|
6
|
+
Operation
|
|
7
|
+
} from 'document-model/document';
|
|
8
|
+
import { mergeOperations } from '..';
|
|
9
|
+
import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
|
|
4
10
|
|
|
5
11
|
export class BrowserStorage implements IDriveStorage {
|
|
6
12
|
private db: Promise<LocalForage>;
|
|
@@ -39,7 +45,7 @@ export class BrowserStorage implements IDriveStorage {
|
|
|
39
45
|
return document;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
async
|
|
48
|
+
async createDocument(drive: string, id: string, document: DocumentStorage) {
|
|
43
49
|
await (await this.db).setItem(this.buildKey(drive, id), document);
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -47,56 +53,80 @@ export class BrowserStorage implements IDriveStorage {
|
|
|
47
53
|
await (await this.db).removeItem(this.buildKey(drive, id));
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
async addDocumentOperations(
|
|
57
|
+
drive: string,
|
|
58
|
+
id: string,
|
|
59
|
+
operations: Operation[],
|
|
60
|
+
header: DocumentHeader
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const document = await this.getDocument(drive, id);
|
|
63
|
+
if (!document) {
|
|
64
|
+
throw new Error(`Document with id ${id} not found`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const mergedOperations = mergeOperations(
|
|
68
|
+
document.operations,
|
|
69
|
+
operations
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await (
|
|
73
|
+
await this.db
|
|
74
|
+
).setItem(this.buildKey(drive, id), {
|
|
75
|
+
...document,
|
|
76
|
+
...header,
|
|
77
|
+
operations: mergedOperations
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
50
81
|
async getDrives() {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
const keys = (await (await this.db).keys()) ?? [];
|
|
83
|
+
return keys
|
|
84
|
+
.filter(key => key.startsWith(BrowserStorage.DRIVES_KEY))
|
|
85
|
+
.map(key =>
|
|
86
|
+
key.slice(
|
|
87
|
+
BrowserStorage.DRIVES_KEY.length + BrowserStorage.SEP.length
|
|
88
|
+
)
|
|
89
|
+
);
|
|
57
90
|
}
|
|
58
91
|
|
|
59
92
|
async getDrive(id: string) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const drive = drives.find(drive => drive.state.global.id === id);
|
|
93
|
+
const drive = await (
|
|
94
|
+
await this.db
|
|
95
|
+
).getItem<DocumentDriveStorage>(
|
|
96
|
+
this.buildKey(BrowserStorage.DRIVES_KEY, id)
|
|
97
|
+
);
|
|
66
98
|
if (!drive) {
|
|
67
99
|
throw new Error(`Drive with id ${id} not found`);
|
|
68
100
|
}
|
|
69
101
|
return drive;
|
|
70
102
|
}
|
|
71
103
|
|
|
72
|
-
async
|
|
104
|
+
async createDrive(id: string, drive: DocumentDriveStorage) {
|
|
73
105
|
const db = await this.db;
|
|
74
|
-
|
|
75
|
-
(await db.getItem<DocumentDriveDocument[]>(
|
|
76
|
-
BrowserStorage.DRIVES_KEY
|
|
77
|
-
)) ?? [];
|
|
78
|
-
const index = drives.findIndex(
|
|
79
|
-
d => d.state.global.id === drive.state.global.id
|
|
80
|
-
);
|
|
81
|
-
if (index > -1) {
|
|
82
|
-
drives[index] = drive;
|
|
83
|
-
} else {
|
|
84
|
-
drives.push(drive);
|
|
85
|
-
}
|
|
86
|
-
await db.setItem(BrowserStorage.DRIVES_KEY, drives);
|
|
106
|
+
await db.setItem(this.buildKey(BrowserStorage.DRIVES_KEY, id), drive);
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
async deleteDrive(id: string) {
|
|
90
110
|
const documents = await this.getDocuments(id);
|
|
91
111
|
await Promise.all(documents.map(doc => this.deleteDocument(id, doc)));
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
(await db.getItem<DocumentDriveDocument[]>(
|
|
95
|
-
BrowserStorage.DRIVES_KEY
|
|
96
|
-
)) ?? [];
|
|
97
|
-
await db.setItem(
|
|
98
|
-
BrowserStorage.DRIVES_KEY,
|
|
99
|
-
drives.filter(drive => drive.state.global.id !== id)
|
|
112
|
+
return (await this.db).removeItem(
|
|
113
|
+
this.buildKey(BrowserStorage.DRIVES_KEY, id)
|
|
100
114
|
);
|
|
101
115
|
}
|
|
116
|
+
|
|
117
|
+
async addDriveOperations(
|
|
118
|
+
id: string,
|
|
119
|
+
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
120
|
+
header: DocumentHeader
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const drive = await this.getDrive(id);
|
|
123
|
+
const mergedOperations = mergeOperations(drive.operations, operations);
|
|
124
|
+
|
|
125
|
+
(await this.db).setItem(this.buildKey(BrowserStorage.DRIVES_KEY, id), {
|
|
126
|
+
...drive,
|
|
127
|
+
...header,
|
|
128
|
+
operations: mergedOperations
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
102
132
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { DocumentDriveAction } from 'document-model-libs/document-drive';
|
|
2
|
+
import { BaseAction, DocumentHeader, Operation } from 'document-model/document';
|
|
3
3
|
import type { Dirent } from 'fs';
|
|
4
4
|
import {
|
|
5
5
|
existsSync,
|
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
import fs from 'fs/promises';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import sanitize from 'sanitize-filename';
|
|
14
|
-
import {
|
|
15
|
-
import { IDriveStorage } from './types';
|
|
14
|
+
import { mergeOperations } from '..';
|
|
15
|
+
import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
|
|
16
16
|
|
|
17
17
|
type FSError = {
|
|
18
18
|
errno: number;
|
|
@@ -81,14 +81,14 @@ export class FilesystemStorage implements IDriveStorage {
|
|
|
81
81
|
const content = readFileSync(this._buildDocumentPath(drive, id), {
|
|
82
82
|
encoding: 'utf-8'
|
|
83
83
|
});
|
|
84
|
-
return JSON.parse(content)
|
|
84
|
+
return JSON.parse(content) as Promise<DocumentStorage>;
|
|
85
85
|
} catch (error) {
|
|
86
86
|
console.error(error);
|
|
87
87
|
throw new Error(`Document with id ${id} not found`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
async
|
|
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
94
|
await writeFileSync(documentPath, JSON.stringify(document), {
|
|
@@ -100,6 +100,29 @@ export class FilesystemStorage implements IDriveStorage {
|
|
|
100
100
|
return fs.rm(this._buildDocumentPath(drive, id));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
async addDocumentOperations(
|
|
104
|
+
drive: string,
|
|
105
|
+
id: string,
|
|
106
|
+
operations: Operation[],
|
|
107
|
+
header: DocumentHeader
|
|
108
|
+
) {
|
|
109
|
+
const document = await this.getDocument(drive, id);
|
|
110
|
+
if (!document) {
|
|
111
|
+
throw new Error(`Document with id ${id} not found`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const mergedOperations = mergeOperations(
|
|
115
|
+
document.operations,
|
|
116
|
+
operations
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
this.createDocument(drive, id, {
|
|
120
|
+
...document,
|
|
121
|
+
...header,
|
|
122
|
+
operations: mergedOperations
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
103
126
|
async getDrives() {
|
|
104
127
|
const files = await readdirSync(this.drivesPath, {
|
|
105
128
|
withFileTypes: true
|
|
@@ -120,25 +143,18 @@ export class FilesystemStorage implements IDriveStorage {
|
|
|
120
143
|
}
|
|
121
144
|
|
|
122
145
|
async getDrive(id: string) {
|
|
123
|
-
let document: Document;
|
|
124
146
|
try {
|
|
125
|
-
|
|
147
|
+
return (await this.getDocument(
|
|
148
|
+
FilesystemStorage.DRIVES_DIR,
|
|
149
|
+
id
|
|
150
|
+
)) as DocumentDriveStorage;
|
|
126
151
|
} catch {
|
|
127
152
|
throw new Error(`Drive with id ${id} not found`);
|
|
128
153
|
}
|
|
129
|
-
if (isDocumentDrive(document)) {
|
|
130
|
-
return document;
|
|
131
|
-
} else {
|
|
132
|
-
throw new Error('Invalid drive document');
|
|
133
|
-
}
|
|
134
154
|
}
|
|
135
155
|
|
|
136
|
-
|
|
137
|
-
return this.
|
|
138
|
-
FilesystemStorage.DRIVES_DIR,
|
|
139
|
-
drive.state.global.id,
|
|
140
|
-
drive
|
|
141
|
-
);
|
|
156
|
+
createDrive(id: string, drive: DocumentDriveStorage) {
|
|
157
|
+
return this.createDocument(FilesystemStorage.DRIVES_DIR, id, drive);
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
async deleteDrive(id: string) {
|
|
@@ -148,4 +164,19 @@ export class FilesystemStorage implements IDriveStorage {
|
|
|
148
164
|
documents.map(document => this.deleteDocument(id, document))
|
|
149
165
|
);
|
|
150
166
|
}
|
|
167
|
+
|
|
168
|
+
async addDriveOperations(
|
|
169
|
+
id: string,
|
|
170
|
+
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
171
|
+
header: DocumentHeader
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
const drive = await this.getDrive(id);
|
|
174
|
+
const mergedOperations = mergeOperations(drive.operations, operations);
|
|
175
|
+
|
|
176
|
+
this.createDrive(id, {
|
|
177
|
+
...drive,
|
|
178
|
+
...header,
|
|
179
|
+
operations: mergedOperations
|
|
180
|
+
});
|
|
181
|
+
}
|
|
151
182
|
}
|
package/src/storage/index.ts
CHANGED
package/src/storage/memory.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { DocumentDriveAction } from 'document-model-libs/document-drive';
|
|
2
|
+
import {
|
|
3
|
+
BaseAction,
|
|
4
|
+
Document,
|
|
5
|
+
DocumentHeader,
|
|
6
|
+
Operation
|
|
7
|
+
} from 'document-model/document';
|
|
8
|
+
import { mergeOperations } from '..';
|
|
9
|
+
import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
|
|
4
10
|
|
|
5
11
|
export class MemoryStorage implements IDriveStorage {
|
|
6
|
-
private documents: Record<string, Record<string,
|
|
7
|
-
private drives: Record<string,
|
|
12
|
+
private documents: Record<string, Record<string, DocumentStorage>>;
|
|
13
|
+
private drives: Record<string, DocumentDriveStorage>;
|
|
8
14
|
|
|
9
15
|
constructor() {
|
|
10
16
|
this.documents = {};
|
|
@@ -32,6 +38,51 @@ export class MemoryStorage implements IDriveStorage {
|
|
|
32
38
|
this.documents[drive]![id] = document;
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
async createDocument(drive: string, id: string, document: DocumentStorage) {
|
|
42
|
+
this.documents[drive] = this.documents[drive] ?? {};
|
|
43
|
+
const {
|
|
44
|
+
operations,
|
|
45
|
+
initialState,
|
|
46
|
+
name,
|
|
47
|
+
revision,
|
|
48
|
+
documentType,
|
|
49
|
+
created,
|
|
50
|
+
lastModified
|
|
51
|
+
} = document;
|
|
52
|
+
this.documents[drive]![id] = {
|
|
53
|
+
operations,
|
|
54
|
+
initialState,
|
|
55
|
+
name,
|
|
56
|
+
revision,
|
|
57
|
+
documentType,
|
|
58
|
+
created,
|
|
59
|
+
lastModified
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async addDocumentOperations(
|
|
64
|
+
drive: string,
|
|
65
|
+
id: string,
|
|
66
|
+
operations: Operation[],
|
|
67
|
+
header: DocumentHeader
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const document = await this.getDocument(drive, id);
|
|
70
|
+
if (!document) {
|
|
71
|
+
throw new Error(`Document with id ${id} not found`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const mergedOperations = mergeOperations(
|
|
75
|
+
document.operations,
|
|
76
|
+
operations
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
this.documents[drive]![id] = {
|
|
80
|
+
...document,
|
|
81
|
+
...header,
|
|
82
|
+
operations: mergedOperations
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
35
86
|
async deleteDocument(drive: string, id: string) {
|
|
36
87
|
if (!this.documents[drive]) {
|
|
37
88
|
throw new Error(`Drive with id ${drive} not found`);
|
|
@@ -51,8 +102,23 @@ export class MemoryStorage implements IDriveStorage {
|
|
|
51
102
|
return drive;
|
|
52
103
|
}
|
|
53
104
|
|
|
54
|
-
async
|
|
55
|
-
this.drives[
|
|
105
|
+
async createDrive(id: string, drive: DocumentDriveStorage) {
|
|
106
|
+
this.drives[id] = drive;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async addDriveOperations(
|
|
110
|
+
id: string,
|
|
111
|
+
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
112
|
+
header: DocumentHeader
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
const drive = await this.getDrive(id);
|
|
115
|
+
const mergedOperations = mergeOperations(drive.operations, operations);
|
|
116
|
+
|
|
117
|
+
this.drives[id] = {
|
|
118
|
+
...drive,
|
|
119
|
+
...header,
|
|
120
|
+
operations: mergedOperations
|
|
121
|
+
};
|
|
56
122
|
}
|
|
57
123
|
|
|
58
124
|
async deleteDrive(id: string) {
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { PrismaClient, type Prisma } from '@prisma/client';
|
|
2
|
+
import {
|
|
3
|
+
DocumentDriveLocalState,
|
|
4
|
+
DocumentDriveState
|
|
5
|
+
} from 'document-model-libs/document-drive';
|
|
6
|
+
import {
|
|
7
|
+
Document,
|
|
8
|
+
DocumentHeader,
|
|
9
|
+
ExtendedState,
|
|
10
|
+
Operation,
|
|
11
|
+
OperationScope
|
|
12
|
+
} from 'document-model/document';
|
|
13
|
+
import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
|
|
14
|
+
|
|
15
|
+
export class PrismaStorage implements IDriveStorage {
|
|
16
|
+
private db: PrismaClient;
|
|
17
|
+
|
|
18
|
+
constructor(db: PrismaClient) {
|
|
19
|
+
this.db = db;
|
|
20
|
+
}
|
|
21
|
+
async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
|
|
22
|
+
await this.createDocument(
|
|
23
|
+
'drives',
|
|
24
|
+
id,
|
|
25
|
+
drive as DocumentStorage
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
async addDriveOperations(
|
|
29
|
+
id: string,
|
|
30
|
+
operations: Operation[],
|
|
31
|
+
header: DocumentHeader
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
await this.addDocumentOperations('drives', id, operations, header);
|
|
34
|
+
}
|
|
35
|
+
async createDocument(
|
|
36
|
+
drive: string,
|
|
37
|
+
id: string,
|
|
38
|
+
document: DocumentStorage
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
await this.db.document.upsert({
|
|
41
|
+
where: {
|
|
42
|
+
id_driveId: {
|
|
43
|
+
id,
|
|
44
|
+
driveId: drive
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
update: {},
|
|
48
|
+
create: {
|
|
49
|
+
name: document.name,
|
|
50
|
+
documentType: document.documentType,
|
|
51
|
+
driveId: drive,
|
|
52
|
+
initialState: document.initialState as Prisma.InputJsonObject,
|
|
53
|
+
lastModified: document.lastModified,
|
|
54
|
+
revision: document.revision,
|
|
55
|
+
id
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async addDocumentOperations(
|
|
60
|
+
drive: string,
|
|
61
|
+
id: string,
|
|
62
|
+
operations: Operation[],
|
|
63
|
+
header: DocumentHeader
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const document = await this.getDocument(drive, id);
|
|
66
|
+
if (!document) {
|
|
67
|
+
throw new Error(`Document with id ${id} not found`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await Promise.all(
|
|
72
|
+
operations.map(async op => {
|
|
73
|
+
return this.db.operation.upsert({
|
|
74
|
+
where: {
|
|
75
|
+
driveId_documentId_scope_branch_index: {
|
|
76
|
+
driveId: drive,
|
|
77
|
+
documentId: id,
|
|
78
|
+
scope: op.scope,
|
|
79
|
+
branch: 'main',
|
|
80
|
+
index: op.index
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
create: {
|
|
84
|
+
driveId: drive,
|
|
85
|
+
documentId: id,
|
|
86
|
+
hash: op.hash,
|
|
87
|
+
index: op.index,
|
|
88
|
+
input: op.input as Prisma.InputJsonObject,
|
|
89
|
+
timestamp: op.timestamp,
|
|
90
|
+
type: op.type,
|
|
91
|
+
scope: op.scope,
|
|
92
|
+
branch: 'main'
|
|
93
|
+
},
|
|
94
|
+
update: {
|
|
95
|
+
driveId: drive,
|
|
96
|
+
documentId: id,
|
|
97
|
+
hash: op.hash,
|
|
98
|
+
index: op.index,
|
|
99
|
+
input: op.input as Prisma.InputJsonObject,
|
|
100
|
+
timestamp: op.timestamp,
|
|
101
|
+
type: op.type,
|
|
102
|
+
scope: op.scope,
|
|
103
|
+
branch: 'main'
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await this.db.document.update({
|
|
110
|
+
where: {
|
|
111
|
+
id_driveId: {
|
|
112
|
+
id,
|
|
113
|
+
driveId: 'drives'
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
data: {
|
|
117
|
+
lastModified: header.lastModified,
|
|
118
|
+
revision: header.revision
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.log(e);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await this.db.document.upsert({
|
|
126
|
+
where: {
|
|
127
|
+
id_driveId: {
|
|
128
|
+
id: 'drives',
|
|
129
|
+
driveId: id
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
create: {
|
|
133
|
+
id: 'drives',
|
|
134
|
+
driveId: id,
|
|
135
|
+
documentType: header.documentType,
|
|
136
|
+
initialState: document.initialState,
|
|
137
|
+
lastModified: header.lastModified,
|
|
138
|
+
revision: header.revision,
|
|
139
|
+
created: header.created
|
|
140
|
+
},
|
|
141
|
+
update: {
|
|
142
|
+
lastModified: header.lastModified,
|
|
143
|
+
revision: header.revision
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getDocuments(drive: string) {
|
|
149
|
+
const docs = await this.db.document.findMany({
|
|
150
|
+
where: {
|
|
151
|
+
AND: {
|
|
152
|
+
driveId: drive,
|
|
153
|
+
NOT: {
|
|
154
|
+
id: 'drives'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return docs.map(doc => doc.id);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async getDocument(driveId: string, id: string) {
|
|
164
|
+
const result = await this.db.document.findFirst({
|
|
165
|
+
where: {
|
|
166
|
+
id: id,
|
|
167
|
+
driveId: driveId
|
|
168
|
+
},
|
|
169
|
+
include: {
|
|
170
|
+
operations: {
|
|
171
|
+
include: {
|
|
172
|
+
attachments: true
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (result === null) {
|
|
179
|
+
throw new Error(`Document with id ${id} not found`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const dbDoc = result;
|
|
183
|
+
|
|
184
|
+
const doc = {
|
|
185
|
+
created: dbDoc.created.toISOString(),
|
|
186
|
+
name: dbDoc.name ? dbDoc.name : '',
|
|
187
|
+
documentType: dbDoc.documentType,
|
|
188
|
+
initialState: dbDoc.initialState as ExtendedState<
|
|
189
|
+
DocumentDriveState,
|
|
190
|
+
DocumentDriveLocalState
|
|
191
|
+
>,
|
|
192
|
+
lastModified: dbDoc.lastModified.toISOString(),
|
|
193
|
+
operations: {
|
|
194
|
+
global: dbDoc.operations
|
|
195
|
+
.filter(op => op.scope === 'global')
|
|
196
|
+
.map(op => ({
|
|
197
|
+
hash: op.hash,
|
|
198
|
+
index: op.index,
|
|
199
|
+
timestamp: new Date(op.timestamp).toISOString(),
|
|
200
|
+
input: op.input,
|
|
201
|
+
type: op.type,
|
|
202
|
+
scope: op.scope as OperationScope
|
|
203
|
+
// attachments: fileRegistry
|
|
204
|
+
})),
|
|
205
|
+
local: dbDoc.operations
|
|
206
|
+
.filter(op => op.scope === 'local')
|
|
207
|
+
.map(op => ({
|
|
208
|
+
hash: op.hash,
|
|
209
|
+
index: op.index,
|
|
210
|
+
timestamp: new Date(op.timestamp).toISOString(),
|
|
211
|
+
input: op.input,
|
|
212
|
+
type: op.type,
|
|
213
|
+
scope: op.scope as OperationScope
|
|
214
|
+
// attachments: fileRegistry
|
|
215
|
+
}))
|
|
216
|
+
},
|
|
217
|
+
revision: dbDoc.revision
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return doc;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async deleteDocument(drive: string, id: string) {
|
|
224
|
+
await this.db.document.deleteMany({
|
|
225
|
+
where: {
|
|
226
|
+
driveId: drive,
|
|
227
|
+
id: id
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async getDrives() {
|
|
233
|
+
return this.getDocuments('drives');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async getDrive(id: string) {
|
|
237
|
+
return this.getDocument('drives', id) as Promise<DocumentDriveStorage>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async deleteDrive(id: string) {
|
|
241
|
+
await this.deleteDocument('drives', id);
|
|
242
|
+
await this.db.document.deleteMany({
|
|
243
|
+
where: {
|
|
244
|
+
driveId: id
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
package/src/storage/types.ts
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
DocumentDriveAction,
|
|
3
|
+
DocumentDriveDocument
|
|
4
|
+
} from 'document-model-libs/document-drive';
|
|
5
|
+
import type {
|
|
6
|
+
BaseAction,
|
|
7
|
+
Document,
|
|
8
|
+
DocumentHeader,
|
|
9
|
+
Operation
|
|
10
|
+
} from 'document-model/document';
|
|
11
|
+
|
|
12
|
+
export type DocumentStorage<D extends Document = Document> = Omit<
|
|
13
|
+
D,
|
|
14
|
+
'state' | 'attachments'
|
|
15
|
+
>;
|
|
16
|
+
export type DocumentDriveStorage = DocumentStorage<DocumentDriveDocument>;
|
|
3
17
|
|
|
4
18
|
export interface IStorage {
|
|
5
19
|
getDocuments: (drive: string) => Promise<string[]>;
|
|
6
|
-
getDocument(drive: string, id: string): Promise<
|
|
7
|
-
|
|
20
|
+
getDocument(drive: string, id: string): Promise<DocumentStorage>;
|
|
21
|
+
createDocument(
|
|
22
|
+
drive: string,
|
|
23
|
+
id: string,
|
|
24
|
+
document: DocumentStorage
|
|
25
|
+
): Promise<void>;
|
|
26
|
+
addDocumentOperations(
|
|
27
|
+
drive: string,
|
|
28
|
+
id: string,
|
|
29
|
+
operations: Operation[],
|
|
30
|
+
header: DocumentHeader
|
|
31
|
+
): Promise<void>;
|
|
8
32
|
deleteDocument(drive: string, id: string): Promise<void>;
|
|
9
33
|
}
|
|
10
34
|
|
|
11
35
|
export interface IDriveStorage extends IStorage {
|
|
12
36
|
getDrives(): Promise<string[]>;
|
|
13
|
-
getDrive(id: string): Promise<
|
|
14
|
-
|
|
37
|
+
getDrive(id: string): Promise<DocumentDriveStorage>;
|
|
38
|
+
createDrive(id: string, drive: DocumentDriveStorage): Promise<void>;
|
|
15
39
|
deleteDrive(id: string): Promise<void>;
|
|
40
|
+
addDriveOperations(
|
|
41
|
+
id: string,
|
|
42
|
+
operations: Operation<DocumentDriveAction | BaseAction>[],
|
|
43
|
+
header: DocumentHeader
|
|
44
|
+
): Promise<void>;
|
|
16
45
|
}
|
package/src/utils.ts
CHANGED
|
@@ -3,7 +3,13 @@ import {
|
|
|
3
3
|
documentModel as DocumentDriveModel,
|
|
4
4
|
z
|
|
5
5
|
} from 'document-model-libs/document-drive';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Action,
|
|
8
|
+
BaseAction,
|
|
9
|
+
Document,
|
|
10
|
+
DocumentOperations,
|
|
11
|
+
Operation
|
|
12
|
+
} from 'document-model/document';
|
|
7
13
|
|
|
8
14
|
export function isDocumentDrive(
|
|
9
15
|
document: Document
|
|
@@ -13,3 +19,14 @@ export function isDocumentDrive(
|
|
|
13
19
|
z.DocumentDriveStateSchema().safeParse(document.state.global).success
|
|
14
20
|
);
|
|
15
21
|
}
|
|
22
|
+
|
|
23
|
+
export function mergeOperations<A extends Action = Action>(
|
|
24
|
+
currentOperations: DocumentOperations<A>,
|
|
25
|
+
newOperations: Operation<A | BaseAction>[]
|
|
26
|
+
): DocumentOperations<A> {
|
|
27
|
+
return newOperations.reduce((acc, curr) => {
|
|
28
|
+
const operations = acc[curr.scope] ?? [];
|
|
29
|
+
acc[curr.scope] = [...operations, curr] as Operation<A>[];
|
|
30
|
+
return acc;
|
|
31
|
+
}, currentOperations);
|
|
32
|
+
}
|