document-drive 0.0.22 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-drive",
3
- "version": "0.0.22",
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.17",
29
- "document-model-libs": "^1.1.26",
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-eslint/eslint-plugin": "^6.12.0",
37
- "@typescript-eslint/parser": "^6.12.0",
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.18",
40
- "document-model-libs": "^1.1.26",
41
- "eslint": "^8.54.0",
42
- "eslint-config-prettier": "^9.0.0",
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.0",
47
+ "prettier": "^3.1.1",
46
48
  "prettier-plugin-organize-imports": "^3.2.4",
47
- "typescript": "^5.3.2",
49
+ "prisma": "^5.8.0",
50
+ "typescript": "^5.3.3",
48
51
  "vitest": "^0.34.6"
49
52
  }
50
53
  }
@@ -1,17 +1,17 @@
1
+ import { DocumentDriveAction, utils } from 'document-model-libs/document-drive';
1
2
  import {
2
- DocumentDriveAction,
3
- DocumentDriveDocument,
4
- utils
5
- } from 'document-model-libs/document-drive';
6
- import { BaseAction, DocumentModel, Operation } from 'document-model/document';
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.saveDrive(document);
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
- return this.storage.getDrive(drive);
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
- return this.storage.getDocument(drive, id);
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.saveDocument(driveId, input.id, document);
113
+ return this.storage.createDocument(driveId, input.id, document);
76
114
  }
77
115
 
78
- async deleteDocument(driveId: string, id: string): Promise<void> {
116
+ async deleteDocument(driveId: string, id: string) {
79
117
  return this.storage.deleteDocument(driveId, id);
80
118
  }
81
119
 
82
- async addOperation(drive: string, id: string, operation: Operation) {
83
- // retrieves document from storage
84
- const document = await (id
85
- ? this.storage.getDocument(drive, id)
86
- : this.storage.getDrive(drive));
87
- try {
88
- // retrieves the document's document model and
89
- // applies operation using its reducer
90
- const documentModel = this._getDocumentModel(document.documentType);
91
- const signalHandlers: Promise<SignalResult>[] = [];
92
- const newDocument = documentModel.reducer(
93
- document,
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
- signalHandlers.push(
169
+ operationSignals.push(
122
170
  handler.then(result => ({ signal, result }))
123
171
  );
124
172
  }
125
173
  }
126
174
  );
127
- const signals = await Promise.all(signalHandlers);
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
- if (id) {
131
- await this.storage.saveDocument(drive, id, newDocument);
132
- } else if (isDocumentDrive(newDocument)) {
133
- await this.storage.saveDrive(newDocument);
134
- } else {
135
- throw new Error('Invalid document');
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: newDocument,
140
- operation,
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
- operation,
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.addOperation(drive, '', operation) as Promise<
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
- return this.addOperations(drive, '', operations) as Promise<
176
- IOperationResult<DocumentDriveDocument>[]
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
  }
@@ -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
- operation: Operation;
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
  }
@@ -1,6 +1,12 @@
1
- import { DocumentDriveDocument } from 'document-model-libs/document-drive';
2
- import { Document } from 'document-model/document';
3
- import { IDriveStorage } from './types';
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 saveDocument(drive: string, id: string, document: Document) {
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 drives =
52
- (await (
53
- await this.db
54
- ).getItem<DocumentDriveDocument[]>(BrowserStorage.DRIVES_KEY)) ??
55
- [];
56
- return drives.map(drive => drive.state.global.id);
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 drives =
61
- (await (
62
- await this.db
63
- ).getItem<DocumentDriveDocument[]>(BrowserStorage.DRIVES_KEY)) ??
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 saveDrive(drive: DocumentDriveDocument) {
104
+ async createDrive(id: string, drive: DocumentDriveStorage) {
73
105
  const db = await this.db;
74
- const drives =
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
- const db = await this.db;
93
- const drives =
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 { DocumentDriveDocument } from 'document-model-libs/document-drive';
2
- import { Document } from 'document-model/document';
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 { isDocumentDrive } from '../utils';
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 saveDocument(drive: string, id: string, document: Document) {
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
- document = await this.getDocument(FilesystemStorage.DRIVES_DIR, id);
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
- saveDrive(drive: DocumentDriveDocument) {
137
- return this.saveDocument(
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
  }
@@ -1,4 +1,5 @@
1
1
  export { BrowserStorage } from './browser';
2
2
  export { FilesystemStorage } from './filesystem';
3
3
  export { MemoryStorage } from './memory';
4
+ export { PrismaStorage } from './prisma';
4
5
  export type * from './types';
@@ -1,10 +1,16 @@
1
- import { DocumentDriveDocument } from 'document-model-libs/document-drive';
2
- import { Document } from 'document-model/document';
3
- import { IDriveStorage } from './types';
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, Document>>;
7
- private drives: Record<string, DocumentDriveDocument>;
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 saveDrive(drive: DocumentDriveDocument) {
55
- this.drives[drive.state.global.id] = drive;
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
+ }
@@ -1,16 +1,45 @@
1
- import { DocumentDriveDocument } from 'document-model-libs/document-drive';
2
- import { Document } from 'document-model/document';
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<Document>;
7
- saveDocument(drive: string, id: string, document: Document): Promise<void>;
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<DocumentDriveDocument>;
14
- saveDrive(drive: DocumentDriveDocument): Promise<void>;
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 { Document } from 'document-model/document';
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
+ }