@vida-global/core 2.3.2 → 2.3.4

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.
@@ -0,0 +1,326 @@
1
+ const afterCommitHooks = require('../../lib/activeRecord/afterCommitHooks');
2
+ const TestHelpers = require('@vida-global/test-helpers');
3
+ const Helpers = require('./helpers/afterCommitHooks');
4
+
5
+
6
+ afterEach(() => {
7
+ jest.clearAllMocks();
8
+ });
9
+
10
+
11
+ describe('afterCommitHooks', () => {
12
+ describe('addCallback', () => {
13
+ it ('registers an unnamed callback', () => {
14
+ const model = Helpers.buildModel();
15
+ const callback = jest.fn();
16
+
17
+ afterCommitHooks.addCallback(model, callback);
18
+
19
+ expect(afterCommitHooks.callbacksFor(model)).toEqual([callback]);
20
+ });
21
+
22
+ it ('registers a named callback', () => {
23
+ const model = Helpers.buildModel();
24
+ const name = TestHelpers.Faker.Text.randomString();
25
+ const callback = jest.fn();
26
+
27
+ afterCommitHooks.addCallback(model, name, callback);
28
+
29
+ expect(afterCommitHooks.callbacksFor(model)).toEqual([callback]);
30
+ });
31
+
32
+ it ('keeps callbacks in the order they were registered', () => {
33
+ const model = Helpers.buildModel();
34
+ const first = jest.fn();
35
+ const second = jest.fn();
36
+
37
+ afterCommitHooks.addCallback(model, first);
38
+ afterCommitHooks.addCallback(model, second);
39
+
40
+ expect(afterCommitHooks.callbacksFor(model)).toEqual([first, second]);
41
+ });
42
+ });
43
+
44
+
45
+ describe('removeCallback', () => {
46
+ it ('removes an unnamed callback by reference', () => {
47
+ const model = Helpers.buildModel();
48
+ const removed = jest.fn();
49
+ const remaining = jest.fn();
50
+ afterCommitHooks.addCallback(model, removed);
51
+ afterCommitHooks.addCallback(model, remaining);
52
+
53
+ afterCommitHooks.removeCallback(model, removed);
54
+
55
+ expect(afterCommitHooks.callbacksFor(model)).toEqual([remaining]);
56
+ });
57
+
58
+ it ('removes a named callback by name', () => {
59
+ const model = Helpers.buildModel();
60
+ const name = TestHelpers.Faker.Text.randomString();
61
+ const removed = jest.fn();
62
+ const remaining = jest.fn();
63
+ afterCommitHooks.addCallback(model, name, removed);
64
+ afterCommitHooks.addCallback(model, remaining);
65
+
66
+ afterCommitHooks.removeCallback(model, name);
67
+
68
+ expect(afterCommitHooks.callbacksFor(model)).toEqual([remaining]);
69
+ });
70
+
71
+ it ('leaves unnamed callbacks alone when removing by name', () => {
72
+ const model = Helpers.buildModel();
73
+ const callback = function namedFunction() {};
74
+ afterCommitHooks.addCallback(model, callback);
75
+
76
+ afterCommitHooks.removeCallback(model, 'namedFunction');
77
+
78
+ expect(afterCommitHooks.callbacksFor(model)).toEqual([callback]);
79
+ });
80
+
81
+ it ('does not remove callbacks registered on an ancestor', () => {
82
+ const parent = Helpers.buildModel();
83
+ const child = Helpers.buildModel(parent);
84
+ const callback = jest.fn();
85
+ afterCommitHooks.addCallback(parent, callback);
86
+
87
+ afterCommitHooks.removeCallback(child, callback);
88
+
89
+ expect(afterCommitHooks.callbacksFor(parent)).toEqual([callback]);
90
+ expect(afterCommitHooks.callbacksFor(child)).toEqual([callback]);
91
+ });
92
+ });
93
+
94
+
95
+ describe('callbacksFor', () => {
96
+ it ('returns an empty list for a model with no callbacks', () => {
97
+ expect(afterCommitHooks.callbacksFor(Helpers.buildModel())).toEqual([]);
98
+ });
99
+
100
+ it ('includes callbacks registered on an ancestor', () => {
101
+ const parent = Helpers.buildModel();
102
+ const child = Helpers.buildModel(parent);
103
+ const parentCallback = jest.fn();
104
+ const childCallback = jest.fn();
105
+ afterCommitHooks.addCallback(parent, parentCallback);
106
+ afterCommitHooks.addCallback(child, childCallback);
107
+
108
+ expect(afterCommitHooks.callbacksFor(child)).toEqual([parentCallback, childCallback]);
109
+ });
110
+
111
+ it ('does not include callbacks registered on a descendant', () => {
112
+ const parent = Helpers.buildModel();
113
+ const child = Helpers.buildModel(parent);
114
+ const childCallback = jest.fn();
115
+ afterCommitHooks.addCallback(child, childCallback);
116
+
117
+ expect(afterCommitHooks.callbacksFor(parent)).toEqual([]);
118
+ });
119
+
120
+ it ('does not include callbacks registered on a sibling', () => {
121
+ const parent = Helpers.buildModel();
122
+ const sibling = Helpers.buildModel(parent);
123
+ const otherSibling = Helpers.buildModel(parent);
124
+ const siblingCallback = jest.fn();
125
+ afterCommitHooks.addCallback(sibling, siblingCallback);
126
+
127
+ expect(afterCommitHooks.callbacksFor(otherSibling)).toEqual([]);
128
+ });
129
+ });
130
+
131
+
132
+ describe('runCallbacks', () => {
133
+ describe('without a transaction', () => {
134
+ it ('calls every registered callback with the record and options', async () => {
135
+ const model = Helpers.buildModel();
136
+ const record = Helpers.buildRecord();
137
+ const options = {};
138
+ const callback = jest.fn();
139
+ afterCommitHooks.addCallback(model, callback);
140
+
141
+ await afterCommitHooks.runCallbacks(model, record, options);
142
+
143
+ expect(callback).toHaveBeenCalledTimes(1);
144
+ expect(callback).toHaveBeenCalledWith(record, options);
145
+ });
146
+
147
+ it ('calls the callback with the model as its context', async () => {
148
+ const model = Helpers.buildModel();
149
+ let context;
150
+ afterCommitHooks.addCallback(model, function() { context = this; });
151
+
152
+ await afterCommitHooks.runCallbacks(model, Helpers.buildRecord(), {});
153
+
154
+ expect(context).toBe(model);
155
+ });
156
+
157
+ it ('awaits each callback before running the next', async () => {
158
+ const model = Helpers.buildModel();
159
+ const order = [];
160
+ afterCommitHooks.addCallback(model, async () => {
161
+ await Promise.resolve();
162
+ order.push('first');
163
+ });
164
+ afterCommitHooks.addCallback(model, () => order.push('second'));
165
+
166
+ await afterCommitHooks.runCallbacks(model, Helpers.buildRecord(), {});
167
+
168
+ expect(order).toEqual(['first', 'second']);
169
+ });
170
+
171
+ it ('lets a callback error propagate', async () => {
172
+ const model = Helpers.buildModel();
173
+ const message = TestHelpers.Faker.Text.randomString();
174
+ afterCommitHooks.addCallback(model, () => { throw new Error(message); });
175
+
176
+ await expect(afterCommitHooks.runCallbacks(model, Helpers.buildRecord(), {}))
177
+ .rejects.toThrow(message);
178
+ });
179
+ });
180
+
181
+
182
+ describe('with a transaction', () => {
183
+ it ('defers the callback to the transaction instead of running it', async () => {
184
+ const model = Helpers.buildModel();
185
+ const transaction = Helpers.buildTransaction();
186
+ const callback = jest.fn();
187
+ afterCommitHooks.addCallback(model, callback);
188
+
189
+ await afterCommitHooks.runCallbacks(model, Helpers.buildRecord(), { transaction });
190
+
191
+ expect(transaction.afterCommit).toHaveBeenCalledTimes(1);
192
+ expect(callback).toHaveBeenCalledTimes(0);
193
+ });
194
+
195
+ it ('calls the callback with the record and options once the transaction commits', async () => {
196
+ const model = Helpers.buildModel();
197
+ const transaction = Helpers.buildTransaction();
198
+ const record = Helpers.buildRecord();
199
+ const options = { transaction };
200
+ const callback = jest.fn();
201
+ afterCommitHooks.addCallback(model, callback);
202
+
203
+ await afterCommitHooks.runCallbacks(model, record, options);
204
+ await Helpers.commit(transaction);
205
+
206
+ expect(callback).toHaveBeenCalledTimes(1);
207
+ expect(callback).toHaveBeenCalledWith(record, options);
208
+ });
209
+
210
+ it ('lets a callback error propagate out of the commit', async () => {
211
+ const model = Helpers.buildModel();
212
+ const transaction = Helpers.buildTransaction();
213
+ const message = TestHelpers.Faker.Text.randomString();
214
+ afterCommitHooks.addCallback(model, () => { throw new Error(message); });
215
+
216
+ await afterCommitHooks.runCallbacks(model, Helpers.buildRecord(), { transaction });
217
+
218
+ await expect(Helpers.commit(transaction)).rejects.toThrow(message);
219
+ });
220
+
221
+ it ('defers to the outermost transaction when the write is in a savepoint', async () => {
222
+ const model = Helpers.buildModel();
223
+ const transaction = Helpers.buildTransaction();
224
+ const savepoint = Helpers.buildSavepoint(transaction);
225
+ afterCommitHooks.addCallback(model, jest.fn());
226
+
227
+ await afterCommitHooks.runCallbacks(model, Helpers.buildRecord(), { transaction: savepoint });
228
+
229
+ expect(transaction.afterCommit).toHaveBeenCalledTimes(1);
230
+ expect(savepoint.afterCommit).toHaveBeenCalledTimes(0);
231
+ });
232
+
233
+ it ('defers to the outermost transaction through nested savepoints', async () => {
234
+ const model = Helpers.buildModel();
235
+ const transaction = Helpers.buildTransaction();
236
+ const outerPoint = Helpers.buildSavepoint(transaction);
237
+ const innerPoint = Helpers.buildSavepoint(outerPoint);
238
+ afterCommitHooks.addCallback(model, jest.fn());
239
+
240
+ await afterCommitHooks.runCallbacks(model, Helpers.buildRecord(), { transaction: innerPoint });
241
+
242
+ expect(transaction.afterCommit).toHaveBeenCalledTimes(1);
243
+ expect(outerPoint.afterCommit).toHaveBeenCalledTimes(0);
244
+ expect(innerPoint.afterCommit).toHaveBeenCalledTimes(0);
245
+ });
246
+ });
247
+
248
+
249
+ describe('deduplication', () => {
250
+ it ('defers a callback once when the same record is reported twice', async () => {
251
+ const model = Helpers.buildModel();
252
+ const transaction = Helpers.buildTransaction();
253
+ const record = Helpers.buildRecord();
254
+ const callback = jest.fn();
255
+ afterCommitHooks.addCallback(model, callback);
256
+
257
+ await afterCommitHooks.runCallbacks(model, record, { transaction });
258
+ await afterCommitHooks.runCallbacks(model, record, { transaction });
259
+ await Helpers.commit(transaction);
260
+
261
+ expect(callback).toHaveBeenCalledTimes(1);
262
+ });
263
+
264
+ it ('defers a callback for each distinct record', async () => {
265
+ const model = Helpers.buildModel();
266
+ const transaction = Helpers.buildTransaction();
267
+ const first = Helpers.buildRecord();
268
+ const second = Helpers.buildRecord();
269
+ const callback = jest.fn();
270
+ afterCommitHooks.addCallback(model, callback);
271
+
272
+ await afterCommitHooks.runCallbacks(model, first, { transaction });
273
+ await afterCommitHooks.runCallbacks(model, second, { transaction });
274
+ await Helpers.commit(transaction);
275
+
276
+ expect(callback).toHaveBeenCalledTimes(2);
277
+ });
278
+
279
+ it ('defers every registered callback for the same record', async () => {
280
+ const model = Helpers.buildModel();
281
+ const transaction = Helpers.buildTransaction();
282
+ const first = jest.fn();
283
+ const second = jest.fn();
284
+ afterCommitHooks.addCallback(model, first);
285
+ afterCommitHooks.addCallback(model, second);
286
+
287
+ await afterCommitHooks.runCallbacks(model, Helpers.buildRecord(), { transaction });
288
+ await Helpers.commit(transaction);
289
+
290
+ expect(first).toHaveBeenCalledTimes(1);
291
+ expect(second).toHaveBeenCalledTimes(1);
292
+ });
293
+
294
+ it ('treats the same record in a separate transaction as a new registration', async () => {
295
+ const model = Helpers.buildModel();
296
+ const first = Helpers.buildTransaction();
297
+ const second = Helpers.buildTransaction();
298
+ const record = Helpers.buildRecord();
299
+ const callback = jest.fn();
300
+ afterCommitHooks.addCallback(model, callback);
301
+
302
+ await afterCommitHooks.runCallbacks(model, record, { transaction: first });
303
+ await afterCommitHooks.runCallbacks(model, record, { transaction: second });
304
+ await Helpers.commit(first);
305
+ await Helpers.commit(second);
306
+
307
+ expect(callback).toHaveBeenCalledTimes(2);
308
+ });
309
+
310
+ it ('deduplicates against the outermost transaction across savepoints', async () => {
311
+ const model = Helpers.buildModel();
312
+ const transaction = Helpers.buildTransaction();
313
+ const savepoint = Helpers.buildSavepoint(transaction);
314
+ const record = Helpers.buildRecord();
315
+ const callback = jest.fn();
316
+ afterCommitHooks.addCallback(model, callback);
317
+
318
+ await afterCommitHooks.runCallbacks(model, record, { transaction });
319
+ await afterCommitHooks.runCallbacks(model, record, { transaction: savepoint });
320
+ await Helpers.commit(transaction);
321
+
322
+ expect(callback).toHaveBeenCalledTimes(1);
323
+ });
324
+ });
325
+ });
326
+ });
@@ -844,6 +844,98 @@ describe('BaseRecord', () => {
844
844
  });
