document-drive 0.0.22 → 0.0.24

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.
@@ -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,243 @@
1
+ import { type Prisma } from '@prisma/client';
2
+ import {
3
+ DocumentDriveLocalState,
4
+ DocumentDriveState
5
+ } from 'document-model-libs/document-drive';
6
+ import {
7
+ DocumentHeader,
8
+ ExtendedState,
9
+ Operation,
10
+ OperationScope
11
+ } from 'document-model/document';
12
+ import { DocumentDriveStorage, DocumentStorage, IDriveStorage } from './types';
13
+
14
+ export class PrismaStorage implements IDriveStorage {
15
+ private db: Prisma.TransactionClient;
16
+
17
+ constructor(db: Prisma.TransactionClient) {
18
+ this.db = db;
19
+ }
20
+ async createDrive(id: string, drive: DocumentDriveStorage): Promise<void> {
21
+ await this.createDocument('drives', id, drive as DocumentStorage);
22
+ }
23
+ async addDriveOperations(
24
+ id: string,
25
+ operations: Operation[],
26
+ header: DocumentHeader
27
+ ): Promise<void> {
28
+ await this.addDocumentOperations('drives', id, operations, header);
29
+ }
30
+ async createDocument(
31
+ drive: string,
32
+ id: string,
33
+ document: DocumentStorage
34
+ ): Promise<void> {
35
+ await this.db.document.upsert({
36
+ where: {
37
+ id_driveId: {
38
+ id,
39
+ driveId: drive
40
+ }
41
+ },
42
+ update: {},
43
+ create: {
44
+ name: document.name,
45
+ documentType: document.documentType,
46
+ driveId: drive,
47
+ initialState: document.initialState as Prisma.InputJsonObject,
48
+ lastModified: document.lastModified,
49
+ revision: document.revision,
50
+ id
51
+ }
52
+ });
53
+ }
54
+ async addDocumentOperations(
55
+ drive: string,
56
+ id: string,
57
+ operations: Operation[],
58
+ header: DocumentHeader
59
+ ): Promise<void> {
60
+ const document = await this.getDocument(drive, id);
61
+ if (!document) {
62
+ throw new Error(`Document with id ${id} not found`);
63
+ }
64
+
65
+ try {
66
+ await Promise.all(
67
+ operations.map(async op => {
68
+ return this.db.operation.upsert({
69
+ where: {
70
+ driveId_documentId_scope_branch_index: {
71
+ driveId: drive,
72
+ documentId: id,
73
+ scope: op.scope,
74
+ branch: 'main',
75
+ index: op.index
76
+ }
77
+ },
78
+ create: {
79
+ driveId: drive,
80
+ documentId: id,
81
+ hash: op.hash,
82
+ index: op.index,
83
+ input: op.input as Prisma.InputJsonObject,
84
+ timestamp: op.timestamp,
85
+ type: op.type,
86
+ scope: op.scope,
87
+ branch: 'main'
88
+ },
89
+ update: {
90
+ driveId: drive,
91
+ documentId: id,
92
+ hash: op.hash,
93
+ index: op.index,
94
+ input: op.input as Prisma.InputJsonObject,
95
+ timestamp: op.timestamp,
96
+ type: op.type,
97
+ scope: op.scope,
98
+ branch: 'main'
99
+ }
100
+ });
101
+ })
102
+ );
103
+
104
+ await this.db.document.update({
105
+ where: {
106
+ id_driveId: {
107
+ id,
108
+ driveId: 'drives'
109
+ }
110
+ },
111
+ data: {
112
+ lastModified: header.lastModified,
113
+ revision: header.revision
114
+ }
115
+ });
116
+ } catch (e) {
117
+ console.log(e);
118
+ }
119
+
120
+ await this.db.document.upsert({
121
+ where: {
122
+ id_driveId: {
123
+ id: 'drives',
124
+ driveId: id
125
+ }
126
+ },
127
+ create: {
128
+ id: 'drives',
129
+ driveId: id,
130
+ documentType: header.documentType,
131
+ initialState: document.initialState,
132
+ lastModified: header.lastModified,
133
+ revision: header.revision,
134
+ created: header.created
135
+ },
136
+ update: {
137
+ lastModified: header.lastModified,
138
+ revision: header.revision
139
+ }
140
+ });
141
+ }
142
+
143
+ async getDocuments(drive: string) {
144
+ const docs = await this.db.document.findMany({
145
+ where: {
146
+ AND: {
147
+ driveId: drive,
148
+ NOT: {
149
+ id: 'drives'
150
+ }
151
+ }
152
+ }
153
+ });
154
+
155
+ return docs.map(doc => doc.id);
156
+ }
157
+
158
+ async getDocument(driveId: string, id: string) {
159
+ const result = await this.db.document.findFirst({
160
+ where: {
161
+ id: id,
162
+ driveId: driveId
163
+ },
164
+ include: {
165
+ operations: {
166
+ include: {
167
+ attachments: true
168
+ }
169
+ }
170
+ }
171
+ });
172
+
173
+ if (result === null) {
174
+ throw new Error(`Document with id ${id} not found`);
175
+ }
176
+
177
+ const dbDoc = result;
178
+
179
+ const doc = {
180
+ created: dbDoc.created.toISOString(),
181
+ name: dbDoc.name ? dbDoc.name : '',
182
+ documentType: dbDoc.documentType,
183
+ initialState: dbDoc.initialState as ExtendedState<
184
+ DocumentDriveState,
185
+ DocumentDriveLocalState
186
+ >,
187
+ lastModified: dbDoc.lastModified.toISOString(),
188
+ operations: {
189
+ global: dbDoc.operations
190
+ .filter(op => op.scope === 'global')
191
+ .map(op => ({
192
+ hash: op.hash,
193
+ index: op.index,
194
+ timestamp: new Date(op.timestamp).toISOString(),
195
+ input: op.input,
196
+ type: op.type,
197
+ scope: op.scope as OperationScope
198
+ // attachments: fileRegistry
199
+ })),
200
+ local: dbDoc.operations
201
+ .filter(op => op.scope === 'local')
202
+ .map(op => ({
203
+ hash: op.hash,
204
+ index: op.index,
205
+ timestamp: new Date(op.timestamp).toISOString(),
206
+ input: op.input,
207
+ type: op.type,
208
+ scope: op.scope as OperationScope
209
+ // attachments: fileRegistry
210
+ }))
211
+ },
212
+ revision: dbDoc.revision as Required<Record<OperationScope, number>>
213
+ };
214
+
215
+ return doc;
216
+ }
217
+
218
+ async deleteDocument(drive: string, id: string) {
219
+ await this.db.document.deleteMany({
220
+ where: {
221
+ driveId: drive,
222
+ id: id
223
+ }
224
+ });
225
+ }
226
+
227
+ async getDrives() {
228
+ return this.getDocuments('drives');
229
+ }
230
+
231
+ async getDrive(id: string) {
232
+ return this.getDocument('drives', id) as Promise<DocumentDriveStorage>;
233
+ }
234
+
235
+ async deleteDrive(id: string) {
236
+ await this.deleteDocument('drives', id);
237
+ await this.db.document.deleteMany({
238
+ where: {
239
+ driveId: id
240
+ }
241
+ });
242
+ }
243
+ }
@@ -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
+ }