document-drive 0.0.23 → 0.0.25

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.23",
3
+ "version": "0.0.25",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "module": "./src/index.ts",
@@ -25,29 +25,33 @@
25
25
  "test:watch": "vitest watch"
26
26
  },
27
27
  "peerDependencies": {
28
- "document-model": "^1.0.20",
29
- "document-model-libs": "^1.1.27",
30
- "localforage": "^1.10.0"
28
+ "@prisma/client": "5.7.1",
29
+ "document-model": "^1.0.22",
30
+ "document-model-libs": "^1.1.31",
31
+ "localforage": "^1.10.0",
32
+ "sequelize": "^6.35.2",
33
+ "sqlite3": "^5.1.7"
31
34
  },
32
35
  "dependencies": {
33
- "@prisma/client": "5.7.1",
34
36
  "sanitize-filename": "^1.6.3"
35
37
  },
36
38
  "devDependencies": {
39
+ "@prisma/client": "5.7.1",
37
40
  "@total-typescript/ts-reset": "^0.5.1",
38
41
  "@typescript-eslint/eslint-plugin": "^6.18.1",
39
42
  "@typescript-eslint/parser": "^6.18.1",
40
43
  "@vitest/coverage-v8": "^0.34.6",
41
- "document-model": "^1.0.20",
42
- "document-model-libs": "^1.1.27",
44
+ "document-model": "^1.0.22",
45
+ "document-model-libs": "^1.1.31",
43
46
  "eslint": "^8.56.0",
44
47
  "eslint-config-prettier": "^9.1.0",
45
48
  "fake-indexeddb": "^5.0.1",
46
49
  "localforage": "^1.10.0",
47
50
  "prettier": "^3.1.1",
48
51
  "prettier-plugin-organize-imports": "^3.2.4",
49
- "prisma": "^5.8.0",
50
- "typescript": "^5.3.3",
52
+ "sequelize": "^6.35.2",
53
+ "sqlite3": "^5.1.7",
54
+ "typescript": "^5.3.2",
51
55
  "vitest": "^0.34.6"
52
56
  }
53
57
  }
@@ -1,10 +1,9 @@
1
- import { PrismaClient, type Prisma } from '@prisma/client';
1
+ import { type Prisma } from '@prisma/client';
2
2
  import {
3
3
  DocumentDriveLocalState,
4
4
  DocumentDriveState
5
5
  } from 'document-model-libs/document-drive';
6
6
  import {
7
- Document,
8
7
  DocumentHeader,
9
8
  ExtendedState,
10
9
  Operation,
@@ -13,17 +12,13 @@ import {
13
12
  import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
14
13
 
15
14
  export class PrismaStorage implements IDriveStorage {
16
- private db: PrismaClient;
15
+ private db: Prisma.TransactionClient;
17
16
 
18
- constructor(db: PrismaClient) {
17
+ constructor(db: Prisma.TransactionClient) {
19
18
  this.db = db;
20
19
  }
21
20
  async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
22
- await this.createDocument(
23
- 'drives',
24
- id,
25
- drive as DocumentStorage
26
- );
21
+ await this.createDocument('drives', id, drive as DocumentStorage);
27
22
  }
28
23
  async addDriveOperations(
29
24
  id: string,
@@ -214,7 +209,7 @@ export class PrismaStorage implements IDriveStorage {
214
209
  // attachments: fileRegistry
215
210
  }))
216
211
  },
217
- revision: dbDoc.revision
212
+ revision: dbDoc.revision as Required<Record<OperationScope, number>>
218
213
  };
219
214
 
220
215
  return doc;
