document-drive 0.0.4 → 0.0.6

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.4",
3
+ "version": "0.0.6",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -23,8 +23,9 @@
23
23
  "test:watch": "vitest watch"
24
24
  },
25
25
  "peerDependencies": {
26
- "document-model": "^1.0.11",
27
- "document-model-libs": "^1.1.14"
26
+ "document-model": "^1.0.13",
27
+ "document-model-libs": "^1.1.21",
28
+ "localforage": "^1.10.0"
28
29
  },
29
30
  "dependencies": {
30
31
  "sanitize-filename": "^1.6.3"
@@ -33,10 +34,12 @@
33
34
  "@typescript-eslint/eslint-plugin": "^6.12.0",
34
35
  "@typescript-eslint/parser": "^6.12.0",
35
36
  "@vitest/coverage-v8": "^0.34.6",
36
- "document-model": "^1.0.11",
37
- "document-model-libs": "^1.1.20",
37
+ "document-model": "^1.0.15",
38
+ "document-model-libs": "^1.1.23",
38
39
  "eslint": "^8.54.0",
39
40
  "eslint-config-prettier": "^9.0.0",
41
+ "fake-indexeddb": "^5.0.1",
42
+ "localforage": "^1.10.0",
40
43
  "prettier": "^3.1.0",
41
44
  "prettier-plugin-organize-imports": "^3.2.4",
42
45
  "typescript": "^5.3.2",
@@ -1,9 +1,19 @@
1
- import { utils } from 'document-model-libs/document-drive';
2
- import { Document, DocumentModel, Operation } from 'document-model/document';
1
+ import {
2
+ DocumentDriveAction,
3
+ DocumentDriveDocument,
4
+ utils
5
+ } from 'document-model-libs/document-drive';
6
+ import { BaseAction, DocumentModel, Operation } from 'document-model/document';
3
7
  import { IDriveStorage } from '../storage';
4
8
  import { MemoryStorage } from '../storage/memory';
5
9
  import { isDocumentDrive } from '../utils';
6
- import { CreateDocumentInput, DriveInput, IDocumentDriveServer } from './types';
10
+ import {
11
+ CreateDocumentInput,
12
+ DriveInput,
13
+ IDocumentDriveServer,
14
+ IOperationResult,
15
+ SignalResult
16
+ } from './types';
7
17
 
8
18
  export type * from './types';
9
19
 
@@ -67,47 +77,101 @@ export class DocumentDriveServer implements IDocumentDriveServer {
67
77
  return this.storage.deleteDocument(driveId, id);
68
78
  }
69
79
 
70
- async addOperation(
71
- drive: string,
72
- id: string,
73
- operation: Operation
74
- ): Promise<Document> {
80
+ async addOperation(drive: string, id: string, operation: Operation) {
75
81
  // retrieves document from storage
76
82
  const document = await (id
77
83
  ? this.storage.getDocument(drive, id)
78
84
  : this.storage.getDrive(drive));
79
-
80
- // retrieves the document's document model and
81
- // applies operation using its reducer
82
- const documentModel = this._getDocumentModel(document.documentType);
83
- const signalResults: Promise<unknown>[] = [];
84
- const newDocument = documentModel.reducer(
85
- document,
86
- operation,
87
- signal => {
88
- let result: Promise<unknown> | undefined = undefined;
89
- switch (signal.type) {
90
- case 'CREATE_CHILD_DOCUMENT':
91
- result = this.createDocument(drive, signal.input);
92
- break;
93
- case 'DELETE_CHILD_DOCUMENT':
94
- result = this.deleteDocument(drive, signal.input.id);
95
- break;
96
- }
97
- if (result) {
98
- signalResults.push(result);
85
+ try {
86
+ // retrieves the document's document model and
87
+ // applies operation using its reducer
88
+ const documentModel = this._getDocumentModel(document.documentType);
89
+ const signalHandlers: Promise<SignalResult>[] = [];
90
+ const newDocument = documentModel.reducer(
91
+ document,
92
+ operation,
93
+ signal => {
94
+ let handler: Promise<unknown> | undefined = undefined;
95
+ switch (signal.type) {
96
+ case 'CREATE_CHILD_DOCUMENT':
97
+ handler = this.createDocument(drive, signal.input);
98
+ break;
99
+ case 'DELETE_CHILD_DOCUMENT':
100
+ handler = this.deleteDocument(
101
+ drive,
102
+ signal.input.id
103
+ );
104
+ break;
105
+ case 'COPY_CHILD_DOCUMENT':
106
+ handler = this.getDocument(
107
+ drive,
108
+ signal.input.id
109
+ ).then(documentToCopy =>
110
+ this.createDocument(drive, {
111
+ id: signal.input.newId,
112
+ documentType: documentToCopy.documentType,
113
+ document: documentToCopy
114
+ })
115
+ );
116
+ break;
117
+ }
118
+ if (handler) {
119
+ signalHandlers.push(
120
+ handler.then(result => ({ signal, result }))
121
+ );
122
+ }
99
123
  }
124
+ );
125
+ const signals = await Promise.all(signalHandlers);
126
+
127
+ // saves the updated state of the document and returns it
128
+ if (id) {
129
+ await this.storage.saveDocument(drive, id, newDocument);
130
+ } else if (isDocumentDrive(newDocument)) {
131
+ await this.storage.saveDrive(newDocument);
132
+ } else {
133
+ throw new Error('Invalid document');
100
134
  }
101
- );
102
- await Promise.all(signalResults);
103
- // saves the updated state of the document and returns it
104
- if (id) {
105
- await this.storage.saveDocument(drive, id, newDocument);
106
- } else if (isDocumentDrive(newDocument)) {
107
- await this.storage.saveDrive(newDocument);
108
- } else {
109
- throw new Error('Invalid document');
135
+ return {
136
+ success: true,
137
+ document: newDocument,
138
+ operation,
139
+ signals
140
+ };
141
+ } catch (error) {
142
+ return {
143
+ success: false,
144
+ error: error as Error,
145
+ document,
146
+ operation,
147
+ signals: []
148
+ };
110
149
  }
111
- return newDocument;
150
+ }
151
+
152
+ async addOperations(drive: string, id: string, operations: Operation[]) {
153
+ const results: IOperationResult[] = [];
154
+ for (const operation of operations) {
155
+ results.push(await this.addOperation(drive, id, operation));
156
+ }
157
+ return results;
158
+ }
159
+
160
+ addDriveOperation(
161
+ drive: string,
162
+ operation: Operation<DocumentDriveAction | BaseAction>
163
+ ) {
164
+ return this.addOperation(drive, '', operation) as Promise<
165
+ IOperationResult<DocumentDriveDocument>
166
+ >;
167
+ }
168
+
169
+ addDriveOperations(
170
+ drive: string,
171
+ operations: Operation<DocumentDriveAction | BaseAction>[]
172
+ ) {
173
+ return this.addOperations(drive, '', operations) as Promise<
174
+ IOperationResult<DocumentDriveDocument>[]
175
+ >;
112
176
  }
113
177
  }
@@ -1,8 +1,14 @@
1
- import {
1
+ import type {
2
+ DocumentDriveAction,
2
3
  DocumentDriveDocument,
3
4
  DocumentDriveState
4
5
  } from 'document-model-libs/document-drive';
5
- import { Document, Operation } from 'document-model/document';
6
+ import type {
7
+ BaseAction,
8
+ Document,
9
+ Operation,
10
+ Signal
11
+ } from 'document-model/document';
6
12
 
7
13
  export type DriveInput = Omit<
8
14
  DocumentDriveState,
@@ -15,9 +21,18 @@ export type CreateDocumentInput = {
15
21
  document?: Document;
16
22
  };
17
23
 
18
- export interface SortOptions {
19
- afterNodePath?: string;
20
- }
24
+ export type SignalResult = {
25
+ signal: Signal;
26
+ result: unknown; // infer from return types on document-model
27
+ };
28
+
29
+ export type IOperationResult<T extends Document = Document> = {
30
+ success: boolean;
31
+ error?: Error;
32
+ operation: Operation;
33
+ document: T;
34
+ signals: SignalResult[];
35
+ };
21
36
 
22
37
  export interface IDocumentDriveServer {
23
38
  getDrives(): Promise<string[]>;
@@ -34,5 +49,19 @@ export interface IDocumentDriveServer {
34
49
  drive: string,
35
50
  id: string,
36
51
  operation: Operation
37
- ): Promise<Document>;
52
+ ): Promise<IOperationResult>;
53
+ addOperations(
54
+ drive: string,
55
+ id: string,
56
+ operations: Operation[]
57
+ ): Promise<IOperationResult[]>;
58
+
59
+ addDriveOperation(
60
+ drive: string,
61
+ operation: Operation<DocumentDriveAction | BaseAction>
62
+ ): Promise<IOperationResult<DocumentDriveDocument>>;
63
+ addDriveOperations(
64
+ drive: string,
65
+ operations: Operation<DocumentDriveAction | BaseAction>[]
66
+ ): Promise<IOperationResult<DocumentDriveDocument>[]>;
38
67
  }
@@ -0,0 +1,100 @@
1
+ import { DocumentDriveDocument } from 'document-model-libs/document-drive';
2
+ import { Document } from 'document-model/document';
3
+ import { IDriveStorage } from './types';
4
+
5
+ export class BrowserStorage implements IDriveStorage {
6
+ private db: Promise<LocalForage>;
7
+
8
+ static DBName = 'DOCUMENT_DRIVES';
9
+ static SEP = ':';
10
+ static DRIVES_KEY = 'DRIVES';
11
+
12
+ constructor() {
13
+ this.db = import('localforage').then(localForage =>
14
+ localForage.createInstance({
15
+ name: BrowserStorage.DBName
16
+ })
17
+ );
18
+ }
19
+
20
+ buildKey(...args: string[]) {
21
+ return args.join(BrowserStorage.SEP);
22
+ }
23
+
24
+ async getDocuments(drive: string) {
25
+ const keys = await (await this.db).keys();
26
+ const driveKey = `${drive}${BrowserStorage.SEP}`;
27
+ return keys
28
+ .filter(key => key.startsWith(driveKey))
29
+ .map(key => key.slice(driveKey.length));
30
+ }
31
+
32
+ async getDocument(driveId: string, id: string) {
33
+ const document = await (
34
+ await this.db
35
+ ).getItem<Document>(this.buildKey(driveId, id));
36
+ if (!document) {
37
+ throw new Error(`Document with id ${id} not found`);
38
+ }
39
+ return document;
40
+ }
41
+
42
+ async saveDocument(drive: string, id: string, document: Document) {
43
+ await (await this.db).setItem(this.buildKey(drive, id), document);
44
+ }
45
+
46
+ async deleteDocument(drive: string, id: string) {
47
+ await (await this.db).removeItem(this.buildKey(drive, id));
48
+ }
49
+
50
+ 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.id);
57
+ }
58
+
59
+ 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.id === id);
66
+ if (!drive) {
67
+ throw new Error(`Drive with id ${id} not found`);
68
+ }
69
+ return drive;
70
+ }
71
+
72
+ async saveDrive(drive: DocumentDriveDocument) {
73
+ const db = await this.db;
74
+ const drives =
75
+ (await db.getItem<DocumentDriveDocument[]>(
76
+ BrowserStorage.DRIVES_KEY
77
+ )) ?? [];
78
+ const index = drives.findIndex(d => d.state.id === drive.state.id);
79
+ if (index > -1) {
80
+ drives[index] = drive;
81
+ } else {
82
+ drives.push(drive);
83
+ }
84
+ await db.setItem(BrowserStorage.DRIVES_KEY, drives);
85
+ }
86
+
87
+ async deleteDrive(id: string) {
88
+ const documents = await this.getDocuments(id);
89
+ await Promise.all(documents.map(doc => this.deleteDocument(id, doc)));
90
+ const db = await this.db;
91
+ const drives =
92
+ (await db.getItem<DocumentDriveDocument[]>(
93
+ BrowserStorage.DRIVES_KEY
94
+ )) ?? [];
95
+ await db.setItem(
96
+ BrowserStorage.DRIVES_KEY,
97
+ drives.filter(drive => drive.state.id !== id)
98
+ );
99
+ }
100
+ }
@@ -1,3 +1,4 @@
1
+ export { BrowserStorage } from './browser';
1
2
  export { FilesystemStorage } from './filesystem';
2
3
  export { MemoryStorage } from './memory';
3
4
  export type * from './types';
@@ -56,6 +56,7 @@ export class MemoryStorage implements IDriveStorage {
56
56
  }
57
57
 
58
58
  async deleteDrive(id: string) {
59
+ delete this.documents[id];
59
60
  delete this.drives[id];
60
61
  }
61
62
  }