@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,254 @@
1
+ import { Db } from './Db';
2
+ import {
3
+ DbPatch, DbTablesSet, DbRelationType, DbEntitySchema, DbPkFieldType,
4
+ } from './types';
5
+ import { DbTable } from './DbTable';
6
+ import * as I from 'immutable';
7
+ import { objectKeys } from './utils';
8
+ import groupBy from 'lodash.groupby';
9
+ import map from 'lodash.map';
10
+ import merge from 'lodash.merge';
11
+ import countBy from 'lodash.countby';
12
+ import filter from 'lodash.filter';
13
+ import forEach from 'lodash.foreach';
14
+ import pick from 'lodash.pick';
15
+ import { IClientIdsMap } from './tempIds';
16
+
17
+ export function unionPatches<TTables extends DbTablesSet<TTables>>(patches: DbPatch<TTables>[]): DbPatch<TTables> {
18
+ const result: DbPatch<TTables> = {};
19
+
20
+ patches.forEach((patch) => {
21
+ objectKeys(patch).forEach((tableName) => {
22
+ result[tableName] = result[tableName] ? [...result[tableName], ...patch[tableName]] : patch[tableName];
23
+ });
24
+ });
25
+
26
+ return result;
27
+ }
28
+
29
+ export function mergeEntityPatches<TTables extends DbTablesSet<TTables>>(tables: TTables, patch: DbPatch<TTables>): DbPatch<TTables> {
30
+ const result: DbPatch<TTables> = {};
31
+
32
+ objectKeys(patch).forEach((key) => {
33
+ const table = tables[key];
34
+ const groupedEntities = groupBy(patch[key], (item) => table.getId(item));
35
+ const tablePatch = map(groupedEntities, (entities) => {
36
+ const tablePatchResult = merge({}, ...entities);
37
+
38
+ if (process.env.NODE_ENV !== 'production') {
39
+ const props = objectKeys(tablePatchResult).filter(
40
+ (prop) =>
41
+ Object.keys(
42
+ countBy(
43
+ entities.filter((entity) => entity[prop as string] !== undefined),
44
+ (entity) => entity[prop as string],
45
+ ),
46
+ ).length > 1,
47
+ );
48
+ if (props.length) {
49
+ console.warn('mergeEntityPatches: Different property values found for the same entity.\nProperty names:\n', props, '\nEntities:\n', ...entities);
50
+ }
51
+ }
52
+
53
+ return tablePatchResult;
54
+ });
55
+
56
+ result[key] = tablePatch;
57
+ });
58
+
59
+ return result;
60
+ }
61
+
62
+ function flattenResponseHelper<TTables extends DbTablesSet<any>>(result: DbPatch<TTables>, object: any, tablesByTypeName: Record<string, DbTable<any, any, any>>) {
63
+ if (Array.isArray(object)) {
64
+ object.forEach((item) => flattenResponseHelper(result, item, tablesByTypeName));
65
+ } else if (typeof object === 'object') {
66
+ const clone: any = {};
67
+ let tableKey;
68
+ let fields: { [id: string]: any } = {};
69
+
70
+ if ('__typename' in object) {
71
+ const table = tablesByTypeName[object.__typename];
72
+ if (table) {
73
+ tableKey = table.schema.tableName as string;
74
+ fields = table.schema.fields;
75
+ }
76
+ }
77
+
78
+ for (const key in object) {
79
+ const value = object[key];
80
+ const field = fields[key];
81
+
82
+ if (
83
+ (field && !!field.disableFlatten)
84
+ || typeof value !== 'object'
85
+ || value === null
86
+ || (Array.isArray(value) && value.every((item) => typeof item !== 'object'))
87
+ ) {
88
+ clone[key] = value;
89
+ } else {
90
+ flattenResponseHelper(result, value, tablesByTypeName);
91
+ }
92
+ }
93
+
94
+ if (tableKey) {
95
+ result[tableKey].push(clone);
96
+ }
97
+ }
98
+ }
99
+
100
+ export function flattenResponse<TTables extends DbTablesSet<any>>(response: any, tables: TTables): DbPatch<TTables> {
101
+ const result: DbPatch<TTables> = {};
102
+
103
+ const tablesByTypeName: Record<string, DbTable<any, any, any>> = {};
104
+ Object.values(tables).forEach((table) => (tablesByTypeName[table.schema.typeName] = table));
105
+ objectKeys(tables).forEach(() => tablesByTypeName);
106
+
107
+ objectKeys(tables).forEach((key) => (result[key] = []));
108
+
109
+ flattenResponseHelper(result, response, tablesByTypeName);
110
+ return result;
111
+ }
112
+
113
+ export function isPatchBlank<TTables extends DbTablesSet<TTables>>(patch: DbPatch<TTables>): boolean {
114
+ return !objectKeys(patch).some((key) => patch[key].length > 0);
115
+ }
116
+
117
+ /**
118
+ * Given several patches, creates a new single patch with all fields from all entities in all patches.
119
+ */
120
+ export function makeCumulativePatch<TTables extends DbTablesSet<TTables>>(
121
+ updatedDb: Db<TTables>,
122
+ patches: DbPatch<TTables>[],
123
+ clientIdsMap: IClientIdsMap,
124
+ ): DbPatch<TTables> {
125
+ const result: DbPatch<TTables> = {};
126
+
127
+ objectKeys(updatedDb.tables).forEach((entityName) => {
128
+ const table = updatedDb.tables[entityName];
129
+ const schema = table.schema;
130
+
131
+ let ids = I.Set();
132
+ patches.forEach((patch) => {
133
+ const filteredPatch = (patch[entityName] || []).filter((entity) => hasServerFields(entity, schema, clientIdsMap, updatedDb.tables));
134
+ ids = ids.union(filteredPatch.map((e) => table.getId(e)));
135
+ });
136
+
137
+ const entities = ids
138
+ .map((id) => table.byId(id))
139
+ .filter((e) => !isClientOnly(e, schema))
140
+ .toArray();
141
+
142
+ if (entities.length > 0) {
143
+ result[entityName] = entities;
144
+ }
145
+ });
146
+
147
+ return result;
148
+ }
149
+
150
+ export function isClientOnly<T, TId extends DbPkFieldType, TTables extends DbTablesSet<TTables>>(entity: T, schema: DbEntitySchema<T, TId, TTables>) {
151
+ if (typeof schema.isClientOnly === 'function') {
152
+ return schema.isClientOnly(entity);
153
+ }
154
+
155
+ return !!schema.isClientOnly;
156
+ }
157
+
158
+ export function getId<T, TId extends DbPkFieldType, TTables extends DbTablesSet<TTables>>(entity: Partial<T>, schema: DbEntitySchema<T, TId, TTables>): TId {
159
+ if (typeof schema.primaryKey == 'string') {
160
+ return (entity as any)[schema.primaryKey] as TId;
161
+ }
162
+
163
+ const result: (string | number)[] = [];
164
+ forEach(pick(entity, schema.primaryKey), (value) => {
165
+ result.push(value as unknown as string | number);
166
+ });
167
+
168
+ return result as TId;
169
+ }
170
+
171
+ export function isNew<T, TId extends DbPkFieldType, TTables extends DbTablesSet<TTables>>(
172
+ entity: Partial<T>,
173
+ schema: DbEntitySchema<T, TId, TTables>,
174
+ clientIdsMap: IClientIdsMap,
175
+ ) {
176
+ const id = getId(entity, schema);
177
+ return !clientIdsMap.clientToServer(id as number);
178
+ }
179
+
180
+ export function hasServerFields<T, TId extends DbPkFieldType, TTables extends DbTablesSet<TTables>>(
181
+ entity: Partial<T>,
182
+ schema: DbEntitySchema<T, TId, TTables>,
183
+ clientIdsMap: IClientIdsMap,
184
+ tables: TTables,
185
+ ) {
186
+ const fullEntity = {
187
+ ...(tables[schema.tableName].byId((entity as any).id) || {}),
188
+ ...entity,
189
+ };
190
+
191
+ if (isClientOnly(fullEntity, schema)) {
192
+ return false;
193
+ }
194
+
195
+ return objectKeys(entity).some((fieldName) => {
196
+ const fieldSchema = schema.fields[fieldName];
197
+ return !fieldSchema || !(fieldSchema.isClientOnly || fieldSchema.isReadOnly || (fieldSchema.isGenerated && !isNew(entity, schema, clientIdsMap)));
198
+ });
199
+ }
200
+
201
+ export function getParentEntities<TTables extends DbTablesSet<TTables>>(patch: DbPatch<TTables>, tables: TTables): DbPatch<TTables> {
202
+ const dependenciesPatch: DbPatch<TTables> = {};
203
+ objectKeys(patch).forEach((key) => {
204
+ const table = tables[key];
205
+ const tableSchema = table.schema;
206
+
207
+ const fields = filter(
208
+ map(tableSchema.fields, (field, name) => ({ ...field, name })),
209
+ (field) => field.fk && field.fk.relationType == DbRelationType.Aggregation,
210
+ );
211
+
212
+ const tablePatch = patch[key];
213
+ fields.forEach((field) => {
214
+ const {
215
+ fk: { tableName },
216
+ name,
217
+ } = field;
218
+ tablePatch.forEach((entity) => {
219
+ const actualTableName = (typeof tableName == 'function' ? tableName(entity) : tableName) as keyof TTables;
220
+ const dbEntity = table.byId(table.getId(entity));
221
+ const parentEntity = tables[actualTableName].byId(dbEntity[name]);
222
+ const idFields = pick(parentEntity, tables[actualTableName].schema.primaryKey);
223
+ if (parentEntity) {
224
+ dependenciesPatch[actualTableName] = dependenciesPatch[actualTableName] || [];
225
+ dependenciesPatch[actualTableName].push(idFields as any);
226
+ }
227
+ });
228
+ });
229
+ });
230
+ return dependenciesPatch;
231
+ }
232
+
233
+ export function getEntityParentDependencies<TEntity, TTables extends DbTablesSet<TTables>>(
234
+ entity: Partial<TEntity>,
235
+ entityName: keyof TTables,
236
+ tables: TTables,
237
+ ): DbPatch<TTables> {
238
+ let patch = { [entityName]: [entity] } as DbPatch<TTables>;
239
+ const resultPatches: DbPatch<TTables>[] = [];
240
+
241
+ while (true) {
242
+ const parents = getParentEntities(patch, tables);
243
+ if (isPatchBlank(parents)) {
244
+ break;
245
+ }
246
+ resultPatches.push(parents);
247
+ patch = parents;
248
+ }
249
+
250
+ let result = unionPatches([patch, ...resultPatches]);
251
+ result = mergeEntityPatches(tables, result);
252
+
253
+ return result;
254
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ useState, createContext, useContext, useMemo, useEffect,
3
+ } from 'react';
4
+ import { DbRef } from './DbRef';
5
+ import { DbViewOptions } from '.';
6
+
7
+ export const DbContext = createContext(null);
8
+
9
+ export function useDbRef<TDbRef extends DbRef<any, any>>() {
10
+ return useContext(DbContext) as TDbRef;
11
+ }
12
+
13
+ export function useDbView<TDb, TResult, TParams = void, TDependencies = void>(
14
+ fn: (db: TDb, params: TParams, dependencies: TDependencies) => TResult,
15
+ params?: TParams,
16
+ options?: DbViewOptions<TDb, TResult, TParams, TDependencies>,
17
+ ): TResult {
18
+ const dbRef = useDbRef();
19
+ const [state, setState] = useState<TResult>();
20
+ const subscription = useMemo(() => dbRef.subscribe<TResult, TParams, TDependencies>({ compute: fn, ...options }, params, setState), []);
21
+ subscription.update(params);
22
+
23
+ useEffect(() => {
24
+ return () => subscription.unsubscribe();
25
+ }, []);
26
+
27
+ return subscription.currentValue;
28
+ }
@@ -0,0 +1,236 @@
1
+ import {
2
+ DbTablePatch, DbTablesSet, DbPatch, DbFieldSchema, DbSaveResponse,
3
+ } from './types';
4
+ import { Db } from './Db';
5
+ import { objectKeys } from './utils';
6
+ import { DataQueryFilter, DataQueryFilterCondition } from '@epam/uui-core';
7
+
8
+ let lastTempId = -1;
9
+
10
+ export function getTempId() {
11
+ return --lastTempId;
12
+ }
13
+
14
+ export function isTempId(val: any): val is number {
15
+ return typeof val === 'number' && val < 0;
16
+ }
17
+
18
+ export interface IClientIdsMap {
19
+ clientToServer(id: number): any;
20
+ serverToClient(tableName: string, id: any): number;
21
+ clientToServerRequest<T>(request: T): T;
22
+ clientToServerDataFilter(filter: DataQueryFilter<any>): DataQueryFilter<any>;
23
+ }
24
+
25
+ export class TempIdMap<TTables extends DbTablesSet<TTables>> implements IClientIdsMap {
26
+ private serverToClientIds = new Map<keyof TTables, Map<any, number>>();
27
+ private clientToServerIds = new Map<number, any>();
28
+ constructor(private db: Db<TTables>) {}
29
+ public clientToServer = (id: number): any => this.clientToServerIds.get(id);
30
+ public serverToClient = (tableName: string, id: any): number => this.getServerToClientMap(tableName as any).get(id);
31
+ public clientToServerRequest = (request: any) => {
32
+ request = { ...request };
33
+
34
+ if (request.filter) {
35
+ request.filter = this.clientToServerDataFilter(request.filter);
36
+ }
37
+
38
+ return request;
39
+ };
40
+
41
+ public clientToServerDataFilter = (filter: DataQueryFilter<any>) => {
42
+ filter = { ...filter };
43
+ const keys = Object.keys(filter);
44
+
45
+ // I hope that we can rely that there won't be any negative numbers, which are not IDs, in queries.
46
+ // We need to invent better heuristics or a schema otherwise.
47
+ const clientToServer = (id: number) => {
48
+ const found = this.clientToServer(id);
49
+ if (found) {
50
+ return found;
51
+ } else {
52
+ return id;
53
+ }
54
+ };
55
+
56
+ for (let n = 0; n < keys.length; n++) {
57
+ const key = keys[n];
58
+ let condition = filter[key] as DataQueryFilterCondition<any>;
59
+ if (condition != null && typeof condition === 'object') {
60
+ condition = { ...condition };
61
+ if ('in' in condition && Array.isArray(condition.in) && condition.in.length) {
62
+ condition.in = condition.in.map(clientToServer);
63
+ }
64
+ } else {
65
+ condition = clientToServer(condition);
66
+ }
67
+ filter[key] = condition;
68
+ }
69
+ return filter;
70
+ };
71
+
72
+ private copyPatch(patch: DbPatch<TTables>) {
73
+ const newPatch = { ...patch };
74
+
75
+ objectKeys(patch).forEach((tableName) => {
76
+ const tablePatch: DbTablePatch<any> = patch[tableName].map((entityPatch) => ({ ...entityPatch }));
77
+ newPatch[tableName] = tablePatch;
78
+ });
79
+
80
+ return newPatch;
81
+ }
82
+
83
+ // Iterate thru every field in every patch for every table, matching certain field criteria. The patch is mutated!
84
+ protected mapFields(
85
+ patch: DbPatch<TTables>,
86
+ filter: (schema: DbFieldSchema<any, any>, name: string) => boolean,
87
+ callback: (fieldName: any, fieldSchema: DbFieldSchema<any, any>, fieldValue: any, entityPatch: any, tableName: keyof TTables) => void,
88
+ ): void {
89
+ objectKeys(patch).forEach((tableName) => {
90
+ const schema = this.db.tables[tableName].schema;
91
+ const fields = objectKeys(schema.fields || [])
92
+ .map((fieldName) => {
93
+ const fieldSchema = schema.fields[fieldName as any];
94
+ if (filter(fieldSchema, fieldName as string)) {
95
+ return { name: fieldName, schema: fieldSchema };
96
+ }
97
+ })
98
+ .filter((f) => !!f);
99
+
100
+ if (!fields.length) {
101
+ return;
102
+ }
103
+
104
+ patch[tableName].forEach((entityPatch) => {
105
+ for (let n = 0; n < fields.length; n++) {
106
+ const field = fields[n];
107
+
108
+ const fieldValue = entityPatch[field.name as any];
109
+
110
+ callback(field.name, field.schema, fieldValue, entityPatch, tableName);
111
+ }
112
+ });
113
+ });
114
+ }
115
+
116
+ protected getServerToClientMap(tableName: keyof TTables) {
117
+ const existing = this.serverToClientIds.get(tableName);
118
+
119
+ if (existing) {
120
+ return existing;
121
+ } else {
122
+ const newMap = new Map();
123
+ this.serverToClientIds.set(tableName, newMap);
124
+ return newMap;
125
+ }
126
+ }
127
+
128
+ public serverToClientPatch(patch: DbPatch<TTables>) {
129
+ patch = this.copyPatch(patch);
130
+
131
+ // We map PK in a separate pass, as we need to remap all PKs before re-mapping FKs
132
+ this.mapFields(
133
+ patch,
134
+ (f) => f.isGenerated,
135
+ (fieldName, fieldSchema, fieldValue, entityPatch, tableName) => {
136
+ let clientId = this.getServerToClientMap(tableName).get(fieldValue);
137
+ if (!clientId) {
138
+ clientId = getTempId();
139
+ this.clientToServerIds.set(clientId, fieldValue);
140
+ this.getServerToClientMap(tableName).set(fieldValue, clientId);
141
+ }
142
+ entityPatch[fieldName] = clientId;
143
+ },
144
+ );
145
+
146
+ this.mapFields(
147
+ patch,
148
+ (f) => !!(f.fk || f.default != null || f.toClient),
149
+ (fieldName, fieldSchema, fieldValue, entityPatch) => {
150
+ let isUndefined = typeof fieldValue === 'undefined';
151
+
152
+ if (fieldSchema.default != null && fieldValue == null) {
153
+ fieldValue = fieldSchema.default;
154
+ isUndefined = false;
155
+ }
156
+
157
+ if (fieldSchema.fk && fieldValue != null) {
158
+ const fkTableName = typeof fieldSchema.fk.tableName === 'function' ? fieldSchema.fk.tableName(entityPatch) : fieldSchema.fk.tableName;
159
+ let clientId = this.getServerToClientMap(fkTableName as keyof TTables).get(fieldValue);
160
+ if (!clientId) {
161
+ clientId = getTempId();
162
+ this.clientToServerIds.set(clientId, fieldValue);
163
+ this.getServerToClientMap(fkTableName as keyof TTables).set(fieldValue, clientId);
164
+ }
165
+ fieldValue = clientId;
166
+ }
167
+
168
+ if (!isUndefined && fieldSchema.toClient) {
169
+ fieldValue = fieldSchema.toClient(fieldValue);
170
+ }
171
+
172
+ if (!isUndefined) {
173
+ entityPatch[fieldName] = fieldValue;
174
+ }
175
+ },
176
+ );
177
+
178
+ return patch;
179
+ }
180
+
181
+ public clientToServerPatch(patch: DbPatch<TTables>) {
182
+ patch = this.copyPatch(patch);
183
+
184
+ this.mapFields(
185
+ patch,
186
+ (f) => f.isGenerated || !!f.fk,
187
+ (fieldName, fieldSchema, fieldValue, entityPatch) => {
188
+ if (fieldValue != null) {
189
+ const existingId = this.clientToServerIds.get(fieldValue);
190
+ if (existingId) {
191
+ entityPatch[fieldName] = existingId;
192
+ }
193
+ }
194
+ },
195
+ );
196
+
197
+ this.mapFields(
198
+ patch,
199
+ (f) => !(f.isClientOnly || f.isReadOnly) && !!f.toServer,
200
+ (fieldName, fieldSchema, _, entityPatch) => {
201
+ entityPatch[fieldName] = fieldSchema.toServer(entityPatch[fieldName]);
202
+ },
203
+ );
204
+
205
+ this.mapFields(
206
+ patch,
207
+ (meta) => meta.isClientOnly || meta.isReadOnly,
208
+ (fieldName, _, __, entityPatch) => {
209
+ delete entityPatch[fieldName];
210
+ },
211
+ );
212
+
213
+ return patch;
214
+ }
215
+
216
+ public appendServerMapping(response: DbSaveResponse<TTables>) {
217
+ objectKeys(response.submit).forEach((tableName) => {
218
+ if (this.db.tables[tableName]) {
219
+ const tablePatch = response.submit[tableName];
220
+ if (tablePatch) {
221
+ const serverToClientIds = this.getServerToClientMap(tableName);
222
+ const table = this.db.tables[tableName];
223
+
224
+ tablePatch.forEach((patch) => {
225
+ if (isTempId(patch.id)) {
226
+ const clientId = patch.id;
227
+ const serverId = table.getId(patch.payload);
228
+ serverToClientIds.set(serverId, clientId);
229
+ this.clientToServerIds.set(clientId, serverId);
230
+ }
231
+ });
232
+ }
233
+ }
234
+ });
235
+ }
236
+ }
package/src/types.ts ADDED
@@ -0,0 +1,126 @@
1
+ import { DataQuery } from '@epam/uui-core';
2
+ import { DbTable } from './DbTable';
3
+ import { IClientIdsMap } from './tempIds';
4
+
5
+ export type DbTablesSet<TTables extends DbTablesSet<TTables>> = { [TEntityName in keyof TTables]: DbTable<any, any, TTables> };
6
+
7
+ export type DbTablePatch<TEntity> = Partial<TEntity>[];
8
+
9
+ export type DbTableEntityByTable<TTable, TTables extends DbTablesSet<TTables>> = TTable extends DbTable<infer TEntity, any, TTables> ? TEntity : void;
10
+ export type DbTableIdByTable<TTable, TTables extends DbTablesSet<TTables>> = TTable extends DbTable<any, infer TId, TTables> ? TId : void;
11
+ type DbTablePatchByTable<TTable, TTables extends DbTablesSet<TTables>> = DbTablePatch<DbTableEntityByTable<TTable, TTables>>;
12
+
13
+ export type DbPatch<TTables extends DbTablesSet<TTables>> = { [TEntityName in keyof TTables]?: DbTablePatchByTable<TTables[TEntityName], TTables> };
14
+
15
+ export enum DbRelationType {
16
+ Aggregation,
17
+ Association
18
+ }
19
+
20
+ export type DbFieldFk<TEntity> = {
21
+ tableName: string | ((entity: TEntity) => string);
22
+ relationType: DbRelationType;
23
+ };
24
+
25
+ export interface DbFieldSchema<TField extends keyof TEntity, TEntity> {
26
+ fk?: DbFieldFk<TEntity>;
27
+ isGenerated?: boolean;
28
+ isClientOnly?: boolean;
29
+ isReadOnly?: boolean;
30
+ default?: TEntity[TField];
31
+ toServer?: (value: TEntity[TField]) => TEntity[TField];
32
+ toClient?: (value: TEntity[TField]) => TEntity[TField];
33
+ disableFlatten?: boolean;
34
+ }
35
+
36
+ export type DbPkFieldType = string | number | (string | number)[];
37
+
38
+ export type BeforeUpdateContext<TEntity, TId extends DbPkFieldType, TTables extends DbTablesSet<TTables>> = {
39
+ clientIdsMap: IClientIdsMap;
40
+ schema: DbEntitySchema<TEntity, TId, TTables>;
41
+ tables: DbTablesSet<TTables>;
42
+ };
43
+
44
+ type DbEntityBeforeUpdateResult<TEntity, TTables extends DbTablesSet<TTables>> = {
45
+ entity: TEntity;
46
+ dependentEntities?: DbPatch<TTables>;
47
+ };
48
+
49
+ type DbTableIndexDefinition<TEntity> = keyof TEntity;
50
+
51
+ export type DbEntitySchema<TEntity, TId extends DbPkFieldType, TTables extends DbTablesSet<TTables>> = {
52
+ tableName: keyof TTables;
53
+ typeName: string;
54
+ searchBy?: (keyof TEntity)[];
55
+ fields?: Partial<{ [TField in keyof TEntity]: DbFieldSchema<TField, TEntity> }>;
56
+ isClientOnly?: boolean | ((entity: Partial<TEntity>) => boolean);
57
+ beforeUpdate?: (entity: TEntity, context: BeforeUpdateContext<TEntity, TId, TTables>) => DbEntityBeforeUpdateResult<TEntity, TTables>;
58
+ primaryKey: TId extends any[] ? KeysOfType<TEntity, string | number>[] : KeysOfType<TEntity, string | number>;
59
+ indexes?: DbTableIndexDefinition<TEntity>[];
60
+ deleteFlag?: keyof TEntity;
61
+ };
62
+
63
+ export type DbSchema<TTables extends DbTablesSet<TTables>> = {
64
+ [TTypeName in keyof TTables]: DbEntitySchema<any, any, TTables>;
65
+ };
66
+
67
+ export interface DbQuery<TEntity> extends DataQuery<TEntity> {}
68
+
69
+ export interface IQueryable<TEntity, TId> {
70
+ query(q: DbQuery<TEntity>): TEntity[];
71
+ byId(id: TId): TEntity;
72
+ }
73
+
74
+ export interface ServerError {
75
+ message?: string;
76
+ }
77
+
78
+ type DbSaveEntityResponse<TEntity, TId> = {
79
+ id: TId;
80
+ payload: Partial<TEntity>;
81
+ };
82
+
83
+ export interface DbSaveResponse<TTables extends DbTablesSet<TTables>> {
84
+ submit: {
85
+ [EntityName in keyof TTables]?: DbSaveEntityResponse<DbTableEntityByTable<TTables[EntityName], TTables>, DbTableIdByTable<TTables[EntityName], TTables>>[];
86
+ };
87
+ }
88
+
89
+ export type DbViewFunction<TDb, TResult, TParams = void, TDependencies = void> = (db: TDb, parameters?: TParams, dependencies?: TDependencies) => TResult;
90
+
91
+ export interface DbViewOptions<TDb, TResult, TParams, TDependencies = void> {
92
+ getKey?: (parameters: TParams) => string;
93
+ getDependencies?: (db: TDb, parameters: TParams) => TDependencies;
94
+ compareDependencies?: (prev: TDependencies, next: TDependencies) => boolean;
95
+ compareResults?: (prev: TResult, current: TResult) => boolean;
96
+ traceDiff?: boolean;
97
+ }
98
+
99
+ export interface DbView<TDb, TResult, TParams = void, TDependencies = void> extends DbViewOptions<TDb, TResult, TParams, TDependencies> {
100
+ compute: DbViewFunction<TDb, TResult, TParams, TDependencies>;
101
+ }
102
+
103
+ export type KeysOfType<T, TProp> = { [P in keyof T]: T[P] extends TProp ? P : never }[keyof T];
104
+
105
+ export interface DbSubscription<TValue, TParams> {
106
+ update(params: TParams): TValue;
107
+ currentValue: TValue;
108
+ currentParams: TParams;
109
+ onUpdate: (newValue: TValue) => void;
110
+ unsubscribe: () => void;
111
+ }
112
+
113
+ export type ViewCacheItem = { dependencies: any; currentValue: any; computedOnDb: any };
114
+
115
+ export interface LoadingState<TRequest> {
116
+ isLoading: boolean;
117
+ isComplete: boolean;
118
+ request: TRequest;
119
+ missing?: TRequest;
120
+ }
121
+
122
+ export interface ILoadingTracker<TRequest, TResult> {
123
+ diff(request: TRequest): TRequest;
124
+ append(request: TRequest, result?: TResult): void;
125
+ count?(request: TRequest): { knownCount: number; exactCount: number };
126
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,34 @@
1
+ import shallowEqual from 'shallowequal';
2
+ import zip from 'lodash.zip';
3
+ import transform from 'lodash.transform';
4
+ import isEqual from 'lodash.isequal';
5
+
6
+ // TBD: New typescript would contain better typing for this. Remove this hack after update.
7
+ export const objectKeys: <T>(obj: T) => (keyof T)[] = Object.keys as any;
8
+
9
+ export function defaultCompareViewDependencies<T>(prev: T, next: T): boolean {
10
+ if (Array.isArray(prev) && Array.isArray(next)) {
11
+ if (prev.length === next.length) {
12
+ return zip(prev, next).every(([p, n]) => shallowEqual(p, n));
13
+ }
14
+ return false;
15
+ }
16
+ return shallowEqual(prev, next);
17
+ }
18
+
19
+ /**
20
+ * Deep diff between two object, using lodash
21
+ * @param {Object} object Object compared
22
+ * @param {Object} base Object to compare with
23
+ * @return {Object} Return a new object who represent the diff
24
+ */
25
+ export function difference(obj: any, baseObj: any) {
26
+ function changes(object: any, base: any) {
27
+ return transform(object, function (result: any, value: any, key: any) {
28
+ if (!isEqual(value, base[key])) {
29
+ result[key] = (typeof value === 'object' && typeof base[key] === 'object') ? changes(value, base[key]) : value;
30
+ }
31
+ });
32
+ }
33
+ return changes(obj, baseObj);
34
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "./build"
6
+ },
7
+ "exclude": [
8
+ "**/__tests__",
9
+ "build",
10
+ "node_modules"
11
+ ]
12
+ }
File without changes
File without changes