@node-c/data-redis 1.0.0-alpha64 → 1.0.0-beta1

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 (38) hide show
  1. package/dist/entityService/redis.entity.service.d.ts +3 -2
  2. package/dist/entityService/redis.entity.service.js +3 -2
  3. package/dist/entityService/redis.entity.service.js.map +1 -1
  4. package/dist/repository/redis.repository.module.js +1 -1
  5. package/dist/repository/redis.repository.module.js.map +1 -1
  6. package/dist/repository/redis.repository.service.d.ts +6 -3
  7. package/dist/repository/redis.repository.service.js +83 -20
  8. package/dist/repository/redis.repository.service.js.map +1 -1
  9. package/dist/store/redis.store.module.js +3 -3
  10. package/dist/store/redis.store.module.js.map +1 -1
  11. package/dist/store/redis.store.service.d.ts +2 -1
  12. package/dist/store/redis.store.service.js +6 -6
  13. package/dist/store/redis.store.service.js.map +1 -1
  14. package/package.json +5 -4
  15. package/src/common/definitions/common.constants.ts +10 -0
  16. package/src/common/definitions/index.ts +1 -0
  17. package/src/entityService/index.ts +2 -0
  18. package/src/entityService/redis.entity.service.definitions.ts +71 -0
  19. package/src/entityService/redis.entity.service.spec.ts +190 -0
  20. package/src/entityService/redis.entity.service.ts +291 -0
  21. package/src/index.ts +5 -0
  22. package/src/module/index.ts +2 -0
  23. package/src/module/redis.module.definitions.ts +18 -0
  24. package/src/module/redis.module.spec.ts +80 -0
  25. package/src/module/redis.module.ts +31 -0
  26. package/src/repository/index.ts +3 -0
  27. package/src/repository/redis.repository.definitions.ts +97 -0
  28. package/src/repository/redis.repository.module.spec.ts +60 -0
  29. package/src/repository/redis.repository.module.ts +34 -0
  30. package/src/repository/redis.repository.service.ts +657 -0
  31. package/src/repository/redis.repository.spec.ts +384 -0
  32. package/src/store/index.ts +3 -0
  33. package/src/store/redis.store.definitions.ts +25 -0
  34. package/src/store/redis.store.module.spec.ts +70 -0
  35. package/src/store/redis.store.module.ts +34 -0
  36. package/src/store/redis.store.service.spec.ts +392 -0
  37. package/src/store/redis.store.service.ts +395 -0
  38. package/src/vitest.config.ts +9 -0
