@epam/uui-db 5.3.0-rc.1 → 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 +5 -5
  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,187 @@
1
+ import { DbRef } from '../DbRef';
2
+ import {
3
+ Db, DbTable, DbRelationType, DbView,
4
+ } from '../index';
5
+ import { DbPatch, DbSaveResponse } from '../types';
6
+
7
+ export interface Task {
8
+ id: number;
9
+ name: string;
10
+ isDone: boolean;
11
+ createdBy: string;
12
+ assignedTo: string;
13
+ isDraft: boolean;
14
+ tags?: string[];
15
+ details?: any;
16
+ estimate?: number;
17
+ isDeleted?: boolean;
18
+ }
19
+
20
+ export interface User {
21
+ id: string;
22
+ name: string;
23
+ title: string;
24
+ sex: 'm' | 'f';
25
+ orderStr: string;
26
+ }
27
+
28
+ export interface Manager {
29
+ subordinateId: string;
30
+ managerId: string;
31
+ role: string;
32
+ }
33
+
34
+ export interface View {
35
+ id: number;
36
+ title: string;
37
+ }
38
+
39
+ const tasks = new DbTable<Task, number, any>({
40
+ tableName: 'tasks',
41
+ typeName: 'Task',
42
+ primaryKey: 'id',
43
+ fields: {
44
+ id: { isGenerated: true },
45
+ createdBy: {
46
+ isReadOnly: true,
47
+ fk: {
48
+ tableName: 'users',
49
+ relationType: DbRelationType.Aggregation,
50
+ },
51
+ },
52
+ assignedTo: {
53
+ fk: {
54
+ tableName: 'users',
55
+ relationType: DbRelationType.Association,
56
+ },
57
+ },
58
+ estimate: {
59
+ toClient: (value: any) => value && parseInt(value),
60
+ toServer: (value: number) => value && ((value + '') as any),
61
+ },
62
+ isDone: { default: false },
63
+ isDraft: { isClientOnly: true },
64
+ },
65
+ indexes: ['createdBy', 'assignedTo'],
66
+ deleteFlag: 'isDeleted',
67
+ });
68
+
69
+ const users = new DbTable<User, string, any>({
70
+ tableName: 'users',
71
+ typeName: 'User',
72
+ primaryKey: 'id',
73
+ fields: {
74
+ id: { isGenerated: true },
75
+ },
76
+ indexes: ['title'],
77
+ });
78
+
79
+ const managers = new DbTable<Manager, [string, string], any>({
80
+ tableName: 'managers',
81
+ typeName: 'Manager',
82
+ primaryKey: ['subordinateId', 'managerId'],
83
+ fields: {
84
+ subordinateId: { isGenerated: true },
85
+ managerId: { isGenerated: true },
86
+ },
87
+ indexes: ['subordinateId', 'managerId'],
88
+ });
89
+
90
+ const views = new DbTable<View, number, any>({
91
+ tableName: 'views',
92
+ typeName: 'View',
93
+ primaryKey: 'id',
94
+ fields: {
95
+ id: { isGenerated: true },
96
+ },
97
+ isClientOnly: true,
98
+ });
99
+
100
+ const viewsFn = new DbTable<View, number, any>({
101
+ tableName: 'viewsFn',
102
+ typeName: 'ViewFn',
103
+ primaryKey: 'id',
104
+ fields: {
105
+ id: { isGenerated: true },
106
+ },
107
+ isClientOnly: (view) => view.id < 0,
108
+ });
109
+
110
+ const taskDbTables = {
111
+ tasks, users, managers, views, viewsFn,
112
+ };
113
+ export type TaskDbTables = typeof taskDbTables;
114
+
115
+ export class TaskDb extends Db<TaskDbTables> {
116
+ public get tasks() {
117
+ return this.tables.tasks;
118
+ }
119
+
120
+ public get users() {
121
+ return this.tables.users;
122
+ }
123
+
124
+ public get managers() {
125
+ return this.tables.managers;
126
+ }
127
+
128
+ public get views() {
129
+ return this.tables.views;
130
+ }
131
+
132
+ public get viewsFn() {
133
+ return this.tables.viewsFn;
134
+ }
135
+ }
136
+
137
+ export const emptyDb = new TaskDb(taskDbTables);
138
+
139
+ export const sampleUsers = {
140
+ daenerys: {
141
+ id: 'DT', name: 'Daenerys Targaryen', sex: 'f', orderStr: 'b',
142
+ } as User,
143
+ john: {
144
+ id: 'JS', name: 'John Snow', sex: 'm', orderStr: 'a',
145
+ } as User,
146
+ arya: {
147
+ id: 'AS', name: 'Arya Stark', sex: 'f', orderStr: 'c',
148
+ } as User,
149
+ };
150
+
151
+ export const sampleData = {
152
+ tasks: [
153
+ {
154
+ id: 1, name: 'Implement DB', isDone: true, createdBy: 'DT', assignedTo: 'JS',
155
+ }, {
156
+ id: 2, name: 'Write Some Tests', isDone: true, createdBy: 'DT', assignedTo: null,
157
+ }, {
158
+ id: 3, name: 'Basic Indexes', isDone: true, createdBy: 'DT', assignedTo: 'JS',
159
+ }, {
160
+ id: 4, name: 'Views', isDone: false, createdBy: 'DT', assignedTo: 'JS',
161
+ }, {
162
+ id: 5, name: 'IO Scheduling', isDone: false, createdBy: 'DT', assignedTo: 'AS',
163
+ }, {
164
+ id: 6, name: 'DataSources', isDone: false, createdBy: 'JS', assignedTo: 'JS',
165
+ }, {
166
+ id: 7, name: 'Loaders', isDone: false, createdBy: 'AS', assignedTo: 'AS',
167
+ },
168
+ ] as Task[],
169
+ users: [
170
+ sampleUsers.daenerys, sampleUsers.john, sampleUsers.arya,
171
+ ] as User[],
172
+ };
173
+
174
+ export const sampleDb = emptyDb.with(sampleData);
175
+
176
+ export type TaskDbView<TResult, TParams, TDependencies = void> = DbView<TaskDb, TResult, TParams, TDependencies>;
177
+
178
+ export class TasksDbRef extends DbRef<TaskDbTables, TaskDb> {
179
+ constructor(private savePatchHandler: (patch: DbPatch<TaskDbTables>) => Promise<DbSaveResponse<TaskDbTables>>) {
180
+ super(emptyDb);
181
+ this.throttleSaveMs = 1;
182
+ }
183
+
184
+ override savePatch(patch: DbPatch<TaskDbTables>) {
185
+ return this.savePatchHandler?.(patch);
186
+ }
187
+ }
@@ -0,0 +1,293 @@
1
+ import { emptyDb, sampleDb } from './TaskDb';
2
+
3
+ describe('db - DB queries and updates', () => {
4
+ describe('Queries', () => {
5
+ it('user by id with query/one', () => expect(sampleDb.users.find({ id: 'AS' }).one().name).toEqual('Arya Stark'));
6
+ it("user by id with 'byId'", () => expect(sampleDb.users.byId('AS').name).toEqual('Arya Stark'));
7
+ it("users by multiple id with 'in'", () =>
8
+ expect(
9
+ sampleDb.users
10
+ .find({ id: { in: ['AS', 'JS'] } })
11
+ .orderBy('id')
12
+ .toArray()
13
+ .map((u) => u.id),
14
+ ).toEqual(['AS', 'JS']));
15
+ it('user by name', () => expect(sampleDb.users.find({ name: 'Arya Stark' }).one().id).toEqual('AS'));
16
+ it('user by [null] name', () => expect(sampleDb.users.find({ name: null }).one()).toBeNull());
17
+ it('sort by name', () =>
18
+ expect(
19
+ sampleDb.users
20
+ .orderBy('name')
21
+ .toArray()
22
+ .map((e) => e.name),
23
+ ).toEqual([
24
+ 'Arya Stark', 'Daenerys Targaryen', 'John Snow',
25
+ ]));
26
+ it('sort by name and sex', () =>
27
+ expect(
28
+ sampleDb.users
29
+ .orderBy('sex', 'desc')
30
+ .thenBy('name')
31
+ .map((e) => e.name),
32
+ ).toEqual([
33
+ 'John Snow', 'Arya Stark', 'Daenerys Targaryen',
34
+ ]));
35
+ it('sort by name and sex via order', () =>
36
+ expect(sampleDb.users.order([{ field: 'sex' }, { field: 'name', direction: 'desc' }]).map((e) => e.name)).toEqual([
37
+ 'Daenerys Targaryen', 'Arya Stark', 'John Snow',
38
+ ]));
39
+ it("find tasks by multiple values with 'in'", () =>
40
+ expect(
41
+ sampleDb.tasks
42
+ .find({ createdBy: { in: ['AS', 'JS'] } })
43
+ .orderBy('id')
44
+ .map((t) => t.id),
45
+ ).toEqual([6, 7]));
46
+
47
+ it('find tasks by both indexed and non-indexed fields', () =>
48
+ expect(
49
+ sampleDb.tasks
50
+ .find({ createdBy: { in: ['DT'] }, assignedTo: 'JS', isDone: false })
51
+ .orderBy('id')
52
+ .map((t) => t.id),
53
+ ).toEqual([4]));
54
+
55
+ it('non-indexable filter criteria should work', () =>
56
+ expect(
57
+ sampleDb.tasks
58
+ .find({ id: { gte: 4 }, assignedTo: 'AS', isDone: false })
59
+ .orderBy('id')
60
+ .map((t) => t.id),
61
+ ).toEqual([5, 7]));
62
+
63
+ it('non-indexable and indexable criteria for the same field', () =>
64
+ expect(sampleDb.tasks.find({ createdBy: { in: ['AS', 'JS'], gt: 'B' }, isDone: false }).map((t) => t.id)).toEqual([6]));
65
+
66
+ it('count should work with filter', () => expect(sampleDb.tasks.find({ assignedTo: 'AS' }).count()).toEqual(2));
67
+
68
+ it('range should work with filter', () =>
69
+ expect(
70
+ sampleDb.tasks
71
+ .find({ createdBy: 'DT', assignedTo: { isNull: false } })
72
+ .orderBy('assignedTo')
73
+ .thenBy('id')
74
+ .range(1, 3)
75
+ .map((i) => i.id),
76
+ ).toEqual([
77
+ 1, 3, 4,
78
+ ]));
79
+ });
80
+
81
+ describe('Indexes and updates', () => {
82
+ it('query after insert', () => {
83
+ const db = sampleDb.with({
84
+ tasks: [
85
+ {
86
+ id: 100, name: 'Test', assignedTo: 'QQ', createdBy: 'WW', isDone: false, isDraft: false,
87
+ },
88
+ ],
89
+ });
90
+ expect(db.tasks.find({ assignedTo: 'QQ', createdBy: 'WW' }).one().id).toBe(100);
91
+ });
92
+
93
+ it('query after update', () => {
94
+ const db = sampleDb.with({
95
+ tasks: [
96
+ {
97
+ id: 1, name: 'Test', createdBy: 'QQ', isDone: false, isDraft: false,
98
+ },
99
+ ],
100
+ });
101
+ expect(db.tasks.find({ createdBy: 'QQ' }).one().id).toBe(1);
102
+ expect(
103
+ db.tasks
104
+ .find({ createdBy: 'DT' })
105
+ .orderBy('id')
106
+ .map((u) => u.id),
107
+ ).toEqual([
108
+ 2, 3, 4, 5,
109
+ ]);
110
+ });
111
+
112
+ it('query after delete', () => {
113
+ const db = sampleDb.with({ tasks: [{ id: 1, isDeleted: true }] });
114
+ expect(db.tasks.byId(1)).toBeFalsy();
115
+ expect(db.tasks.find({ createdBy: 'AS' }).map((i) => i.id)).not.toContain(1);
116
+ });
117
+
118
+ it('user name update', () => {
119
+ const newDb = sampleDb.with({ users: [{ id: 'AS', name: 'Ivan Ivanov' }] });
120
+ expect(newDb.users.byId('AS').name).toEqual('Ivan Ivanov');
121
+ // DB is immutable, so shallow copy should be created at all levels.
122
+ expect(newDb.users).not.toBe(sampleDb.users);
123
+ expect(newDb.tables.users).not.toBe(sampleDb.tables.users);
124
+ expect(newDb.users.byId('AS')).not.toBe(sampleDb.users.byId('AS'));
125
+ expect(sampleDb.users.byId('AS').name).toEqual('Arya Stark');
126
+ });
127
+
128
+ it('Should return updated record if index is not touched', () => {
129
+ const db = sampleDb.with({
130
+ tasks: [
131
+ {
132
+ id: 7, name: 'Test', isDone: true, isDraft: false,
133
+ },
134
+ ],
135
+ });
136
+ expect(db.tasks.find({ createdBy: 'AS' }).one().id).toBe(7);
137
+ expect(db.tasks.find({ createdBy: 'AS' }).one().name).toBe('Test');
138
+ });
139
+
140
+ it('should handle null and undefined values', () => {
141
+ const testDb = emptyDb.with({
142
+ tasks: [{ id: 1, assignedTo: null, isDone: null }, { id: 2 }],
143
+ });
144
+
145
+ expect(
146
+ testDb.tasks
147
+ .find({ isDone: null })
148
+ .toArray()
149
+ .map((i) => i.id),
150
+ ).toEqual([1]);
151
+ expect(
152
+ testDb.tasks
153
+ .find({ isDone: undefined })
154
+ .toArray()
155
+ .map((i) => i.id),
156
+ ).toEqual([2]);
157
+ expect(
158
+ testDb.tasks
159
+ .find({ isDone: { isNull: true } })
160
+ .toArray()
161
+ .map((i) => i.id),
162
+ ).toEqual([1, 2]);
163
+
164
+ expect(
165
+ testDb.tasks
166
+ .find({ assignedTo: null })
167
+ .toArray()
168
+ .map((i) => i.id),
169
+ ).toEqual([1]);
170
+ expect(
171
+ testDb.tasks
172
+ .find({ assignedTo: undefined })
173
+ .toArray()
174
+ .map((i) => i.id),
175
+ ).toEqual([2]);
176
+ expect(
177
+ testDb.tasks
178
+ .find({ assignedTo: { isNull: true } })
179
+ .toArray()
180
+ .map((i) => i.id),
181
+ ).toEqual([1, 2]);
182
+ });
183
+
184
+ it('Should correctly handle changes from undefined to non-undefined and reverse (non-indexed)', () => {
185
+ const db1 = sampleDb.with({ tasks: [{ id: 100 }] }); // initial - undefined
186
+ expect(db1.tasks.find({ name: { isNull: true } }).one().id).toBe(100);
187
+ expect(db1.tasks.find({ name: undefined }).one().id).toBe(100);
188
+ expect(db1.tasks.find({ name: { in: [undefined] } }).one().id).toBe(100);
189
+ const db2 = db1.with({ tasks: [{ id: 100, name: 'Test' }] }); // set value
190
+ expect(db2.tasks.find({ name: 'Test' }).one().id).toBe(100);
191
+ const db3 = db2.with({ tasks: [{ id: 100, isDone: true }] }); // keep value
192
+ expect(db2.tasks.find({ name: 'Test' }).one().id).toBe(100);
193
+ const db4 = db3.with({ tasks: [{ id: 100, name: undefined }] }); // set to undefined
194
+ expect(db4.tasks.find({ name: { isNull: true } }).one().id).toBe(100);
195
+ expect(db4.tasks.find({ name: undefined }).one().id).toBe(100);
196
+ expect(db4.tasks.find({ name: { in: [undefined] } }).one().id).toBe(100);
197
+ });
198
+
199
+ it('Should correctly handle changes from null to non-null and reverse (non-indexed)', () => {
200
+ const db1 = sampleDb.with({ tasks: [{ id: 100, name: null }] }); // initial - null
201
+ expect(db1.tasks.find({ name: { isNull: true } }).one().id).toBe(100);
202
+ expect(db1.tasks.find({ name: null }).one().id).toBe(100);
203
+ expect(db1.tasks.find({ name: { in: [null] } }).one().id).toBe(100);
204
+ const db2 = db1.with({ tasks: [{ id: 100, name: 'Test' }] }); // set value
205
+ expect(db2.tasks.find({ name: 'Test' }).one().id).toBe(100);
206
+ const db3 = db2.with({ tasks: [{ id: 100, isDone: true }] }); // keep value
207
+ expect(db2.tasks.find({ name: 'Test' }).one().id).toBe(100);
208
+ const db4 = db3.with({ tasks: [{ id: 100, name: null }] }); // set to null
209
+ expect(db4.tasks.find({ name: { isNull: true } }).one().id).toBe(100);
210
+ expect(db4.tasks.find({ name: null }).one().id).toBe(100);
211
+ expect(db4.tasks.find({ name: { in: [null] } }).one().id).toBe(100);
212
+ });
213
+
214
+ it('Should correctly handle changes from null to non-null and reverse (indexed)', () => {
215
+ const db1 = sampleDb.with({ tasks: [{ id: 100, name: 'Test', createdBy: null }] }); // initial - null
216
+ expect(db1.tasks.find({ createdBy: { isNull: true } }).one().id).toBe(100);
217
+ expect(db1.tasks.find({ createdBy: null }).one().id).toBe(100);
218
+ expect(db1.tasks.find({ createdBy: { in: [null] } }).one().id).toBe(100);
219
+ const db2 = db1.with({ tasks: [{ id: 100, createdBy: 'QQ' }] }); // set value
220
+ expect(db2.tasks.find({ createdBy: 'QQ' }).one().id).toBe(100);
221
+ const db3 = db2.with({ tasks: [{ id: 100, isDone: true }] }); // keep value
222
+ expect(db2.tasks.find({ createdBy: 'QQ' }).one().id).toBe(100);
223
+ const db4 = db3.with({ tasks: [{ id: 100, createdBy: null }] }); // set to null
224
+ expect(db4.tasks.find({ createdBy: { isNull: true } }).one().id).toBe(100);
225
+ expect(db4.tasks.find({ createdBy: null }).one().id).toBe(100);
226
+ expect(db4.tasks.find({ createdBy: { in: [null] } }).one().id).toBe(100);
227
+ });
228
+
229
+ it('Should correctly handle changes from undefined to non-undefined and reverse (indexed)', () => {
230
+ const db1 = sampleDb.with({ tasks: [{ id: 100, name: 'Test' }] }); // initial - undefined
231
+ expect(db1.tasks.find({ createdBy: { isNull: true } }).one().id).toBe(100);
232
+ expect(db1.tasks.find({ createdBy: undefined }).one().id).toBe(100);
233
+ expect(db1.tasks.find({ createdBy: { in: [undefined] } }).one().id).toBe(100);
234
+ const db2 = db1.with({ tasks: [{ id: 100, createdBy: 'QQ' }] }); // set value
235
+ expect(db2.tasks.find({ createdBy: 'QQ' }).one().id).toBe(100);
236
+ const db3 = db2.with({ tasks: [{ id: 100, isDone: true }] }); // keep value
237
+ expect(db2.tasks.find({ createdBy: 'QQ' }).one().id).toBe(100);
238
+ const db4 = db3.with({ tasks: [{ id: 100, createdBy: undefined }] }); // set to undefined
239
+ expect(db4.tasks.find({ createdBy: { isNull: true } }).one().id).toBe(100);
240
+ expect(db4.tasks.find({ createdBy: undefined }).one().id).toBe(100);
241
+ expect(db4.tasks.find({ createdBy: { in: [undefined] } }).one().id).toBe(100);
242
+ });
243
+
244
+ it("Should work with update which doesn't touch indexed field", () => {
245
+ const db = sampleDb.with({
246
+ tasks: [
247
+ // existing, index field exists, should update index
248
+ {
249
+ id: 6, name: 'Test', isDone: true, isDraft: false, createdBy: 'XX',
250
+ },
251
+ // existing, index field omitted, should skip index update
252
+ {
253
+ id: 7, name: 'Test', isDone: true, isDraft: false,
254
+ },
255
+ // new, index field omitted, should be indexed as undefined
256
+ {
257
+ id: 123, name: 'Test', isDone: true, isDraft: false,
258
+ },
259
+ ],
260
+ });
261
+ expect(db.tasks.find({ createdBy: 'XX' }).one().id).toBe(6);
262
+ expect(db.tasks.find({ createdBy: 'AS' }).one().id).toBe(7);
263
+ expect(db.tasks.find({ createdBy: undefined }).one().id).toBe(123);
264
+ const db2 = sampleDb.with({ tasks: [{ id: 123, createdBy: 'ZZ' }] });
265
+ expect(db2.tasks.find({ createdBy: 'ZZ' }).one().id).toBe(123);
266
+ });
267
+ });
268
+
269
+ describe('Composite key updates', () => {
270
+ it('Can merge new managers correctly', () => {
271
+ expect(sampleDb.managers.count()).toEqual(0);
272
+ const role1 = { subordinateId: 'JS', managerId: 'DT', role: 'RM' };
273
+ const role2 = { subordinateId: 'AS', managerId: 'DT', role: 'PM' };
274
+
275
+ // Add one role
276
+ const db1 = sampleDb.with({ managers: [role1] });
277
+ expect(db1.managers.one()).toEqual(role1);
278
+ expect(db1.managers).not.toBe(sampleDb.managers);
279
+
280
+ // Add second role
281
+ const db2 = db1.with({ managers: [role2] });
282
+ expect(db2.managers.find({ managerId: 'DT' }).count()).toEqual(2);
283
+ expect(db2.managers.byId(['JS', 'DT'])).toEqual(role1);
284
+ expect(db2.managers.find({ subordinateId: 'JS', managerId: 'DT' }).one()).toEqual(role1);
285
+ expect(db2.managers.find({ subordinateId: 'AS' }).one()).toEqual(role2);
286
+
287
+ // Update first role
288
+ const role1New = { ...role1, role: 'DM' };
289
+ const db3 = db2.with({ managers: [role1New] });
290
+ expect(db3.managers.find({ subordinateId: 'JS', managerId: 'DT' }).one()).toEqual(role1New);
291
+ });
292
+ });
293
+ });
@@ -0,0 +1,100 @@
1
+ import {
2
+ sampleDb, sampleData, TasksDbRef,
3
+ } from './TaskDb';
4
+ import { DbRef } from '../index';
5
+ import { delay } from '@epam/uui-test-utils';
6
+
7
+ describe('db - DbRef query and updates', () => {
8
+ it('Get user by id', () => {
9
+ const db = new DbRef(sampleDb);
10
+ db.commitFetch(sampleData);
11
+ expect(db.db.users.byId('DT').name).toEqual('Daenerys Targaryen');
12
+ });
13
+
14
+ it('Basic update', () => {
15
+ const db = new DbRef(sampleDb);
16
+ db.commitFetch(sampleData);
17
+ db.commit({ users: [{ id: 'DT', name: 'Daenerys Snow' }] });
18
+ expect(db.db.users.byId('DT').name).toEqual('Daenerys Snow');
19
+ });
20
+
21
+ it('Revert changes', () => {
22
+ const db = new DbRef(sampleDb);
23
+ db.commitFetch(sampleData);
24
+ expect(db.db.users.byId('DT').name).toEqual('Daenerys Targaryen');
25
+ db.commit({ users: [{ id: 'DT', name: 'Daenerys Snow' }] });
26
+ expect(db.db.users.byId('DT').name).toEqual('Daenerys Snow');
27
+ db.revert();
28
+ expect(db.db.users.byId('DT').name).toEqual('Daenerys Targaryen');
29
+ });
30
+
31
+ it('Auto-saves changes', async () => {
32
+ const handleSave = jest.fn(async () => ({ submit: {} }));
33
+ const db = new TasksDbRef(handleSave);
34
+ db.commitFetch(sampleData);
35
+ const patch = { users: [{ id: 'DT', name: 'Daenerys Snow' }] };
36
+ db.commit(patch);
37
+ await delay(2);
38
+ expect(handleSave).toHaveBeenCalledTimes(1);
39
+ expect(handleSave).toHaveBeenCalledWith(patch);
40
+ });
41
+
42
+ it('Saves are throttled in auto-save mode', async () => {
43
+ const handleSave = jest.fn(async () => ({ submit: {} }));
44
+ const db = new TasksDbRef(handleSave);
45
+ db.commitFetch(sampleData);
46
+ const patch1 = { users: [{ id: 'DT', name: 'Daenerys Snow' }] };
47
+ db.commit(patch1);
48
+ const patch2 = { users: [{ id: 'DT', name: 'Daenerys' }] };
49
+ db.commit(patch2);
50
+ expect(handleSave).toHaveBeenCalledTimes(0);
51
+ await delay(2);
52
+ expect(handleSave).toHaveBeenCalledTimes(1);
53
+ expect(handleSave).toHaveBeenCalledWith(patch2);
54
+
55
+ // 2nd change
56
+ handleSave.mockClear();
57
+ db.commit(patch1);
58
+ expect(handleSave).toHaveBeenCalledTimes(0);
59
+ await delay(2);
60
+ expect(handleSave).toHaveBeenCalledTimes(1);
61
+ expect(handleSave).toHaveBeenCalledWith(patch1);
62
+ });
63
+
64
+ it('Manually saves changes', async () => {
65
+ const handleSave = jest.fn(async () => ({ submit: {} }));
66
+ const db = new TasksDbRef(handleSave);
67
+ db.setAutoSave(false);
68
+ db.commitFetch(sampleData);
69
+ const patch = { users: [{ id: 'DT', name: 'Daenerys Snow' }] };
70
+ db.commit(patch);
71
+ await delay(2);
72
+ expect(handleSave).not.toHaveBeenCalled();
73
+ await db.save();
74
+ expect(handleSave).toHaveBeenCalledWith(patch);
75
+ });
76
+
77
+ it('Pass-thru save errors', async () => {
78
+ const handleSave = jest.fn(async () => {
79
+ throw new Error();
80
+ });
81
+ const db = new TasksDbRef(handleSave);
82
+ db.setAutoSave(false);
83
+ const patch = { users: [{ id: 'DT', name: 'Daenerys Snow' }] };
84
+ db.commit(patch);
85
+ await expect(db.save()).rejects.toBeInstanceOf(Error);
86
+ });
87
+
88
+ describe('Basic lenses', () => {
89
+ it('update with entity lens', () => {
90
+ const db = new DbRef(sampleDb);
91
+ db.commitFetch(sampleData);
92
+ // const lens = db.entityLens('users', { id: 'DT' });
93
+ // expect(lens.get().name).eq("Daenerys Targaryen");
94
+ // lens.update({ name: "Daenerys Snow" });
95
+ // expect(lens.get().name).eq("Daenerys Snow");
96
+ // lens.prop('name').onValueChange("Ivan");
97
+ // expect(lens.get().name).eq("Ivan");
98
+ });
99
+ });
100
+ });
@@ -0,0 +1,15 @@
1
+ import { emptyDb, sampleDb } from './TaskDb';
2
+
3
+ describe('db - DbTable', () => {
4
+ describe('Basic updates', () => {
5
+ it('user name update', () => {
6
+ const users = sampleDb.tables.users;
7
+ const newUsers = users.with([{ id: 'AS', name: 'Ivan Ivanov' }]);
8
+ expect(newUsers.byId('AS').name).toEqual('Ivan Ivanov');
9
+ expect(users.byId('AS').name).toEqual('Arya Stark');
10
+ // DbTable is immutable, so shallow copy should be created at all levels.
11
+ expect(newUsers).not.toBe(users);
12
+ expect(newUsers.byId('AS')).not.toBe(users.byId('AS'));
13
+ });
14
+ });
15
+ });
@@ -0,0 +1,76 @@
1
+ import {
2
+ emptyDb, sampleDb, TaskDbView, TaskDb, Task,
3
+ } from './TaskDb';
4
+
5
+ describe('db - Views', () => {
6
+ it('Should work with whole db as a view', () => {
7
+ const view: TaskDbView<TaskDb, void> = {
8
+ compute: (db) => db,
9
+ };
10
+
11
+ const baseResult = sampleDb.runView(view);
12
+ const updatedResult = sampleDb.with({ tasks: [{ id: 123, name: '123' }] }).runView(view);
13
+ const baseResult2 = sampleDb.runView(view);
14
+
15
+ expect(baseResult).toBe(baseResult2);
16
+ expect(baseResult).not.toBe(updatedResult);
17
+ expect(updatedResult.tables.tasks.byId(123).name).toBe('123');
18
+ });
19
+
20
+ it('Should correctly cache result', () => {
21
+ const view: TaskDbView<Task, number> = {
22
+ compute: jest.fn((db, id) => db.tasks.byId(id)),
23
+ };
24
+
25
+ sampleDb.runView(view, 1);
26
+
27
+ const baseResult = sampleDb.runView(view, 1);
28
+
29
+ const sampleDb2 = sampleDb.with({ tasks: [{ id: 1, name: 'updated' }] });
30
+
31
+ const result1 = sampleDb2.runView(view, 1);
32
+ const result2 = sampleDb2.runView(view, 1);
33
+ expect(baseResult).toBe(sampleDb.tasks.byId(1));
34
+ expect(result1).toBe(sampleDb2.tasks.byId(1));
35
+ expect(result1).not.toBe(baseResult);
36
+ expect(result1).toBe(result2);
37
+
38
+ expect(view.compute).toBeCalledTimes(2);
39
+ });
40
+
41
+ it('Should correctly reuse results when forked', () => {
42
+ const view: TaskDbView<Task, number> = {
43
+ compute: jest.fn((db, id) => db.tasks.byId(id)),
44
+ };
45
+
46
+ const db = emptyDb.with({ tasks: [{ id: 1, name: 'base' }] });
47
+ const baseResult = db.runView(view, 1);
48
+
49
+ expect(baseResult).toEqual({ id: 1, name: 'base' });
50
+
51
+ // Make three db versions:
52
+ // first updating a different entity
53
+ const sampleDb1 = db.with({ tasks: [{ id: 2, name: 'e2 - updated' }] });
54
+ // second - updating the same entity with the same value
55
+ const sampleDb2 = db.with({ tasks: [{ id: 1, name: 'base' }] });
56
+ // third - updating our entity with new value
57
+ const sampleDb3 = db.with({ tasks: [{ id: 1, name: 'updated' }] });
58
+
59
+ // In first db, the view should keep exactly same (===) existing value in the first fork
60
+ const result1 = sampleDb1.runView(view, 1);
61
+ expect(result1).toBe(baseResult);
62
+ expect(result1).toEqual({ id: 1, name: 'base' });
63
+
64
+ // In the second db, view should still return the same (===) object.
65
+ // It should re-compute the view, but find results the same as previous with deepEQ compare, and re-use previous results
66
+ const result2 = sampleDb2.runView(view, 1);
67
+ expect(result2).toBe(result1);
68
+ expect(result2).toEqual({ id: 1, name: 'base' });
69
+
70
+ // In the third db, view should return a new value
71
+ const result3 = sampleDb3.runView(view, 1);
72
+ expect(result3).toEqual({ id: 1, name: 'updated' });
73
+
74
+ expect(view.compute).toBeCalledTimes(4);
75
+ });
76
+ });