@epam/uui-db 5.3.0-rc.0 → 5.3.0

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.
Files changed (103) hide show
  1. package/benchmark/results/filter.json +39 -0
  2. package/benchmark/results/pkLookup.json +39 -0
  3. package/benchmark/results/pkUpdate-10.json +51 -0
  4. package/benchmark/results/pkUpdate-100.json +51 -0
  5. package/benchmark/results/pkUpdate-1000.json +51 -0
  6. package/benchmark/results/pkUpdate-10000.json +51 -0
  7. package/benchmark/results/pkUpdate-100000.json +51 -0
  8. package/benchmark/results/pkUpdate-1000000.json +51 -0
  9. package/benchmark/results/query-10.json +63 -0
  10. package/benchmark/results/query-100.json +63 -0
  11. package/benchmark/results/query-1000.json +63 -0
  12. package/benchmark/results/query-10000.json +63 -0
  13. package/benchmark/results/query-100000.json +63 -0
  14. package/benchmark/results/sorting.json +33 -0
  15. package/build/package.json +69 -0
  16. package/build/readme.md +17 -0
  17. package/{stats.html → build/stats.html} +1 -1
  18. package/globals.d.ts +9 -0
  19. package/index.tsx +1 -0
  20. package/package.json +4 -4
  21. package/src/Db.ts +89 -0
  22. package/src/DbRef.ts +254 -0
  23. package/src/DbTable.ts +264 -0
  24. package/src/IxSet.ts +251 -0
  25. package/src/ListLoadingTracker.ts +131 -0
  26. package/src/Loader.ts +96 -0
  27. package/src/Scheduler.ts +72 -0
  28. package/src/SimpleLoadingTracker.ts +17 -0
  29. package/src/__benchmarks__/filter.ts +50 -0
  30. package/src/__benchmarks__/index.ts +5 -0
  31. package/src/__benchmarks__/pkLookup.ts +33 -0
  32. package/src/__benchmarks__/pkUpdate.ts +75 -0
  33. package/src/__benchmarks__/query.ts +101 -0
  34. package/src/__benchmarks__/sorting.ts +33 -0
  35. package/src/__benchmarks__/testData.ts +15 -0
  36. package/src/__tests__/ListLoadingTracker.test.ts +83 -0
  37. package/src/__tests__/Scheduler.test.ts +24 -0
  38. package/src/__tests__/TaskDb.ts +187 -0
  39. package/src/__tests__/db.test.ts +293 -0
  40. package/src/__tests__/dbRef.test.ts +100 -0
  41. package/src/__tests__/dbTable.test.ts +15 -0
  42. package/src/__tests__/dbViews.test.ts +76 -0
  43. package/src/__tests__/ixSet.perf.test.ts +129 -0
  44. package/src/__tests__/ixSet.test.ts +73 -0
  45. package/src/__tests__/listLoader.test.ts +85 -0
  46. package/src/__tests__/loader.test.ts +92 -0
  47. package/src/__tests__/patchHelpers.test.ts +202 -0
  48. package/src/__tests__/tempIds.test.ts +98 -0
  49. package/src/index.ts +8 -0
  50. package/src/patchHelpers.ts +254 -0
  51. package/src/reactHooks.ts +28 -0
  52. package/src/tempIds.tsx +236 -0
  53. package/src/types.ts +126 -0
  54. package/src/utils.ts +34 -0
  55. package/tsconfig.json +12 -0
  56. /package/{index.d.ts → build/index.d.ts} +0 -0
  57. /package/{index.d.ts.map → build/index.d.ts.map} +0 -0
  58. /package/{index.esm.js → build/index.esm.js} +0 -0
  59. /package/{index.esm.js.map → build/index.esm.js.map} +0 -0
  60. /package/{index.js → build/index.js} +0 -0
  61. /package/{index.js.map → build/index.js.map} +0 -0
  62. /package/{src → build/src}/Db.d.ts +0 -0
  63. /package/{src → build/src}/Db.d.ts.map +0 -0
  64. /package/{src → build/src}/DbRef.d.ts +0 -0
  65. /package/{src → build/src}/DbRef.d.ts.map +0 -0
  66. /package/{src → build/src}/DbTable.d.ts +0 -0
  67. /package/{src → build/src}/DbTable.d.ts.map +0 -0
  68. /package/{src → build/src}/IxSet.d.ts +0 -0
  69. /package/{src → build/src}/IxSet.d.ts.map +0 -0
  70. /package/{src → build/src}/ListLoadingTracker.d.ts +0 -0
  71. /package/{src → build/src}/ListLoadingTracker.d.ts.map +0 -0
  72. /package/{src → build/src}/Loader.d.ts +0 -0
  73. /package/{src → build/src}/Loader.d.ts.map +0 -0
  74. /package/{src → build/src}/Scheduler.d.ts +0 -0
  75. /package/{src → build/src}/Scheduler.d.ts.map +0 -0
  76. /package/{src → build/src}/SimpleLoadingTracker.d.ts +0 -0
  77. /package/{src → build/src}/SimpleLoadingTracker.d.ts.map +0 -0
  78. /package/{src → build/src}/__benchmarks__/filter.d.ts +0 -0
  79. /package/{src → build/src}/__benchmarks__/filter.d.ts.map +0 -0
  80. /package/{src → build/src}/__benchmarks__/index.d.ts +0 -0
  81. /package/{src → build/src}/__benchmarks__/index.d.ts.map +0 -0
  82. /package/{src → build/src}/__benchmarks__/pkLookup.d.ts +0 -0
  83. /package/{src → build/src}/__benchmarks__/pkLookup.d.ts.map +0 -0
  84. /package/{src → build/src}/__benchmarks__/pkUpdate.d.ts +0 -0
  85. /package/{src → build/src}/__benchmarks__/pkUpdate.d.ts.map +0 -0
  86. /package/{src → build/src}/__benchmarks__/query.d.ts +0 -0
  87. /package/{src → build/src}/__benchmarks__/query.d.ts.map +0 -0
  88. /package/{src → build/src}/__benchmarks__/sorting.d.ts +0 -0
  89. /package/{src → build/src}/__benchmarks__/sorting.d.ts.map +0 -0
  90. /package/{src → build/src}/__benchmarks__/testData.d.ts +0 -0
  91. /package/{src → build/src}/__benchmarks__/testData.d.ts.map +0 -0
  92. /package/{src → build/src}/index.d.ts +0 -0
  93. /package/{src → build/src}/index.d.ts.map +0 -0
  94. /package/{src → build/src}/patchHelpers.d.ts +0 -0
  95. /package/{src → build/src}/patchHelpers.d.ts.map +0 -0
  96. /package/{src → build/src}/reactHooks.d.ts +0 -0
  97. /package/{src → build/src}/reactHooks.d.ts.map +0 -0
  98. /package/{src → build/src}/tempIds.d.ts +0 -0
  99. /package/{src → build/src}/tempIds.d.ts.map +0 -0
  100. /package/{src → build/src}/types.d.ts +0 -0
  101. /package/{src → build/src}/types.d.ts.map +0 -0
  102. /package/{src → build/src}/utils.d.ts +0 -0
  103. /package/{src → build/src}/utils.d.ts.map +0 -0
