@vida-global/core 2.1.1 → 2.2.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/lib/activeRecord/README.md +2 -6
- package/lib/activeRecord/baseRecord.js +151 -41
- package/lib/activeRecord/db/migrator.js +14 -0
- package/lib/activeRecord/db/queryInterface.js +1 -1
- package/lib/cache/memoryCache.js +31 -7
- package/lib/server/controllerMixins/renderer.js +51 -33
- package/lib/server/doc/renderer.md +1 -1
- package/lib/server/serverController.js +2 -1
- package/package.json +1 -1
- package/test/activeRecord/baseRecord.test.js +250 -9
- package/test/activeRecord/db/migrator.test.js +21 -0
- package/test/activeRecord/db/queryInterface.test.js +4 -2
- package/test/activeRecord/helpers/baseRecordMocks.js +23 -4
- package/test/cache/memoryCache.test.js +76 -0
- package/test/server/controllerMixins/callbacks.test.js +6 -5
- package/test/server/controllerMixins/renderer.test.js +74 -14
- package/test/server/helpers/response.js +19 -0
- package/test/server/serverController.test.js +8 -7
|
@@ -3,7 +3,7 @@ const { Connection } = require('../../lib/activeRecord/db/connection'
|
|
|
3
3
|
const { getActiveRecordSchema } = require('../../lib/activeRecord/db/schema');
|
|
4
4
|
const importSchema = require('../../lib/activeRecord/db/importSchema');
|
|
5
5
|
const { Model, Sequelize } = require('sequelize');
|
|
6
|
-
const { mockRedisClientFactory } = require('./helpers/baseRecordMocks');
|
|
6
|
+
const { mockRedisClientFactory, mockScannedKeys, randomCacheKeys } = require('./helpers/baseRecordMocks');
|
|
7
7
|
const TestHelpers = require('@vida-global/test-helpers');
|
|
8
8
|
const { camelize } = require('inflection');
|
|
9
9
|
const Helpers = require('./helpers/baseRecord');
|
|
@@ -165,8 +165,58 @@ describe('BaseRecord', () => {
|
|
|
165
165
|
sequelize: new Helpers.MockSequelize(),
|
|
166
166
|
tableName: 'users',
|
|
167
167
|
timestamps: false,
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
updatedAt: 'updated_at'
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it ('uses `paranoid: true` when the schema has timestamps and deleted_at', () => {
|
|
173
|
+
importSchema.mockImplementation(() => ({
|
|
174
|
+
created_at: {type: Helpers.dataTypeValue1},
|
|
175
|
+
deleted_at: {type: Helpers.dataTypeValue2},
|
|
176
|
+
updated_at: {type: Helpers.dataTypeValue2},
|
|
177
|
+
}));
|
|
178
|
+
class User extends BaseRecord {}
|
|
179
|
+
User.initialize();
|
|
180
|
+
|
|
181
|
+
expect(Model.init).toHaveBeenCalledWith(
|
|
182
|
+
{
|
|
183
|
+
created_at: {type: Sequelize.DataTypes[Helpers.dataTypeKey1]},
|
|
184
|
+
deleted_at: {type: Sequelize.DataTypes[Helpers.dataTypeKey2]},
|
|
185
|
+
updated_at: {type: Sequelize.DataTypes[Helpers.dataTypeKey2]},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
createdAt: 'created_at',
|
|
189
|
+
deletedAt: 'deleted_at',
|
|
190
|
+
modelName: 'User',
|
|
191
|
+
paranoid: true,
|
|
192
|
+
sequelize: new Helpers.MockSequelize(),
|
|
193
|
+
tableName: 'users',
|
|
194
|
+
updatedAt: 'updated_at'
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it ('does not use `paranoid: true` when the schema has deleted_at but no timestamps', () => {
|
|
200
|
+
importSchema.mockImplementation(() => ({
|
|
201
|
+
deleted_at: {type: Helpers.dataTypeValue2},
|
|
202
|
+
}));
|
|
203
|
+
class User extends BaseRecord {}
|
|
204
|
+
User.initialize();
|
|
205
|
+
|
|
206
|
+
expect(Model.init).toHaveBeenCalledWith(
|
|
207
|
+
{
|
|
208
|
+
deleted_at: {type: Sequelize.DataTypes[Helpers.dataTypeKey2]},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
createdAt: 'created_at',
|
|
212
|
+
deletedAt: 'deleted_at',
|
|
213
|
+
modelName: 'User',
|
|
214
|
+
sequelize: new Helpers.MockSequelize(),
|
|
215
|
+
tableName: 'users',
|
|
216
|
+
timestamps: false,
|
|
217
|
+
updatedAt: 'updated_at'
|
|
218
|
+
}
|
|
219
|
+
);
|
|
170
220
|
});
|
|
171
221
|
|
|
172
222
|
it ('does not run twice on the same model', () => {
|
|
@@ -382,7 +432,7 @@ describe('BaseRecord', () => {
|
|
|
382
432
|
const id = Math.random();
|
|
383
433
|
await Person.find(id);
|
|
384
434
|
expect(Person.findByPk).toHaveBeenCalledTimes(1);
|
|
385
|
-
expect(Person.findByPk).toHaveBeenCalledWith(id);
|
|
435
|
+
expect(Person.findByPk).toHaveBeenCalledWith(id, {useMaster: false});
|
|
386
436
|
});
|
|
387
437
|
|
|
388
438
|
it ('calls the sequelize `findAll` method when passed multiple value', async () => {
|
|
@@ -390,7 +440,13 @@ describe('BaseRecord', () => {
|
|
|
390
440
|
const id2 = Math.random();
|
|
391
441
|
await Person.find([id1, id2]);
|
|
392
442
|
expect(Person.findAll).toHaveBeenCalledTimes(1);
|
|
393
|
-
expect(Person.findAll).toHaveBeenCalledWith({where: {[pk]: [id1, id2]}});
|
|
443
|
+
expect(Person.findAll).toHaveBeenCalledWith({useMaster: false, where: {[pk]: [id1, id2]}});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it ('forwards useWriteDB to findByPk as useMaster', async () => {
|
|
447
|
+
const id = Math.random();
|
|
448
|
+
await Person.find(id, {useWriteDB: true});
|
|
449
|
+
expect(Person.findByPk).toHaveBeenCalledWith(id, {useMaster: true});
|
|
394
450
|
});
|
|
395
451
|
});
|
|
396
452
|
|
|
@@ -421,10 +477,33 @@ describe('BaseRecord', () => {
|
|
|
421
477
|
});
|
|
422
478
|
|
|
423
479
|
|
|
480
|
+
describe('BaseRecord.transaction', () => {
|
|
481
|
+
it ('delegates to the query interface and returns the result', async () => {
|
|
482
|
+
const expected = {};
|
|
483
|
+
const callback = jest.fn(() => expected);
|
|
484
|
+
const queryInterface = {
|
|
485
|
+
transaction: jest.fn(cb => cb())
|
|
486
|
+
};
|
|
487
|
+
class FakeConnection {
|
|
488
|
+
get queryInterface() { return queryInterface; }
|
|
489
|
+
}
|
|
490
|
+
class User extends BaseRecord {
|
|
491
|
+
static get connectionClass() { return FakeConnection; }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const result = await User.transaction(callback);
|
|
495
|
+
|
|
496
|
+
expect(queryInterface.transaction).toHaveBeenCalledTimes(1);
|
|
497
|
+
expect(queryInterface.transaction).toHaveBeenCalledWith(callback);
|
|
498
|
+
expect(result).toBe(expected);
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
|
|
424
503
|
describe('Caching', () => {
|
|
425
504
|
class Person extends BaseRecord {
|
|
426
505
|
static get isCacheable() { return true; }
|
|
427
|
-
static get
|
|
506
|
+
static get _redisClientFactory() { return mockRedisClientFactory; }
|
|
428
507
|
}
|
|
429
508
|
let redisClient
|
|
430
509
|
beforeEach(() => {
|
|
@@ -439,7 +518,7 @@ describe('BaseRecord', () => {
|
|
|
439
518
|
await Person.find(id);
|
|
440
519
|
expect(Person.findByPk).toHaveBeenCalledTimes(0);
|
|
441
520
|
expect(findSpy).toHaveBeenCalledTimes(1);
|
|
442
|
-
expect(findSpy).toHaveBeenCalledWith(id);
|
|
521
|
+
expect(findSpy).toHaveBeenCalledWith(id, {});
|
|
443
522
|
});
|
|
444
523
|
|
|
445
524
|
it ('calls _cachedFind when multiple ids are passed', async () => {
|
|
@@ -448,7 +527,7 @@ describe('BaseRecord', () => {
|
|
|
448
527
|
await Person.find([id1, id2]);
|
|
449
528
|
expect(Person.findByPk).toHaveBeenCalledTimes(0);
|
|
450
529
|
expect(findSpy).toHaveBeenCalledTimes(1);
|
|
451
|
-
expect(findSpy).toHaveBeenCalledWith([id1, id2]);
|
|
530
|
+
expect(findSpy).toHaveBeenCalledWith([id1, id2], {});
|
|
452
531
|
});
|
|
453
532
|
});
|
|
454
533
|
|
|
@@ -693,6 +772,116 @@ describe('BaseRecord', () => {
|
|
|
693
772
|
});
|
|
694
773
|
|
|
695
774
|
|
|
775
|
+
describe('BaseRecord.clearCacheKey', () => {
|
|
776
|
+
it ('deletes the given key from cache', async () => {
|
|
777
|
+
const key = `Person#find:${Math.random()}`;
|
|
778
|
+
|
|
779
|
+
await Person.clearCacheKey(key);
|
|
780
|
+
expect(redisClient.del).toHaveBeenCalledTimes(1);
|
|
781
|
+
expect(redisClient.del).toHaveBeenCalledWith(key);
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
describe('BaseRecord.forEachCacheKeyByPattern', () => {
|
|
787
|
+
const pattern = `Person#find:*`;
|
|
788
|
+
|
|
789
|
+
it ('scans with the given pattern and a default count of 100', async () => {
|
|
790
|
+
mockScannedKeys([]);
|
|
791
|
+
|
|
792
|
+
await Person.forEachCacheKeyByPattern(pattern, jest.fn());
|
|
793
|
+
|
|
794
|
+
expect(redisClient.scanIterator).toHaveBeenCalledTimes(1);
|
|
795
|
+
expect(redisClient.scanIterator).toHaveBeenCalledWith({ MATCH: pattern, COUNT: 100 });
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it ('passes a custom count to the scan', async () => {
|
|
799
|
+
const count = TestHelpers.Faker.Math.randomNumber();
|
|
800
|
+
mockScannedKeys([]);
|
|
801
|
+
|
|
802
|
+
await Person.forEachCacheKeyByPattern(pattern, jest.fn(), count);
|
|
803
|
+
|
|
804
|
+
expect(redisClient.scanIterator).toHaveBeenCalledWith({ MATCH: pattern, COUNT: count });
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it ('buffers the individually yielded keys into one batch per count', async () => {
|
|
808
|
+
const keys = randomCacheKeys(5);
|
|
809
|
+
const callback = jest.fn();
|
|
810
|
+
mockScannedKeys(keys);
|
|
811
|
+
|
|
812
|
+
await Person.forEachCacheKeyByPattern(pattern, callback, 2);
|
|
813
|
+
|
|
814
|
+
expect(callback).toHaveBeenCalledTimes(3);
|
|
815
|
+
expect(callback).toHaveBeenNthCalledWith(1, keys.slice(0, 2), redisClient);
|
|
816
|
+
expect(callback).toHaveBeenNthCalledWith(2, keys.slice(2, 4), redisClient);
|
|
817
|
+
expect(callback).toHaveBeenNthCalledWith(3, keys.slice(4), redisClient);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it ('invokes the callback once when fewer keys than the count are scanned', async () => {
|
|
821
|
+
const keys = randomCacheKeys(2);
|
|
822
|
+
const callback = jest.fn();
|
|
823
|
+
mockScannedKeys(keys);
|
|
824
|
+
|
|
825
|
+
await Person.forEachCacheKeyByPattern(pattern, callback);
|
|
826
|
+
|
|
827
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
828
|
+
expect(callback).toHaveBeenCalledWith(keys, redisClient);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
it ('flattens keys yielded as arrays', async () => {
|
|
832
|
+
const keys = randomCacheKeys(3);
|
|
833
|
+
const callback = jest.fn();
|
|
834
|
+
mockScannedKeys([keys.slice(0, 2), keys.slice(2)]);
|
|
835
|
+
|
|
836
|
+
await Person.forEachCacheKeyByPattern(pattern, callback);
|
|
837
|
+
|
|
838
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
839
|
+
expect(callback).toHaveBeenCalledWith(keys, redisClient);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it ('does not invoke the callback when the scan yields nothing', async () => {
|
|
843
|
+
const callback = jest.fn();
|
|
844
|
+
mockScannedKeys([]);
|
|
845
|
+
|
|
846
|
+
await Person.forEachCacheKeyByPattern(pattern, callback);
|
|
847
|
+
|
|
848
|
+
expect(callback).not.toHaveBeenCalled();
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
describe('BaseRecord.clearCacheKeysByPattern', () => {
|
|
854
|
+
const pattern = `Person#find:*`;
|
|
855
|
+
|
|
856
|
+
it ('scans for matching keys with the given pattern', async () => {
|
|
857
|
+
mockScannedKeys([]);
|
|
858
|
+
|
|
859
|
+
await Person.clearCacheKeysByPattern(pattern);
|
|
860
|
+
|
|
861
|
+
expect(redisClient.scanIterator).toHaveBeenCalledTimes(1);
|
|
862
|
+
expect(redisClient.scanIterator).toHaveBeenCalledWith({ MATCH: pattern, COUNT: 100 });
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it ('deletes all scanned keys in a single batched call', async () => {
|
|
866
|
+
const keys = randomCacheKeys(3);
|
|
867
|
+
mockScannedKeys(keys);
|
|
868
|
+
|
|
869
|
+
await Person.clearCacheKeysByPattern(pattern);
|
|
870
|
+
|
|
871
|
+
expect(redisClient.del).toHaveBeenCalledTimes(1);
|
|
872
|
+
expect(redisClient.del).toHaveBeenCalledWith(keys);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it ('does not delete when the scan yields nothing', async () => {
|
|
876
|
+
mockScannedKeys([]);
|
|
877
|
+
|
|
878
|
+
await Person.clearCacheKeysByPattern(pattern);
|
|
879
|
+
|
|
880
|
+
expect(redisClient.del).not.toHaveBeenCalled();
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
|
|
696
885
|
describe('BaseRecord.#clearSelfCache', () => {
|
|
697
886
|
it ('deletes the key from cache', async () => {
|
|
698
887
|
const pk = Person.primaryKeyAttribute;
|
|
@@ -704,6 +893,18 @@ describe('BaseRecord', () => {
|
|
|
704
893
|
expect(redisClient.del).toHaveBeenCalledTimes(1);
|
|
705
894
|
expect(redisClient.del).toHaveBeenCalledWith(key);
|
|
706
895
|
});
|
|
896
|
+
|
|
897
|
+
it ('delegates deletion to clearCacheKey', async () => {
|
|
898
|
+
const pk = Person.primaryKeyAttribute;
|
|
899
|
+
const id = Math.random();
|
|
900
|
+
const key = Person._recordCacheKey(id);
|
|
901
|
+
const person = new Person({[pk]: id});
|
|
902
|
+
const clearSpy = jest.spyOn(Person, 'clearCacheKey');
|
|
903
|
+
|
|
904
|
+
await person.clearSelfCache();
|
|
905
|
+
expect(clearSpy).toHaveBeenCalledTimes(1);
|
|
906
|
+
expect(clearSpy).toHaveBeenCalledWith(key);
|
|
907
|
+
});
|
|
707
908
|
});
|
|
708
909
|
|
|
709
910
|
|
|
@@ -711,7 +912,7 @@ describe('BaseRecord', () => {
|
|
|
711
912
|
describe.each(hooks)('BaseRecord._after%sCacheHook', (hook) => {
|
|
712
913
|
class Cacheable extends BaseRecord {
|
|
713
914
|
static get isCacheable() { return true; }
|
|
714
|
-
static get
|
|
915
|
+
static get _redisClientFactory() { return mockRedisClientFactory; }
|
|
715
916
|
}
|
|
716
917
|
const hookMethod = `_after${hook}CacheHook`;
|
|
717
918
|
|
|
@@ -842,5 +1043,45 @@ describe('BaseRecord', () => {
|
|
|
842
1043
|
|
|
843
1044
|
expect(record.toApiResponse()).toEqual({ name: 'OVERRIDDEN' });
|
|
844
1045
|
});
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
it ('leaves column keys unchanged when camelizeFieldsForApiResponse is disabled', () => {
|
|
1049
|
+
const record = buildRecord(
|
|
1050
|
+
{ org_id: { type: 'integer' }, external_task_id: { type: 'string' } },
|
|
1051
|
+
{ org_id: 7, external_task_id: 'abc' },
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
expect(record.toApiResponse()).toEqual({ org_id: 7, external_task_id: 'abc' });
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
it ('camelizes column keys when camelizeFieldsForApiResponse is enabled', () => {
|
|
1059
|
+
const record = buildRecord(
|
|
1060
|
+
{ org_id: { type: 'integer' }, external_task_id: { type: 'string' } },
|
|
1061
|
+
{ org_id: 7, external_task_id: 'abc' },
|
|
1062
|
+
);
|
|
1063
|
+
Object.defineProperty(record, 'camelizeFieldsForApiResponse', { get: () => true, configurable: true });
|
|
1064
|
+
|
|
1065
|
+
expect(record.toApiResponse()).toEqual({ orgId: 7, externalTaskId: 'abc' });
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
describe('#apiResponseKey', () => {
|
|
1071
|
+
it ('returns the column unchanged by default', () => {
|
|
1072
|
+
class Thing extends BaseRecord {}
|
|
1073
|
+
const record = Object.create(Thing.prototype);
|
|
1074
|
+
|
|
1075
|
+
expect(record.apiResponseKey('external_task_id')).toEqual('external_task_id');
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
it ('camelizes the column when camelizeFieldsForApiResponse is enabled', () => {
|
|
1080
|
+
class Thing extends BaseRecord {}
|
|
1081
|
+
const record = Object.create(Thing.prototype);
|
|
1082
|
+
Object.defineProperty(record, 'camelizeFieldsForApiResponse', { get: () => true, configurable: true });
|
|
1083
|
+
|
|
1084
|
+
expect(record.apiResponseKey('external_task_id')).toEqual('externalTaskId');
|
|
1085
|
+
});
|
|
845
1086
|
});
|
|
846
1087
|
});
|
|
@@ -61,6 +61,27 @@ describe('Migrator', () => {
|
|
|
61
61
|
expect(Helpers.mockQueryInterface.createTable).toHaveBeenCalledWith(tableName, details);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
it ('adds a deleted_at column for paranoid tables', async () => {
|
|
65
|
+
const tableName = Helpers.randomString();
|
|
66
|
+
const details = { ...defaultColumns };
|
|
67
|
+
|
|
68
|
+
await Helpers.migrator.createTable(tableName, details, { paranoid: true });
|
|
69
|
+
|
|
70
|
+
expect(Helpers.mockQueryInterface.createTable).toHaveBeenCalledWith(tableName, {
|
|
71
|
+
...defaultColumns,
|
|
72
|
+
deleted_at: { allowNull: true, type: 'DATE' },
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it ('leaves deleted_at off tables that are not paranoid', async () => {
|
|
77
|
+
const tableName = Helpers.randomString();
|
|
78
|
+
const details = { ...defaultColumns };
|
|
79
|
+
|
|
80
|
+
await Helpers.migrator.createTable(tableName, details);
|
|
81
|
+
|
|
82
|
+
expect(Helpers.mockQueryInterface.createTable).toHaveBeenCalledWith(tableName, defaultColumns);
|
|
83
|
+
});
|
|
84
|
+
|
|
64
85
|
it ('snake_cases created table columns', async () => {
|
|
65
86
|
const tableName = Helpers.randomString();
|
|
66
87
|
const details = { ...defaultColumns, myColumn: 1 };
|
|
@@ -29,10 +29,12 @@ describe('QueryInterface', () => {
|
|
|
29
29
|
|
|
30
30
|
describe('QueryInterface#transaction', () => {
|
|
31
31
|
it('delegates transaction callback to sequelize', async () => {
|
|
32
|
-
const
|
|
32
|
+
const expected = {};
|
|
33
|
+
const callback = jest.fn(() => expected);
|
|
33
34
|
|
|
34
|
-
await queryInterface.transaction(callback);
|
|
35
|
+
const result = await queryInterface.transaction(callback);
|
|
35
36
|
|
|
37
|
+
expect(result).toBe(expected);
|
|
36
38
|
expect(connection._sequelize.transaction).toHaveBeenCalledTimes(1);
|
|
37
39
|
expect(connection._sequelize.transaction).toHaveBeenCalledWith(callback);
|
|
38
40
|
});
|
|
@@ -10,6 +10,11 @@ const dataTypeKey2 = TestHelpers.Faker.Text.randomString();
|
|
|
10
10
|
const dataTypeValue2 = TestHelpers.Faker.Text.randomString();
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
function randomCacheKeys(count) {
|
|
14
|
+
return Array.from({ length: count }, () => `Person#find:${TestHelpers.Faker.Text.randomString()}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
13
18
|
/***************************************************************************************************
|
|
14
19
|
* MOCKS
|
|
15
20
|
***************************************************************************************************/
|
|
@@ -42,14 +47,26 @@ class MockSequelize {
|
|
|
42
47
|
|
|
43
48
|
|
|
44
49
|
const redisClient = {
|
|
45
|
-
del:
|
|
46
|
-
mGet:
|
|
47
|
-
mSet:
|
|
48
|
-
connect:
|
|
50
|
+
del: jest.fn(),
|
|
51
|
+
mGet: jest.fn(),
|
|
52
|
+
mSet: jest.fn(),
|
|
53
|
+
connect: jest.fn(),
|
|
54
|
+
scanIterator: jest.fn(),
|
|
49
55
|
}
|
|
50
56
|
const mockRedisClientFactory = () => redisClient;
|
|
51
57
|
|
|
52
58
|
|
|
59
|
+
// redis 4's scanIterator yields one key string at a time. Pass an array element to model the
|
|
60
|
+
// batches redis 5 yields instead.
|
|
61
|
+
function mockScannedKeys(scanned) {
|
|
62
|
+
redisClient.scanIterator.mockImplementation(async function* () {
|
|
63
|
+
for (const item of scanned) {
|
|
64
|
+
yield item;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
53
70
|
module.exports = {
|
|
54
71
|
dataTypeKey1,
|
|
55
72
|
dataTypeValue1,
|
|
@@ -57,5 +74,7 @@ module.exports = {
|
|
|
57
74
|
dataTypeValue2,
|
|
58
75
|
MockModel,
|
|
59
76
|
mockRedisClientFactory,
|
|
77
|
+
mockScannedKeys,
|
|
60
78
|
MockSequelize,
|
|
79
|
+
randomCacheKeys,
|
|
61
80
|
}
|
|
@@ -16,6 +16,82 @@ describe('MemoryCache', () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
describe('#delete', () => {
|
|
20
|
+
const options = {
|
|
21
|
+
namespace: 'squire:shop-data',
|
|
22
|
+
key: { shopId: 'shop-1' },
|
|
23
|
+
ttlSeconds: 300,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
it('deletes a cached value', async () => {
|
|
28
|
+
await cache.fetch(options, async () => ({ version: 1 }));
|
|
29
|
+
|
|
30
|
+
await expect(cache.delete(options)).resolves.toBe(true);
|
|
31
|
+
await expect(cache.fetch(options, async () => ({ version: 2 }))).resolves.toEqual({
|
|
32
|
+
value: { version: 2 },
|
|
33
|
+
cacheHit: false,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
it('deletes values using an equivalent object key', async () => {
|
|
39
|
+
await cache.fetch({
|
|
40
|
+
...options,
|
|
41
|
+
key: { agentId: 'agent-1', shopId: 'shop-1' },
|
|
42
|
+
}, async () => ({ version: 1 }));
|
|
43
|
+
|
|
44
|
+
await expect(cache.delete({
|
|
45
|
+
namespace: options.namespace,
|
|
46
|
+
key: { shopId: 'shop-1', agentId: 'agent-1' },
|
|
47
|
+
})).resolves.toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
it('returns false when the value is not cached', async () => {
|
|
52
|
+
await expect(cache.delete(options)).resolves.toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
it('invalidates an in-flight fetch without interfering with its replacement', async () => {
|
|
57
|
+
let resolveOldFetch;
|
|
58
|
+
let resolveNewFetch;
|
|
59
|
+
const oldFetcher = jest.fn(() => new Promise((resolve) => {
|
|
60
|
+
resolveOldFetch = resolve;
|
|
61
|
+
}));
|
|
62
|
+
const newFetcher = jest.fn(() => new Promise((resolve) => {
|
|
63
|
+
resolveNewFetch = resolve;
|
|
64
|
+
}));
|
|
65
|
+
const oldFetch = cache.fetch(options, oldFetcher);
|
|
66
|
+
await expect(cache.delete(options)).resolves.toBe(true);
|
|
67
|
+
const newFetch = cache.fetch(options, newFetcher);
|
|
68
|
+
|
|
69
|
+
resolveOldFetch({ version: 1 });
|
|
70
|
+
await expect(oldFetch).resolves.toEqual({ value: { version: 1 }, cacheHit: false });
|
|
71
|
+
const joinedFetcher = jest.fn();
|
|
72
|
+
const joinedFetch = cache.fetch(options, joinedFetcher);
|
|
73
|
+
resolveNewFetch({ version: 2 });
|
|
74
|
+
|
|
75
|
+
await expect(Promise.all([newFetch, joinedFetch])).resolves.toEqual([
|
|
76
|
+
{ value: { version: 2 }, cacheHit: false },
|
|
77
|
+
{ value: { version: 2 }, cacheHit: false },
|
|
78
|
+
]);
|
|
79
|
+
expect(oldFetcher).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(newFetcher).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(joinedFetcher).not.toHaveBeenCalled();
|
|
82
|
+
await expect(cache.fetch(options, joinedFetcher)).resolves.toEqual({
|
|
83
|
+
value: { version: 2 },
|
|
84
|
+
cacheHit: true,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
it('requires a namespace', async () => {
|
|
90
|
+
await expect(cache.delete()).rejects.toThrow('MemoryCache namespace is required.');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
19
95
|
describe('#fetch', () => {
|
|
20
96
|
const options = {
|
|
21
97
|
namespace: 'squire:shop-data',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { VidaServerController } = require('../../../lib/server/serverController');
|
|
2
|
+
const { buildResponseMock } = require('../helpers/response');
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
describe('VidaServerController', () => {
|
|
@@ -75,7 +76,7 @@ describe('VidaServerController', () => {
|
|
|
75
76
|
namedBefore() { order.push('before2') }
|
|
76
77
|
testAction() { order.push('action'); return {} }
|
|
77
78
|
}
|
|
78
|
-
const response = {
|
|
79
|
+
const response = buildResponseMock({ end: jest.fn() });
|
|
79
80
|
const controller = new C({}, response);
|
|
80
81
|
|
|
81
82
|
await controller.performRequest('testAction');
|
|
@@ -92,7 +93,7 @@ describe('VidaServerController', () => {
|
|
|
92
93
|
namedAfter() { order.push('after2') }
|
|
93
94
|
testAction() { order.push('action'); return {} }
|
|
94
95
|
}
|
|
95
|
-
const response = {
|
|
96
|
+
const response = buildResponseMock({ end: jest.fn() });
|
|
96
97
|
const controller = new C({}, response);
|
|
97
98
|
|
|
98
99
|
await controller.performRequest('testAction');
|
|
@@ -105,7 +106,7 @@ describe('VidaServerController', () => {
|
|
|
105
106
|
setupCallbacks() { this.beforeCallback(() => false) }
|
|
106
107
|
}
|
|
107
108
|
C.prototype.testAction = testAction;
|
|
108
|
-
const response = {
|
|
109
|
+
const response = buildResponseMock({ end: jest.fn() });
|
|
109
110
|
const controller = new C({}, response);
|
|
110
111
|
|
|
111
112
|
await controller.performRequest('testAction');
|
|
@@ -121,7 +122,7 @@ describe('VidaServerController', () => {
|
|
|
121
122
|
}
|
|
122
123
|
testAction() { return {} }
|
|
123
124
|
}
|
|
124
|
-
const response = {
|
|
125
|
+
const response = buildResponseMock({ end: jest.fn() });
|
|
125
126
|
const controller = new C({}, response);
|
|
126
127
|
|
|
127
128
|
await controller.performRequest('testAction');
|
|
@@ -137,7 +138,7 @@ describe('VidaServerController', () => {
|
|
|
137
138
|
}
|
|
138
139
|
testAction() { return {} }
|
|
139
140
|
}
|
|
140
|
-
const response = {
|
|
141
|
+
const response = buildResponseMock({ end: jest.fn() });
|
|
141
142
|
const controller = new C({}, response);
|
|
142
143
|
|
|
143
144
|
await controller.performRequest('testAction');
|