@node-c/data-redis 1.0.0-alpha64 → 1.0.0-beta0
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/dist/repository/redis.repository.module.js +1 -1
- package/dist/repository/redis.repository.module.js.map +1 -1
- package/dist/repository/redis.repository.service.d.ts +6 -3
- package/dist/repository/redis.repository.service.js +83 -20
- package/dist/repository/redis.repository.service.js.map +1 -1
- package/dist/store/redis.store.module.js +1 -1
- package/dist/store/redis.store.module.js.map +1 -1
- package/dist/store/redis.store.service.js +1 -1
- package/dist/store/redis.store.service.js.map +1 -1
- package/package.json +4 -4
- package/src/common/definitions/common.constants.ts +10 -0
- package/src/common/definitions/index.ts +1 -0
- package/src/entityService/index.ts +2 -0
- package/src/entityService/redis.entity.service.definitions.ts +73 -0
- package/src/entityService/redis.entity.service.spec.ts +190 -0
- package/src/entityService/redis.entity.service.ts +291 -0
- package/src/index.ts +5 -0
- package/src/module/index.ts +2 -0
- package/src/module/redis.module.definitions.ts +18 -0
- package/src/module/redis.module.spec.ts +80 -0
- package/src/module/redis.module.ts +31 -0
- package/src/repository/index.ts +3 -0
- package/src/repository/redis.repository.definitions.ts +97 -0
- package/src/repository/redis.repository.module.spec.ts +60 -0
- package/src/repository/redis.repository.module.ts +34 -0
- package/src/repository/redis.repository.service.ts +657 -0
- package/src/repository/redis.repository.spec.ts +384 -0
- package/src/store/index.ts +3 -0
- package/src/store/redis.store.definitions.ts +25 -0
- package/src/store/redis.store.module.spec.ts +70 -0
- package/src/store/redis.store.module.ts +34 -0
- package/src/store/redis.store.service.spec.ts +392 -0
- package/src/store/redis.store.service.ts +391 -0
- package/src/vitest.config.ts +9 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { DataFindResults } from '@node-c/core';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
BulkCreateOptions,
|
|
6
|
+
CreateOptions,
|
|
7
|
+
DeleteOptions,
|
|
8
|
+
FindOneOptions,
|
|
9
|
+
RedisEntityService,
|
|
10
|
+
UpdateOptions
|
|
11
|
+
} from './index';
|
|
12
|
+
|
|
13
|
+
import type { RedisRepositoryService } from '../repository';
|
|
14
|
+
import type { RedisStoreService } from '../store';
|
|
15
|
+
|
|
16
|
+
interface DummyEntity {
|
|
17
|
+
createdAt?: Date;
|
|
18
|
+
id: string;
|
|
19
|
+
updatedAt?: Date;
|
|
20
|
+
value?: number;
|
|
21
|
+
}
|
|
22
|
+
const entity1: DummyEntity = { createdAt: new Date(), id: '1', updatedAt: new Date(), value: 10 };
|
|
23
|
+
const entity2: DummyEntity = { createdAt: new Date(), id: '2', updatedAt: new Date(), value: 20 };
|
|
24
|
+
|
|
25
|
+
describe('RedisEntityService', () => {
|
|
26
|
+
let dummyRepository: Partial<RedisRepositoryService<DummyEntity>>;
|
|
27
|
+
let dummyStore: Partial<RedisStoreService>;
|
|
28
|
+
let service: RedisEntityService<DummyEntity>;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
dummyRepository = {
|
|
31
|
+
save: vi.fn(),
|
|
32
|
+
find: vi.fn()
|
|
33
|
+
};
|
|
34
|
+
dummyStore = {
|
|
35
|
+
createTransaction: vi.fn(),
|
|
36
|
+
endTransaction: vi.fn().mockResolvedValue(undefined)
|
|
37
|
+
};
|
|
38
|
+
service = new RedisEntityService(
|
|
39
|
+
dummyRepository as unknown as RedisRepositoryService<DummyEntity>,
|
|
40
|
+
dummyStore as unknown as RedisStoreService
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('bulkCreate', () => {
|
|
45
|
+
it('returns repository.save result when no options provided', async () => {
|
|
46
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue([entity1, entity2]);
|
|
47
|
+
const res = await service.bulkCreate([entity1, entity2]);
|
|
48
|
+
expect(dummyRepository.save).toHaveBeenCalledWith([entity1, entity2], { transactionId: undefined });
|
|
49
|
+
expect(res).toEqual([entity1, entity2]);
|
|
50
|
+
});
|
|
51
|
+
it('returns repository.save result when transactionId provided', async () => {
|
|
52
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue([entity1, entity2]);
|
|
53
|
+
const opts: BulkCreateOptions = { transactionId: 'tx1' };
|
|
54
|
+
const res = await service.bulkCreate([entity1, entity2], opts);
|
|
55
|
+
expect(dummyRepository.save).toHaveBeenCalledWith([entity1, entity2], { transactionId: 'tx1' });
|
|
56
|
+
expect(res).toEqual([entity1, entity2]);
|
|
57
|
+
});
|
|
58
|
+
it('wraps call in transaction when forceTransaction is true and no transactionId', async () => {
|
|
59
|
+
(dummyStore.createTransaction as ReturnType<typeof vi.fn>).mockReturnValue('tx2');
|
|
60
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue([entity1]);
|
|
61
|
+
const opts: BulkCreateOptions = { forceTransaction: true };
|
|
62
|
+
const res = await service.bulkCreate([entity1], opts);
|
|
63
|
+
expect(dummyStore.createTransaction).toHaveBeenCalled();
|
|
64
|
+
expect(dummyRepository.save).toHaveBeenCalledWith([entity1], { transactionId: 'tx2' });
|
|
65
|
+
expect(dummyStore.endTransaction).toHaveBeenCalledWith('tx2');
|
|
66
|
+
expect(res).toEqual([entity1]);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('create', () => {
|
|
71
|
+
it('returns first element of repository.save when no options are provided', async () => {
|
|
72
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue([entity1]);
|
|
73
|
+
const res = await service.create(entity1);
|
|
74
|
+
expect(dummyRepository.save).toHaveBeenCalledWith(entity1, { transactionId: undefined });
|
|
75
|
+
expect(res).toEqual(entity1);
|
|
76
|
+
});
|
|
77
|
+
it('returns first element of repository.save when transactionId provided', async () => {
|
|
78
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue([entity1]);
|
|
79
|
+
const opts: CreateOptions = { transactionId: 'tx3' };
|
|
80
|
+
const res = await service.create(entity1, opts);
|
|
81
|
+
expect(dummyRepository.save).toHaveBeenCalledWith(entity1, { transactionId: 'tx3' });
|
|
82
|
+
expect(res).toEqual(entity1);
|
|
83
|
+
});
|
|
84
|
+
it('wraps call in transaction when forceTransaction is true and no transactionId', async () => {
|
|
85
|
+
(dummyStore.createTransaction as ReturnType<typeof vi.fn>).mockReturnValue('tx4');
|
|
86
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue([entity1]);
|
|
87
|
+
const opts: CreateOptions = { forceTransaction: true };
|
|
88
|
+
const res = await service.create(entity1, opts);
|
|
89
|
+
expect(dummyStore.createTransaction).toHaveBeenCalled();
|
|
90
|
+
expect(dummyRepository.save).toHaveBeenCalledWith(entity1, { transactionId: 'tx4' });
|
|
91
|
+
expect(dummyStore.endTransaction).toHaveBeenCalledWith('tx4');
|
|
92
|
+
expect(res).toEqual(entity1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('count', () => {
|
|
97
|
+
it('returns the length of repository.find result', async () => {
|
|
98
|
+
(dummyRepository.find as ReturnType<typeof vi.fn>).mockResolvedValue([entity1, entity2]);
|
|
99
|
+
const cnt = await service.count({ filters: {}, findAll: false });
|
|
100
|
+
expect(dummyRepository.find).toHaveBeenCalledWith({ filters: {}, findAll: false });
|
|
101
|
+
expect(cnt).toEqual(2);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('delete', () => {
|
|
106
|
+
it('wraps call in transaction when forceTransaction is true and no transactionId', async () => {
|
|
107
|
+
(dummyStore.createTransaction as ReturnType<typeof vi.fn>).mockReturnValue('tx5');
|
|
108
|
+
vi.spyOn(service, 'find').mockResolvedValue({ items: [entity1] } as DataFindResults<DummyEntity>);
|
|
109
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue(['key1']);
|
|
110
|
+
const opts: DeleteOptions = { filters: {}, forceTransaction: true };
|
|
111
|
+
const res = await service.delete(opts);
|
|
112
|
+
expect(dummyStore.createTransaction).toHaveBeenCalled();
|
|
113
|
+
expect(dummyRepository.save).toHaveBeenCalledWith([entity1], { delete: true, transactionId: 'tx5' });
|
|
114
|
+
expect(dummyStore.endTransaction).toHaveBeenCalledWith('tx5');
|
|
115
|
+
expect(res).toEqual({ count: 1 });
|
|
116
|
+
});
|
|
117
|
+
it('deletes normally when transactionId provided', async () => {
|
|
118
|
+
vi.spyOn(service, 'find').mockResolvedValue({ items: [entity1, entity2] } as DataFindResults<DummyEntity>);
|
|
119
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue(['k1', 'k2']);
|
|
120
|
+
const opts: DeleteOptions = { filters: {}, transactionId: 'tx6' };
|
|
121
|
+
const res = await service.delete(opts);
|
|
122
|
+
expect(dummyRepository.save).toHaveBeenCalledWith([entity1, entity2], { delete: true, transactionId: 'tx6' });
|
|
123
|
+
expect(res).toEqual({ count: 2 });
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('find', () => {
|
|
128
|
+
it('parses page and perPage and sets "more" flag when items length equals perPage+1', async () => {
|
|
129
|
+
const items = new Array(11).fill(entity1);
|
|
130
|
+
(dummyRepository.find as ReturnType<typeof vi.fn>).mockResolvedValue(items);
|
|
131
|
+
const res = await service.find({ filters: {}, findAll: false, page: 2, perPage: 10 });
|
|
132
|
+
expect(dummyRepository.find).toHaveBeenCalledWith({ filters: {}, findAll: false, page: 2, perPage: 10 });
|
|
133
|
+
expect(res.page).toEqual(2);
|
|
134
|
+
expect(res.perPage).toEqual(10);
|
|
135
|
+
expect(res.more).toBe(true);
|
|
136
|
+
expect(res.items.length).toEqual(10);
|
|
137
|
+
});
|
|
138
|
+
it('sets perPage to items length when findAll is true', async () => {
|
|
139
|
+
(dummyRepository.find as ReturnType<typeof vi.fn>).mockResolvedValue([entity1, entity2]);
|
|
140
|
+
const res = await service.find({ filters: {}, findAll: true });
|
|
141
|
+
expect(dummyRepository.find).toHaveBeenCalledWith({ filters: {}, findAll: true, page: 1, perPage: 10 });
|
|
142
|
+
expect(res.perPage).toEqual(2);
|
|
143
|
+
expect(res.more).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('findOne', () => {
|
|
148
|
+
it('returns the first element when repository.find returns a non-empty array', async () => {
|
|
149
|
+
const expected = { id: '1', value: 1 } as unknown as DummyEntity;
|
|
150
|
+
(dummyRepository.find as ReturnType<typeof vi.fn>).mockResolvedValue([expected]);
|
|
151
|
+
const options: FindOneOptions = { filters: {} };
|
|
152
|
+
const result = await service.findOne(options);
|
|
153
|
+
expect(result).toEqual(expected);
|
|
154
|
+
});
|
|
155
|
+
it('returns null when repository.find returns an empty array', async () => {
|
|
156
|
+
(dummyRepository.find as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
157
|
+
const options: FindOneOptions = { filters: {} };
|
|
158
|
+
const result = await service.findOne(options);
|
|
159
|
+
expect(result).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('update', () => {
|
|
164
|
+
it('wraps call in transaction when forceTransaction is true and no transactionId', async () => {
|
|
165
|
+
(dummyStore.createTransaction as ReturnType<typeof vi.fn>).mockReturnValue('tx7');
|
|
166
|
+
vi.spyOn(service, 'findOne').mockResolvedValue(entity1);
|
|
167
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue([entity2]);
|
|
168
|
+
const opts: UpdateOptions = { filters: {}, forceTransaction: true };
|
|
169
|
+
const res = await service.update(entity2, opts);
|
|
170
|
+
expect(dummyStore.createTransaction).toHaveBeenCalled();
|
|
171
|
+
expect(dummyRepository.save).toHaveBeenCalledWith(expect.any(Object), { transactionId: 'tx7' });
|
|
172
|
+
expect(dummyStore.endTransaction).toHaveBeenCalledWith('tx7');
|
|
173
|
+
expect(res).toEqual({ count: 1, items: [entity2] });
|
|
174
|
+
});
|
|
175
|
+
it('returns update result with count 0 when findOne returns null', async () => {
|
|
176
|
+
vi.spyOn(service, 'findOne').mockResolvedValue(null);
|
|
177
|
+
const opts: UpdateOptions = { filters: {}, transactionId: 'tx8' };
|
|
178
|
+
const res = await service.update(entity2, opts);
|
|
179
|
+
expect(res).toEqual({ count: 0, items: [] });
|
|
180
|
+
});
|
|
181
|
+
it('updates normally when findOne returns an item', async () => {
|
|
182
|
+
vi.spyOn(service, 'findOne').mockResolvedValue(entity1);
|
|
183
|
+
(dummyRepository.save as ReturnType<typeof vi.fn>).mockResolvedValue([entity2]);
|
|
184
|
+
const opts: UpdateOptions = { filters: {}, transactionId: 'tx9' };
|
|
185
|
+
const res = await service.update(entity2, opts);
|
|
186
|
+
expect(dummyRepository.save).toHaveBeenCalledWith(expect.any(Object), { transactionId: 'tx9' });
|
|
187
|
+
expect(res).toEqual({ count: 1, items: [entity2] });
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppConfigCommonDataNoSQLEntityServiceSettings,
|
|
3
|
+
ApplicationError,
|
|
4
|
+
ConfigProviderService,
|
|
5
|
+
DataDeleteResult,
|
|
6
|
+
DataEntityService,
|
|
7
|
+
DataFindResults,
|
|
8
|
+
DataUpdateResult,
|
|
9
|
+
GenericObject,
|
|
10
|
+
ProcessObjectAllowedFieldsType
|
|
11
|
+
} from '@node-c/core';
|
|
12
|
+
|
|
13
|
+
import ld from 'lodash';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
BulkCreateOptions,
|
|
17
|
+
BulkCreatePrivateOptions,
|
|
18
|
+
CountOptions,
|
|
19
|
+
CountPrivateOptions,
|
|
20
|
+
CreateOptions,
|
|
21
|
+
CreatePrivateOptions,
|
|
22
|
+
DeleteOptions,
|
|
23
|
+
DeletePrivateOptions,
|
|
24
|
+
FindOneOptions,
|
|
25
|
+
FindOnePrivateOptions,
|
|
26
|
+
FindOptions,
|
|
27
|
+
FindPrivateOptions,
|
|
28
|
+
ServiceSaveOptions,
|
|
29
|
+
UpdateOptions,
|
|
30
|
+
UpdatePrivateOptions
|
|
31
|
+
} from './redis.entity.service.definitions';
|
|
32
|
+
|
|
33
|
+
import { RedisRepositoryService } from '../repository';
|
|
34
|
+
import { RedisStoreService } from '../store';
|
|
35
|
+
|
|
36
|
+
// TODO: support "pseudo-relations"
|
|
37
|
+
// TODO: support update of multiple items in the update method
|
|
38
|
+
export class RedisEntityService<Entity extends object> extends DataEntityService<Entity> {
|
|
39
|
+
protected settings: AppConfigCommonDataNoSQLEntityServiceSettings;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
// eslint-disable-next-line no-unused-vars
|
|
43
|
+
protected configProvider: ConfigProviderService,
|
|
44
|
+
// eslint-disable-next-line no-unused-vars
|
|
45
|
+
protected repository: RedisRepositoryService<Entity>,
|
|
46
|
+
// eslint-disable-next-line no-unused-vars
|
|
47
|
+
protected store: RedisStoreService
|
|
48
|
+
) {
|
|
49
|
+
super(configProvider, repository.dataModuleName);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async bulkCreate(
|
|
53
|
+
data: Partial<Entity>[],
|
|
54
|
+
options?: BulkCreateOptions,
|
|
55
|
+
privateOptions?: BulkCreatePrivateOptions
|
|
56
|
+
): Promise<Entity[]> {
|
|
57
|
+
const { store } = this;
|
|
58
|
+
const actualOptions = options || {};
|
|
59
|
+
const actualPrivateOptions = privateOptions || {};
|
|
60
|
+
const { forceTransaction, transactionId } = actualOptions;
|
|
61
|
+
if (!transactionId && forceTransaction) {
|
|
62
|
+
const tId = store.createTransaction();
|
|
63
|
+
const result = await this.bulkCreate(data, { ...actualOptions, transactionId: tId }, actualPrivateOptions);
|
|
64
|
+
await store.endTransaction(tId);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
const { processInputAllowedFieldsEnabled, validate } = actualPrivateOptions;
|
|
68
|
+
return await this.save(data, {
|
|
69
|
+
generatePrimaryKeys: true,
|
|
70
|
+
processObjectAllowedFieldsEnabled: processInputAllowedFieldsEnabled,
|
|
71
|
+
transactionId,
|
|
72
|
+
validate
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async count(options: CountOptions, privateOptions?: CountPrivateOptions): Promise<number | undefined> {
|
|
77
|
+
const { repository } = this;
|
|
78
|
+
const { filters, findAll } = options;
|
|
79
|
+
const { allowCountWithoutFilters, processFiltersAllowedFieldsEnabled } = privateOptions || {};
|
|
80
|
+
const parsedFilters = (await this.processObjectAllowedFields<GenericObject>(filters || {}, {
|
|
81
|
+
allowedFields: repository.columnNames,
|
|
82
|
+
isEnabled: processFiltersAllowedFieldsEnabled,
|
|
83
|
+
objectType: ProcessObjectAllowedFieldsType.Filters
|
|
84
|
+
})) as GenericObject;
|
|
85
|
+
if (!allowCountWithoutFilters && !Object.keys(parsedFilters).length) {
|
|
86
|
+
throw new ApplicationError('At least one filter field for counting is required.');
|
|
87
|
+
}
|
|
88
|
+
return (await repository.find({ filters: parsedFilters, findAll, individualSearch: false })).items.length;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async create(data: Partial<Entity>, options?: CreateOptions, privateOptions?: CreatePrivateOptions): Promise<Entity> {
|
|
92
|
+
const { store } = this;
|
|
93
|
+
const actualOptions = options || {};
|
|
94
|
+
const actualPrivateOptions = privateOptions || {};
|
|
95
|
+
const { forceTransaction, transactionId } = actualOptions;
|
|
96
|
+
if (!transactionId && forceTransaction) {
|
|
97
|
+
const tId = store.createTransaction();
|
|
98
|
+
const result = await this.create(data, { ...actualOptions, transactionId: tId }, actualPrivateOptions);
|
|
99
|
+
await store.endTransaction(tId);
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
const { processInputAllowedFieldsEnabled, validate } = actualPrivateOptions;
|
|
103
|
+
return await this.save<Partial<Entity>, Entity>(data instanceof Array ? data[0] : data, {
|
|
104
|
+
generatePrimaryKeys: false,
|
|
105
|
+
processObjectAllowedFieldsEnabled: processInputAllowedFieldsEnabled,
|
|
106
|
+
transactionId,
|
|
107
|
+
validate
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async delete(options: DeleteOptions, privateOptions?: DeletePrivateOptions): Promise<DataDeleteResult<Entity>> {
|
|
112
|
+
const { repository, store } = this;
|
|
113
|
+
const { filters, forceTransaction, returnOriginalItems, transactionId } = options;
|
|
114
|
+
const actualPrivateOptions = privateOptions || {};
|
|
115
|
+
if (!transactionId && forceTransaction) {
|
|
116
|
+
const tId = store.createTransaction();
|
|
117
|
+
const result = await this.delete({ ...options, transactionId: tId }, actualPrivateOptions);
|
|
118
|
+
await store.endTransaction(tId);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
const { processFiltersAllowedFieldsEnabled, requirePrimaryKeys = true } = actualPrivateOptions;
|
|
122
|
+
const parsedFilters = (await this.processObjectAllowedFields<GenericObject>(filters, {
|
|
123
|
+
allowedFields: repository.columnNames,
|
|
124
|
+
isEnabled: processFiltersAllowedFieldsEnabled,
|
|
125
|
+
objectType: ProcessObjectAllowedFieldsType.Filters
|
|
126
|
+
})) as GenericObject;
|
|
127
|
+
if (!Object.keys(parsedFilters).length) {
|
|
128
|
+
throw new ApplicationError('At least one filter field for deleting data is required.');
|
|
129
|
+
}
|
|
130
|
+
const { items: itemsToDelete } = await this.find({ filters, findAll: true }, { requirePrimaryKeys });
|
|
131
|
+
const results: string[] = await this.save(itemsToDelete, {
|
|
132
|
+
delete: true,
|
|
133
|
+
generatePrimaryKeys: false,
|
|
134
|
+
transactionId
|
|
135
|
+
});
|
|
136
|
+
const dataToReturn: DataDeleteResult<Entity> = { count: results.length };
|
|
137
|
+
if (returnOriginalItems) {
|
|
138
|
+
dataToReturn.originalItems = itemsToDelete;
|
|
139
|
+
}
|
|
140
|
+
return dataToReturn;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async find(options: FindOptions, privateOptions?: FindPrivateOptions): Promise<DataFindResults<Entity>> {
|
|
144
|
+
const { repository } = this;
|
|
145
|
+
const {
|
|
146
|
+
filters,
|
|
147
|
+
getTotalCount = true,
|
|
148
|
+
individualSearch,
|
|
149
|
+
page: optPage,
|
|
150
|
+
perPage: optPerPage,
|
|
151
|
+
findAll: optFindAll
|
|
152
|
+
} = options;
|
|
153
|
+
const { processFiltersAllowedFieldsEnabled, requirePrimaryKeys } = privateOptions || {};
|
|
154
|
+
// make sure it's truly a number - it could come as string from GET requests
|
|
155
|
+
const page = optPage ? parseInt(optPage as unknown as string, 10) : 1;
|
|
156
|
+
// same as above - must be a number
|
|
157
|
+
const perPage = optPerPage ? parseInt(optPerPage as unknown as string, 10) : 10;
|
|
158
|
+
const findAll = optFindAll === true || (optFindAll as unknown) === 'true';
|
|
159
|
+
const findResults: DataFindResults<Entity> = { page: 1, perPage: 0, items: [], more: false };
|
|
160
|
+
const parsedFilters = (await this.processObjectAllowedFields<GenericObject>(filters || {}, {
|
|
161
|
+
allowedFields: repository.columnNames,
|
|
162
|
+
isEnabled: processFiltersAllowedFieldsEnabled,
|
|
163
|
+
objectType: ProcessObjectAllowedFieldsType.Filters
|
|
164
|
+
})) as GenericObject;
|
|
165
|
+
if (!findAll) {
|
|
166
|
+
findResults.page = page;
|
|
167
|
+
findResults.perPage = perPage;
|
|
168
|
+
}
|
|
169
|
+
const { items, more } = await repository.find(
|
|
170
|
+
{ filters: parsedFilters, findAll, individualSearch, page, perPage },
|
|
171
|
+
{ requirePrimaryKeys }
|
|
172
|
+
);
|
|
173
|
+
if (findAll) {
|
|
174
|
+
findResults.perPage = items.length;
|
|
175
|
+
} else {
|
|
176
|
+
findResults.more = more;
|
|
177
|
+
if (getTotalCount) {
|
|
178
|
+
findResults.totalCount = await this.count(options, { allowCountWithoutFilters: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
findResults.items = items;
|
|
182
|
+
return findResults;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async findOne(options: FindOneOptions, privateOptions?: FindOnePrivateOptions): Promise<Entity | null> {
|
|
186
|
+
const { filters } = options;
|
|
187
|
+
const { processFiltersAllowedFieldsEnabled, requirePrimaryKeys } = privateOptions || {};
|
|
188
|
+
const parsedFilters = (await this.processObjectAllowedFields<GenericObject>(filters, {
|
|
189
|
+
allowedFields: this.repository.columnNames,
|
|
190
|
+
isEnabled: processFiltersAllowedFieldsEnabled,
|
|
191
|
+
objectType: ProcessObjectAllowedFieldsType.Filters
|
|
192
|
+
})) as GenericObject;
|
|
193
|
+
if (!Object.keys(parsedFilters).length) {
|
|
194
|
+
throw new ApplicationError('At least one filter field is required for the findOne method.');
|
|
195
|
+
}
|
|
196
|
+
const result = await this.repository.find(
|
|
197
|
+
{ filters, individualSearch: true, page: 1, perPage: 1 },
|
|
198
|
+
{ requirePrimaryKeys }
|
|
199
|
+
);
|
|
200
|
+
return result.items[0] || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected async save<Data extends Partial<Entity> | Partial<Entity>[], ReturnData = unknown>(
|
|
204
|
+
data: Data,
|
|
205
|
+
options: ServiceSaveOptions
|
|
206
|
+
): Promise<ReturnData> {
|
|
207
|
+
const { repository, settings } = this;
|
|
208
|
+
const { validationSettings } = settings;
|
|
209
|
+
const {
|
|
210
|
+
delete: optDelete,
|
|
211
|
+
generatePrimaryKeys,
|
|
212
|
+
processObjectAllowedFieldsEnabled,
|
|
213
|
+
transactionId,
|
|
214
|
+
validate
|
|
215
|
+
} = options || {};
|
|
216
|
+
if (optDelete) {
|
|
217
|
+
return (await repository.save(data as unknown as Entity, {
|
|
218
|
+
delete: true,
|
|
219
|
+
generatePrimaryKeys: false,
|
|
220
|
+
transactionId,
|
|
221
|
+
validate: false
|
|
222
|
+
})) as ReturnData;
|
|
223
|
+
}
|
|
224
|
+
const dataToSave: Data | Data[] = await this.processObjectAllowedFields<Data>(data, {
|
|
225
|
+
allowedFields: repository.columnNames,
|
|
226
|
+
isEnabled: processObjectAllowedFieldsEnabled,
|
|
227
|
+
objectType: ProcessObjectAllowedFieldsType.Input
|
|
228
|
+
});
|
|
229
|
+
return (await repository.save(dataToSave as Entity, {
|
|
230
|
+
generatePrimaryKeys,
|
|
231
|
+
transactionId,
|
|
232
|
+
validate: typeof validate !== 'undefined' ? validate : !!validationSettings?.isEnabled
|
|
233
|
+
})) as ReturnData;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// TODO: reduce to need to double 2 finds (one here and one in the repository's save method)
|
|
237
|
+
// by refactoring both methods
|
|
238
|
+
async update(
|
|
239
|
+
data: Entity,
|
|
240
|
+
options: UpdateOptions,
|
|
241
|
+
privateOptions?: UpdatePrivateOptions
|
|
242
|
+
): Promise<DataUpdateResult<Entity>> {
|
|
243
|
+
const { store } = this;
|
|
244
|
+
const { filters, forceTransaction, returnData, returnOriginalItems, transactionId } = options;
|
|
245
|
+
const actualPrivateOptions = privateOptions || {};
|
|
246
|
+
if (!transactionId && forceTransaction) {
|
|
247
|
+
const tId = store.createTransaction();
|
|
248
|
+
const result = await this.update(data, { ...options, transactionId: tId }, actualPrivateOptions);
|
|
249
|
+
await store.endTransaction(tId);
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
const {
|
|
253
|
+
processFiltersAllowedFieldsEnabled,
|
|
254
|
+
processInputAllowedFieldsEnabled,
|
|
255
|
+
requirePrimaryKeys = true,
|
|
256
|
+
validate
|
|
257
|
+
} = actualPrivateOptions;
|
|
258
|
+
const dataToReturn: DataUpdateResult<Entity> = {};
|
|
259
|
+
const { items: itemsToUpdate } = await this.find(
|
|
260
|
+
{ filters, findAll: true },
|
|
261
|
+
{ processFiltersAllowedFieldsEnabled, requirePrimaryKeys }
|
|
262
|
+
);
|
|
263
|
+
if (!itemsToUpdate.length) {
|
|
264
|
+
dataToReturn.count = 0;
|
|
265
|
+
if (returnData) {
|
|
266
|
+
dataToReturn.items = [];
|
|
267
|
+
}
|
|
268
|
+
if (returnOriginalItems) {
|
|
269
|
+
dataToReturn.originalItems = [];
|
|
270
|
+
}
|
|
271
|
+
return dataToReturn;
|
|
272
|
+
}
|
|
273
|
+
const updateResult = await this.save<Entity[], Entity[]>(
|
|
274
|
+
itemsToUpdate.map(item => ld.merge(item, data)),
|
|
275
|
+
{
|
|
276
|
+
generatePrimaryKeys: false,
|
|
277
|
+
processObjectAllowedFieldsEnabled: processInputAllowedFieldsEnabled,
|
|
278
|
+
transactionId,
|
|
279
|
+
validate
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
dataToReturn.count = updateResult.length;
|
|
283
|
+
if (returnData) {
|
|
284
|
+
dataToReturn.items = updateResult;
|
|
285
|
+
}
|
|
286
|
+
if (returnOriginalItems) {
|
|
287
|
+
dataToReturn.originalItems = itemsToUpdate;
|
|
288
|
+
}
|
|
289
|
+
return dataToReturn;
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { GenericObject } from '@node-c/core';
|
|
4
|
+
|
|
5
|
+
export interface RedisModuleOptions {
|
|
6
|
+
entityModuleRegisterOptions?: unknown;
|
|
7
|
+
exports?: ModuleMetadata['exports'];
|
|
8
|
+
folderData: GenericObject<unknown>;
|
|
9
|
+
imports?: {
|
|
10
|
+
atEnd?: ModuleMetadata['imports'];
|
|
11
|
+
postStore?: ModuleMetadata['imports'];
|
|
12
|
+
preStore?: ModuleMetadata['imports'];
|
|
13
|
+
};
|
|
14
|
+
moduleClass: unknown;
|
|
15
|
+
moduleName: string;
|
|
16
|
+
providers?: ModuleMetadata['providers'];
|
|
17
|
+
registerOptionsPerEntityModule?: GenericObject;
|
|
18
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { loadDynamicModules } from '@node-c/core';
|
|
2
|
+
import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { RedisModule, RedisModuleOptions } from './index';
|
|
5
|
+
|
|
6
|
+
import { RedisStoreModule } from '../store';
|
|
7
|
+
|
|
8
|
+
vi.mock('@node-c/core', () => ({
|
|
9
|
+
loadDynamicModules: vi.fn()
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('../store', () => ({
|
|
12
|
+
RedisStoreModule: {
|
|
13
|
+
register: vi.fn()
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('RedisModule', () => {
|
|
18
|
+
describe('register', () => {
|
|
19
|
+
const dummyModules = ['moduleA', 'moduleB'];
|
|
20
|
+
const dummyStoreDynamic = { module: 'StoreModule', global: false };
|
|
21
|
+
const dummyModuleClass = class TestModule {};
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
(loadDynamicModules as unknown as Mock).mockReturnValue({ modules: dummyModules });
|
|
24
|
+
(RedisStoreModule.register as unknown as Mock).mockReturnValue(dummyStoreDynamic);
|
|
25
|
+
});
|
|
26
|
+
it('should return dynamic module with all additionalImports and options provided', () => {
|
|
27
|
+
const additionalImports = {
|
|
28
|
+
preStore: ['pre1', 'pre2'],
|
|
29
|
+
postStore: ['post1'],
|
|
30
|
+
atEnd: ['end1']
|
|
31
|
+
};
|
|
32
|
+
const options = {
|
|
33
|
+
folderData: 'dummyFolder',
|
|
34
|
+
imports: additionalImports,
|
|
35
|
+
moduleClass: dummyModuleClass,
|
|
36
|
+
moduleName: 'testModule',
|
|
37
|
+
storeKey: 'store123',
|
|
38
|
+
providers: ['prov1'],
|
|
39
|
+
exports: ['exp1']
|
|
40
|
+
} as unknown as RedisModuleOptions;
|
|
41
|
+
const result = RedisModule.register(options);
|
|
42
|
+
expect(result.global).toBe(true);
|
|
43
|
+
expect(result.module).toBe(dummyModuleClass);
|
|
44
|
+
expect(result.imports).toEqual([
|
|
45
|
+
...additionalImports.preStore,
|
|
46
|
+
dummyStoreDynamic,
|
|
47
|
+
...additionalImports.postStore,
|
|
48
|
+
...dummyModules,
|
|
49
|
+
...additionalImports.atEnd
|
|
50
|
+
]);
|
|
51
|
+
expect(result.providers).toEqual(['prov1']);
|
|
52
|
+
expect(result.exports).toEqual([...dummyModules, 'exp1']);
|
|
53
|
+
expect(RedisStoreModule.register).toHaveBeenCalledWith({
|
|
54
|
+
dataModuleName: options.moduleName
|
|
55
|
+
});
|
|
56
|
+
expect(loadDynamicModules).toHaveBeenCalledWith(options.folderData);
|
|
57
|
+
});
|
|
58
|
+
it('should return dynamic module when additionalImports, providers, and exports are undefined', () => {
|
|
59
|
+
const options = {
|
|
60
|
+
folderData: 'dummyFolder',
|
|
61
|
+
moduleClass: dummyModuleClass,
|
|
62
|
+
moduleName: 'testModule',
|
|
63
|
+
storeKey: 'store123'
|
|
64
|
+
} as unknown as RedisModuleOptions;
|
|
65
|
+
(loadDynamicModules as unknown as Mock).mockReturnValue({ modules: undefined });
|
|
66
|
+
const result = RedisModule.register(options);
|
|
67
|
+
expect(result.global).toBe(true);
|
|
68
|
+
expect(result.module).toBe(dummyModuleClass);
|
|
69
|
+
expect(result.imports).toEqual([
|
|
70
|
+
...(undefined || []),
|
|
71
|
+
dummyStoreDynamic,
|
|
72
|
+
...(undefined || []),
|
|
73
|
+
...(undefined || []),
|
|
74
|
+
...(undefined || [])
|
|
75
|
+
]);
|
|
76
|
+
expect(result.providers).toEqual([]);
|
|
77
|
+
expect(result.exports).toEqual([...(undefined || []), ...(undefined || [])]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { loadDynamicModules } from '@node-c/core';
|
|
4
|
+
|
|
5
|
+
import { RedisModuleOptions } from './redis.module.definitions';
|
|
6
|
+
|
|
7
|
+
import { RedisStoreModule } from '../store';
|
|
8
|
+
|
|
9
|
+
export class RedisModule {
|
|
10
|
+
static register(options: RedisModuleOptions): DynamicModule {
|
|
11
|
+
const { folderData, imports: additionalImports, moduleClass, moduleName } = options;
|
|
12
|
+
const { atEnd: importsAtEnd, postStore: importsPostStore, preStore: importsPreStore } = additionalImports || {};
|
|
13
|
+
const { modules } = loadDynamicModules(folderData, {
|
|
14
|
+
moduleRegisterOptions: options.entityModuleRegisterOptions,
|
|
15
|
+
registerOptionsPerModule: options.registerOptionsPerEntityModule
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
global: true,
|
|
19
|
+
module: moduleClass as DynamicModule['module'],
|
|
20
|
+
imports: [
|
|
21
|
+
...(importsPreStore || []),
|
|
22
|
+
RedisStoreModule.register({ dataModuleName: moduleName }),
|
|
23
|
+
...(importsPostStore || []),
|
|
24
|
+
...(modules || []),
|
|
25
|
+
...(importsAtEnd || [])
|
|
26
|
+
],
|
|
27
|
+
providers: [...(options.providers || [])],
|
|
28
|
+
exports: [...(modules || []), ...(options.exports || [])]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|