@@ -0,0 +1,375 @@
1
+ import {
2
+ DocumentDriveLocalState,
3
+ DocumentDriveState
4
+ } from 'document-model-libs/document-drive';
5
+ import {
6
+ AttachmentInput,
7
+ DocumentHeader,
8
+ ExtendedState,
9
+ Operation,
10
+ OperationScope
11
+ } from 'document-model/document';
12
+ import { DataTypes, Options, Sequelize } from 'sequelize';
13
+ import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
14
+
15
+ export class SequelizeStorage implements IDriveStorage {
16
+ private db: Sequelize;
17
+
18
+ constructor(options: Options) {
19
+ this.db = new Sequelize(options);
20
+ }
21
+
22
+ public syncModels() {
23
+ const Document = this.db.define('document', {
24
+ id: {
25
+ type: DataTypes.STRING,
26
+ primaryKey: true
27
+ },
28
+ driveId: {
29
+ type: DataTypes.STRING,
30
+ primaryKey: true
31
+ },
32
+ name: DataTypes.STRING,
33
+ documentType: DataTypes.STRING,
34
+ initialState: DataTypes.JSON,
35
+ lastModified: DataTypes.DATE,
36
+ revision: DataTypes.JSON
37
+ });
38
+
39
+ const Operation = this.db.define('operation', {
40
+ driveId: {
41
+ type: DataTypes.STRING,
42
+ primaryKey: true
43
+ },
44
+ documentId: {
45
+ type: DataTypes.STRING,
46
+ primaryKey: true
47
+ },
48
+ hash: DataTypes.STRING,
49
+ index: {
50
+ type: DataTypes.INTEGER,
51
+ primaryKey: true
52
+ },
53
+ input: DataTypes.JSON,
54
+ timestamp: DataTypes.DATE,
55
+ type: DataTypes.STRING,
56
+ scope: {
57
+ type: DataTypes.STRING,
58
+ primaryKey: true
59
+ },
60
+ branch: {
61
+ type: DataTypes.STRING,
62
+ primaryKey: true
63
+ }
64
+ });
65
+
66
+ const Attachment = this.db.define('attachment', {
67
+ driveId: {
68
+ type: DataTypes.STRING,
69
+ primaryKey: true
70
+ },
71
+ documentId: {
72
+ type: DataTypes.STRING,
73
+ primaryKey: true
74
+ },
75
+ scope: {
76
+ type: DataTypes.STRING,
77
+ primaryKey: true
78
+ },
79
+ branch: {
80
+ type: DataTypes.STRING,
81
+ primaryKey: true
82
+ },
83
+ index: {
84
+ type: DataTypes.STRING,
85
+ primaryKey: true
86
+ },
87
+ hash: {
88
+ type: DataTypes.STRING,
89
+ primaryKey: true
90
+ },
91
+ mimeType: DataTypes.STRING,
92
+ fileName: DataTypes.STRING,
93
+ extension: DataTypes.STRING,
94
+ data: DataTypes.BLOB
95
+ });
96
+
97
+ Operation.hasMany(Attachment, {
98
+ onDelete: 'CASCADE'
99
+ });
100
+ Attachment.belongsTo(Operation);
101
+ Document.hasMany(Operation, {
102
+ onDelete: 'CASCADE'
103
+ });
104
+ Operation.belongsTo(Document);
105
+
106
+ return this.db.sync({ force: true });
107
+ }
108
+
109
+ async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
110
+ await this.createDocument('drives', id, drive as DocumentStorage);
111
+ }
112
+ async addDriveOperations(
113
+ id: string,
114
+ operations: Operation[],
115
+ header: DocumentHeader
116
+ ): Promise<void> {
117
+ await this.addDocumentOperations('drives', id, operations, header);
118
+ }
119
+ async createDocument(
120
+ drive: string,
121
+ id: string,
122
+ document: DocumentStorage
123
+ ): Promise<void> {
124
+ const Document = this.db.models.document;
125
+
126
+ if (!Document) {
127
+ throw new Error('Document model not found');
128
+ }
129
+
130
+ await Document.create({
131
+ id: id,
132
+ driveId: drive,
133
+ name: document.name,
134
+ documentType: document.documentType,
135
+ initialState: document.initialState,
136
+ lastModified: document.lastModified,
137
+ revision: document.revision
138
+ });
139
+ }
140
+ async addDocumentOperations(
141
+ drive: string,
142
+ id: string,
143
+ operations: Operation[],
144
+ header: DocumentHeader
145
+ ): Promise<void> {
146
+ const document = await this.getDocument(drive, id);
147
+ if (!document) {
148
+ throw new Error(`Document with id ${id} not found`);
149
+ }
150
+
151
+ const Operation = this.db.models.operation;
152
+ if (!Operation) {
153
+ throw new Error('Operation model not found');
154
+ }
155
+
156
+ await Promise.all(
157
+ operations.map(async op => {
158
+ return Operation.create({
159
+ driveId: drive,
160
+ documentId: id,
161
+ hash: op.hash,
162
+ index: op.index,
163
+ input: op.input,
164
+ timestamp: op.timestamp,
165
+ type: op.type,
166
+ scope: op.scope,
167
+ branch: 'main'
168
+ }).then(async () => {
169
+ if (op.attachments) {
170
+ await this._addDocumentOperationAttachments(
171
+ drive,
172
+ id,
173
+ op,
174
+ op.attachments
175
+ );
176
+ }
177
+ });
178
+ })
179
+ );
180
+
181
+ const Document = this.db.models.document;
182
+ if (!Document) {
183
+ throw new Error('Document model not found');
184
+ }
185
+
186
+ await Document.update(
187
+ {
188
+ lastModified: header.lastModified,
189
+ revision: header.revision
190
+ },
191
+ {
192
+ where: {
193
+ id: id,
194
+ driveId: drive
195
+ }
196
+ }
197
+ );
198
+ }
199
+
200
+ async _addDocumentOperationAttachments(
201
+ driveId: string,
202
+ documentId: string,
203
+ operation: Operation,
204
+ attachments: AttachmentInput[]
205
+ ) {
206
+ const Attachment = this.db.models.attachment;
207
+ if (!Attachment) {
208
+ throw new Error('Attachment model not found');
209
+ }
210
+
211
+ await Promise.all(
212
+ attachments.map(async attachment => {
213
+ return Attachment.create({
214
+ driveId: driveId,
215
+ documentId: documentId,
216
+ scope: operation.scope,
217
+ branch: 'main',
218
+ index: operation.index,
219
+ mimeType: attachment.mimeType,
220
+ fileName: attachment.fileName,
221
+ extension: attachment.extension,
222
+ data: attachment.data,
223
+ hash: attachment.hash
224
+ });
225
+ })
226
+ );
227
+ }
228
+
229
+ async getDocuments(drive: string) {
230
+ const Document = this.db.models.document;
231
+ if (!Document) {
232
+ throw new Error('Document model not found');
233
+ }
234
+
235
+ const result = await Document.findAll({
236
+ attributes: ['id'],
237
+ where: {
238
+ driveId: drive
239
+ }
240
+ });
241
+
242
+ const ids = result.map((e: { dataValues: { id: string } }) => {
243
+ const { id } = e.dataValues;
244
+ return id;
245
+ });
246
+ return ids;
247
+ }
248
+
249
+ async getDocument(driveId: string, id: string) {
250
+ const Document = this.db.models.document;
251
+ if (!Document) {
252
+ throw new Error('Document model not found');
253
+ }
254
+
255
+ const entry = await Document.findOne({
256
+ where: {
257
+ id: id,
258
+ driveId: driveId
259
+ },
260
+ include: [
261
+ {
262
+ model: this.db.models.operation,
263
+ as: 'operations'
264
+ }
265
+ ]
266
+ });
267
+
268
+ if (entry === null) {
269
+ throw new Error(`Document with id ${id} not found`);
270
+ }
271
+
272
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
273
+ const document: {
274
+ operations: [
275
+ {
276
+ hash: string;
277
+ index: number;
278
+ timestamp: Date;
279
+ input: JSON;
280
+ type: string;
281
+ scope: string;
282
+ }
283
+ ];
284
+ revision: Required<Record<OperationScope, number>>;
285
+ createdAt: Date;
286
+ name: string;
287
+ updatedAt: Date;
288
+ documentType: string;
289
+ initialState: ExtendedState<
290
+ DocumentDriveState,
291
+ DocumentDriveLocalState
292
+ >;
293
+ } = entry.dataValues;
294
+ const Operation = this.db.models.operation;
295
+ if (!Operation) {
296
+ throw new Error('Operation model not found');
297
+ }
298
+
299
+ const operations = document.operations.map(
300
+ (op: {
301
+ hash: string;
302
+ index: number;
303
+ timestamp: Date;
304
+ input: JSON;
305
+ type: string;
306
+ scope: string;
307
+ }) => ({
308
+ hash: op.hash,
309
+ index: op.index,
310
+ timestamp: new Date(op.timestamp).toISOString(),
311
+ input: op.input,
312
+ type: op.type,
313
+ scope: op.scope as OperationScope
314
+ // attachments: fileRegistry
315
+ })
316
+ );
317
+
318
+ const doc = {
319
+ created: document.createdAt.toISOString(),
320
+ name: document.name ? document.name : '',
321
+ documentType: document.documentType,
322
+ initialState: document.initialState,
323
+ lastModified: document.updatedAt.toISOString(),
324
+ operations: {
325
+ global: operations.filter(
326
+ (op: Operation) => op.scope === 'global'
327
+ ),
328
+ local: operations.filter(
329
+ (op: Operation) => op.scope === 'local'
330
+ )
331
+ },
332
+ revision: document.revision
333
+ };
334
+
335
+ return doc;
336
+ }
337
+
338
+ async deleteDocument(drive: string, id: string) {
339
+ const Document = this.db.models.document;
340
+ if (!Document) {
341
+ throw new Error('Document model not found');
342
+ }
343
+
344
+ await Document.destroy({
345
+ where: {
346
+ id: id,
347
+ driveId: drive
348
+ }
349
+ });
350
+ }
351
+
352
+ async getDrives() {
353
+ return this.getDocuments('drives');
354
+ }
355
+
356
+ async getDrive(id: string) {
357
+ const doc = await this.getDocument('drives', id);
358
+ return doc as DocumentDriveStorage;
359
+ }
360
+
361
+ async deleteDrive(id: string) {
362
+ await this.deleteDocument('drives', id);
363
+
364
+ const Document = this.db.models.document;
365
+ if (!Document) {
366
+ throw new Error('Document model not found');
367
+ }
368
+
369
+ await Document.destroy({
370
+ where: {
371
+ driveId: id
372
+ }
373
+ });
374
+ }
375
+ }