@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.
- package/benchmark/results/filter.json +39 -0
- package/benchmark/results/pkLookup.json +39 -0
- package/benchmark/results/pkUpdate-10.json +51 -0
- package/benchmark/results/pkUpdate-100.json +51 -0
- package/benchmark/results/pkUpdate-1000.json +51 -0
- package/benchmark/results/pkUpdate-10000.json +51 -0
- package/benchmark/results/pkUpdate-100000.json +51 -0
- package/benchmark/results/pkUpdate-1000000.json +51 -0
- package/benchmark/results/query-10.json +63 -0
- package/benchmark/results/query-100.json +63 -0
- package/benchmark/results/query-1000.json +63 -0
- package/benchmark/results/query-10000.json +63 -0
- package/benchmark/results/query-100000.json +63 -0
- package/benchmark/results/sorting.json +33 -0
- package/build/package.json +69 -0
- package/build/readme.md +17 -0
- package/{stats.html → build/stats.html} +1 -1
- package/globals.d.ts +9 -0
- package/index.tsx +1 -0
- package/package.json +5 -5
- package/src/Db.ts +89 -0
- package/src/DbRef.ts +254 -0
- package/src/DbTable.ts +264 -0
- package/src/IxSet.ts +251 -0
- package/src/ListLoadingTracker.ts +131 -0
- package/src/Loader.ts +96 -0
- package/src/Scheduler.ts +72 -0
- package/src/SimpleLoadingTracker.ts +17 -0
- package/src/__benchmarks__/filter.ts +50 -0
- package/src/__benchmarks__/index.ts +5 -0
- package/src/__benchmarks__/pkLookup.ts +33 -0
- package/src/__benchmarks__/pkUpdate.ts +75 -0
- package/src/__benchmarks__/query.ts +101 -0
- package/src/__benchmarks__/sorting.ts +33 -0
- package/src/__benchmarks__/testData.ts +15 -0
- package/src/__tests__/ListLoadingTracker.test.ts +83 -0
- package/src/__tests__/Scheduler.test.ts +24 -0
- package/src/__tests__/TaskDb.ts +187 -0
- package/src/__tests__/db.test.ts +293 -0
- package/src/__tests__/dbRef.test.ts +100 -0
- package/src/__tests__/dbTable.test.ts +15 -0
- package/src/__tests__/dbViews.test.ts +76 -0
- package/src/__tests__/ixSet.perf.test.ts +129 -0
- package/src/__tests__/ixSet.test.ts +73 -0
- package/src/__tests__/listLoader.test.ts +85 -0
- package/src/__tests__/loader.test.ts +92 -0
- package/src/__tests__/patchHelpers.test.ts +202 -0
- package/src/__tests__/tempIds.test.ts +98 -0
- package/src/index.ts +8 -0
- package/src/patchHelpers.ts +254 -0
- package/src/reactHooks.ts +28 -0
- package/src/tempIds.tsx +236 -0
- package/src/types.ts +126 -0
- package/src/utils.ts +34 -0
- package/tsconfig.json +12 -0
- /package/{index.d.ts → build/index.d.ts} +0 -0
- /package/{index.d.ts.map → build/index.d.ts.map} +0 -0
- /package/{index.esm.js → build/index.esm.js} +0 -0
- /package/{index.esm.js.map → build/index.esm.js.map} +0 -0
- /package/{index.js → build/index.js} +0 -0
- /package/{index.js.map → build/index.js.map} +0 -0
- /package/{src → build/src}/Db.d.ts +0 -0
- /package/{src → build/src}/Db.d.ts.map +0 -0
- /package/{src → build/src}/DbRef.d.ts +0 -0
- /package/{src → build/src}/DbRef.d.ts.map +0 -0
- /package/{src → build/src}/DbTable.d.ts +0 -0
- /package/{src → build/src}/DbTable.d.ts.map +0 -0
- /package/{src → build/src}/IxSet.d.ts +0 -0
- /package/{src → build/src}/IxSet.d.ts.map +0 -0
- /package/{src → build/src}/ListLoadingTracker.d.ts +0 -0
- /package/{src → build/src}/ListLoadingTracker.d.ts.map +0 -0
- /package/{src → build/src}/Loader.d.ts +0 -0
- /package/{src → build/src}/Loader.d.ts.map +0 -0
- /package/{src → build/src}/Scheduler.d.ts +0 -0
- /package/{src → build/src}/Scheduler.d.ts.map +0 -0
- /package/{src → build/src}/SimpleLoadingTracker.d.ts +0 -0
- /package/{src → build/src}/SimpleLoadingTracker.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/filter.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/filter.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/index.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/index.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/pkLookup.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/pkLookup.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/pkUpdate.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/pkUpdate.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/query.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/query.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/sorting.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/sorting.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/testData.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/testData.d.ts.map +0 -0
- /package/{src → build/src}/index.d.ts +0 -0
- /package/{src → build/src}/index.d.ts.map +0 -0
- /package/{src → build/src}/patchHelpers.d.ts +0 -0
- /package/{src → build/src}/patchHelpers.d.ts.map +0 -0
- /package/{src → build/src}/reactHooks.d.ts +0 -0
- /package/{src → build/src}/reactHooks.d.ts.map +0 -0
- /package/{src → build/src}/tempIds.d.ts +0 -0
- /package/{src → build/src}/tempIds.d.ts.map +0 -0
- /package/{src → build/src}/types.d.ts +0 -0
- /package/{src → build/src}/types.d.ts.map +0 -0
- /package/{src → build/src}/utils.d.ts +0 -0
- /package/{src → build/src}/utils.d.ts.map +0 -0
package/src/DbTable.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DbTablePatch, DbQuery, DbEntitySchema, DbPkFieldType, DbTablesSet,
|
|
3
|
+
} from './types';
|
|
4
|
+
import * as I from 'immutable';
|
|
5
|
+
import {
|
|
6
|
+
SortDirection, getFilterPredicate, getOrderComparer, DataQueryFilter, SortingOption, getSearchFilter, DataQuery,
|
|
7
|
+
} from '@epam/uui-core';
|
|
8
|
+
import { Seq } from 'immutable';
|
|
9
|
+
|
|
10
|
+
interface DbIndex<TEntity> {
|
|
11
|
+
field: keyof TEntity;
|
|
12
|
+
map: I.Map<any, I.Set<any>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DbTableState<TEntity, TTables extends DbTablesSet<TTables>> {
|
|
16
|
+
pk: I.Map<any, TEntity>;
|
|
17
|
+
indexes: DbIndex<TEntity>[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class DbTable<TEntity, TId extends DbPkFieldType, TTables extends DbTablesSet<TTables>> {
|
|
21
|
+
constructor(public readonly schema: DbEntitySchema<TEntity, TId, TTables>, private state?: DbTableState<TEntity, TTables>, private q?: DbQuery<TEntity>) {
|
|
22
|
+
if (!state) {
|
|
23
|
+
const indexes: DbIndex<TEntity>[] = (schema.indexes || []).map((indexDef) => {
|
|
24
|
+
return {
|
|
25
|
+
field: indexDef,
|
|
26
|
+
map: I.Map(),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.state = { pk: I.Map(), indexes };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!q) {
|
|
34
|
+
this.q = {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected keyToImmutable(id: TId): any {
|
|
39
|
+
if (Array.isArray(id)) {
|
|
40
|
+
return I.List(id);
|
|
41
|
+
} else {
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public byId(id: TId): TEntity {
|
|
47
|
+
return this.state.pk.get(this.keyToImmutable(id));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public getId(entity: Partial<TEntity>): TId {
|
|
51
|
+
if (Array.isArray(this.schema.primaryKey)) {
|
|
52
|
+
return this.schema.primaryKey.map((key: keyof TEntity) => entity[key]) as any as TId;
|
|
53
|
+
}
|
|
54
|
+
return entity[this.schema.primaryKey as keyof TEntity] as any as TId;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private update(mutate: (t: DbTable<TEntity, TId, TTables>) => void): this {
|
|
58
|
+
const clone = new (this.constructor as any)(this.schema, this.state, this.q);
|
|
59
|
+
mutate(clone);
|
|
60
|
+
return clone;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Mutation */
|
|
64
|
+
|
|
65
|
+
public with(patch: DbTablePatch<TEntity>) {
|
|
66
|
+
const updates = patch.map((entityPatch) => {
|
|
67
|
+
const id = this.getId(entityPatch);
|
|
68
|
+
const immId = this.keyToImmutable(id);
|
|
69
|
+
const existing = this.state.pk.get(immId);
|
|
70
|
+
let isDeleted = false;
|
|
71
|
+
let updated = entityPatch as TEntity;
|
|
72
|
+
if (existing) {
|
|
73
|
+
updated = { ...existing, ...entityPatch };
|
|
74
|
+
|
|
75
|
+
if (this.schema.deleteFlag) {
|
|
76
|
+
isDeleted = (updated[this.schema.deleteFlag] as unknown) === true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
id, immId, patch: entityPatch, existing, updated, isNew: !existing, isDeleted,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const idVal = Seq.Keyed<TId, TEntity>(updates.map((entity) => [entity.immId, entity.updated]));
|
|
86
|
+
let newPk = this.state.pk.merge(idVal);
|
|
87
|
+
|
|
88
|
+
// TBD: replace with deleteAll after migrating to immutable 4
|
|
89
|
+
updates
|
|
90
|
+
.filter((u) => u.isDeleted)
|
|
91
|
+
.forEach((u) => {
|
|
92
|
+
newPk = newPk.delete(u.immId);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const newIndexes = this.state.indexes.map((index) => {
|
|
96
|
+
let newMap = index.map;
|
|
97
|
+
|
|
98
|
+
for (let n = 0; n < updates.length; n++) {
|
|
99
|
+
const update = updates[n];
|
|
100
|
+
|
|
101
|
+
const newKey = update.updated[index.field];
|
|
102
|
+
|
|
103
|
+
if (update.existing) {
|
|
104
|
+
const oldKey = update.existing[index.field];
|
|
105
|
+
|
|
106
|
+
if (oldKey !== newKey || update.isDeleted) {
|
|
107
|
+
newMap = newMap.update(oldKey, (set) => set.remove(update.immId));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!update.isDeleted) {
|
|
112
|
+
newMap = newMap.update(newKey, I.Set(), (set) => set.add(update.immId));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { ...index, map: newMap };
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const newState: DbTableState<TEntity, TTables> = { pk: newPk, indexes: newIndexes };
|
|
120
|
+
|
|
121
|
+
return this.update((t) => (t.state = newState));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Query */
|
|
125
|
+
|
|
126
|
+
public find(filter: DataQueryFilter<TEntity>): DbTable<TEntity, TId, TTables> {
|
|
127
|
+
return this.update((t) => (t.q = { ...t.q, filter: { ...(this.q.filter as any), ...(filter as any) } }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public order(order: SortingOption[]) {
|
|
131
|
+
return this.update((t) => (t.q = { ...t.q, sorting: order }));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public orderBy(field: Extract<keyof TEntity, string>, direction: SortDirection = 'asc') {
|
|
135
|
+
return this.update((t) => (t.q = { ...t.q, sorting: [{ field, direction }] }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public thenBy(field: Extract<keyof TEntity, string>, direction?: SortDirection) {
|
|
139
|
+
return this.update((t) => (t.q = { ...t.q, sorting: [...t.q.sorting, { field, direction }] }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public search(text: string) {
|
|
143
|
+
if (!this.schema.searchBy) {
|
|
144
|
+
throw new Error(`Can't search in the ${this.schema.typeName} table - searchBy is not defined in the schema.`);
|
|
145
|
+
}
|
|
146
|
+
return this.update((t) => (t.q = { ...t.q, search: text }));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Materializing */
|
|
150
|
+
|
|
151
|
+
public range(from: number, count: number): TEntity[] {
|
|
152
|
+
return this.runQuery({ ...this.q, range: { from, count } });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public count() {
|
|
156
|
+
return this.runQuery(this.q).length;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public one(): TEntity {
|
|
160
|
+
return this.runQuery(this.q)[0] || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public toArray(): TEntity[] {
|
|
164
|
+
return this.runQuery(this.q);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
public map<T>(fn: (item: TEntity, index?: number) => T) {
|
|
168
|
+
return this.toArray().map(fn);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Query implementation */
|
|
172
|
+
|
|
173
|
+
private runQuery(q: DataQuery<TEntity>) {
|
|
174
|
+
let result: TEntity[] = null;
|
|
175
|
+
let filter = q.filter;
|
|
176
|
+
|
|
177
|
+
if (filter) {
|
|
178
|
+
const indexes = this.state.indexes;
|
|
179
|
+
|
|
180
|
+
// Try to use indexes to fulfill filter conditions
|
|
181
|
+
for (let n = 0; n < indexes.length; n++) {
|
|
182
|
+
const index = indexes[n];
|
|
183
|
+
|
|
184
|
+
if (index.field in filter) {
|
|
185
|
+
const { [index.field]: condition, ...rest } = filter as any;
|
|
186
|
+
let conditionValues: any[] = null;
|
|
187
|
+
|
|
188
|
+
if (condition != null && typeof condition === 'object') {
|
|
189
|
+
// Attempt to use index for 'in' and 'isNull' criteria.
|
|
190
|
+
// We need to be very conservative here, indexed field should work the same way as getPatternPredicate. So
|
|
191
|
+
// - it's better to leave corner cases to getPatternPredicate
|
|
192
|
+
|
|
193
|
+
const { in: inCriteria, isNull: nullCriteria, ...restCriteria } = condition as any;
|
|
194
|
+
if (inCriteria && Array.isArray(inCriteria)) {
|
|
195
|
+
conditionValues = inCriteria;
|
|
196
|
+
} else {
|
|
197
|
+
restCriteria.in = inCriteria;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Conditions in getPatternPredicate are composed with 'and' logic,
|
|
201
|
+
// For now, let's not attempt to handle tricky cases like { in: [1,2] isNull: true }, or { in: [1, null] isNull: false }
|
|
202
|
+
// - just leave this to getPatternPredicate.
|
|
203
|
+
// Let's just handle the a most common case { isNull: true } - find all null and undefined
|
|
204
|
+
if (nullCriteria === true && !conditionValues) {
|
|
205
|
+
conditionValues = [null, undefined];
|
|
206
|
+
} else {
|
|
207
|
+
restCriteria.in = inCriteria;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Keep other conditions, e.g. { in: [1,2,3], isNull: true } - in works via index, isNull - via filter
|
|
211
|
+
if (Object.keys(restCriteria).length > 0) {
|
|
212
|
+
rest[index.field] = restCriteria;
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
conditionValues = [condition];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (conditionValues) {
|
|
219
|
+
result = [];
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < conditionValues.length; i++) {
|
|
222
|
+
const idsSet = index.map.get(conditionValues[i]);
|
|
223
|
+
if (idsSet) {
|
|
224
|
+
const idsArray = idsSet.toArray();
|
|
225
|
+
for (let j = 0; j < idsArray.length; j++) {
|
|
226
|
+
const item = this.state.pk.get(idsArray[j]);
|
|
227
|
+
result.push(item);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
filter = Object.keys(rest).length > 0 ? (rest as any) : null;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!result) {
|
|
240
|
+
result = this.state.pk.toArray();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (filter) {
|
|
244
|
+
const predicate = getFilterPredicate<TEntity>(q.filter);
|
|
245
|
+
result = result.filter(predicate);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (q.search) {
|
|
249
|
+
const searchFilter = getSearchFilter(q.search);
|
|
250
|
+
result = result.filter((item) => searchFilter(this.schema.searchBy.map((field) => (item as any)[field])));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (q.sorting) {
|
|
254
|
+
const comparer = getOrderComparer(q.sorting);
|
|
255
|
+
result = result.sort(comparer);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (q.range) {
|
|
259
|
+
result = result.slice(q.range.from, q.range.from + q.range.count);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
}
|
package/src/IxSet.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import BTree from 'sorted-btree';
|
|
2
|
+
import { DbQuery } from './types';
|
|
3
|
+
import {
|
|
4
|
+
DataQueryFilterCondition, DataQueryFilter, getFilterPredicate,
|
|
5
|
+
} from '@epam/uui-core';
|
|
6
|
+
import orderBy from 'lodash.orderby';
|
|
7
|
+
|
|
8
|
+
export interface IxSetIndexDefinition<TEntity> {
|
|
9
|
+
fields: (keyof TEntity)[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ID = Symbol('ID');
|
|
13
|
+
|
|
14
|
+
type IndexKey<TEntity> = TEntity & { [ID]: any };
|
|
15
|
+
|
|
16
|
+
export interface IxSetIndex<TEntity> extends IxSetIndexDefinition<TEntity> {
|
|
17
|
+
tree: BTree<IndexKey<TEntity>, void>;
|
|
18
|
+
compare: (a: IndexKey<TEntity>, b: IndexKey<TEntity>) => number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Filter condition types
|
|
22
|
+
enum FilterConditionType {
|
|
23
|
+
Single = 1,
|
|
24
|
+
Multi = 2
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function defaultComparator(a: any, b: any) {
|
|
28
|
+
if (a === null) return -1;
|
|
29
|
+
if (b === null) return 1;
|
|
30
|
+
if (a === -Infinity || b === Infinity) return -1;
|
|
31
|
+
if (b === Infinity || a === -Infinity) return 1;
|
|
32
|
+
|
|
33
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function iterator<T>(next: () => { done: boolean; value?: T } = () => ({ done: true, value: undefined })): IterableIterator<T> {
|
|
37
|
+
const result: any = { next };
|
|
38
|
+
result[Symbol.iterator] = function () {
|
|
39
|
+
return this;
|
|
40
|
+
};
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class IxSet<TEntity, TId> {
|
|
45
|
+
empty: BTree<TId, TEntity>;
|
|
46
|
+
/** Primary key - a b-tree with entity ID as a key, and entity itself as a value */
|
|
47
|
+
pk: BTree<TId, TEntity>;
|
|
48
|
+
indexes: IxSetIndex<TEntity>[];
|
|
49
|
+
constructor(private getId: (e: Partial<TEntity>) => TId, indexesDefinition: IxSetIndexDefinition<TEntity>[]) {
|
|
50
|
+
this.empty = new BTree([]);
|
|
51
|
+
this.pk = this.empty;
|
|
52
|
+
this.indexes = indexesDefinition.map((definition) => {
|
|
53
|
+
const fields = definition.fields;
|
|
54
|
+
const compare = (a: IndexKey<TEntity>, b: IndexKey<TEntity>) => {
|
|
55
|
+
if (a === b) return 0;
|
|
56
|
+
if (a == null) return -1;
|
|
57
|
+
if (b == null) return 1;
|
|
58
|
+
|
|
59
|
+
for (let i = 0; i < fields.length; i++) {
|
|
60
|
+
const field = fields[i];
|
|
61
|
+
const result = defaultComparator(a[field], b[field]);
|
|
62
|
+
if (result !== 0) {
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return defaultComparator(a[ID], b[ID]);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
...definition,
|
|
72
|
+
compare,
|
|
73
|
+
tree: new BTree<IndexKey<TEntity>>([], compare),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public clone() {
|
|
79
|
+
const blank = Object.create(IxSet.prototype) as IxSet<TEntity, TId>;
|
|
80
|
+
blank.empty = this.empty;
|
|
81
|
+
blank.getId = this.getId;
|
|
82
|
+
blank.pk = this.pk;
|
|
83
|
+
blank.indexes = this.indexes;
|
|
84
|
+
return blank;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public byId(id: TId): TEntity {
|
|
88
|
+
return this.pk.get(id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public with(items: Partial<TEntity>[]): IxSet<TEntity, TId> {
|
|
92
|
+
const newSet = this.clone();
|
|
93
|
+
newSet.pk = this.pk.clone();
|
|
94
|
+
|
|
95
|
+
const updatedFields = {} as any;
|
|
96
|
+
|
|
97
|
+
const updates = items.map((patch) => {
|
|
98
|
+
const id = this.getId(patch);
|
|
99
|
+
const existing = this.pk.get(id);
|
|
100
|
+
let updated = patch as TEntity;
|
|
101
|
+
if (existing) {
|
|
102
|
+
updated = { ...existing, ...patch };
|
|
103
|
+
}
|
|
104
|
+
const existingIndexEntry = { ...existing, [ID]: id };
|
|
105
|
+
const updatedIndexEntry = { ...updated, [ID]: id };
|
|
106
|
+
Object.keys(patch).forEach((f) => {
|
|
107
|
+
updatedFields[f] = true;
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
id, patch, existing, updated, existingIndexEntry, updatedIndexEntry,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
updates.forEach((update) => newSet.pk.set(update.id, update.updated));
|
|
115
|
+
|
|
116
|
+
newSet.indexes = this.indexes.map((existingIndex) => {
|
|
117
|
+
if (existingIndex.fields.some((f) => updatedFields[f])) {
|
|
118
|
+
const newIndex = { ...existingIndex };
|
|
119
|
+
newIndex.tree = existingIndex.tree.clone();
|
|
120
|
+
|
|
121
|
+
const keysToRemove = updates.filter((u) => !!u.existing).map((u) => u.existingIndexEntry);
|
|
122
|
+
keysToRemove.sort(existingIndex.compare);
|
|
123
|
+
newIndex.tree.deleteKeys(keysToRemove);
|
|
124
|
+
|
|
125
|
+
const keysToAdd = updates.map((u) => u.updatedIndexEntry);
|
|
126
|
+
keysToAdd.sort(existingIndex.compare);
|
|
127
|
+
keysToAdd.forEach((key) => newIndex.tree.set(key));
|
|
128
|
+
|
|
129
|
+
return newIndex;
|
|
130
|
+
} else {
|
|
131
|
+
return existingIndex;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return newSet;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public query(query: DbQuery<TEntity>) {
|
|
139
|
+
const filter: DataQueryFilter<TEntity> = query.filter || {};
|
|
140
|
+
const filterFields = Object.keys(filter) as (keyof TEntity)[];
|
|
141
|
+
const filterFieldTypes = {} as Record<keyof TEntity, FilterConditionType>;
|
|
142
|
+
filterFields.forEach((f) => {
|
|
143
|
+
const condition = filter[f] as DataQueryFilterCondition<any>;
|
|
144
|
+
if (condition != null && typeof condition === 'object') {
|
|
145
|
+
if (condition.in) {
|
|
146
|
+
filterFieldTypes[f] = FilterConditionType.Multi;
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
filterFieldTypes[f] = FilterConditionType.Single;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
let plans = this.indexes
|
|
154
|
+
.map((index) => {
|
|
155
|
+
if (filterFieldTypes[index.fields[0]]) {
|
|
156
|
+
let matchCount = 0;
|
|
157
|
+
const from: IndexKey<TEntity> = { [ID]: -Infinity } as any;
|
|
158
|
+
const to: IndexKey<TEntity> = { [ID]: +Infinity } as any;
|
|
159
|
+
const lookupFields = {} as Record<keyof TEntity, boolean>;
|
|
160
|
+
let isRemainingUnused = false;
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < index.fields.length; i++) {
|
|
163
|
+
const field: keyof TEntity = index.fields[i];
|
|
164
|
+
if (!isRemainingUnused && filterFieldTypes[field] === FilterConditionType.Single) {
|
|
165
|
+
lookupFields[field] = true;
|
|
166
|
+
from[field] = filter[field] as any;
|
|
167
|
+
to[field] = filter[field] as any;
|
|
168
|
+
matchCount++;
|
|
169
|
+
} else {
|
|
170
|
+
isRemainingUnused = true;
|
|
171
|
+
from[field] = -Infinity as any;
|
|
172
|
+
to[field] = Infinity as any;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let remainingFilter: DataQueryFilter<TEntity> = null;
|
|
177
|
+
filterFields
|
|
178
|
+
.filter((f) => !lookupFields[f])
|
|
179
|
+
.forEach((f) => {
|
|
180
|
+
if (!remainingFilter) {
|
|
181
|
+
remainingFilter = {};
|
|
182
|
+
}
|
|
183
|
+
remainingFilter[f] = filter[f];
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const score = matchCount;
|
|
187
|
+
if (matchCount > 0) {
|
|
188
|
+
return {
|
|
189
|
+
score, index, from, to, remainingFilter,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
})
|
|
196
|
+
.filter((p) => p != null);
|
|
197
|
+
|
|
198
|
+
plans = orderBy(plans, 'score', 'desc');
|
|
199
|
+
const plan = plans[0];
|
|
200
|
+
|
|
201
|
+
// const indexEntities = plan.index.tree.entries();
|
|
202
|
+
|
|
203
|
+
let treeIterator: IterableIterator<TEntity>;
|
|
204
|
+
|
|
205
|
+
if (plan) {
|
|
206
|
+
const indexIterator = plan.index.tree.entries(plan.from);
|
|
207
|
+
treeIterator = iterator(() => {
|
|
208
|
+
const next = indexIterator.next() as any;
|
|
209
|
+
if (!next.value) {
|
|
210
|
+
return next;
|
|
211
|
+
}
|
|
212
|
+
next.value = next.value[0];
|
|
213
|
+
if (plan.index.compare(next.value, plan.to) > 0) {
|
|
214
|
+
next.done = true;
|
|
215
|
+
next.value = null;
|
|
216
|
+
return next;
|
|
217
|
+
}
|
|
218
|
+
next.value = this.pk.get(next.value[ID]);
|
|
219
|
+
return next;
|
|
220
|
+
});
|
|
221
|
+
} else {
|
|
222
|
+
treeIterator = this.pk.values();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result: TEntity[] = [];
|
|
226
|
+
|
|
227
|
+
const filterPredicate = getFilterPredicate(plan ? plan.remainingFilter : filter);
|
|
228
|
+
|
|
229
|
+
let current;
|
|
230
|
+
let index = 0;
|
|
231
|
+
let count = 0;
|
|
232
|
+
const rangeFrom = (query.range && query.range.from) || 0;
|
|
233
|
+
const rangeCount = (query.range && query.range.count) || Number.MAX_SAFE_INTEGER;
|
|
234
|
+
while (!(current = treeIterator.next()).done && count < rangeCount) {
|
|
235
|
+
const entity: TEntity = current.value;
|
|
236
|
+
const passedFilter = !filterPredicate || filterPredicate(entity);
|
|
237
|
+
|
|
238
|
+
if (passedFilter && index >= rangeFrom) {
|
|
239
|
+
result.push(entity);
|
|
240
|
+
count++;
|
|
241
|
+
}
|
|
242
|
+
index++;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public queryOne(query: DbQuery<TEntity>) {
|
|
249
|
+
return this.query(query)[0];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { DataQuery, LazyDataSourceApiResponse } from '@epam/uui-core';
|
|
2
|
+
import { ILoadingTracker } from './types';
|
|
3
|
+
|
|
4
|
+
interface ListRecord<TItem> {
|
|
5
|
+
options: DataQuery<TItem>;
|
|
6
|
+
count: number;
|
|
7
|
+
maxCount: number;
|
|
8
|
+
isComplete: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ListLoadingTrackerOptions<TItem, TResponse> {
|
|
12
|
+
extractList?: (response: TResponse) => LazyDataSourceApiResponse<TItem>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ListLoadingTracker<TItem, TRequest extends DataQuery<TItem> = DataQuery<TItem>, TResponse = LazyDataSourceApiResponse<TItem>>
|
|
16
|
+
implements ILoadingTracker<TRequest, TResponse> {
|
|
17
|
+
private cache: Map<string, ListRecord<TItem>> = new Map();
|
|
18
|
+
private extractList: (res: TResponse) => LazyDataSourceApiResponse<TItem>;
|
|
19
|
+
constructor(private options?: ListLoadingTrackerOptions<TItem, TResponse>) {
|
|
20
|
+
this.extractList = options?.extractList || ((r: any) => r);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private prepare(request: DataQuery<TItem>) {
|
|
24
|
+
const {
|
|
25
|
+
filter, sorting, search, range, ...unknownFields
|
|
26
|
+
} = request;
|
|
27
|
+
const options: DataQuery<TItem> = {};
|
|
28
|
+
|
|
29
|
+
if (filter) {
|
|
30
|
+
options.filter = filter;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (sorting) {
|
|
34
|
+
options.sorting = sorting;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (search) {
|
|
38
|
+
options.search = search;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const keyObject = { ...options, ...unknownFields };
|
|
42
|
+
|
|
43
|
+
const key = JSON.stringify(keyObject);
|
|
44
|
+
let entry = this.cache.get(key);
|
|
45
|
+
if (!entry) {
|
|
46
|
+
entry = {
|
|
47
|
+
options, count: 0, maxCount: null, isComplete: false,
|
|
48
|
+
};
|
|
49
|
+
this.cache.set(key, entry);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
options, range, unknownFields, entry,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public diff(request: TRequest): TRequest {
|
|
58
|
+
const {
|
|
59
|
+
options, range, unknownFields, entry,
|
|
60
|
+
} = this.prepare(request);
|
|
61
|
+
|
|
62
|
+
if (entry.isComplete) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!request.range) {
|
|
67
|
+
return request;
|
|
68
|
+
} else {
|
|
69
|
+
const requestedMax = request.range.from + request.range.count;
|
|
70
|
+
let missingCount = requestedMax - entry.count;
|
|
71
|
+
|
|
72
|
+
if (entry.maxCount) {
|
|
73
|
+
missingCount = Math.min(missingCount, entry.maxCount);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (missingCount > 0) {
|
|
77
|
+
return {
|
|
78
|
+
...options,
|
|
79
|
+
...unknownFields,
|
|
80
|
+
range: { from: entry.count, count: requestedMax - entry.count },
|
|
81
|
+
} as TRequest;
|
|
82
|
+
} else {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public count(request: TRequest) {
|
|
89
|
+
const { entry } = this.prepare(request);
|
|
90
|
+
|
|
91
|
+
return { knownCount: Math.max(entry.count, entry.maxCount), exactCount: entry.maxCount };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public append(request: TRequest, response?: TResponse) {
|
|
95
|
+
const {
|
|
96
|
+
options, range, unknownFields, entry,
|
|
97
|
+
} = this.prepare(request);
|
|
98
|
+
|
|
99
|
+
if (response) {
|
|
100
|
+
const list = this.extractList(response);
|
|
101
|
+
|
|
102
|
+
if (list.count) {
|
|
103
|
+
entry.maxCount = list.count;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!range) {
|
|
107
|
+
entry.count = list.items.length;
|
|
108
|
+
entry.maxCount = list.items.length;
|
|
109
|
+
entry.isComplete = true;
|
|
110
|
+
} else {
|
|
111
|
+
const from = list.from == null ? range.from : list.from;
|
|
112
|
+
|
|
113
|
+
if (list.items.length > 0) {
|
|
114
|
+
entry.count = Math.max(entry.count, from + list.items.length);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (list.items.length < range.count) {
|
|
118
|
+
entry.maxCount = from + list.items.length;
|
|
119
|
+
entry.count = entry.maxCount;
|
|
120
|
+
entry.isComplete = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
if (range) {
|
|
125
|
+
entry.count = Math.max(entry.count, request.range.from + request.range.count);
|
|
126
|
+
} else {
|
|
127
|
+
entry.isComplete = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|