@@ -0,0 +1,129 @@
1
+ import { IxSet } from '../IxSet';
2
+ import sortBy from 'lodash.sortby';
3
+ import range from 'lodash.range';
4
+
5
+ interface Person {
6
+ id: number;
7
+ name: string;
8
+ departmentId: number;
9
+ location?: string;
10
+ }
11
+
12
+ const alice = {
13
+ id: 1, name: 'Alice', departmentId: 1, location: 'UK',
14
+ } as Person;
15
+ const bob = {
16
+ id: 2, name: 'Bob', departmentId: 1, location: 'UK',
17
+ } as Person;
18
+ const sandra = {
19
+ id: 3, name: 'Sandra', departmentId: 1, location: 'UK',
20
+ } as Person;
21
+ const edward = {
22
+ id: 4, name: 'Edward', departmentId: 1, location: 'UK',
23
+ } as Person;
24
+ const pete = {
25
+ id: 5, name: 'Pete', departmentId: 1, location: 'US',
26
+ } as Person;
27
+ const jack = {
28
+ id: 6, name: 'Jack', departmentId: 2, location: 'UK',
29
+ } as Person;
30
+ const william = {
31
+ id: 7, name: 'William', departmentId: 2, location: 'US',
32
+ } as Person;
33
+
34
+ const john = {
35
+ id: 8, name: 'John', departmentId: 1, location: 'US',
36
+ } as Person;
37
+
38
+ const blankSet = new IxSet<Person, number>((i) => i.id, [{ fields: ['name'] }, { fields: ['departmentId', 'name'] }]);
39
+
40
+ const smallSet = blankSet.with([
41
+ alice, bob, sandra, edward, pete, jack, william,
42
+ ]);
43
+
44
+ describe.skip('db - IxSet Performance', () => {
45
+ let hugeSet = smallSet;
46
+ let test100Persons: Person[];
47
+ let test1KPersons: Person[];
48
+ let test10KPersons: Person[];
49
+ let test100KPersons: Person[];
50
+ let completeTestSetInArray: Person[];
51
+
52
+ // let test1MPersons: Person[];
53
+
54
+ // Generating and loading data is split into separate tests to see timing in JEST logs
55
+ it('Can generate huge data set', () => {
56
+ test100Persons = range(10, 110).map((id) => ({ id, name: 'Just Person', departmentId: 2 }));
57
+ test1KPersons = range(110, 1110).map((id) => ({ id, name: `Person ${id}`, departmentId: 3 }));
58
+ test10KPersons = range(1110, 11110).map((id) => ({ id, name: `Person ${id}`, departmentId: 3 }));
59
+ test100KPersons = range(111110, 111110).map((id) => ({ id, name: `Person ${id}`, departmentId: 3 }));
60
+ // test1MPersons = range(200000, 1200000).map((id) => ({ id, name: 'Person ' + id, departmentId: 4 }));
61
+
62
+ completeTestSetInArray = [
63
+ ...smallSet.query({}), ...test100Persons, ...test1KPersons, ...test10KPersons, ...test100KPersons,
64
+ ];
65
+ });
66
+
67
+ it('Can load 100 Persons', () => {
68
+ hugeSet = hugeSet.with(test100Persons);
69
+ });
70
+ it('Can load 1K Persons', () => {
71
+ hugeSet = hugeSet.with(test1KPersons);
72
+ });
73
+ it('Can load 10K Persons', () => {
74
+ hugeSet = hugeSet.with(test10KPersons);
75
+ });
76
+ it('Can load 100K Persons', () => {
77
+ hugeSet = hugeSet.with(test100KPersons);
78
+ });
79
+ // it("Can load 1M Persons", () => { hugeSet = hugeSet.with(test1MPersons); });
80
+
81
+ [
82
+ 200, 1000, 100000, 111109,
83
+ ].forEach((id) => {
84
+ it(`Can find Person ${id} by name`, () => {
85
+ const result = hugeSet.queryOne({ filter: { name: `Person ${id}` } });
86
+ expect(result.id).toEqual(id);
87
+ });
88
+ });
89
+
90
+ it("Can find all 'Just Person' people", () => {
91
+ const result = hugeSet.query({ filter: { name: 'Just Person' } });
92
+ expect(result.length).toEqual(100);
93
+ expect(result[54].name).toEqual('Just Person');
94
+ });
95
+
96
+ it('Can find by DepartmentId sorted by name', () => {
97
+ expect(
98
+ hugeSet.query({
99
+ filter: { departmentId: 1 },
100
+ sorting: [{ field: 'name' }],
101
+ }),
102
+ ).toEqual([
103
+ alice, bob, edward, pete, sandra,
104
+ ]);
105
+ });
106
+
107
+ it('Can find by DepartmentId sorted by name - with basic array', () => {
108
+ let result = completeTestSetInArray.filter((p) => p.departmentId === 1);
109
+ result = sortBy(result, 'name');
110
+ expect(result).toEqual([
111
+ alice, bob, edward, pete, sandra,
112
+ ]);
113
+ });
114
+
115
+ it('Can find by location and sort by name', () => {
116
+ expect(
117
+ hugeSet.query({
118
+ filter: { location: 'US' },
119
+ sorting: [{ field: 'name' }],
120
+ }),
121
+ ).toEqual([pete, william]);
122
+ });
123
+
124
+ it('Find by location and sort by name - with basic array', () => {
125
+ let result = completeTestSetInArray.filter((p) => p.location == 'US');
126
+ result = sortBy(result, 'name');
127
+ expect(result).toEqual([pete, william]);
128
+ });
129
+ });
@@ -0,0 +1,73 @@
1
+ import { IxSet } from '../IxSet';
2
+
3
+ interface Person {
4
+ id: number;
5
+ name: string;
6
+ departmentId: number;
7
+ location?: string;
8
+ }
9
+
10
+ const alice = {
11
+ id: 1, name: 'Alice', departmentId: 1, location: 'UK',
12
+ } as Person;
13
+ const bob = {
14
+ id: 2, name: 'Bob', departmentId: 1, location: 'UK',
15
+ } as Person;
16
+ const sandra = {
17
+ id: 3, name: 'Sandra', departmentId: 1, location: 'UK',
18
+ } as Person;
19
+ const edward = {
20
+ id: 4, name: 'Edward', departmentId: 1, location: 'UK',
21
+ } as Person;
22
+ const pete = {
23
+ id: 5, name: 'Pete', departmentId: 1, location: 'US',
24
+ } as Person;
25
+ const jack = {
26
+ id: 6, name: 'Jack', departmentId: 2, location: 'UK',
27
+ } as Person;
28
+ const william = {
29
+ id: 7, name: 'William', departmentId: 2, location: 'US',
30
+ } as Person;
31
+
32
+ const john = {
33
+ id: 8, name: 'John', departmentId: 1, location: 'US',
34
+ } as Person;
35
+
36
+ const blankSet = new IxSet<Person, number>((i) => i.id, [{ fields: ['name'] }, { fields: ['departmentId', 'name'] }]);
37
+
38
+ const smallSet = blankSet.with([
39
+ alice, bob, sandra, edward, pete, jack, william,
40
+ ]);
41
+
42
+ describe('db - IxSet', () => {
43
+ describe('Small data set', () => {
44
+ it('Can put some values', () => {
45
+ const set = blankSet.with([bob, alice]);
46
+ expect(set.byId(1).name).toBe(alice.name);
47
+ });
48
+
49
+ it('Can get byId', () => {
50
+ expect(smallSet.byId(2).name).toBe(bob.name);
51
+ });
52
+
53
+ it('Can find by department ID', () => {
54
+ const result = smallSet.query({ filter: { departmentId: 1 } });
55
+ expect(result.length).toEqual(5);
56
+ });
57
+
58
+ it('Can find Alice by name', () => {
59
+ expect(smallSet.query({ filter: { name: alice.name } })).toEqual([alice]);
60
+ });
61
+
62
+ it('Can add John and find him', () => {
63
+ const updated = smallSet.with([john]);
64
+ const result = updated.query({ filter: { name: john.name } });
65
+ expect(result.length).toEqual(1);
66
+ expect(result[0]).toEqual(john);
67
+ });
68
+
69
+ it('Can find by Name and DepartmentId', () => {
70
+ expect(smallSet.query({ filter: { name: bob.name, departmentId: bob.departmentId } })).toEqual([bob]);
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,85 @@
1
+ import {
2
+ emptyDb, TaskDb, Task, TaskDbTables,
3
+ } from './TaskDb';
4
+ import { DbRef } from '../DbRef';
5
+ import { DbSaveResponse, DbPatch } from '../types';
6
+ import { DataQuery, runDataQuery } from '@epam/uui-core';
7
+ import range from 'lodash.range';
8
+
9
+ const delay = (t: number) =>
10
+ new Promise((resolve) => {
11
+ setTimeout(resolve, t || 1);
12
+ });
13
+
14
+ describe('db - list loaders', () => {
15
+ const testItems = range(200).map((id) => {
16
+ return {
17
+ id,
18
+ name: 'Task',
19
+ isDone: false,
20
+ createdBy: 'JS',
21
+ isDraft: false,
22
+ assignedTo: 'DT',
23
+ } as Task;
24
+ });
25
+
26
+ class TaskDbRef extends DbRef<TaskDbTables, TaskDb> {
27
+ constructor() {
28
+ super(emptyDb);
29
+ }
30
+
31
+ public saveCallback = jest.fn(() => Promise.resolve({ submit: {} }));
32
+ public apiCallback = jest.fn((rq: DataQuery<Task> & { t?: number }) => delay(rq?.t).then(() => runDataQuery(testItems, rq)));
33
+ protected savePatch(patch: DbPatch<TaskDbTables>): Promise<DbSaveResponse<TaskDbTables>> {
34
+ return Promise.resolve({ submit: {} });
35
+ }
36
+
37
+ public tasksLoader = this.makeListLoader({
38
+ api: this.apiCallback,
39
+ convertToPatch: (r) => ({ tasks: r.items }),
40
+ });
41
+ }
42
+
43
+ it('Should correctly process happy-path', async () => {
44
+ const dbRef = new TaskDbRef();
45
+ const r1 = dbRef.tasksLoader.load({ range: { from: 0, count: 10 } });
46
+ expect(r1.isComplete).toBe(false);
47
+ expect(r1.isLoading).toBe(true);
48
+ expect(dbRef.db.tasks.toArray()).toEqual([]);
49
+ await r1.promise;
50
+ expect(dbRef.apiCallback).toBeCalledTimes(1);
51
+ expect(r1.isLoading).toBe(false);
52
+ expect(r1.isComplete).toBe(true);
53
+ expect(dbRef.db.tasks.count()).toEqual(10);
54
+ r1.reload();
55
+ expect(r1.isComplete).toBe(true);
56
+ expect(r1.isLoading).toBe(true);
57
+ await r1.promise;
58
+ expect(dbRef.apiCallback).toBeCalledTimes(2);
59
+ expect(r1.isLoading).toBe(false);
60
+ expect(r1.isComplete).toBe(true);
61
+ });
62
+
63
+ it('Should correctly process overlapping ranges', async () => {
64
+ const dbRef = new TaskDbRef();
65
+ const r1 = dbRef.tasksLoader.load({ range: { from: 0, count: 100 } });
66
+ const r2 = dbRef.tasksLoader.load({ range: { from: 95, count: 20 } });
67
+ expect(r1.isComplete).toBe(false);
68
+ expect(r1.isLoading).toBe(true);
69
+ expect(r1.missing).toEqual({ range: { from: 0, count: 100 } });
70
+ expect(r2.isComplete).toBe(false);
71
+ expect(r2.isLoading).toBe(true);
72
+ expect(r2.missing).toEqual({ range: { from: 100, count: 15 } });
73
+ expect(dbRef.db.tasks.toArray()).toEqual([]);
74
+ await r1.promise;
75
+ await r2.promise;
76
+ expect(dbRef.apiCallback).toBeCalledTimes(2);
77
+ expect(dbRef.apiCallback).nthCalledWith(1, { range: { from: 0, count: 100 } });
78
+ expect(dbRef.apiCallback).nthCalledWith(2, { range: { from: 100, count: 15 } });
79
+ expect(r1.isComplete).toBe(true);
80
+ expect(r1.isLoading).toBe(false);
81
+ expect(r2.isComplete).toBe(true);
82
+ expect(r2.isLoading).toBe(false);
83
+ expect(dbRef.db.tasks.count()).toEqual(115);
84
+ });
85
+ });
@@ -0,0 +1,92 @@
1
+ import { emptyDb, TaskDb, TaskDbTables, sampleData } from './TaskDb';
2
+ import { DbRef } from '../DbRef';
3
+ import { DbSaveResponse, DbPatch } from '../types';
4
+
5
+ const delay = (t: number) =>
6
+ new Promise((resolve) => {
7
+ setTimeout(resolve, t || 1);
8
+ });
9
+
10
+ describe('db - loaders', () => {
11
+ class TaskDbRef extends DbRef<TaskDbTables, TaskDb> {
12
+ constructor() {
13
+ super(emptyDb);
14
+ }
15
+
16
+ public saveCallback = jest.fn(() => Promise.resolve({ submit: {} }));
17
+ public apiCallback = jest.fn((rq: { ids: string[]; t?: number }) =>
18
+ delay(rq?.t).then(() => ({ items: sampleData.users.filter((u) => !rq?.ids || rq.ids.includes(u.id)) })));
19
+
20
+ protected savePatch(): Promise<DbSaveResponse<TaskDbTables>> {
21
+ return Promise.resolve({ submit: {} });
22
+ }
23
+
24
+ public tasksLoader = this.makeLoader({
25
+ api: this.apiCallback,
26
+ convertToPatch: (r) => ({ users: r.items }),
27
+ });
28
+ }
29
+
30
+ it('Should correctly process happy-path', async () => {
31
+ const dbRef = new TaskDbRef();
32
+ const r1 = dbRef.tasksLoader.load({ ids: ['JS'] });
33
+ expect(r1.isComplete).toBe(false);
34
+ expect(r1.isLoading).toBe(true);
35
+ expect(dbRef.db.users.toArray()).toEqual([]);
36
+ await r1.promise;
37
+ expect(dbRef.apiCallback).toBeCalledTimes(1);
38
+ expect(r1.isLoading).toBe(false);
39
+ expect(r1.isComplete).toBe(true);
40
+ expect(dbRef.db.users.count()).toEqual(1);
41
+ r1.reload();
42
+ expect(r1.isComplete).toBe(true);
43
+ expect(r1.isLoading).toBe(true);
44
+ await r1.promise;
45
+ expect(dbRef.apiCallback).toBeCalledTimes(2);
46
+ expect(r1.isLoading).toBe(false);
47
+ expect(r1.isComplete).toBe(true);
48
+ });
49
+
50
+ it('Same requests should not result same parallel requests', async () => {
51
+ const dbRef = new TaskDbRef();
52
+ let r1 = dbRef.tasksLoader.load({ ids: ['JS'] });
53
+ let r2 = dbRef.tasksLoader.load({ ids: ['JS'] });
54
+ expect(r1.isComplete).toBe(false);
55
+ expect(r1.isLoading).toBe(true);
56
+ expect(r2.isComplete).toBe(false);
57
+ expect(r2.isLoading).toBe(true);
58
+ await r1.promise;
59
+ await r2.promise;
60
+ expect(dbRef.apiCallback).toBeCalledTimes(1);
61
+ r1 = dbRef.tasksLoader.load({ ids: ['JS'] });
62
+ r2 = dbRef.tasksLoader.load({ ids: ['JS'] });
63
+ expect(r1.isComplete).toBe(true);
64
+ expect(r1.isLoading).toBe(false);
65
+ expect(r2.isComplete).toBe(true);
66
+ expect(r2.isLoading).toBe(false);
67
+ });
68
+
69
+ it('Different requests should be processed separately', async () => {
70
+ const dbRef = new TaskDbRef();
71
+ const r1 = dbRef.tasksLoader.load({ ids: ['JS'] });
72
+ const r2 = dbRef.tasksLoader.load({ ids: ['DT'], t: 2 });
73
+ expect(r1).not.toBe(r2);
74
+ expect(r1.isComplete).toBe(false);
75
+ expect(r1.isLoading).toBe(true);
76
+ expect(r2.isComplete).toBe(false);
77
+ expect(r2.isLoading).toBe(true);
78
+ await r1.promise;
79
+ expect(r1.isComplete).toBe(true);
80
+ expect(r1.isLoading).toBe(false);
81
+ expect(r2.isComplete).toBe(false);
82
+ expect(r2.isLoading).toBe(true);
83
+ expect(dbRef.db.users.count()).toEqual(1);
84
+ await r2.promise;
85
+ expect(dbRef.apiCallback).toBeCalledTimes(2);
86
+ expect(r1.isComplete).toBe(true);
87
+ expect(r1.isLoading).toBe(false);
88
+ expect(r2.isComplete).toBe(true);
89
+ expect(r2.isLoading).toBe(false);
90
+ expect(dbRef.db.users.count()).toEqual(2);
91
+ });
92
+ });
@@ -0,0 +1,202 @@
1
+ import {
2
+ getId, isClientOnly, unionPatches, mergeEntityPatches, flattenResponse, isNew, hasServerFields, getParentEntities,
3
+ } from '../patchHelpers';
4
+ import {
5
+ emptyDb, sampleDb, Task, User, Manager, View, TaskDbTables,
6
+ } from './TaskDb';
7
+ import { DbPatch } from '../types';
8
+ import orderBy from 'lodash.orderby';
9
+ import { IClientIdsMap } from '..';
10
+
11
+ const task: Partial<Task> = {
12
+ id: 100500,
13
+ };
14
+
15
+ const user: User = {
16
+ id: 'AB',
17
+ name: 'Cruella de Vil',
18
+ title: 'Villain',
19
+ sex: 'f',
20
+ orderStr: 'h',
21
+ };
22
+
23
+ const manager: Partial<Manager> = {
24
+ managerId: 'AB',
25
+ subordinateId: 'CD',
26
+ };
27
+
28
+ const view: View = {
29
+ id: 10,
30
+ title: 'list',
31
+ };
32
+
33
+ const clientOnlyView: View = {
34
+ id: -10,
35
+ title: 'list',
36
+ };
37
+
38
+ describe('db - patchHelpers', () => {
39
+ const newClientIdsMap: IClientIdsMap = {
40
+ clientToServer: (id: number): any => undefined,
41
+ serverToClient: (tableName: string, id: any): any => undefined,
42
+ clientToServerDataFilter: null,
43
+ clientToServerRequest: null,
44
+ };
45
+
46
+ const existingClientIdsMap: IClientIdsMap = {
47
+ clientToServer: (id: number): any => id,
48
+ serverToClient: (tableName: string, id: any): any => id,
49
+ clientToServerDataFilter: null,
50
+ clientToServerRequest: null,
51
+ };
52
+
53
+ describe('getId', () => {
54
+ it('should return numeric id', () => {
55
+ expect(getId(task, emptyDb.tables.tasks.schema)).toEqual(task.id);
56
+ });
57
+
58
+ it('should return string id', () => {
59
+ expect(getId(user, emptyDb.tables.users.schema)).toEqual(user.id);
60
+ });
61
+
62
+ it('should return compound id', () => {
63
+ expect(getId(manager, emptyDb.tables.managers.schema)).toEqual([manager.subordinateId, manager.managerId]);
64
+ });
65
+ });
66
+
67
+ describe('isClientOnly', () => {
68
+ it('should return false if isClientOnly is not provided', () => {
69
+ expect(isClientOnly(user, emptyDb.tables.users.schema)).toEqual(false);
70
+ });
71
+
72
+ it('should return true if isClientOnly == true', () => {
73
+ expect(isClientOnly(view, emptyDb.tables.views.schema)).toEqual(true);
74
+ });
75
+
76
+ it('should return result of isClientOnly function', () => {
77
+ expect(isClientOnly(view, emptyDb.tables.viewsFn.schema)).toEqual(false);
78
+ expect(isClientOnly(clientOnlyView, emptyDb.tables.viewsFn.schema)).toEqual(true);
79
+ });
80
+ });
81
+
82
+ describe('unionPatches', () => {
83
+ it('should union patches', () => {
84
+ const patches: DbPatch<TaskDbTables>[] = [
85
+ {
86
+ users: [{ id: 'AB' }],
87
+ }, {
88
+ users: [{ id: 'CD' }],
89
+ },
90
+ ];
91
+
92
+ const result = unionPatches(patches);
93
+ expect(result).toEqual({ users: [{ id: 'AB' }, { id: 'CD' }] });
94
+ });
95
+ });
96
+
97
+ describe('mergeEntityPatches', () => {
98
+ it('should merge entities with the same ids', () => {
99
+ const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
100
+
101
+ const patch: DbPatch<TaskDbTables> = {
102
+ users: [{ id: 'YZ', name: 'Yakov Zhmurov' }, { id: 'YZ', sex: 'm' }],
103
+ };
104
+
105
+ const result = mergeEntityPatches(emptyDb.tables, patch);
106
+ expect(result).toEqual({ users: [{ id: 'YZ', name: 'Yakov Zhmurov', sex: 'm' }] });
107
+ expect(spy).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('should log console warning if properties of entities intersect', () => {
111
+ const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
112
+
113
+ const patch: DbPatch<TaskDbTables> = {
114
+ users: [{ id: 'YZ', name: 'Yakov Zhmurov' }, { id: 'YZ', name: 'Ilya Kuznetsov' }],
115
+ };
116
+
117
+ mergeEntityPatches(emptyDb.tables, patch);
118
+ expect(spy).toHaveBeenCalled();
119
+ });
120
+ });
121
+
122
+ describe('flattenResponse', () => {
123
+ it('should flatten provided object', () => {
124
+ const response: any = {
125
+ users: [
126
+ {
127
+ id: 'YZ',
128
+ name: 'Yakov Zhmurov',
129
+ __typename: 'User',
130
+ manager: { id: 'AU', name: 'Andrei Urban', __typename: 'User' },
131
+ subordinates: [{ id: 'PS', name: 'Pavel Shchur', __typename: 'User' }, { id: 'IK', name: 'Ilya Kuznetsov', __typename: 'User' }],
132
+ },
133
+ ],
134
+ };
135
+
136
+ const users = orderBy(flattenResponse(response, emptyDb.tables).users, 'id');
137
+ expect(users).toEqual([
138
+ { id: 'AU', name: 'Andrei Urban', __typename: 'User' }, { id: 'IK', name: 'Ilya Kuznetsov', __typename: 'User' }, { id: 'PS', name: 'Pavel Shchur', __typename: 'User' }, { id: 'YZ', name: 'Yakov Zhmurov', __typename: 'User' },
139
+ ]);
140
+ });
141
+
142
+ it('should process non-entity arrays and objects as fields', () => {
143
+ const processTask: any = {
144
+ id: 'YZ',
145
+ tags: ['important', 'db'],
146
+ // details: { test: 'test' }, // FIXME
147
+ __typename: 'Task',
148
+ };
149
+
150
+ const result = flattenResponse(processTask, emptyDb.tables);
151
+ expect(result.tasks).toEqual([processTask]);
152
+ });
153
+ });
154
+
155
+ describe('isNew', () => {
156
+ it('should return false if no server-client mapping exists', () => {
157
+ expect(isNew(user, emptyDb.tables.users.schema, newClientIdsMap)).toEqual(true);
158
+ });
159
+
160
+ it('should return true if server-client mapping exists', () => {
161
+ expect(isNew(user, emptyDb.tables.users.schema, existingClientIdsMap)).toEqual(false);
162
+ });
163
+ });
164
+
165
+ describe('hasServerFields', () => {
166
+ it('should return false if entity is client only', () => {
167
+ expect(hasServerFields(view, emptyDb.tables.views.schema, newClientIdsMap, emptyDb.tables)).toEqual(false);
168
+ expect(hasServerFields(view, emptyDb.tables.views.schema, existingClientIdsMap, emptyDb.tables)).toEqual(false);
169
+ expect(hasServerFields(clientOnlyView, emptyDb.tables.viewsFn.schema, newClientIdsMap, emptyDb.tables)).toEqual(false);
170
+ expect(hasServerFields(clientOnlyView, emptyDb.tables.viewsFn.schema, existingClientIdsMap, emptyDb.tables)).toEqual(false);
171
+ });
172
+
173
+ it('should return true if entity has id only and it is a new entity', () => {
174
+ expect(hasServerFields({ id: 100 }, emptyDb.tables.tasks.schema, newClientIdsMap, emptyDb.tables)).toEqual(true);
175
+ });
176
+
177
+ it('should return false if entity has id only and it is an existing entity', () => {
178
+ expect(hasServerFields({ id: 100 }, emptyDb.tables.tasks.schema, existingClientIdsMap, emptyDb.tables)).toEqual(false);
179
+ });
180
+
181
+ it('should return false if entity has readonly fields only', () => {
182
+ expect(hasServerFields({ id: 100, createdBy: 'Ilya Kuznetsov' }, emptyDb.tables.tasks.schema, existingClientIdsMap, emptyDb.tables)).toEqual(false);
183
+ });
184
+
185
+ it('should return false if entity has client-only fields only', () => {
186
+ expect(hasServerFields({ id: 100, isDraft: false }, emptyDb.tables.tasks.schema, existingClientIdsMap, emptyDb.tables)).toEqual(false);
187
+ });
188
+ });
189
+
190
+ describe('getParentEntities', () => {
191
+ it('should return parent aggregate entities only', () => {
192
+ const parents = getParentEntities(
193
+ {
194
+ tasks: [{ id: 1 }],
195
+ },
196
+ sampleDb.tables,
197
+ );
198
+
199
+ expect(parents.users).toEqual([{ id: 'DT' }]);
200
+ });
201
+ });
202
+ });
@@ -0,0 +1,98 @@
1
+ import { emptyDb } from './TaskDb';
2
+ import { TempIdMap, getTempId, isTempId } from '../tempIds';
3
+
4
+ describe('tempIds', () => {
5
+ beforeEach(() => {
6
+ // testTask.mockReset();
7
+ });
8
+
9
+ it('generates ids', () => {
10
+ const id1 = getTempId();
11
+ const id2 = getTempId();
12
+ expect(id1).not.toBe(id2);
13
+ });
14
+
15
+ it('isTempId works', () => {
16
+ const id = getTempId();
17
+ expect(isTempId(id)).toBe(true);
18
+ expect(isTempId(0)).toBe(false);
19
+ expect(isTempId(1)).toBe(false);
20
+ expect(isTempId(null)).toBe(false);
21
+ });
22
+
23
+ it('serverToClientPatch - should re-map PK', () => {
24
+ const ids = new TempIdMap(emptyDb);
25
+
26
+ const patch = ids.serverToClientPatch({
27
+ tasks: [{ id: 1, name: 'Test', isDone: true }],
28
+ });
29
+
30
+ expect(patch.tasks).toEqual([
31
+ {
32
+ id: ids.serverToClient('tasks', 1),
33
+ name: 'Test',
34
+ isDone: true,
35
+ },
36
+ ]);
37
+ });
38
+
39
+ it('serverToClientPatch - should re-map FK', () => {
40
+ const ids = new TempIdMap(emptyDb);
41
+
42
+ const patch = ids.serverToClientPatch({
43
+ tasks: [
44
+ {
45
+ id: 1, name: 'Test', assignedTo: 'J', isDone: false,
46
+ },
47
+ ],
48
+ users: [{ id: 'J', name: 'John' }],
49
+ });
50
+
51
+ expect(patch.tasks).toEqual([
52
+ {
53
+ id: ids.serverToClient('tasks', 1),
54
+ assignedTo: ids.serverToClient('users', 'J'),
55
+ name: 'Test',
56
+ isDone: false,
57
+ },
58
+ ]);
59
+ });
60
+
61
+ it('serverToClientPatch - handles defaults', () => {
62
+ const ids = new TempIdMap(emptyDb);
63
+
64
+ const patch = ids.serverToClientPatch({
65
+ tasks: [{ id: 1, name: 'Test' }],
66
+ });
67
+
68
+ expect(patch.tasks).toEqual([
69
+ {
70
+ id: ids.serverToClient('tasks', 1),
71
+ name: 'Test',
72
+ isDone: false,
73
+ },
74
+ ]);
75
+ });
76
+
77
+ it('clientToServerPatch - should re-map PKs and FKs back, and apply toServer conversions', () => {
78
+ const ids = new TempIdMap(emptyDb);
79
+
80
+ const initialPatch = {
81
+ tasks: [
82
+ {
83
+ id: 1, estimate: '1' as any, assignedTo: 'JS', isDone: true,
84
+ }, { id: 2, estimate: '2' as any, isDone: false },
85
+ ],
86
+ users: [{ id: 'JS', name: 'John Snow' }, { id: 'DT', name: 'Daenerys Targaryen' }],
87
+ managers: [{ subordinateId: 'JS', managerId: 'DT' }],
88
+ };
89
+
90
+ const clientPatch = ids.serverToClientPatch(initialPatch);
91
+
92
+ expect(clientPatch.managers[0].managerId).toBe(ids.serverToClient('managers', 'DT'));
93
+
94
+ const serverPatch = ids.clientToServerPatch(clientPatch);
95
+
96
+ expect(serverPatch).toEqual(initialPatch);
97
+ });
98
+ });
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './types';
2
+ export * from './Db';
3
+ export * from './DbTable';
4
+ export * from './DbRef';
5
+ export * from './tempIds';
6
+ export * from './utils';
7
+ export * from './patchHelpers';
8
+ export * from './reactHooks';