localdb-ces6q 0.0.2

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.
@@ -0,0 +1,1137 @@
1
+ import { arraysEqual, dedupKeys } from 'util-3gcvv/array.js';
2
+ import { objectMap } from 'util-3gcvv/object.js';
3
+ import { describe, expect, jest, test } from '@jest/globals';
4
+ import { LocalDB } from '../src/LocalDB.js';
5
+ jest.useFakeTimers();
6
+ describe('LocalDB', () => {
7
+ test('LocalDB', () => {
8
+ const initData = {
9
+ person: {
10
+ p1: { id: 'p1', age: 10, name: 'john' },
11
+ p2: { id: 'p2', age: 18, name: 'sean' },
12
+ },
13
+ city: {
14
+ c1: { id: 'c1', name: 'Tokyo', location: [1, 2] },
15
+ },
16
+ };
17
+ const db = new LocalDB({
18
+ person: {
19
+ fields: {
20
+ id: { type: 'string' },
21
+ age: { type: 'number' },
22
+ name: { type: 'string' },
23
+ },
24
+ },
25
+ city: {
26
+ fields: {
27
+ id: { type: 'string' },
28
+ name: { type: 'string' },
29
+ location: {
30
+ type: 'array',
31
+ normalize: (x) => x.sort(),
32
+ equals: arraysEqual,
33
+ },
34
+ },
35
+ },
36
+ }, initData, { undoable: true });
37
+ expect(db.toJSON()).toEqual(initData);
38
+ expect(db.state.canUndo).toBe(false);
39
+ expect(db.state.canRedo).toBe(false);
40
+ expect(db.collection('person')).toEqual(initData.person);
41
+ expect(db.collection('city')).toEqual(initData.city);
42
+ expect(db.doc('person', 'p1')).toEqual(initData.person.p1);
43
+ expect(db.doc('person', 'p2')).toEqual(initData.person.p2);
44
+ expect(db.doc('city', 'c1')).toEqual(initData.city.c1);
45
+ const dbChanges = [];
46
+ const unsubDb = db.subToDB((next, prev, change) => {
47
+ dbChanges.push([change, next, prev]);
48
+ });
49
+ const personChanges = [];
50
+ const unsubPerson = db.subToCol('person', (next, prev, change) => {
51
+ personChanges.push([change, next, prev]);
52
+ });
53
+ const p1Changes = [];
54
+ const unsubP1 = db.subToDoc('person', 'p1', (next, prev, change) => {
55
+ p1Changes.push([change, next, prev]);
56
+ });
57
+ const p2Changes = [];
58
+ const unsubP2 = db.subToDoc('person', 'p2', (next, prev, change) => {
59
+ p2Changes.push([change, next, prev]);
60
+ });
61
+ const p1AgeChanges = [];
62
+ const unsubP1Age = db.subToField('person', 'p1', 'age', (next, prev, nextDoc, prevDoc) => {
63
+ p1AgeChanges.push([next, prev, nextDoc, prevDoc]);
64
+ });
65
+ const p1NameChanges = [];
66
+ const unsubP1Name = db.subToField('person', 'p1', 'name', (next, prev, nextDoc, prevDoc) => {
67
+ p1NameChanges.push([next, prev, nextDoc, prevDoc]);
68
+ });
69
+ /// update
70
+ db.updateDoc('person', 'p1', { age: 11 });
71
+ expect(db.doc('person', 'p1')).toEqual({ id: 'p1', age: 11, name: 'john' });
72
+ expect(dbChanges).toEqual([
73
+ [
74
+ { person: { p1: { age: 11 } } },
75
+ {
76
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
77
+ person: {
78
+ p1: { age: 11, id: 'p1', name: 'john' },
79
+ p2: { age: 18, id: 'p2', name: 'sean' },
80
+ },
81
+ },
82
+ {
83
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
84
+ person: {
85
+ p1: { age: 10, id: 'p1', name: 'john' },
86
+ p2: { age: 18, id: 'p2', name: 'sean' },
87
+ },
88
+ },
89
+ ],
90
+ ]);
91
+ expect(personChanges).toEqual([
92
+ [
93
+ { p1: { age: 11 } },
94
+ {
95
+ p1: { age: 11, id: 'p1', name: 'john' },
96
+ p2: { age: 18, id: 'p2', name: 'sean' },
97
+ },
98
+ {
99
+ p1: { age: 10, id: 'p1', name: 'john' },
100
+ p2: { age: 18, id: 'p2', name: 'sean' },
101
+ },
102
+ ],
103
+ ]);
104
+ expect(p1Changes).toEqual([
105
+ [
106
+ { age: 11 },
107
+ { age: 11, id: 'p1', name: 'john' },
108
+ { age: 10, id: 'p1', name: 'john' },
109
+ ],
110
+ ]);
111
+ expect(p2Changes).toEqual([]);
112
+ expect(p1AgeChanges).toEqual([
113
+ [
114
+ 11,
115
+ 10,
116
+ { age: 11, id: 'p1', name: 'john' },
117
+ { age: 10, id: 'p1', name: 'john' },
118
+ ],
119
+ ]);
120
+ expect(p1NameChanges).toEqual([]);
121
+ expect(db.state.canUndo).toBe(true);
122
+ expect(db.state.canRedo).toBe(false);
123
+ dbChanges.length = 0;
124
+ personChanges.length = 0;
125
+ p1Changes.length = 0;
126
+ p2Changes.length = 0;
127
+ p1AgeChanges.length = 0;
128
+ p1NameChanges.length = 0;
129
+ /// update
130
+ db.updateDoc('person', 'p1', { age: 12, name: 'jack' });
131
+ expect(db.doc('person', 'p1')).toEqual({ id: 'p1', age: 12, name: 'jack' });
132
+ expect(dbChanges).toEqual([
133
+ [
134
+ { person: { p1: { age: 12, name: 'jack' } } },
135
+ {
136
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
137
+ person: {
138
+ p1: { age: 12, id: 'p1', name: 'jack' },
139
+ p2: { age: 18, id: 'p2', name: 'sean' },
140
+ },
141
+ },
142
+ {
143
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
144
+ person: {
145
+ p1: { age: 11, id: 'p1', name: 'john' },
146
+ p2: { age: 18, id: 'p2', name: 'sean' },
147
+ },
148
+ },
149
+ ],
150
+ ]);
151
+ expect(personChanges).toEqual([
152
+ [
153
+ { p1: { age: 12, name: 'jack' } },
154
+ {
155
+ p1: { age: 12, id: 'p1', name: 'jack' },
156
+ p2: { age: 18, id: 'p2', name: 'sean' },
157
+ },
158
+ {
159
+ p1: { age: 11, id: 'p1', name: 'john' },
160
+ p2: { age: 18, id: 'p2', name: 'sean' },
161
+ },
162
+ ],
163
+ ]);
164
+ expect(p1Changes).toEqual([
165
+ [
166
+ { age: 12, name: 'jack' },
167
+ { age: 12, id: 'p1', name: 'jack' },
168
+ { age: 11, id: 'p1', name: 'john' },
169
+ ],
170
+ ]);
171
+ expect(p2Changes).toEqual([]);
172
+ expect(p1AgeChanges).toEqual([
173
+ [
174
+ 12,
175
+ 11,
176
+ { age: 12, id: 'p1', name: 'jack' },
177
+ { age: 11, id: 'p1', name: 'john' },
178
+ ],
179
+ ]);
180
+ expect(p1NameChanges).toEqual([
181
+ [
182
+ 'jack',
183
+ 'john',
184
+ { age: 12, id: 'p1', name: 'jack' },
185
+ { age: 11, id: 'p1', name: 'john' },
186
+ ],
187
+ ]);
188
+ expect(db.state.canUndo).toBe(true);
189
+ expect(db.state.canRedo).toBe(false);
190
+ dbChanges.length = 0;
191
+ personChanges.length = 0;
192
+ p1Changes.length = 0;
193
+ p2Changes.length = 0;
194
+ p1AgeChanges.length = 0;
195
+ p1NameChanges.length = 0;
196
+ // transaction
197
+ db.tx(() => {
198
+ db.updateDoc('person', 'p1', { age: 13, name: 'jackson' });
199
+ db.updateDoc('person', 'p1', { age: 14, name: 'jackson' });
200
+ db.updateDoc('person', 'p2', { age: 18, name: 'sean' });
201
+ db.updateDoc('city', 'c1', { name: 'austin', location: [2, 1] });
202
+ });
203
+ expect(db.doc('person', 'p1')).toEqual({
204
+ id: 'p1',
205
+ age: 14,
206
+ name: 'jackson',
207
+ });
208
+ expect(db.doc('person', 'p2')).toEqual({ id: 'p2', age: 18, name: 'sean' });
209
+ expect(db.doc('city', 'c1')).toEqual({
210
+ id: 'c1',
211
+ location: [1, 2],
212
+ name: 'austin',
213
+ });
214
+ expect(dbChanges).toEqual([
215
+ [
216
+ {
217
+ city: { c1: { name: 'austin' } },
218
+ person: { p1: { age: 14, name: 'jackson' } },
219
+ },
220
+ {
221
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
222
+ person: {
223
+ p1: { age: 14, id: 'p1', name: 'jackson' },
224
+ p2: { age: 18, id: 'p2', name: 'sean' },
225
+ },
226
+ },
227
+ {
228
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
229
+ person: {
230
+ p1: { age: 12, id: 'p1', name: 'jack' },
231
+ p2: { age: 18, id: 'p2', name: 'sean' },
232
+ },
233
+ },
234
+ ],
235
+ ]);
236
+ expect(personChanges).toEqual([
237
+ [
238
+ { p1: { age: 14, name: 'jackson' } },
239
+ {
240
+ p1: { age: 14, id: 'p1', name: 'jackson' },
241
+ p2: { age: 18, id: 'p2', name: 'sean' },
242
+ },
243
+ {
244
+ p1: { age: 12, id: 'p1', name: 'jack' },
245
+ p2: { age: 18, id: 'p2', name: 'sean' },
246
+ },
247
+ ],
248
+ ]);
249
+ expect(p1Changes).toEqual([
250
+ [
251
+ { age: 14, name: 'jackson' },
252
+ { age: 14, id: 'p1', name: 'jackson' },
253
+ { age: 12, id: 'p1', name: 'jack' },
254
+ ],
255
+ ]);
256
+ expect(p2Changes).toEqual([]);
257
+ expect(p1AgeChanges).toEqual([
258
+ [
259
+ 14,
260
+ 12,
261
+ { age: 14, id: 'p1', name: 'jackson' },
262
+ { age: 12, id: 'p1', name: 'jack' },
263
+ ],
264
+ ]);
265
+ expect(p1NameChanges).toEqual([
266
+ [
267
+ 'jackson',
268
+ 'jack',
269
+ { age: 14, id: 'p1', name: 'jackson' },
270
+ { age: 12, id: 'p1', name: 'jack' },
271
+ ],
272
+ ]);
273
+ expect(db.state.canUndo).toBe(true);
274
+ expect(db.state.canRedo).toBe(false);
275
+ // getters
276
+ expect(db.collection('person')).toEqual({
277
+ p1: { age: 14, id: 'p1', name: 'jackson' },
278
+ p2: { age: 18, id: 'p2', name: 'sean' },
279
+ });
280
+ expect(db.collection('city')).toEqual({
281
+ c1: { id: 'c1', location: [1, 2], name: 'austin' },
282
+ });
283
+ expect(db.docs('person', ['p1', 'p2', 'p3'])).toEqual([
284
+ { age: 14, id: 'p1', name: 'jackson' },
285
+ { age: 18, id: 'p2', name: 'sean' },
286
+ undefined,
287
+ ]);
288
+ expect(db.docs('city', ['c0', 'c1'])).toEqual([
289
+ undefined,
290
+ { id: 'c1', location: [1, 2], name: 'austin' },
291
+ ]);
292
+ // undo / redo
293
+ expect(db.state.canUndo).toBe(true);
294
+ expect(db.state.canRedo).toBe(false);
295
+ db.undo();
296
+ expect(db.state.canUndo).toBe(true);
297
+ expect(db.state.canRedo).toBe(true);
298
+ expect(db.toJSON()).toEqual({
299
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
300
+ person: {
301
+ p1: { age: 12, id: 'p1', name: 'jack' },
302
+ p2: { age: 18, id: 'p2', name: 'sean' },
303
+ },
304
+ });
305
+ db.redo();
306
+ expect(db.state.canUndo).toBe(true);
307
+ expect(db.state.canRedo).toBe(false);
308
+ expect(db.toJSON()).toEqual({
309
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
310
+ person: {
311
+ p1: { age: 14, id: 'p1', name: 'jackson' },
312
+ p2: { age: 18, id: 'p2', name: 'sean' },
313
+ },
314
+ });
315
+ db.undo();
316
+ expect(db.state.canUndo).toBe(true);
317
+ expect(db.state.canRedo).toBe(true);
318
+ expect(db.toJSON()).toEqual({
319
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
320
+ person: {
321
+ p1: { age: 12, id: 'p1', name: 'jack' },
322
+ p2: { age: 18, id: 'p2', name: 'sean' },
323
+ },
324
+ });
325
+ db.undo();
326
+ expect(db.state.canUndo).toBe(true);
327
+ expect(db.state.canRedo).toBe(true);
328
+ expect(db.toJSON()).toEqual({
329
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
330
+ person: {
331
+ p1: { age: 11, id: 'p1', name: 'john' },
332
+ p2: { age: 18, id: 'p2', name: 'sean' },
333
+ },
334
+ });
335
+ db.undo();
336
+ expect(db.state.canUndo).toBe(false);
337
+ expect(db.state.canRedo).toBe(true);
338
+ expect(db.toJSON()).toEqual({
339
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
340
+ person: {
341
+ p1: { age: 10, id: 'p1', name: 'john' },
342
+ p2: { age: 18, id: 'p2', name: 'sean' },
343
+ },
344
+ });
345
+ db.undo();
346
+ expect(db.state.canUndo).toBe(false);
347
+ expect(db.state.canRedo).toBe(true);
348
+ expect(db.toJSON()).toEqual({
349
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
350
+ person: {
351
+ p1: { age: 10, id: 'p1', name: 'john' },
352
+ p2: { age: 18, id: 'p2', name: 'sean' },
353
+ },
354
+ });
355
+ db.redo();
356
+ expect(db.state.canUndo).toBe(true);
357
+ expect(db.state.canRedo).toBe(true);
358
+ expect(db.toJSON()).toEqual({
359
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
360
+ person: {
361
+ p1: { age: 11, id: 'p1', name: 'john' },
362
+ p2: { age: 18, id: 'p2', name: 'sean' },
363
+ },
364
+ });
365
+ db.redo();
366
+ expect(db.state.canUndo).toBe(true);
367
+ expect(db.state.canRedo).toBe(true);
368
+ expect(db.toJSON()).toEqual({
369
+ city: { c1: { id: 'c1', location: [1, 2], name: 'Tokyo' } },
370
+ person: {
371
+ p1: { age: 12, id: 'p1', name: 'jack' },
372
+ p2: { age: 18, id: 'p2', name: 'sean' },
373
+ },
374
+ });
375
+ db.redo();
376
+ expect(db.state.canUndo).toBe(true);
377
+ expect(db.state.canRedo).toBe(false);
378
+ expect(db.toJSON()).toEqual({
379
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
380
+ person: {
381
+ p1: { age: 14, id: 'p1', name: 'jackson' },
382
+ p2: { age: 18, id: 'p2', name: 'sean' },
383
+ },
384
+ });
385
+ // set doc new
386
+ dbChanges.length = 0;
387
+ personChanges.length = 0;
388
+ p1Changes.length = 0;
389
+ p2Changes.length = 0;
390
+ p1AgeChanges.length = 0;
391
+ p1NameChanges.length = 0;
392
+ db.setDoc('person', { id: 'p3', age: 21, name: 'mike' });
393
+ expect(dbChanges).toEqual([
394
+ [
395
+ { person: { p3: { age: 21, id: 'p3', name: 'mike' } } },
396
+ {
397
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
398
+ person: {
399
+ p1: { age: 14, id: 'p1', name: 'jackson' },
400
+ p2: { age: 18, id: 'p2', name: 'sean' },
401
+ p3: { age: 21, id: 'p3', name: 'mike' },
402
+ },
403
+ },
404
+ {
405
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
406
+ person: {
407
+ p1: { age: 14, id: 'p1', name: 'jackson' },
408
+ p2: { age: 18, id: 'p2', name: 'sean' },
409
+ },
410
+ },
411
+ ],
412
+ ]);
413
+ expect(personChanges).toEqual([
414
+ [
415
+ { p3: { age: 21, id: 'p3', name: 'mike' } },
416
+ {
417
+ p1: { age: 14, id: 'p1', name: 'jackson' },
418
+ p2: { age: 18, id: 'p2', name: 'sean' },
419
+ p3: { age: 21, id: 'p3', name: 'mike' },
420
+ },
421
+ {
422
+ p1: { age: 14, id: 'p1', name: 'jackson' },
423
+ p2: { age: 18, id: 'p2', name: 'sean' },
424
+ },
425
+ ],
426
+ ]);
427
+ expect(p1Changes).toEqual([]);
428
+ expect(p2Changes).toEqual([]);
429
+ expect(p1AgeChanges).toEqual([]);
430
+ expect(p1NameChanges).toEqual([]);
431
+ // set doc overwrite
432
+ dbChanges.length = 0;
433
+ personChanges.length = 0;
434
+ p1Changes.length = 0;
435
+ p2Changes.length = 0;
436
+ p1AgeChanges.length = 0;
437
+ p1NameChanges.length = 0;
438
+ db.setDoc('person', { id: 'p2', age: 25, name: 'tom' });
439
+ expect(dbChanges).toEqual([
440
+ [
441
+ { person: { p2: { age: 25, name: 'tom' } } },
442
+ {
443
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
444
+ person: {
445
+ p1: { age: 14, id: 'p1', name: 'jackson' },
446
+ p2: { age: 25, id: 'p2', name: 'tom' },
447
+ p3: { age: 21, id: 'p3', name: 'mike' },
448
+ },
449
+ },
450
+ {
451
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
452
+ person: {
453
+ p1: { age: 14, id: 'p1', name: 'jackson' },
454
+ p2: { age: 18, id: 'p2', name: 'sean' },
455
+ p3: { age: 21, id: 'p3', name: 'mike' },
456
+ },
457
+ },
458
+ ],
459
+ ]);
460
+ expect(personChanges).toEqual([
461
+ [
462
+ { p2: { age: 25, name: 'tom' } },
463
+ {
464
+ p1: { age: 14, id: 'p1', name: 'jackson' },
465
+ p2: { age: 25, id: 'p2', name: 'tom' },
466
+ p3: { age: 21, id: 'p3', name: 'mike' },
467
+ },
468
+ {
469
+ p1: { age: 14, id: 'p1', name: 'jackson' },
470
+ p2: { age: 18, id: 'p2', name: 'sean' },
471
+ p3: { age: 21, id: 'p3', name: 'mike' },
472
+ },
473
+ ],
474
+ ]);
475
+ expect(p1Changes).toEqual([]);
476
+ expect(p2Changes).toEqual([
477
+ [
478
+ { age: 25, name: 'tom' },
479
+ { age: 25, id: 'p2', name: 'tom' },
480
+ { age: 18, id: 'p2', name: 'sean' },
481
+ ],
482
+ ]);
483
+ expect(p1AgeChanges).toEqual([]);
484
+ expect(p1NameChanges).toEqual([]);
485
+ // delete doc
486
+ dbChanges.length = 0;
487
+ personChanges.length = 0;
488
+ p1Changes.length = 0;
489
+ p2Changes.length = 0;
490
+ p1AgeChanges.length = 0;
491
+ p1NameChanges.length = 0;
492
+ db.deleteDoc('person', 'p2');
493
+ expect(dbChanges).toEqual([
494
+ [
495
+ { person: { p2: null } },
496
+ {
497
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
498
+ person: {
499
+ p1: { age: 14, id: 'p1', name: 'jackson' },
500
+ p3: { age: 21, id: 'p3', name: 'mike' },
501
+ },
502
+ },
503
+ {
504
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
505
+ person: {
506
+ p1: { age: 14, id: 'p1', name: 'jackson' },
507
+ p2: { age: 25, id: 'p2', name: 'tom' },
508
+ p3: { age: 21, id: 'p3', name: 'mike' },
509
+ },
510
+ },
511
+ ],
512
+ ]);
513
+ expect(personChanges).toEqual([
514
+ [
515
+ { p2: null },
516
+ {
517
+ p1: { age: 14, id: 'p1', name: 'jackson' },
518
+ p3: { age: 21, id: 'p3', name: 'mike' },
519
+ },
520
+ {
521
+ p1: { age: 14, id: 'p1', name: 'jackson' },
522
+ p2: { age: 25, id: 'p2', name: 'tom' },
523
+ p3: { age: 21, id: 'p3', name: 'mike' },
524
+ },
525
+ ],
526
+ ]);
527
+ expect(p1Changes).toEqual([]);
528
+ expect(p2Changes).toEqual([
529
+ [null, undefined, { age: 25, id: 'p2', name: 'tom' }],
530
+ ]);
531
+ expect(p1AgeChanges).toEqual([]);
532
+ expect(p1NameChanges).toEqual([]);
533
+ // test rollback
534
+ dbChanges.length = 0;
535
+ personChanges.length = 0;
536
+ p1Changes.length = 0;
537
+ p2Changes.length = 0;
538
+ p1AgeChanges.length = 0;
539
+ p1NameChanges.length = 0;
540
+ expect(() => db.tx(() => {
541
+ db.updateDoc('person', 'p1', { age: 15 });
542
+ db.updateDoc('person', 'p1', { age: 16 });
543
+ throw 1;
544
+ db.updateDoc('person', 'p1', { age: 17 });
545
+ db.updateDoc('person', 'p1', { age: 18 });
546
+ })).toThrowError();
547
+ expect(dbChanges).toEqual([]);
548
+ expect(personChanges).toEqual([]);
549
+ expect(p1Changes).toEqual([]);
550
+ expect(p2Changes).toEqual([]);
551
+ expect(p1AgeChanges).toEqual([]);
552
+ expect(p1NameChanges).toEqual([]);
553
+ expect(db.toJSON()).toEqual({
554
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
555
+ person: {
556
+ p1: { age: 14, id: 'p1', name: 'jackson' },
557
+ p3: { age: 21, id: 'p3', name: 'mike' },
558
+ },
559
+ });
560
+ // test tx
561
+ dbChanges.length = 0;
562
+ personChanges.length = 0;
563
+ p1Changes.length = 0;
564
+ p2Changes.length = 0;
565
+ p1AgeChanges.length = 0;
566
+ p1NameChanges.length = 0;
567
+ db.tx(() => {
568
+ db.updateDoc('person', 'p1', { age: 15 });
569
+ db.updateDoc('person', 'p1', { age: 16 });
570
+ });
571
+ expect(dbChanges).toEqual([
572
+ [
573
+ { person: { p1: { age: 16 } } },
574
+ {
575
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
576
+ person: {
577
+ p1: { age: 16, id: 'p1', name: 'jackson' },
578
+ p3: { age: 21, id: 'p3', name: 'mike' },
579
+ },
580
+ },
581
+ {
582
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
583
+ person: {
584
+ p1: { age: 14, id: 'p1', name: 'jackson' },
585
+ p3: { age: 21, id: 'p3', name: 'mike' },
586
+ },
587
+ },
588
+ ],
589
+ ]);
590
+ expect(personChanges).toEqual([
591
+ [
592
+ { p1: { age: 16 } },
593
+ {
594
+ p1: { age: 16, id: 'p1', name: 'jackson' },
595
+ p3: { age: 21, id: 'p3', name: 'mike' },
596
+ },
597
+ {
598
+ p1: { age: 14, id: 'p1', name: 'jackson' },
599
+ p3: { age: 21, id: 'p3', name: 'mike' },
600
+ },
601
+ ],
602
+ ]);
603
+ expect(p1Changes).toEqual([
604
+ [
605
+ { age: 16 },
606
+ { age: 16, id: 'p1', name: 'jackson' },
607
+ { age: 14, id: 'p1', name: 'jackson' },
608
+ ],
609
+ ]);
610
+ expect(p2Changes).toEqual([]);
611
+ expect(p1AgeChanges).toEqual([
612
+ [
613
+ 16,
614
+ 14,
615
+ { age: 16, id: 'p1', name: 'jackson' },
616
+ { age: 14, id: 'p1', name: 'jackson' },
617
+ ],
618
+ ]);
619
+ expect(p1NameChanges).toEqual([]);
620
+ expect(db.toJSON()).toEqual({
621
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
622
+ person: {
623
+ p1: { age: 16, id: 'p1', name: 'jackson' },
624
+ p3: { age: 21, id: 'p3', name: 'mike' },
625
+ },
626
+ });
627
+ // delete docs
628
+ dbChanges.length = 0;
629
+ personChanges.length = 0;
630
+ p1Changes.length = 0;
631
+ p2Changes.length = 0;
632
+ p1AgeChanges.length = 0;
633
+ p1NameChanges.length = 0;
634
+ db.deleteDocs('person', []);
635
+ expect(dbChanges).toEqual([]);
636
+ db.deleteDocs('person', ['p1']);
637
+ expect(dbChanges).toEqual([
638
+ [
639
+ { person: { p1: null } },
640
+ {
641
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
642
+ person: { p3: { age: 21, id: 'p3', name: 'mike' } },
643
+ },
644
+ {
645
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
646
+ person: {
647
+ p1: { age: 16, id: 'p1', name: 'jackson' },
648
+ p3: { age: 21, id: 'p3', name: 'mike' },
649
+ },
650
+ },
651
+ ],
652
+ ]);
653
+ expect(Object.keys(db.collection('person'))).toEqual(['p3']);
654
+ db.undo();
655
+ dbChanges.length = 0;
656
+ personChanges.length = 0;
657
+ p1Changes.length = 0;
658
+ p2Changes.length = 0;
659
+ p1AgeChanges.length = 0;
660
+ p1NameChanges.length = 0;
661
+ expect(Object.keys(db.collection('person'))).toEqual(['p3', 'p1']);
662
+ expect(() => db.deleteDocs('person', ['p1', 'p2'])).toThrowError();
663
+ expect(Object.keys(db.collection('person'))).toEqual(['p3', 'p1']);
664
+ db.deleteDocs('person', ['p1', 'p3']);
665
+ expect(Object.keys(db.collection('person'))).toEqual([]);
666
+ expect(dbChanges).toEqual([
667
+ [
668
+ { person: { p1: null, p3: null } },
669
+ {
670
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
671
+ person: {},
672
+ },
673
+ {
674
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
675
+ person: {
676
+ p1: { age: 16, id: 'p1', name: 'jackson' },
677
+ p3: { age: 21, id: 'p3', name: 'mike' },
678
+ },
679
+ },
680
+ ],
681
+ ]);
682
+ db.undo();
683
+ // update docs
684
+ dbChanges.length = 0;
685
+ personChanges.length = 0;
686
+ p1Changes.length = 0;
687
+ p2Changes.length = 0;
688
+ p1AgeChanges.length = 0;
689
+ p1NameChanges.length = 0;
690
+ db.updateDocs('person', { p1: { age: 17 } });
691
+ expect(db.toJSON()).toEqual({
692
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
693
+ person: {
694
+ p1: { age: 17, id: 'p1', name: 'jackson' },
695
+ p3: { age: 21, id: 'p3', name: 'mike' },
696
+ },
697
+ });
698
+ expect(dbChanges).toEqual([
699
+ [
700
+ { person: { p1: { age: 17 } } },
701
+ {
702
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
703
+ person: {
704
+ p1: { age: 17, id: 'p1', name: 'jackson' },
705
+ p3: { age: 21, id: 'p3', name: 'mike' },
706
+ },
707
+ },
708
+ {
709
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
710
+ person: {
711
+ p1: { age: 16, id: 'p1', name: 'jackson' },
712
+ p3: { age: 21, id: 'p3', name: 'mike' },
713
+ },
714
+ },
715
+ ],
716
+ ]);
717
+ dbChanges.length = 0;
718
+ personChanges.length = 0;
719
+ p1Changes.length = 0;
720
+ p2Changes.length = 0;
721
+ p1AgeChanges.length = 0;
722
+ p1NameChanges.length = 0;
723
+ expect(() => db.updateDocs('person', { p1: { age: 18 }, p2: { age: 13 } })).toThrowError();
724
+ expect(db.toJSON()).toEqual({
725
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
726
+ person: {
727
+ p1: { age: 17, id: 'p1', name: 'jackson' },
728
+ p3: { age: 21, id: 'p3', name: 'mike' },
729
+ },
730
+ });
731
+ expect(dbChanges).toEqual([]);
732
+ db.updateDocs('person', (persons) => objectMap(persons, (x) => {
733
+ return { age: x.age + 1 };
734
+ }));
735
+ expect(db.toJSON()).toEqual({
736
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
737
+ person: {
738
+ p1: { age: 18, id: 'p1', name: 'jackson' },
739
+ p3: { age: 22, id: 'p3', name: 'mike' },
740
+ },
741
+ });
742
+ expect(dbChanges).toEqual([
743
+ [
744
+ { person: { p1: { age: 18 }, p3: { age: 22 } } },
745
+ {
746
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
747
+ person: {
748
+ p1: { age: 18, id: 'p1', name: 'jackson' },
749
+ p3: { age: 22, id: 'p3', name: 'mike' },
750
+ },
751
+ },
752
+ {
753
+ city: { c1: { id: 'c1', location: [1, 2], name: 'austin' } },
754
+ person: {
755
+ p1: { age: 17, id: 'p1', name: 'jackson' },
756
+ p3: { age: 21, id: 'p3', name: 'mike' },
757
+ },
758
+ },
759
+ ],
760
+ ]);
761
+ dbChanges.length = 0;
762
+ personChanges.length = 0;
763
+ p1Changes.length = 0;
764
+ p2Changes.length = 0;
765
+ p1AgeChanges.length = 0;
766
+ p1NameChanges.length = 0;
767
+ //
768
+ // define new collection
769
+ //
770
+ expect(db.collectionNames).toEqual(['person', 'city']);
771
+ expect(db.collection('color')).toEqual(undefined);
772
+ db.defineCollection('color', {
773
+ fields: {
774
+ id: { type: 'string' },
775
+ name: { type: 'string', index: 'asc' },
776
+ code: { type: 'number', index: 'desc' },
777
+ },
778
+ });
779
+ expect(db.collectionNames).toEqual(['person', 'city', 'color']);
780
+ const newDb = db;
781
+ const colorChanges = [];
782
+ newDb.subToCol('color', (next, prev, change, fields) => {
783
+ colorChanges.push([next, prev, change, fields]);
784
+ });
785
+ expect(newDb.collection('color')).toEqual({});
786
+ expect(() => newDb.docsOrderBy('color', 'id')).toThrow();
787
+ expect(newDb.docsOrderBy('color', 'name')).toEqual([]);
788
+ expect(newDb.docsOrderBy('color', 'code')).toEqual([]);
789
+ const red = { id: 'red', name: 'Red', code: 0xff0000 };
790
+ newDb.setDoc('color', red);
791
+ expect(newDb.docsOrderBy('color', 'name')).toEqual([red]);
792
+ expect(newDb.docsOrderBy('color', 'code')).toEqual([red]);
793
+ expect(newDb.collection('color')).toEqual({ red });
794
+ expect(colorChanges).toEqual([
795
+ [
796
+ { red: { code: 16711680, id: 'red', name: 'Red' } },
797
+ {},
798
+ { red: { code: 16711680, id: 'red', name: 'Red' } },
799
+ { code: true, id: true, name: true },
800
+ ],
801
+ ]);
802
+ colorChanges.length = 0;
803
+ newDb.updateDoc('color', 'red', { code: 0xf00000 });
804
+ expect(newDb.docsOrderBy('color', 'name')).toEqual([
805
+ { ...red, code: 0xf00000 },
806
+ ]);
807
+ expect(newDb.docsOrderBy('color', 'code')).toEqual([
808
+ { ...red, code: 0xf00000 },
809
+ ]);
810
+ expect(newDb.collection('color')).toEqual({
811
+ red: { code: 0xf00000, id: 'red', name: 'Red' },
812
+ });
813
+ expect(colorChanges).toEqual([
814
+ [
815
+ { red: { code: 15728640, id: 'red', name: 'Red' } },
816
+ { red: { code: 16711680, id: 'red', name: 'Red' } },
817
+ { red: { code: 15728640 } },
818
+ { code: true },
819
+ ],
820
+ ]);
821
+ newDb.setDocs('color', [
822
+ { id: 'blue', name: 'Blue', code: 0x0000ff },
823
+ { id: 'yellow', name: 'Yellow', code: 0xffff00 },
824
+ { id: 'green', name: 'Green', code: 0x00ff00 },
825
+ ]);
826
+ expect(newDb.docsOrderBy('color', 'code').map((x) => x.code)).toEqual([
827
+ 16776960, 15728640, 65280, 255,
828
+ ]);
829
+ expect(newDb.docsOrderBy('color', 'name').map((x) => x.name)).toEqual([
830
+ 'Blue',
831
+ 'Green',
832
+ 'Red',
833
+ 'Yellow',
834
+ ]);
835
+ newDb.updateDoc('color', 'blue', { name: 'XXX', code: 0xffffff });
836
+ expect(newDb.docsOrderBy('color', 'code').map((x) => x.code)).toEqual([
837
+ 16777215, 16776960, 15728640, 65280,
838
+ ]);
839
+ expect(newDb.docsOrderBy('color', 'name').map((x) => x.name)).toEqual([
840
+ 'Green',
841
+ 'Red',
842
+ 'XXX',
843
+ 'Yellow',
844
+ ]);
845
+ newDb.deleteCollection('color');
846
+ expect(db.collectionNames).toEqual(['person', 'city']);
847
+ expect(newDb.collection('color')).toEqual(undefined);
848
+ expect(() => {
849
+ newDb.setDoc('color', { id: 'red', name: 'Red', code: 0xff0000 });
850
+ }).toThrow();
851
+ });
852
+ test('LocalDB computes', () => {
853
+ const initData = {
854
+ person: {
855
+ p1: { id: 'p1', age: 10, age2: 20, name: 'john', name_age: 'john_10' },
856
+ p2: { id: 'p2', age: 18, age2: 36, name: 'sean', name_age: 'sean_18' },
857
+ },
858
+ };
859
+ const db = new LocalDB({
860
+ person: {
861
+ fields: {
862
+ id: { type: 'string' },
863
+ age: { type: 'number' },
864
+ age2: { type: 'number' },
865
+ name: { type: 'string' },
866
+ name_age: { type: 'string' },
867
+ },
868
+ computes: [
869
+ {
870
+ deps: ['age', 'name'],
871
+ mutates: ['name_age'],
872
+ compute(doc) {
873
+ return {
874
+ name_age: `${doc.name}_${doc.age}`,
875
+ };
876
+ },
877
+ },
878
+ {
879
+ deps: ['name_age'],
880
+ mutates: ['age', 'name'],
881
+ compute(doc) {
882
+ const i = doc.name_age.lastIndexOf('_');
883
+ const name = doc.name_age.substring(0, i);
884
+ const age = parseInt(doc.name_age.substring(i + 1), 10);
885
+ return { name, age };
886
+ },
887
+ },
888
+ {
889
+ deps: ['age'],
890
+ mutates: ['age2'],
891
+ compute(doc) {
892
+ return {
893
+ age2: doc.age * 2,
894
+ };
895
+ },
896
+ },
897
+ {
898
+ deps: ['age2'],
899
+ mutates: ['age'],
900
+ compute(doc) {
901
+ return {
902
+ age: doc.age2 / 2,
903
+ };
904
+ },
905
+ },
906
+ ],
907
+ },
908
+ }, initData, { undoable: true });
909
+ expect(db.toJSON()).toEqual(initData);
910
+ db.updateDoc('person', 'p1', { age: 12 });
911
+ expect(db.doc('person', 'p1')).toEqual({
912
+ age: 12,
913
+ age2: 24,
914
+ id: 'p1',
915
+ name: 'john',
916
+ name_age: 'john_12',
917
+ });
918
+ db.updateDoc('person', 'p1', { age: 20 });
919
+ expect(db.doc('person', 'p1')).toEqual({
920
+ age: 20,
921
+ age2: 40,
922
+ id: 'p1',
923
+ name: 'john',
924
+ name_age: 'john_20',
925
+ });
926
+ db.updateDoc('person', 'p1', { name: 'jack' });
927
+ expect(db.doc('person', 'p1')).toEqual({
928
+ age: 20,
929
+ age2: 40,
930
+ id: 'p1',
931
+ name: 'jack',
932
+ name_age: 'jack_20',
933
+ });
934
+ db.updateDoc('person', 'p1', { age: 11, name: 'sean' });
935
+ expect(db.doc('person', 'p1')).toEqual({
936
+ age: 11,
937
+ age2: 22,
938
+ id: 'p1',
939
+ name: 'sean',
940
+ name_age: 'sean_11',
941
+ });
942
+ db.updateDoc('person', 'p1', { name_age: 'john_12' });
943
+ expect(db.doc('person', 'p1')).toEqual({
944
+ age: 12,
945
+ age2: 24,
946
+ id: 'p1',
947
+ name: 'john',
948
+ name_age: 'john_12',
949
+ });
950
+ db.updateDoc('person', 'p1', { age2: 34 });
951
+ expect(db.doc('person', 'p1')).toEqual({
952
+ age: 17,
953
+ age2: 34,
954
+ id: 'p1',
955
+ name: 'john',
956
+ name_age: 'john_17',
957
+ });
958
+ expect(db.collection('person')).toEqual({
959
+ p1: { age: 17, age2: 34, id: 'p1', name: 'john', name_age: 'john_17' },
960
+ p2: { age: 18, age2: 36, id: 'p2', name: 'sean', name_age: 'sean_18' },
961
+ });
962
+ db.updateDocs('person', {
963
+ p1: { name_age: 'jack_19' },
964
+ p2: { name_age: 'tom_21' },
965
+ }, { undoable: true });
966
+ expect(db.collection('person')).toEqual({
967
+ p1: { age: 19, age2: 38, id: 'p1', name: 'jack', name_age: 'jack_19' },
968
+ p2: { age: 21, age2: 42, id: 'p2', name: 'tom', name_age: 'tom_21' },
969
+ });
970
+ db.undo();
971
+ expect(db.collection('person')).toEqual({
972
+ p1: { age: 17, age2: 34, id: 'p1', name: 'john', name_age: 'john_17' },
973
+ p2: { age: 18, age2: 36, id: 'p2', name: 'sean', name_age: 'sean_18' },
974
+ });
975
+ db.redo();
976
+ expect(db.collection('person')).toEqual({
977
+ p1: { age: 19, age2: 38, id: 'p1', name: 'jack', name_age: 'jack_19' },
978
+ p2: { age: 21, age2: 42, id: 'p2', name: 'tom', name_age: 'tom_21' },
979
+ });
980
+ db.undo();
981
+ expect(db.collection('person')).toEqual({
982
+ p1: { age: 17, age2: 34, id: 'p1', name: 'john', name_age: 'john_17' },
983
+ p2: { age: 18, age2: 36, id: 'p2', name: 'sean', name_age: 'sean_18' },
984
+ });
985
+ db.redo();
986
+ expect(db.collection('person')).toEqual({
987
+ p1: { age: 19, age2: 38, id: 'p1', name: 'jack', name_age: 'jack_19' },
988
+ p2: { age: 21, age2: 42, id: 'p2', name: 'tom', name_age: 'tom_21' },
989
+ });
990
+ db.updateDocs('person', {
991
+ p1: { name_age: 'mike_23' },
992
+ p2: { name_age: 'paul_27' },
993
+ }, { undoable: true });
994
+ expect(db.collection('person')).toEqual({
995
+ p1: { age: 23, age2: 46, id: 'p1', name: 'mike', name_age: 'mike_23' },
996
+ p2: { age: 27, age2: 54, id: 'p2', name: 'paul', name_age: 'paul_27' },
997
+ });
998
+ db.undo();
999
+ expect(db.collection('person')).toEqual({
1000
+ p1: { age: 19, age2: 38, id: 'p1', name: 'jack', name_age: 'jack_19' },
1001
+ p2: { age: 21, age2: 42, id: 'p2', name: 'tom', name_age: 'tom_21' },
1002
+ });
1003
+ });
1004
+ test('LocalDB foreignComputes', () => {
1005
+ const initData = {
1006
+ person: {
1007
+ p1: { id: 'p1', name: 'jack', cars: [] },
1008
+ },
1009
+ car: {
1010
+ c1: { id: 'c1', owner: null, name: 'bmw' },
1011
+ },
1012
+ };
1013
+ const db = new LocalDB({
1014
+ person: {
1015
+ fields: {
1016
+ id: { type: 'string' },
1017
+ name: { type: 'string' },
1018
+ cars: {
1019
+ type: 'string[]',
1020
+ normalize(list) {
1021
+ return dedupKeys(list).sort((a, b) => a.localeCompare(b));
1022
+ },
1023
+ equals: arraysEqual,
1024
+ },
1025
+ },
1026
+ foreignComputes: [
1027
+ {
1028
+ mutates: ['car'],
1029
+ compute(db, updates) {
1030
+ const carUpdates = {};
1031
+ updates.map(({ next, prev }) => {
1032
+ if (next?.cars !== prev?.cars) {
1033
+ prev?.cars.forEach((carId) => {
1034
+ carUpdates[carId] = { owner: null };
1035
+ });
1036
+ next?.cars.forEach((carId) => {
1037
+ carUpdates[carId] = { owner: next.id };
1038
+ });
1039
+ }
1040
+ });
1041
+ db.updateDocs('car', carUpdates);
1042
+ },
1043
+ },
1044
+ ],
1045
+ },
1046
+ car: {
1047
+ fields: {
1048
+ id: { type: 'string' },
1049
+ name: { type: 'string', nullable: true },
1050
+ owner: { type: 'string' },
1051
+ },
1052
+ foreignComputes: [
1053
+ {
1054
+ mutates: ['person'],
1055
+ compute(db, updates) {
1056
+ updates.map(({ next, prev }) => {
1057
+ if (next?.owner !== prev?.owner) {
1058
+ db.updateDocs('person', (persons) => {
1059
+ const personUpdates = {};
1060
+ if (prev?.owner) {
1061
+ const p = persons[prev.owner];
1062
+ if (p) {
1063
+ personUpdates[p.id] = {
1064
+ cars: p.cars.filter((x) => x !== prev.id),
1065
+ };
1066
+ }
1067
+ }
1068
+ if (next?.owner) {
1069
+ const p = persons[next.owner];
1070
+ if (p) {
1071
+ personUpdates[p.id] = {
1072
+ cars: [...p.cars, next.id],
1073
+ };
1074
+ }
1075
+ }
1076
+ return personUpdates;
1077
+ });
1078
+ }
1079
+ });
1080
+ },
1081
+ },
1082
+ ],
1083
+ },
1084
+ }, initData, { undoable: true });
1085
+ expect(db.toJSON()).toEqual(initData);
1086
+ db.updateDoc('person', 'p1', { cars: ['c1', 'c1'] }, { undoable: true });
1087
+ expect(db.toJSON()).toEqual({
1088
+ car: { c1: { id: 'c1', name: 'bmw', owner: 'p1' } },
1089
+ person: { p1: { cars: ['c1'], id: 'p1', name: 'jack' } },
1090
+ });
1091
+ db.undo();
1092
+ expect(db.toJSON()).toEqual({
1093
+ car: { c1: { id: 'c1', name: 'bmw', owner: null } },
1094
+ person: { p1: { cars: [], id: 'p1', name: 'jack' } },
1095
+ });
1096
+ db.redo();
1097
+ expect(db.toJSON()).toEqual({
1098
+ car: { c1: { id: 'c1', name: 'bmw', owner: 'p1' } },
1099
+ person: { p1: { cars: ['c1'], id: 'p1', name: 'jack' } },
1100
+ });
1101
+ db.updateDoc('person', 'p1', { cars: [] }, { undoable: true });
1102
+ expect(db.toJSON()).toEqual({
1103
+ car: { c1: { id: 'c1', name: 'bmw', owner: null } },
1104
+ person: { p1: { cars: [], id: 'p1', name: 'jack' } },
1105
+ });
1106
+ db.updateDoc('person', 'p1', { cars: [] }, { undoable: true });
1107
+ expect(db.toJSON()).toEqual({
1108
+ car: { c1: { id: 'c1', name: 'bmw', owner: null } },
1109
+ person: { p1: { cars: [], id: 'p1', name: 'jack' } },
1110
+ });
1111
+ db.updateDoc('car', 'c1', { owner: 'p1' }, { undoable: true });
1112
+ expect(db.toJSON()).toEqual({
1113
+ car: { c1: { id: 'c1', name: 'bmw', owner: 'p1' } },
1114
+ person: { p1: { cars: ['c1'], id: 'p1', name: 'jack' } },
1115
+ });
1116
+ db.undo();
1117
+ expect(db.toJSON()).toEqual({
1118
+ car: { c1: { id: 'c1', name: 'bmw', owner: null } },
1119
+ person: { p1: { cars: [], id: 'p1', name: 'jack' } },
1120
+ });
1121
+ db.redo();
1122
+ expect(db.toJSON()).toEqual({
1123
+ car: { c1: { id: 'c1', name: 'bmw', owner: 'p1' } },
1124
+ person: { p1: { cars: ['c1'], id: 'p1', name: 'jack' } },
1125
+ });
1126
+ db.updateDoc('car', 'c1', { owner: null }, { undoable: true });
1127
+ expect(db.toJSON()).toEqual({
1128
+ car: { c1: { id: 'c1', name: 'bmw', owner: null } },
1129
+ person: { p1: { cars: [], id: 'p1', name: 'jack' } },
1130
+ });
1131
+ db.undo();
1132
+ expect(db.toJSON()).toEqual({
1133
+ car: { c1: { id: 'c1', name: 'bmw', owner: 'p1' } },
1134
+ person: { p1: { cars: ['c1'], id: 'p1', name: 'jack' } },
1135
+ });
1136
+ });
1137
+ });