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 +8 -5
- package/src/server/index.ts +102 -38
- package/src/server/types.ts +35 -6
- package/src/storage/browser.ts +100 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/memory.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-drive",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
27
|
-
"document-model-libs": "^1.1.
|
|
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.
|
|
37
|
-
"document-model-libs": "^1.1.
|
|
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",
|
package/src/server/index.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 {
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/server/types.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
19
|
-
|
|
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<
|
|
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
|
+
}
|
package/src/storage/index.ts
CHANGED