@@ -0,0 +1,657 @@
1
+ import { Inject, Injectable } from '@nestjs/common';
2
+
3
+ import {
4
+ AppConfigCommonDataNoSQLValidationSettings,
5
+ AppConfigDataNoSQL,
6
+ ApplicationError,
7
+ ConfigProviderService,
8
+ Constants as CoreConstants,
9
+ GenericObject,
10
+ getNested,
11
+ setNested
12
+ } from '@node-c/core';
13
+
14
+ import { ValidationSchema, registerSchema, validate } from 'class-validator';
15
+ import ld from 'lodash';
16
+ import { v4 as uuid } from 'uuid';
17
+
18
+ import {
19
+ EntitySchema,
20
+ EntitySchemaColumnType,
21
+ FilterItemOptions,
22
+ GetValuesFromResultsOptions,
23
+ PrepareOptions,
24
+ RepositoryFindOptions,
25
+ RepositoryFindPrivateOptions,
26
+ SaveOptions,
27
+ SaveOptionsOnConflict
28
+ } from './redis.repository.definitions';
29
+
30
+ import { Constants } from '../common/definitions';
31
+ import { RedisStoreService } from '../store';
32
+
33
+ // TODO: support "paranoid" mode
34
+ // TODO: support complex filtering, not just equality
35
+ // TODO: support indexing
36
+ @Injectable()
37
+ export class RedisRepositoryService<Entity> {
38
+ protected _columnNames: string[];
39
+ protected _innerPrimaryKeys: string[];
40
+ protected _innerPrimaryKeysMap: GenericObject<boolean>;
41
+ protected _primaryKeys: string[];
42
+ protected _primaryKeysMap: GenericObject<boolean>;
43
+ protected defaultTTL?: number;
44
+ protected defaultIndividualSearchEnabled: boolean;
45
+ protected experimentalDeletionEnabled: boolean = false;
46
+ protected storeDelimiter: string;
47
+ protected validationSchemaProperties: ValidationSchema['properties'];
48
+ protected validationSettings: AppConfigCommonDataNoSQLValidationSettings;
49
+
50
+ public get columnNames(): string[] {
51
+ return this._columnNames;
52
+ }
53
+ public get innerPrimaryKeys(): string[] {
54
+ return this._innerPrimaryKeys;
55
+ }
56
+ public get innerPrimaryKeysMap(): GenericObject<boolean> {
57
+ return this._innerPrimaryKeysMap;
58
+ }
59
+ public get dataModuleName(): string {
60
+ return this._dataModuleName;
61
+ }
62
+ public get primaryKeys(): string[] {
63
+ return this._primaryKeys;
64
+ }
65
+ public get primaryKeysMap(): GenericObject<boolean> {
66
+ return this._primaryKeysMap;
67
+ }
68
+
69
+ constructor(
70
+ protected configProvider: ConfigProviderService,
71
+ @Inject(CoreConstants.DATA_MODULE_NAME)
72
+ protected _dataModuleName: string,
73
+ @Inject(Constants.REDIS_REPOSITORY_SCHEMA)
74
+ protected schema: EntitySchema,
75
+ // eslint-disable-next-line no-unused-vars
76
+ protected store: RedisStoreService
77
+ ) {
78
+ const { defaultIndividualSearchEnabled, defaultTTL, storeDelimiter, settingsPerEntity } = configProvider.config
79
+ .data[_dataModuleName] as AppConfigDataNoSQL;
80
+ const { columns, name: entityName } = schema;
81
+ const columnNames: string[] = [];
82
+ const innerPrimaryKeys: string[] = [];
83
+ const innerPrimaryKeysMap: GenericObject<boolean> = {};
84
+ const primaryKeys: string[] = [];
85
+ const primaryKeysMap: GenericObject<boolean> = {};
86
+ const validationSchemaProperties: ValidationSchema['properties'] = {};
87
+ for (const columnName in columns) {
88
+ const { isInnerPrimary, primary, primaryOrder, validationProperties } = columns[columnName];
89
+ columnNames.push(columnName);
90
+ if (primary) {
91
+ if (typeof primaryOrder === 'undefined') {
92
+ throw new ApplicationError(
93
+ `At schema "${entityName}", column "${columnName}": the field "primaryOrder" is required for primary key columns.`
94
+ );
95
+ }
96
+ primaryKeys.push(columnName);
97
+ primaryKeysMap[columnName] = true;
98
+ } else if (isInnerPrimary) {
99
+ innerPrimaryKeys.push(columnName);
100
+ innerPrimaryKeysMap[columnName] = true;
101
+ }
102
+ if (validationProperties) {
103
+ validationSchemaProperties[columnName] = validationProperties;
104
+ }
105
+ }
106
+ this._columnNames = columnNames;
107
+ this._innerPrimaryKeys = innerPrimaryKeys;
108
+ this._innerPrimaryKeysMap = innerPrimaryKeysMap;
109
+ this._primaryKeys = primaryKeys.sort(
110
+ (columnName0, columnName1) => columns[columnName0].primaryOrder! - columns[columnName1].primaryOrder!
111
+ );
112
+ this._primaryKeysMap = primaryKeysMap;
113
+ this.defaultTTL = settingsPerEntity?.[entityName]?.ttl || defaultTTL;
114
+ if (typeof settingsPerEntity?.[entityName]?.defaultIndividualSearchEnabled !== 'undefined') {
115
+ this.defaultIndividualSearchEnabled = settingsPerEntity?.[entityName]?.defaultIndividualSearchEnabled;
116
+ } else if (typeof defaultIndividualSearchEnabled !== 'undefined') {
117
+ this.defaultIndividualSearchEnabled = defaultIndividualSearchEnabled;
118
+ } else {
119
+ this.defaultIndividualSearchEnabled = false;
120
+ }
121
+ this.storeDelimiter = storeDelimiter || Constants.DEFAULT_STORE_DELIMITER;
122
+ this.validationSchemaProperties = validationSchemaProperties;
123
+ registerSchema({ name: entityName, properties: validationSchemaProperties });
124
+ }
125
+
126
+ // protected async delete
127
+
128
+ async find<ResultItem extends Entity | string = Entity>(
129
+ options: RepositoryFindOptions,
130
+ privateOptions?: RepositoryFindPrivateOptions
131
+ ): Promise<{ items: ResultItem[]; more: boolean }> {
132
+ const { primaryKeys, schema, store, storeDelimiter } = this;
133
+ const { name: entityName, storeKey: entityStoreKey } = schema;
134
+ const { filters, findAll, individualSearch, withValues: optWithValues } = options;
135
+ const { requirePrimaryKeys } = privateOptions || {};
136
+ const individualSearchEnabled =
137
+ typeof individualSearch !== 'undefined' ? individualSearch : this.defaultIndividualSearchEnabled;
138
+ const primaryKeyFiltersToForceCheck: GenericObject<boolean> = {};
139
+ const storeEntityKeys: string[] = [];
140
+ const withValues = typeof optWithValues === 'undefined' || optWithValues === true ? true : false;
141
+ let hasNonPrimaryKeyFilters = false;
142
+ let primaryKeyFiltersCount = 0;
143
+ if (filters && Object.keys(filters).length) {
144
+ // set up the construction of the store keys by primary keys
145
+ storeEntityKeys.push('');
146
+ primaryKeys.forEach(field => {
147
+ const value = filters[field];
148
+ if (typeof value !== 'undefined' && typeof value !== 'object' && (typeof value !== 'string' || value.length)) {
149
+ primaryKeyFiltersCount++;
150
+ storeEntityKeys.forEach((_key, keyIndex) => {
151
+ storeEntityKeys[keyIndex] += `${storeDelimiter}${value}`;
152
+ });
153
+ return;
154
+ }
155
+ if (value instanceof Array) {
156
+ const finalValues: (string | number)[] = [];
157
+ value.forEach(valueItem => {
158
+ if (
159
+ (typeof valueItem === 'string' && !valueItem.length) ||
160
+ (typeof valueItem !== 'string' && typeof valueItem !== 'number')
161
+ ) {
162
+ return;
163
+ }
164
+ finalValues.push(valueItem);
165
+ });
166
+ if (finalValues.length) {
167
+ // TODO: this will only work if the previous keys haven't been arrays
168
+ if (individualSearchEnabled) {
169
+ if (storeEntityKeys.length <= 1) {
170
+ const baseStoreEntityKey = storeEntityKeys[0] || '';
171
+ primaryKeyFiltersCount++;
172
+ finalValues.forEach((finalValue, finalValueIndex) => {
173
+ const fullFinalValue = `${baseStoreEntityKey}${storeDelimiter}${finalValue}`;
174
+ if (typeof storeEntityKeys[finalValueIndex] === 'undefined') {
175
+ storeEntityKeys.push(fullFinalValue);
176
+ return;
177
+ }
178
+ storeEntityKeys[finalValueIndex] = fullFinalValue;
179
+ });
180
+ return;
181
+ }
182
+ } else {
183
+ hasNonPrimaryKeyFilters = true;
184
+ primaryKeyFiltersToForceCheck[field] = true;
185
+ storeEntityKeys[0] += `${storeDelimiter}*`;
186
+ return;
187
+ }
188
+ }
189
+ }
190
+ if (requirePrimaryKeys) {
191
+ throw new ApplicationError(
192
+ `[RedisRepositoryService ${entityName}][Find Error]: ` +
193
+ `The primary key field ${field} is required when requirePrimaryKeys is set to true.`
194
+ );
195
+ }
196
+ if (individualSearchEnabled) {
197
+ throw new ApplicationError(
198
+ `[RedisRepositoryService ${entityName}][Find Error]: ` +
199
+ `The primary key field ${field} is required when individualSearchEnabled ` +
200
+ 'is set to true.'
201
+ );
202
+ }
203
+ storeEntityKeys[0] += `${storeDelimiter}*`;
204
+ });
205
+ if (!hasNonPrimaryKeyFilters) {
206
+ hasNonPrimaryKeyFilters = primaryKeyFiltersCount === Object.keys(filters).length;
207
+ }
208
+ } else if (!findAll) {
209
+ throw new ApplicationError(
210
+ `[RedisRepositoryService ${entityName}][Error]: ` +
211
+ 'Either filters or findAll is required when calling the find method.'
212
+ );
213
+ }
214
+ // findAll logic:
215
+ // if doing an inidividual search, go through the store keys one by one; all PKs are required;
216
+ // if doing a wildcard search, iterate a cursor through the whole database until returned to the start
217
+ if (findAll) {
218
+ if (individualSearchEnabled && !primaryKeyFiltersCount && primaryKeys.length) {
219
+ throw new ApplicationError(
220
+ `[RedisRepositoryService ${entityName}][Error]: ` +
221
+ 'Primary key filters are required when findAll and individualSearchEnabled ' +
222
+ 'are enabled in the find method.'
223
+ );
224
+ }
225
+ let initialResults: ResultItem[] = [];
226
+ // get the base results
227
+ if (individualSearchEnabled) {
228
+ initialResults = (await Promise.all(
229
+ storeEntityKeys.map(key => store.get(`${entityStoreKey}${key}`, { parseToJSON: true }))
230
+ )) as ResultItem[];
231
+ } else {
232
+ // TODO: if no filters are provided, this will not return anything
233
+ // TODO: (reply, some point later) WDYIM, Rumen?
234
+ const scanData = await store.scan(`${entityStoreKey}${storeEntityKeys[0]}`, {
235
+ parseToJSON: true,
236
+ scanAll: findAll,
237
+ withValues
238
+ });
239
+ initialResults = scanData.values as ResultItem[];
240
+ }
241
+ // filter the base results by inner keys, as well as retrieve items from arrays and nested items
242
+ return {
243
+ items: this.getValuesFromResults(initialResults, {
244
+ filters,
245
+ hasNonPrimaryKeyFilters,
246
+ primaryKeyFiltersToForceCheck
247
+ }).resultItems,
248
+ more: false
249
+ };
250
+ }
251
+ // non-findAll logic:
252
+ // if doing an inidividual search, go through the store keys one by one; all PKs are required;
253
+ // if doing a wildcard search, iterate a cursor through the whole database until the pagination end is reached
254
+ const { page, perPage } = options || {};
255
+ // for non-individual search, we'll only have the first key anyway
256
+ // TODO: check whether the above is true nad apply it to findAll if it isn't
257
+ const [storeEntityKey] = storeEntityKeys;
258
+ const count: number = perPage || 100;
259
+ const limit = count + 1;
260
+ let cursor = (page ? page - 1 : 0) * count;
261
+ let more = false;
262
+ let results: ResultItem[] = [];
263
+ while (results.length < limit) {
264
+ let endReached = false;
265
+ let iterationResults: ResultItem[] = [];
266
+ // get the base results
267
+ if (individualSearchEnabled) {
268
+ const iterationLimit = cursor + limit;
269
+ const iterationPromises: Promise<ResultItem>[] = [];
270
+ for (let i = cursor; i < iterationLimit; i++) {
271
+ const key = storeEntityKeys[i];
272
+ if (!key) {
273
+ endReached = true;
274
+ break;
275
+ }
276
+ iterationPromises.push(store.get(`${entityStoreKey}${key}`, { parseToJSON: true }));
277
+ }
278
+ iterationResults = (await Promise.all(iterationPromises)) as ResultItem[];
279
+ cursor = iterationLimit;
280
+ } else {
281
+ const { cursor: newCursor, values: innerResults } = await store.scan(`${entityStoreKey}${storeEntityKey}`, {
282
+ count,
283
+ cursor,
284
+ parseToJSON: true,
285
+ scanAll: false,
286
+ withValues
287
+ });
288
+ iterationResults = innerResults as ResultItem[];
289
+ if (newCursor === 0) {
290
+ endReached = true;
291
+ } else {
292
+ cursor = newCursor;
293
+ }
294
+ }
295
+ // filter the base results by inner keys, as well as retrieve items from arrays and nested items;
296
+ // the beauty of this approach is that it follows the rules of the pagination regardless of whether
297
+ // the results are coming from nested items or not
298
+ results = results.concat(
299
+ this.getValuesFromResults(iterationResults, {
300
+ filters,
301
+ hasNonPrimaryKeyFilters,
302
+ primaryKeyFiltersToForceCheck
303
+ }).resultItems
304
+ );
305
+ if (endReached) {
306
+ break;
307
+ }
308
+ }
309
+ // determine whether this is the end of the pagination
310
+ if (results.length > count) {
311
+ more = true;
312
+ results = results.slice(0, count);
313
+ }
314
+ return { items: results, more };
315
+ }
316
+
317
+ protected filterItem<Item>(item: Item, filters: GenericObject<unknown>, options?: FilterItemOptions): boolean {
318
+ if (typeof item === 'undefined' || item === null) {
319
+ return false;
320
+ }
321
+ const { keysToSkip, skippableKeysToForceCheck } = options || {};
322
+ let filterResult = true;
323
+ for (const key in filters) {
324
+ if (keysToSkip?.[key] && !skippableKeysToForceCheck?.[key]) {
325
+ continue;
326
+ }
327
+ const filterValue = filters[key];
328
+ const itemValue = (item as GenericObject<unknown>)[key];
329
+ if (filterValue instanceof Array) {
330
+ if (!filterValue.includes(itemValue)) {
331
+ filterResult = false;
332
+ break;
333
+ }
334
+ continue;
335
+ }
336
+ // TODO: filter operators
337
+ if (filterValue !== itemValue) {
338
+ filterResult = false;
339
+ break;
340
+ }
341
+ }
342
+ return filterResult;
343
+ }
344
+
345
+ // TODO: reduce the large numbers of whole-array iterations by combinging the array methods used
346
+ // here into a big for-loop
347
+ protected getValuesFromResults<ResultItem>(
348
+ inputData: ResultItem[],
349
+ options?: GetValuesFromResultsOptions
350
+ ): { indexes: number[]; resultItems: ResultItem[] } {
351
+ const { primaryKeysMap, schema } = this;
352
+ const { isArray, nestedObjectContainerPath } = schema;
353
+ const { filters, flattenArray, hasNonPrimaryKeyFilters, primaryKeyFiltersToForceCheck } = options || {};
354
+ const filteredResultIndexes: number[] = [];
355
+ const filteredResults: ResultItem[] = [];
356
+ let initialResults = [...inputData];
357
+ if (nestedObjectContainerPath) {
358
+ initialResults = initialResults.map(item => {
359
+ if (item && typeof item === 'object' && !(item instanceof Date)) {
360
+ return getNested(item, nestedObjectContainerPath, { removeNestedFieldEscapeSign: true }).unifiedValue;
361
+ }
362
+ return item;
363
+ }) as ResultItem[];
364
+ }
365
+ // TODO: account for the indexes when flattenning
366
+ if (isArray && (flattenArray || typeof flattenArray === 'undefined')) {
367
+ initialResults = initialResults.flat() as ResultItem[];
368
+ }
369
+ if (!hasNonPrimaryKeyFilters || !filters) {
370
+ initialResults.forEach((resultItem, resultItemIndex) => {
371
+ if (typeof resultItem !== 'undefined' && resultItem !== null) {
372
+ filteredResultIndexes.push(resultItemIndex);
373
+ filteredResults.push(resultItem);
374
+ }
375
+ });
376
+ } else {
377
+ // filter by the results' object data
378
+ initialResults.forEach((resultItem, resultItemIndex) => {
379
+ const filtered = this.filterItem<ResultItem>(resultItem, filters, {
380
+ keysToSkip: primaryKeysMap,
381
+ skippableKeysToForceCheck: primaryKeyFiltersToForceCheck
382
+ });
383
+ if (filtered) {
384
+ filteredResultIndexes.push(resultItemIndex);
385
+ filteredResults.push(resultItem);
386
+ }
387
+ });
388
+ }
389
+ return { indexes: filteredResultIndexes, resultItems: filteredResults };
390
+ }
391
+
392
+ protected async prepare(
393
+ data: Entity | Entity[],
394
+ options?: PrepareOptions
395
+ ): Promise<{ data: Entity | Entity[]; storeEntityKey: string }> {
396
+ const { columnNames, primaryKeys, schema, store, storeDelimiter } = this;
397
+ const { columns, isArray, name: entityName, storeKey: entityStoreKey } = schema;
398
+ const opt = options || ({} as PrepareOptions);
399
+ const { generatePrimaryKeys, onConflict: optOnConflict, validate: optValidate } = opt;
400
+ const onConflict = optOnConflict || SaveOptionsOnConflict.ThrowError;
401
+ let allPKValuesExist = true;
402
+ let preparedData = ld.cloneDeep(data) as GenericObject | GenericObject[];
403
+ let storeEntityKey = '';
404
+ const preparedDataForPrimaryKeyFilters = (isArray && data instanceof Array ? data[0] : data) as GenericObject;
405
+ // set up the construction of the store keys by primary keys
406
+ // additionally, perform the generation of primary keys, if doing a create opearation
407
+ for (const columnName of primaryKeys) {
408
+ const { generated, type } = columns[columnName];
409
+ const value = preparedDataForPrimaryKeyFilters[columnName];
410
+ const valueExists = !(
411
+ typeof value === 'undefined' ||
412
+ (typeof value === 'string' && !value.length) ||
413
+ typeof value === 'object'
414
+ );
415
+ if (generated) {
416
+ if (valueExists) {
417
+ storeEntityKey += `${value}${storeDelimiter}`;
418
+ continue;
419
+ }
420
+ if (allPKValuesExist) {
421
+ allPKValuesExist = false;
422
+ }
423
+ if (!generatePrimaryKeys || isArray) {
424
+ throw new ApplicationError(
425
+ `[RedisRepositoryService ${entityName}][Validation Error]: ` +
426
+ `A value is required for generated PK column ${columnName} when the generatePrimaryKeys is set to false ` +
427
+ 'or isArray is set to true.'
428
+ );
429
+ }
430
+ if (type === EntitySchemaColumnType.Integer) {
431
+ let currentMaxValue =
432
+ (await store.get<number>(`${entityStoreKey}${storeDelimiter}increment${storeDelimiter}${columnName}`, {
433
+ parseToJSON: true
434
+ })) || 0;
435
+ currentMaxValue++;
436
+ await store.set(`${entityStoreKey}${storeDelimiter}increment${storeDelimiter}${columnName}`, currentMaxValue);
437
+ preparedDataForPrimaryKeyFilters[columnName] = currentMaxValue;
438
+ storeEntityKey += `${currentMaxValue}${storeDelimiter}`;
439
+ continue;
440
+ }
441
+ if (type === EntitySchemaColumnType.UUIDV4) {
442
+ let newValue = uuid();
443
+ if (storeDelimiter === '-') {
444
+ newValue = newValue.replace(/-/g, '_');
445
+ }
446
+ (preparedData as GenericObject)[columnName] = newValue;
447
+ storeEntityKey += `${newValue}${storeDelimiter}`;
448
+ continue;
449
+ }
450
+ throw new ApplicationError(
451
+ `[RedisRepositoryService ${entityName}][Validation Error]: ` +
452
+ `Unrecognized type "${type}" for PK column ${columnName}`
453
+ );
454
+ }
455
+ if (!valueExists) {
456
+ throw new ApplicationError(
457
+ `[RedisRepositoryService ${entityName}][Validation Error]: ` +
458
+ `A value is required for non-generated PK column ${columnName}`
459
+ );
460
+ }
461
+ storeEntityKey += `${value}${storeDelimiter}`;
462
+ }
463
+ if (storeEntityKey.endsWith(storeDelimiter)) {
464
+ storeEntityKey = storeEntityKey.substring(0, storeEntityKey.length - storeDelimiter.length);
465
+ }
466
+ if (optValidate) {
467
+ const validationErrors = await validate(entityName, data as GenericObject<unknown>);
468
+ if (validationErrors.length) {
469
+ throw new ApplicationError(
470
+ `[RedisRepositoryService ${entityName}][Validation Error]: ${validationErrors.join('\n')}`
471
+ );
472
+ }
473
+ }
474
+ // TODO: make cases other than SaveOptionsOnConflict.DoNothing work with isArray
475
+ if ((onConflict !== SaveOptionsOnConflict.DoNothing || isArray) && allPKValuesExist) {
476
+ const hasValue = await store.get<string | undefined>(storeEntityKey, { withValues: false });
477
+ if (hasValue) {
478
+ if (onConflict === SaveOptionsOnConflict.ThrowError) {
479
+ throw new ApplicationError(
480
+ `[RedisRepositoryService ${entityName}][Unique Error]: An entry already exists for key ${storeEntityKey}.`
481
+ );
482
+ }
483
+ const existingData = await store.get<GenericObject<unknown> | GenericObject<unknown>[]>(storeEntityKey, {
484
+ parseToJSON: true
485
+ });
486
+ if (onConflict === SaveOptionsOnConflict.Update) {
487
+ // TODO: make this work using getValuesFromResults
488
+ preparedData = ld.merge(existingData, preparedData);
489
+ } else if (onConflict === SaveOptionsOnConflict.DoNothing && isArray) {
490
+ if (existingData instanceof Array && existingData.length) {
491
+ const innerFilters: GenericObject = {};
492
+ columnNames.forEach(fieldName => {
493
+ const fieldValue = data[fieldName as keyof typeof data];
494
+ if (
495
+ typeof fieldValue !== 'undefined' &&
496
+ fieldValue !== null &&
497
+ (typeof fieldValue !== 'string' || fieldValue.length)
498
+ ) {
499
+ innerFilters[fieldName] = fieldValue;
500
+ }
501
+ });
502
+ if (!Object.keys(innerFilters).length) {
503
+ throw new ApplicationError(
504
+ `[RedisRepositoryService ${entityName}][Execution Error]: ` +
505
+ 'Inner filters are required when search inside nested arrays.'
506
+ );
507
+ }
508
+ const innerData = this.getValuesFromResults(existingData, {
509
+ filters: innerFilters,
510
+ flattenArray: false
511
+ });
512
+ if (innerData.resultItems.length) {
513
+ innerData.resultItems.forEach((resultItem, resultItemIndex) => {
514
+ ld.set(existingData, innerData.indexes[resultItemIndex], resultItem);
515
+ });
516
+ preparedData = existingData;
517
+ } else {
518
+ preparedData = existingData.concat(preparedData instanceof Array ? preparedData : [preparedData]);
519
+ }
520
+ }
521
+ // WARNING: this disregards the current values if they're not an array
522
+ else if (!(preparedData instanceof Array)) {
523
+ preparedData = [preparedData];
524
+ }
525
+ } else {
526
+ throw new ApplicationError(
527
+ `[RedisRepositoryService ${entityName}][Execution Error]: ` +
528
+ `Invalid value "${onConflict}" provided for onConflict.`
529
+ );
530
+ }
531
+ }
532
+ }
533
+ return { data: preparedData as Entity | Entity[], storeEntityKey };
534
+ }
535
+
536
+ async save<ResultItem extends Entity | string = Entity>(
537
+ data: Entity | Entity[],
538
+ options?: SaveOptions
539
+ ): Promise<ResultItem[]> {
540
+ const { defaultTTL, experimentalDeletionEnabled, innerPrimaryKeys, primaryKeysMap, schema, store, storeDelimiter } =
541
+ this;
542
+ const { isArray, nestedObjectContainerPath, storeKey: entityStoreKey } = schema;
543
+ const {
544
+ delete: optDelete,
545
+ generatePrimaryKeys,
546
+ onConflict,
547
+ transactionId,
548
+ ttl,
549
+ validate
550
+ } = options || ({} as SaveOptions);
551
+ const actualData = data instanceof Array ? data : [data];
552
+ if (optDelete) {
553
+ const prepareOptions: PrepareOptions = {
554
+ generatePrimaryKeys: false,
555
+ onConflict: SaveOptionsOnConflict.DoNothing,
556
+ validate: false
557
+ };
558
+ const deleteKeys: string[] = [];
559
+ for (const i in actualData) {
560
+ deleteKeys.push(
561
+ `${entityStoreKey}${storeDelimiter}${(await this.prepare(actualData[i], prepareOptions)).storeEntityKey}`
562
+ );
563
+ }
564
+ // delete from arrays and nestedObjects;
565
+ // this is kind of a repeat of find, but with the key paths included and with dedicated
566
+ // filtering by inner primary keys
567
+ if (
568
+ experimentalDeletionEnabled &&
569
+ (isArray || nestedObjectContainerPath) &&
570
+ innerPrimaryKeys.length &&
571
+ deleteKeys.length
572
+ ) {
573
+ const results = await Promise.all(deleteKeys.map(key => store.get<ResultItem>(key, { parseToJSON: true })));
574
+ // const deletePromises: Promise<unknown>[] = [];
575
+ const newResults: ResultItem[] = [];
576
+ results.forEach((resultItem, resultItemIndex) => {
577
+ if (!resultItem || typeof resultItem !== 'object' || resultItem instanceof Date) {
578
+ newResults.push(resultItem);
579
+ return;
580
+ }
581
+ let innerPaths: string[] = [];
582
+ let innerValues: ResultItem[] = [];
583
+ if (nestedObjectContainerPath) {
584
+ const { paths, values } = getNested(resultItem, nestedObjectContainerPath, {
585
+ removeNestedFieldEscapeSign: true
586
+ });
587
+ innerPaths = paths;
588
+ innerValues = values as ResultItem[];
589
+ // TODO: combine with isArray
590
+ } else if (isArray) {
591
+ innerValues = resultItem as ResultItem[];
592
+ } else {
593
+ innerValues = [resultItem];
594
+ }
595
+ // TODO: complete this logic
596
+ innerValues.forEach((innerValue, innerValueIndex) => {
597
+ const shouldDelete = this.filterItem<ResultItem>(
598
+ innerValue,
599
+ actualData[resultItemIndex] as GenericObject<unknown>,
600
+ {
601
+ keysToSkip: primaryKeysMap
602
+ }
603
+ );
604
+ if (!shouldDelete) {
605
+ return;
606
+ }
607
+ if (innerPaths[innerValueIndex]) {
608
+ setNested(results[resultItemIndex], innerPaths[innerValueIndex], undefined, {
609
+ removeNestedFieldEscapeSign: true
610
+ });
611
+ return;
612
+ }
613
+ if (isArray) {
614
+ setNested(results, `${resultItemIndex}`, (resultItem as ResultItem[]).splice(innerValueIndex, 1), {
615
+ removeNestedFieldEscapeSign: true
616
+ });
617
+ return;
618
+ }
619
+ results.splice(resultItemIndex, 1);
620
+ });
621
+ });
622
+ return [];
623
+ }
624
+ // default use case - regular people storing regular objects in redis
625
+ if (deleteKeys.length) {
626
+ await store.delete(deleteKeys, { transactionId });
627
+ }
628
+ return deleteKeys as ResultItem[];
629
+ }
630
+ // TODO: create and update in arrays and nestedObjects
631
+ // TODO: differenatiate between create and update based on generatePrimaryKeys
632
+ const prepareOptions: PrepareOptions = {
633
+ generatePrimaryKeys,
634
+ onConflict,
635
+ validate
636
+ };
637
+ let results: Entity[] = [];
638
+ if (isArray) {
639
+ const { data: validatedEntity, storeEntityKey } = await this.prepare(actualData, prepareOptions);
640
+ await store.set(`${entityStoreKey}${storeDelimiter}${storeEntityKey}`, validatedEntity, {
641
+ transactionId,
642
+ ttl: ttl || defaultTTL
643
+ });
644
+ results = validatedEntity as Entity[];
645
+ } else {
646
+ for (const i in actualData) {
647
+ const { data: validatedEntity, storeEntityKey } = await this.prepare(actualData[i], prepareOptions);
648
+ await store.set(`${entityStoreKey}${storeDelimiter}${storeEntityKey}`, validatedEntity, {
649
+ transactionId,
650
+ ttl: ttl || defaultTTL
651
+ });
652
+ results.push(validatedEntity as Entity);
653
+ }
654
+ }
655
+ return results as ResultItem[];
656
+ }
657
+ }