845
845
 
846
846
 
847
+ describe('BaseRecord#toCachePayload', () => {
848
+ it ('returns the record as JSON', async () => {
849
+ const attrName = TestHelpers.Faker.Text.randomString();
850
+ const attrVal = TestHelpers.Faker.Text.randomString();
851
+ const person = new Person({[attrName]: attrVal});
852
+
853
+ await expect(person.toCachePayload()).resolves.toEqual({[attrName]: attrVal});
854
+ });
855
+ });
856
+
857
+
858
+ describe('BaseRecord.fromCachePayload', () => {
859
+ it ('builds a record that is not new', () => {
860
+ const attrName = TestHelpers.Faker.Text.randomString();
861
+ const attrVal = TestHelpers.Faker.Text.randomString();
862
+
863
+ const person = Person.fromCachePayload({[attrName]: attrVal});
864
+
865
+ expect(person instanceof Person).toBeTruthy();
866
+ expect(person[attrName]).toEqual(attrVal);
867
+ expect(person.options.isNewRecord).toBeFalsy();
868
+ });
869
+ });
870
+
871
+
872
+ describe('cache payload overrides', () => {
873
+ const EXTRA_KEY = 'nicknames';
874
+
875
+ class Player extends Person {
876
+ #nicknames = null;
877
+
878
+
879
+ get nicknames() { return this.#nicknames; }
880
+ set nicknames(names) { this.#nicknames = names; }
881
+
882
+
883
+ async toCachePayload() {
884
+ const payload = await super.toCachePayload();
885
+ payload[EXTRA_KEY] = await this.fetchNicknames();
886
+ return payload;
887
+ }
888
+
889
+
890
+ static fromCachePayload(data) {
891
+ const { [EXTRA_KEY]: nicknames, ...attributes } = data;
892
+ const player = super.fromCachePayload(attributes);
893
+ player.nicknames = nicknames;
894
+ return player;
895
+ }
896
+
897
+
898
+ async fetchNicknames() { return []; }
899
+ }
900
+
901
+ const attrName = TestHelpers.Faker.Text.randomString();
902
+ const attrVal = TestHelpers.Faker.Text.randomString();
903
+ const nickname = TestHelpers.Faker.Text.randomString();
904
+ const payload = {[attrName]: attrVal, [EXTRA_KEY]: [nickname]};
905
+ const cached = `AR:Player:${JSON.stringify(payload)}`;
906
+
907
+ it ('writes the associated data into the cached value', async () => {
908
+ const key = TestHelpers.Faker.Text.randomString();
909
+ const player = new Player({[attrName]: attrVal});
910
+ jest.spyOn(player, 'fetchNicknames').mockResolvedValue([nickname]);
911
+
912
+ await Player._cacheFetchedData({[key]: player});
913
+
914
+ expect(redisClient.mSet).toHaveBeenCalledTimes(1);
915
+ expect(redisClient.mSet).toHaveBeenCalledWith({[key]: cached});
916
+ });
917
+
918
+ it ('restores the associated data when decoding, keeping it out of the attributes', () => {
919
+ const player = Player._decodeCachedValue(cached);
920
+
921
+ expect(player.nicknames).toEqual([nickname]);
922
+ expect(player[attrName]).toEqual(attrVal);
923
+ expect(player.dataValues[EXTRA_KEY]).toBeUndefined();
924
+ });
925
+
926
+ it ('returns the record and its associated data from a single cache read', async () => {
927
+ const id = Math.random();
928
+ redisClient.mGet.mockImplementation(() => [cached]);
929
+
930
+ const player = await Player._cachedFind(id);
931
+
932
+ expect(redisClient.mGet).toHaveBeenCalledTimes(1);
933
+ expect(Player.findAll).not.toHaveBeenCalled();
934
+ expect(player.nicknames).toEqual([nickname]);
935
+ });
936
+ });
937
+
938
+
847
939
  describe('BaseRecord.clearCacheKey', () => {
848
940
  it ('deletes the given key from cache', async () => {
849
941
  const key = `Person#find:${Math.random()}`;
@@ -980,34 +1072,42 @@ describe('BaseRecord', () => {
980
1072
  });
981
1073
 
982
1074
 
983
- const hooks = ['Save', 'Destroy'];
984
- describe.each(hooks)('BaseRecord._after%sCacheHook', (hook) => {
985
- class Cacheable extends BaseRecord {
986
- static get isCacheable() { return true; }
987
- static get _redisClientFactory() { return mockRedisClientFactory; }
988
- }
989
- const hookMethod = `_after${hook}CacheHook`;
1075
+ describe('BaseRecord._afterCommitCacheHook', () => {
1076
+ const buildCacheable = () => {
1077
+ class Cacheable extends BaseRecord {
1078
+ static get isCacheable() { return true; }
1079
+ static get _redisClientFactory() { return mockRedisClientFactory; }
1080
+ }
1081
+ return Cacheable;
1082
+ };
1083
+
1084
+
1085
+ it ('is registered as an afterCommit hook for cached models', () => {
1086
+ const Cacheable = buildCacheable();
1087
+ const addSpy = jest.spyOn(Cacheable, 'addHook');
990
1088
 
991
- it (`is called as an after ${hook.toLowerCase()} hook for cached models`, () => {
992
- expect(Cacheable.addHook).toHaveBeenCalledTimes(0);
993
1089
  Cacheable.initialize();
994
- expect(Cacheable.addHook).toHaveBeenCalledTimes(2);
995
- expect(Cacheable.addHook).toHaveBeenCalledWith(`after${hook}`, Cacheable[hookMethod]);
1090
+
1091
+ expect(addSpy).toHaveBeenCalledWith('afterCommit', Cacheable._afterCommitCacheHook);
996
1092
  });
997
1093
 
998
- it ('is not called as an after save hook for non cached models', () => {
1094
+ it ('is not registered for non cached models', () => {
999
1095
  class Foo extends BaseRecord {}
1096
+ const addSpy = jest.spyOn(Foo, 'addHook');
1097
+
1000
1098
  Foo.initialize();
1001
- expect(Cacheable.addHook).toHaveBeenCalledTimes(0);
1099
+
1100
+ expect(addSpy).not.toHaveBeenCalledWith('afterCommit', expect.anything());
1002
1101
  });
1003
1102
 
1004
1103
  it ('calls BaseRecord#clearSelfCache and BaseRecord#updateCache', async () => {
1104
+ const Cacheable = buildCacheable();
1005
1105
  const person = new Cacheable();
1006
1106
  const clearSpy = jest.spyOn(person, 'clearSelfCache');
1007
1107
  const updateSpy = jest.spyOn(person, 'updateCache');
1008
1108
  const options = {};
1009
1109
 
1010
- await Cacheable[hookMethod](person,options);
1110
+ await Cacheable._afterCommitCacheHook(person, options);
1011
1111
 
1012
1112
  expect(clearSpy).toHaveBeenCalledTimes(1);
1013
1113
  expect(updateSpy).toHaveBeenCalledTimes(1);
@@ -1017,6 +1117,204 @@ describe('BaseRecord', () => {
1017
1117
  });
1018
1118
 
1019
1119
 
1120
+ describe('afterCommit hooks', () => {
1121
+ const sourceHooks = ['afterSave', 'afterDestroy', 'afterRestore', 'afterUpsert'];
1122
+
1123
+ describe('BaseRecord.addHook', () => {
1124
+ it ('delegates hook types sequelize supports to the parent implementation', () => {
1125
+ class User extends BaseRecord {}
1126
+ const callback = jest.fn();
1127
+
1128
+ User.addHook('beforeSave', callback);
1129
+
1130
+ expect(Model.addHook).toHaveBeenCalledTimes(1);
1131
+ expect(Model.addHook).toHaveBeenCalledWith('beforeSave', callback, undefined);
1132
+ });
1133
+
1134
+ it ('does not delegate afterCommit to the parent implementation', () => {
1135
+ class User extends BaseRecord {}
1136
+
1137
+ User.addHook('afterCommit', jest.fn());
1138
+
1139
+ expect(Model.addHook).toHaveBeenCalledTimes(0);
1140
+ });
1141
+
1142
+ it ('registers the callback so it runs on commit', async () => {
1143
+ class User extends BaseRecord {}
1144
+ const callback = jest.fn();
1145
+ const record = new User();
1146
+ const options = {};
1147
+ User.addHook('afterCommit', callback);
1148
+
1149
+ await User._afterCommitHandler(record, options);
1150
+
1151
+ expect(callback).toHaveBeenCalledTimes(1);
1152
+ expect(callback).toHaveBeenCalledWith(record, options);
1153
+ });
1154
+
1155
+ it ('registers a named callback', async () => {
1156
+ class User extends BaseRecord {}
1157
+ const callback = jest.fn();
1158
+ User.addHook('afterCommit', TestHelpers.Faker.Text.randomString(), callback);
1159
+
1160
+ await User._afterCommitHandler(new User(), {});
1161
+
1162
+ expect(callback).toHaveBeenCalledTimes(1);
1163
+ });
1164
+ });
1165
+
1166
+
1167
+ describe('BaseRecord.removeHook', () => {
1168
+ it ('delegates hook types sequelize supports to the parent implementation', () => {
1169
+ class User extends BaseRecord {}
1170
+ const name = TestHelpers.Faker.Text.randomString();
1171
+
1172
+ User.removeHook('beforeSave', name);
1173
+
1174
+ expect(Model.removeHook).toHaveBeenCalledTimes(1);
1175
+ expect(Model.removeHook).toHaveBeenCalledWith('beforeSave', name);
1176
+ });
1177
+
1178
+ it ('removes a registered afterCommit callback', async () => {
1179
+ class User extends BaseRecord {}
1180
+ const callback = jest.fn();
1181
+ User.addHook('afterCommit', callback);
1182
+
1183
+ User.removeHook('afterCommit', callback);
1184
+ await User._afterCommitHandler(new User(), {});
1185
+
1186
+ expect(Model.removeHook).toHaveBeenCalledTimes(0);
1187
+ expect(callback).toHaveBeenCalledTimes(0);
1188
+ });
1189
+ });
1190
+
1191
+
1192
+ describe('BaseRecord.hasHook', () => {
1193
+ it ('delegates hook types sequelize supports to the parent implementation', () => {
1194
+ class User extends BaseRecord {}
1195
+
1196
+ User.hasHook('beforeSave');
1197
+
1198
+ expect(Model.hasHook).toHaveBeenCalledTimes(1);
1199
+ expect(Model.hasHook).toHaveBeenCalledWith('beforeSave');
1200
+ });
1201
+
1202
+ it ('reports false when no afterCommit callback is registered', () => {
1203
+ class User extends BaseRecord {}
1204
+
1205
+ expect(User.hasHook('afterCommit')).toBe(false);
1206
+ });
1207
+
1208
+ it ('reports true once an afterCommit callback is registered', () => {
1209
+ class User extends BaseRecord {}
1210
+ User.addHook('afterCommit', jest.fn());
1211
+
1212
+ expect(User.hasHook('afterCommit')).toBe(true);
1213
+ });
1214
+ });
1215
+
1216
+
1217
+ describe('BaseRecord.hasHooks', () => {
1218
+ it ('reports on afterCommit the same way hasHook does', () => {
1219
+ class User extends BaseRecord {}
1220
+ User.addHook('afterCommit', jest.fn());
1221
+
1222
+ expect(User.hasHooks('afterCommit')).toBe(true);
1223
+ });
1224
+ });
1225
+
1226
+
1227
+ describe('BaseRecord.afterCommit', () => {
1228
+ it ('registers the callback like addHook does', async () => {
1229
+ class User extends BaseRecord {}
1230
+ const callback = jest.fn();
1231
+
1232
+ User.afterCommit(callback);
1233
+ await User._afterCommitHandler(new User(), {});
1234
+
1235
+ expect(callback).toHaveBeenCalledTimes(1);
1236
+ });
1237
+ });
1238
+
1239
+
1240
+ describe('BaseRecord.initializeHooks', () => {
1241
+ it.each(sourceHooks)('registers the afterCommit handler on %s', (hookType) => {
1242
+ class User extends BaseRecord {}
1243
+
1244
+ User.initialize();
1245
+
1246
+ expect(Model.addHook).toHaveBeenCalledWith(hookType, User._afterCommitHandler, undefined);
1247
+ });
1248
+
1249
+ it ('does not register the handler on afterCreate or afterUpdate', () => {
1250
+ class User extends BaseRecord {}
1251
+
1252
+ User.initialize();
1253
+
1254
+ expect(Model.addHook).not.toHaveBeenCalledWith('afterCreate', expect.anything(), undefined);
1255
+ expect(Model.addHook).not.toHaveBeenCalledWith('afterUpdate', expect.anything(), undefined);
1256
+ });
1257
+ });
1258
+
1259
+
1260
+ describe('BaseRecord._afterCommitHandler', () => {
1261
+ it ('runs callbacks inherited from a parent model', async () => {
1262
+ class User extends BaseRecord {}
1263
+ class Admin extends User {}
1264
+ const callback = jest.fn();
1265
+ User.addHook('afterCommit', callback);
1266
+
1267
+ await Admin._afterCommitHandler(new Admin(), {});
1268
+
1269
+ expect(callback).toHaveBeenCalledTimes(1);
1270
+ });
1271
+
1272
+ it ('does not run callbacks registered on an unrelated model', async () => {
1273
+ class User extends BaseRecord {}
1274
+ class Account extends BaseRecord {}
1275
+ const callback = jest.fn();
1276
+ User.addHook('afterCommit', callback);
1277
+
1278
+ await Account._afterCommitHandler(new Account(), {});
1279
+
1280
+ expect(callback).toHaveBeenCalledTimes(0);
1281
+ });
1282
+
1283
+ it ('unwraps the record from the array afterUpsert reports', async () => {
1284
+ class User extends BaseRecord {}
1285
+ const callback = jest.fn();
1286
+ const record = new User();
1287
+ const options = {};
1288
+ User.addHook('afterCommit', callback);
1289
+
1290
+ await User._afterCommitHandler([record, true], options);
1291
+
1292
+ expect(callback).toHaveBeenCalledWith(record, options);
1293
+ });
1294
+
1295
+ it ('defers to the transaction when the write is in one', async () => {
1296
+ class User extends BaseRecord {}
1297
+ const transaction = { afterCommit: jest.fn(), parent: null };
1298
+ const callback = jest.fn();
1299
+ User.addHook('afterCommit', callback);
1300
+
1301
+ await User._afterCommitHandler(new User(), { transaction });
1302
+
1303
+ expect(transaction.afterCommit).toHaveBeenCalledTimes(1);
1304
+ expect(callback).toHaveBeenCalledTimes(0);
1305
+ });
1306
+
1307
+ it ('lets a callback error propagate', async () => {
1308
+ class User extends BaseRecord {}
1309
+ const message = TestHelpers.Faker.Text.randomString();
1310
+ User.addHook('afterCommit', () => { throw new Error(message); });
1311
+
1312
+ await expect(User._afterCommitHandler(new User(), {})).rejects.toThrow(message);
1313
+ });
1314
+ });
1315
+ });
1316
+
1317
+
1020
1318
  describe('.documentationSchema', () => {
1021
1319
  const buildModel = (attributes) => {
1022
1320
  class Thing extends BaseRecord {}