@adtrackify/at-service-common 3.19.18 → 3.19.20
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/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +402 -46
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +1 -1
- package/dist/cjs/services/db/identity-cache-dynamodb-service.d.ts +3 -3
- package/dist/cjs/services/db/identity-cache-dynamodb-service.js +57 -86
- package/dist/cjs/services/db/identity-cache-dynamodb-service.js.map +1 -1
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +402 -46
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +1 -1
- package/dist/esm/services/db/identity-cache-dynamodb-service.d.ts +3 -3
- package/dist/esm/services/db/identity-cache-dynamodb-service.js +58 -87
- package/dist/esm/services/db/identity-cache-dynamodb-service.js.map +1 -1
- package/package.json +2 -2
|
@@ -29,6 +29,15 @@ const mockedDynamoDbClient = index_js_1.DynamoDbClient;
|
|
|
29
29
|
describe('IdentityCacheDynamoDbService', () => {
|
|
30
30
|
beforeEach(() => {
|
|
31
31
|
jest.clearAllMocks();
|
|
32
|
+
mockedDynamoDbClient.safeGet.mockReset();
|
|
33
|
+
mockedDynamoDbClient.safePut.mockReset();
|
|
34
|
+
mockedDynamoDbClient.safeDelete.mockReset();
|
|
35
|
+
mockedDynamoDbClient.safeBatchGet.mockReset();
|
|
36
|
+
mockedDynamoDbClient.safeBatchWrite.mockReset();
|
|
37
|
+
mockedDynamoDbClient.safeQueryByGSI.mockReset();
|
|
38
|
+
mockedDynamoDbClient.batchWrite.mockReset();
|
|
39
|
+
mockInvokeFunction.mockReset();
|
|
40
|
+
identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.lambdaInvokeClient = undefined;
|
|
32
41
|
});
|
|
33
42
|
describe('getIdentityWithCaching', () => {
|
|
34
43
|
const pixelId = 'pixel123';
|
|
@@ -155,7 +164,7 @@ describe('IdentityCacheDynamoDbService', () => {
|
|
|
155
164
|
const incomingIdentity = {
|
|
156
165
|
traits: { emails: ['test@email.com'] },
|
|
157
166
|
};
|
|
158
|
-
mockedDynamoDbClient.
|
|
167
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
159
168
|
mockInvokeFunction.mockResolvedValueOnce({
|
|
160
169
|
statusCode: 200,
|
|
161
170
|
body: JSON.stringify({
|
|
@@ -175,7 +184,7 @@ describe('IdentityCacheDynamoDbService', () => {
|
|
|
175
184
|
const incomingIdentity = {
|
|
176
185
|
traits: { emails: ['test@email.com'] },
|
|
177
186
|
};
|
|
178
|
-
mockedDynamoDbClient.
|
|
187
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
179
188
|
mockInvokeFunction.mockResolvedValueOnce({
|
|
180
189
|
statusCode: 200,
|
|
181
190
|
body: JSON.stringify({ identity: undefined }),
|
|
@@ -209,7 +218,7 @@ describe('IdentityCacheDynamoDbService', () => {
|
|
|
209
218
|
const incomingIdentity = {
|
|
210
219
|
traits: { emails: ['test@email.com'] },
|
|
211
220
|
};
|
|
212
|
-
mockedDynamoDbClient.
|
|
221
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
213
222
|
mockInvokeFunction.mockResolvedValueOnce({
|
|
214
223
|
statusCode: 200,
|
|
215
224
|
body: JSON.stringify({
|
|
@@ -239,26 +248,22 @@ describe('IdentityCacheDynamoDbService', () => {
|
|
|
239
248
|
it('should build correct pk for email lookup (lowercase)', async () => {
|
|
240
249
|
const pixelId = 'pixel123';
|
|
241
250
|
const email = 'Test@Email.COM';
|
|
242
|
-
mockedDynamoDbClient.
|
|
251
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
243
252
|
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
244
253
|
traits: { emails: [email] },
|
|
245
254
|
});
|
|
246
|
-
expect(mockedDynamoDbClient.
|
|
247
|
-
{ pk: `email#${pixelId}#test@email.com` },
|
|
248
|
-
]);
|
|
255
|
+
expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#test@email.com`);
|
|
249
256
|
});
|
|
250
257
|
});
|
|
251
|
-
describe('buildUserIdPk', () => {
|
|
252
|
-
it('should
|
|
258
|
+
describe('buildUserIdPk (no longer used for lookup)', () => {
|
|
259
|
+
it('should NOT lookup by userId (optimization: email-only secondary lookup)', async () => {
|
|
253
260
|
const pixelId = 'pixel123';
|
|
254
261
|
const userId = ' user789 ';
|
|
255
|
-
mockedDynamoDbClient.
|
|
262
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
256
263
|
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
257
264
|
traits: { userIds: [userId] },
|
|
258
265
|
});
|
|
259
|
-
expect(mockedDynamoDbClient.safeBatchGet).
|
|
260
|
-
{ pk: `user_id#${pixelId}#user789` },
|
|
261
|
-
]);
|
|
266
|
+
expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
|
|
262
267
|
});
|
|
263
268
|
});
|
|
264
269
|
});
|
|
@@ -324,66 +329,62 @@ describe('IdentityCacheDynamoDbService', () => {
|
|
|
324
329
|
});
|
|
325
330
|
});
|
|
326
331
|
describe('without identityId (secondary lookup)', () => {
|
|
327
|
-
it('should use
|
|
328
|
-
const
|
|
332
|
+
it('should use single get for email lookup (pointer-based)', async () => {
|
|
333
|
+
const pointerRecord = {
|
|
329
334
|
pk: `email#${pixelId}#test@email.com`,
|
|
330
335
|
pixelId,
|
|
331
336
|
identityId: 'resolved-id',
|
|
332
|
-
|
|
333
|
-
identityId: 'resolved-id',
|
|
334
|
-
traits: { emails: ['test@email.com'] },
|
|
335
|
-
},
|
|
337
|
+
gsi1pk: 'resolved-id',
|
|
336
338
|
updatedAt: new Date().toISOString(),
|
|
337
339
|
};
|
|
338
|
-
mockedDynamoDbClient.
|
|
340
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
|
|
339
341
|
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
340
342
|
traits: { emails: ['test@email.com'], userIds: ['user123'] },
|
|
341
343
|
});
|
|
342
|
-
expect(mockedDynamoDbClient.
|
|
343
|
-
|
|
344
|
-
{ pk: `user_id#${pixelId}#user123` },
|
|
345
|
-
]);
|
|
344
|
+
expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#test@email.com`);
|
|
345
|
+
expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
|
|
346
346
|
expect(result.resolvedIdentity).toBeDefined();
|
|
347
347
|
expect(result.resolvedIdentity?.traits?.emails).toContain('test@email.com');
|
|
348
348
|
expect(result.isCacheStale).toBe(true);
|
|
349
349
|
});
|
|
350
|
-
it('should
|
|
351
|
-
const
|
|
350
|
+
it('should return discovered identityId with stale flag for Neptune resolution', async () => {
|
|
351
|
+
const pointerRecord = {
|
|
352
352
|
pk: `email#${pixelId}#test@email.com`,
|
|
353
353
|
pixelId,
|
|
354
|
-
identityId: '
|
|
355
|
-
|
|
356
|
-
updatedAt: new Date().toISOString(),
|
|
357
|
-
};
|
|
358
|
-
const response2 = {
|
|
359
|
-
pk: `user_id#${pixelId}#user123`,
|
|
360
|
-
pixelId,
|
|
361
|
-
identityId: 'identity-2',
|
|
362
|
-
response: { identityId: 'identity-2' },
|
|
354
|
+
identityId: 'discovered-id',
|
|
355
|
+
gsi1pk: 'discovered-id',
|
|
363
356
|
updatedAt: new Date().toISOString(),
|
|
364
357
|
};
|
|
365
|
-
mockedDynamoDbClient.
|
|
358
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
|
|
366
359
|
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
367
360
|
traits: { emails: ['test@email.com'], userIds: ['user123'] },
|
|
368
361
|
});
|
|
369
|
-
expect(result.resolvedIdentity).
|
|
362
|
+
expect(result.resolvedIdentity).toBeDefined();
|
|
363
|
+
expect(result.resolvedIdentity?.identityId).toBe('discovered-id');
|
|
370
364
|
expect(result.isCacheStale).toBe(true);
|
|
371
365
|
});
|
|
372
366
|
it('should return cache miss when no secondary keys match', async () => {
|
|
373
|
-
mockedDynamoDbClient.
|
|
367
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
374
368
|
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
375
369
|
traits: { emails: ['unknown@email.com'] },
|
|
376
370
|
});
|
|
377
371
|
expect(result.resolvedIdentity).toBeUndefined();
|
|
378
372
|
expect(result.isCacheStale).toBe(true);
|
|
379
373
|
});
|
|
380
|
-
it('should return cache miss when no
|
|
374
|
+
it('should return cache miss when no emails to lookup', async () => {
|
|
375
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
376
|
+
traits: { userIds: ['user123'] },
|
|
377
|
+
});
|
|
378
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
379
|
+
expect(result.isCacheStale).toBe(true);
|
|
380
|
+
expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
|
|
381
|
+
});
|
|
382
|
+
it('should return cache miss when empty traits', async () => {
|
|
381
383
|
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
382
384
|
traits: {},
|
|
383
385
|
});
|
|
384
386
|
expect(result.resolvedIdentity).toBeUndefined();
|
|
385
387
|
expect(result.isCacheStale).toBe(true);
|
|
386
|
-
expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
|
|
387
388
|
});
|
|
388
389
|
});
|
|
389
390
|
describe('error handling (fail-open)', () => {
|
|
@@ -395,8 +396,8 @@ describe('IdentityCacheDynamoDbService', () => {
|
|
|
395
396
|
expect(result.resolvedIdentity).toBeUndefined();
|
|
396
397
|
expect(result.isCacheStale).toBe(true);
|
|
397
398
|
});
|
|
398
|
-
it('should return cache miss on
|
|
399
|
-
mockedDynamoDbClient.
|
|
399
|
+
it('should return cache miss on safeGet error for email lookup', async () => {
|
|
400
|
+
mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
|
|
400
401
|
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
401
402
|
traits: { emails: ['test@email.com'] },
|
|
402
403
|
});
|
|
@@ -419,7 +420,7 @@ describe('IdentityCacheDynamoDbService', () => {
|
|
|
419
420
|
});
|
|
420
421
|
describe('updateIdentityCache', () => {
|
|
421
422
|
const pixelId = 'pixel123';
|
|
422
|
-
it('should write identity with
|
|
423
|
+
it('should write identity with email pointer (no full blob on secondary keys)', async () => {
|
|
423
424
|
const identity = {
|
|
424
425
|
identityId: 'identity456',
|
|
425
426
|
traits: {
|
|
@@ -430,13 +431,21 @@ describe('IdentityCacheDynamoDbService', () => {
|
|
|
430
431
|
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
431
432
|
expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalledTimes(1);
|
|
432
433
|
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
433
|
-
expect(writtenItems).toHaveLength(
|
|
434
|
+
expect(writtenItems).toHaveLength(3);
|
|
434
435
|
const pks = writtenItems.map((item) => item.pk);
|
|
435
436
|
expect(pks).toContain(`identity#${pixelId}#identity456`);
|
|
436
437
|
expect(pks).toContain(`email#${pixelId}#test@email.com`);
|
|
437
438
|
expect(pks).toContain(`email#${pixelId}#other@email.com`);
|
|
438
|
-
expect(pks).toContain(`user_id#${pixelId}#user1`);
|
|
439
|
-
expect(pks).toContain(`user_id#${pixelId}#user2`);
|
|
439
|
+
expect(pks).not.toContain(`user_id#${pixelId}#user1`);
|
|
440
|
+
expect(pks).not.toContain(`user_id#${pixelId}#user2`);
|
|
441
|
+
const identityItem = writtenItems.find((item) => item.pk.startsWith('identity#'));
|
|
442
|
+
expect(identityItem).toBeDefined();
|
|
443
|
+
expect(identityItem.response).toBeDefined();
|
|
444
|
+
expect(identityItem.response.identityId).toBe('identity456');
|
|
445
|
+
const emailItem = writtenItems.find((item) => item.pk.startsWith('email#'));
|
|
446
|
+
expect(emailItem).toBeDefined();
|
|
447
|
+
expect(emailItem.response).toBeUndefined();
|
|
448
|
+
expect(emailItem.identityId).toBe('identity456');
|
|
440
449
|
});
|
|
441
450
|
it('should not throw on safeBatchWrite error (fail-open)', async () => {
|
|
442
451
|
mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('DynamoDB error'));
|
|
@@ -781,5 +790,352 @@ describe('IdentityCacheDynamoDbService', () => {
|
|
|
781
790
|
expect(result.isCacheStale).toBe(false);
|
|
782
791
|
});
|
|
783
792
|
});
|
|
793
|
+
describe('multiple emails pointer writes', () => {
|
|
794
|
+
const pixelId = 'pixel123';
|
|
795
|
+
it('should write ALL emails as pointer records (not just first)', async () => {
|
|
796
|
+
const identity = {
|
|
797
|
+
identityId: 'identity456',
|
|
798
|
+
traits: {
|
|
799
|
+
emails: ['first@email.com', 'second@email.com', 'third@email.com', 'fourth@email.com'],
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
803
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
804
|
+
expect(writtenItems).toHaveLength(5);
|
|
805
|
+
const emailPks = writtenItems
|
|
806
|
+
.filter((item) => item.pk.startsWith('email#'))
|
|
807
|
+
.map((item) => item.pk);
|
|
808
|
+
expect(emailPks).toContain(`email#${pixelId}#first@email.com`);
|
|
809
|
+
expect(emailPks).toContain(`email#${pixelId}#second@email.com`);
|
|
810
|
+
expect(emailPks).toContain(`email#${pixelId}#third@email.com`);
|
|
811
|
+
expect(emailPks).toContain(`email#${pixelId}#fourth@email.com`);
|
|
812
|
+
});
|
|
813
|
+
it('should skip null/undefined emails in array', async () => {
|
|
814
|
+
const identity = {
|
|
815
|
+
identityId: 'identity456',
|
|
816
|
+
traits: {
|
|
817
|
+
emails: ['valid@email.com', null, undefined, '', 'another@email.com'],
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
821
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
822
|
+
const emailItems = writtenItems.filter((item) => item.pk.startsWith('email#'));
|
|
823
|
+
expect(emailItems).toHaveLength(2);
|
|
824
|
+
expect(emailItems.map((e) => e.pk)).toContain(`email#${pixelId}#valid@email.com`);
|
|
825
|
+
expect(emailItems.map((e) => e.pk)).toContain(`email#${pixelId}#another@email.com`);
|
|
826
|
+
});
|
|
827
|
+
it('should handle empty emails array', async () => {
|
|
828
|
+
const identity = {
|
|
829
|
+
identityId: 'identity456',
|
|
830
|
+
traits: { emails: [] },
|
|
831
|
+
};
|
|
832
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
833
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
834
|
+
expect(writtenItems).toHaveLength(1);
|
|
835
|
+
expect(writtenItems[0].pk).toBe(`identity#${pixelId}#identity456`);
|
|
836
|
+
});
|
|
837
|
+
it('should handle email normalization (lowercase)', async () => {
|
|
838
|
+
const identity = {
|
|
839
|
+
identityId: 'identity456',
|
|
840
|
+
traits: {
|
|
841
|
+
emails: ['UPPERCASE@EMAIL.COM', 'MixedCase@Email.Com'],
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
845
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
846
|
+
const emailPks = writtenItems
|
|
847
|
+
.filter((item) => item.pk.startsWith('email#'))
|
|
848
|
+
.map((item) => item.pk);
|
|
849
|
+
expect(emailPks).toContain(`email#${pixelId}#uppercase@email.com`);
|
|
850
|
+
expect(emailPks).toContain(`email#${pixelId}#mixedcase@email.com`);
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
describe('null/undefined traits edge cases', () => {
|
|
854
|
+
const pixelId = 'pixel123';
|
|
855
|
+
it('should handle null traits object', async () => {
|
|
856
|
+
const identity = {
|
|
857
|
+
identityId: 'identity456',
|
|
858
|
+
traits: null,
|
|
859
|
+
};
|
|
860
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
861
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
862
|
+
expect(writtenItems).toHaveLength(1);
|
|
863
|
+
});
|
|
864
|
+
it('should handle undefined traits object', async () => {
|
|
865
|
+
const identity = {
|
|
866
|
+
identityId: 'identity456',
|
|
867
|
+
traits: undefined,
|
|
868
|
+
};
|
|
869
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
870
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
871
|
+
expect(writtenItems).toHaveLength(1);
|
|
872
|
+
});
|
|
873
|
+
it('should return cache miss for identity with null traits in lookup', async () => {
|
|
874
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
875
|
+
traits: null,
|
|
876
|
+
});
|
|
877
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
878
|
+
expect(result.isCacheStale).toBe(true);
|
|
879
|
+
expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
|
|
880
|
+
});
|
|
881
|
+
it('should handle identity with undefined emails in traits', async () => {
|
|
882
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
883
|
+
traits: { userIds: ['user123'] },
|
|
884
|
+
});
|
|
885
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
886
|
+
expect(result.isCacheStale).toBe(true);
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
describe('backward compatibility (old full-blob format)', () => {
|
|
890
|
+
const pixelId = 'pixel123';
|
|
891
|
+
it('should read old format email record with full response blob', async () => {
|
|
892
|
+
const oldFormatEmailRecord = {
|
|
893
|
+
pk: `email#${pixelId}#test@email.com`,
|
|
894
|
+
pixelId,
|
|
895
|
+
identityId: 'discovered-id',
|
|
896
|
+
gsi1pk: 'discovered-id',
|
|
897
|
+
response: {
|
|
898
|
+
identityId: 'discovered-id',
|
|
899
|
+
traits: { emails: ['test@email.com', 'other@email.com'] },
|
|
900
|
+
},
|
|
901
|
+
updatedAt: new Date().toISOString(),
|
|
902
|
+
};
|
|
903
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(oldFormatEmailRecord);
|
|
904
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
905
|
+
traits: { emails: ['test@email.com'] },
|
|
906
|
+
});
|
|
907
|
+
expect(result.resolvedIdentity).toBeDefined();
|
|
908
|
+
expect(result.resolvedIdentity?.identityId).toBe('discovered-id');
|
|
909
|
+
expect(result.isCacheStale).toBe(true);
|
|
910
|
+
});
|
|
911
|
+
it('should read new format pointer record (no response blob)', async () => {
|
|
912
|
+
const newFormatPointerRecord = {
|
|
913
|
+
pk: `email#${pixelId}#test@email.com`,
|
|
914
|
+
pixelId,
|
|
915
|
+
identityId: 'discovered-id',
|
|
916
|
+
gsi1pk: 'discovered-id',
|
|
917
|
+
updatedAt: new Date().toISOString(),
|
|
918
|
+
};
|
|
919
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(newFormatPointerRecord);
|
|
920
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
921
|
+
traits: { emails: ['test@email.com'] },
|
|
922
|
+
});
|
|
923
|
+
expect(result.resolvedIdentity).toBeDefined();
|
|
924
|
+
expect(result.resolvedIdentity?.identityId).toBe('discovered-id');
|
|
925
|
+
expect(result.isCacheStale).toBe(true);
|
|
926
|
+
});
|
|
927
|
+
it('should trigger Neptune resolution after finding pointer record', async () => {
|
|
928
|
+
const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
|
|
929
|
+
const pointerRecord = {
|
|
930
|
+
pk: `email#${pixelId}#test@email.com`,
|
|
931
|
+
pixelId,
|
|
932
|
+
identityId: 'discovered-id',
|
|
933
|
+
gsi1pk: 'discovered-id',
|
|
934
|
+
updatedAt: new Date().toISOString(),
|
|
935
|
+
};
|
|
936
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
|
|
937
|
+
mockInvokeFunction.mockResolvedValueOnce({
|
|
938
|
+
statusCode: 200,
|
|
939
|
+
body: JSON.stringify({
|
|
940
|
+
identity: {
|
|
941
|
+
identityId: 'discovered-id',
|
|
942
|
+
traits: { emails: ['test@email.com', 'resolved@email.com'] },
|
|
943
|
+
},
|
|
944
|
+
}),
|
|
945
|
+
});
|
|
946
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
|
|
947
|
+
expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, expect.objectContaining({
|
|
948
|
+
pixelId,
|
|
949
|
+
context: {
|
|
950
|
+
identity: expect.objectContaining({
|
|
951
|
+
identityId: 'discovered-id',
|
|
952
|
+
}),
|
|
953
|
+
},
|
|
954
|
+
}));
|
|
955
|
+
expect(result?.identityId).toBe('discovered-id');
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
describe('deleteIdentityCache with multiple email pointers', () => {
|
|
959
|
+
const pixelId = 'pixel123';
|
|
960
|
+
it('should delete all email pointers from both incoming and resolved identities', async () => {
|
|
961
|
+
mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
|
|
962
|
+
{ pk: `identity#${pixelId}#identity456`, pixelId },
|
|
963
|
+
]);
|
|
964
|
+
mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
|
|
965
|
+
const incomingIdentity = {
|
|
966
|
+
identityId: 'identity456',
|
|
967
|
+
traits: { emails: ['incoming1@email.com', 'incoming2@email.com'] },
|
|
968
|
+
};
|
|
969
|
+
const resolvedIdentity = {
|
|
970
|
+
identityId: 'identity456',
|
|
971
|
+
traits: { emails: ['resolved1@email.com', 'resolved2@email.com', 'resolved3@email.com'] },
|
|
972
|
+
};
|
|
973
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
|
|
974
|
+
expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
|
|
975
|
+
const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0][0];
|
|
976
|
+
const deleteRequests = batchWriteCall.RequestItems[Object.keys(batchWriteCall.RequestItems)[0]];
|
|
977
|
+
const deletedPks = deleteRequests.map((req) => req.DeleteRequest.Key.pk);
|
|
978
|
+
expect(deletedPks).toContain(`identity#${pixelId}#identity456`);
|
|
979
|
+
expect(deletedPks).toContain(`email#${pixelId}#incoming1@email.com`);
|
|
980
|
+
expect(deletedPks).toContain(`email#${pixelId}#incoming2@email.com`);
|
|
981
|
+
expect(deletedPks).toContain(`email#${pixelId}#resolved1@email.com`);
|
|
982
|
+
expect(deletedPks).toContain(`email#${pixelId}#resolved2@email.com`);
|
|
983
|
+
expect(deletedPks).toContain(`email#${pixelId}#resolved3@email.com`);
|
|
984
|
+
});
|
|
985
|
+
it('should handle empty emails in delete', async () => {
|
|
986
|
+
mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
|
|
987
|
+
{ pk: `identity#${pixelId}#identity456`, pixelId },
|
|
988
|
+
]);
|
|
989
|
+
mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
|
|
990
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456', traits: { emails: [] } }, { identityId: 'identity456' });
|
|
991
|
+
expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
|
|
992
|
+
});
|
|
993
|
+
it('should handle missing traits in delete', async () => {
|
|
994
|
+
mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
|
|
995
|
+
{ pk: `identity#${pixelId}#identity456`, pixelId },
|
|
996
|
+
]);
|
|
997
|
+
mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
|
|
998
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
|
|
999
|
+
expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
|
|
1000
|
+
});
|
|
1001
|
+
it('should not delete items from other pixelIds', async () => {
|
|
1002
|
+
const otherPixelId = 'other-pixel';
|
|
1003
|
+
mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
|
|
1004
|
+
{ pk: `identity#${pixelId}#identity456`, pixelId },
|
|
1005
|
+
{ pk: `identity#${otherPixelId}#identity456`, pixelId: otherPixelId },
|
|
1006
|
+
]);
|
|
1007
|
+
mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
|
|
1008
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
|
|
1009
|
+
const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0][0];
|
|
1010
|
+
const deleteRequests = batchWriteCall.RequestItems[Object.keys(batchWriteCall.RequestItems)[0]];
|
|
1011
|
+
const deletedPks = deleteRequests.map((req) => req.DeleteRequest.Key.pk);
|
|
1012
|
+
expect(deletedPks).toContain(`identity#${pixelId}#identity456`);
|
|
1013
|
+
expect(deletedPks).not.toContain(`identity#${otherPixelId}#identity456`);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
describe('GSI1 query for reverse lookups', () => {
|
|
1017
|
+
const pixelId = 'pixel123';
|
|
1018
|
+
it('should find all records by identityId via GSI1', async () => {
|
|
1019
|
+
const identityId = 'identity456';
|
|
1020
|
+
const mockResults = [
|
|
1021
|
+
{ pk: `identity#${pixelId}#${identityId}`, pixelId, identityId },
|
|
1022
|
+
{ pk: `email#${pixelId}#email1@test.com`, pixelId, identityId },
|
|
1023
|
+
{ pk: `email#${pixelId}#email2@test.com`, pixelId, identityId },
|
|
1024
|
+
];
|
|
1025
|
+
mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue(mockResults);
|
|
1026
|
+
mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
|
|
1027
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId }, { identityId });
|
|
1028
|
+
expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalledWith(expect.any(String), expect.any(String), 'gsi1pk', identityId);
|
|
1029
|
+
});
|
|
1030
|
+
it('should handle GSI1 query returning empty results', async () => {
|
|
1031
|
+
mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([]);
|
|
1032
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'unknown-id' }, { identityId: 'unknown-id' });
|
|
1033
|
+
expect(mockedDynamoDbClient.batchWrite).not.toHaveBeenCalled();
|
|
1034
|
+
});
|
|
1035
|
+
it('should handle GSI1 query error gracefully (fail-open)', async () => {
|
|
1036
|
+
mockedDynamoDbClient.safeQueryByGSI.mockRejectedValueOnce(new Error('GSI query failed'));
|
|
1037
|
+
await expect(identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' })).resolves.toBeUndefined();
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
describe('email pointer lookups', () => {
|
|
1041
|
+
const pixelId = 'pixel123';
|
|
1042
|
+
it('should only lookup first email (optimization)', async () => {
|
|
1043
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1044
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
1045
|
+
traits: { emails: ['first@email.com', 'second@email.com', 'third@email.com'] },
|
|
1046
|
+
});
|
|
1047
|
+
expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledTimes(1);
|
|
1048
|
+
expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#first@email.com`);
|
|
1049
|
+
});
|
|
1050
|
+
it('should handle whitespace in email during lookup', async () => {
|
|
1051
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1052
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
1053
|
+
traits: { emails: [' test@email.com '] },
|
|
1054
|
+
});
|
|
1055
|
+
expect(mockedDynamoDbClient.safeGet).toHaveBeenCalled();
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
describe('fail-open behavior comprehensive', () => {
|
|
1059
|
+
const pixelId = 'pixel123';
|
|
1060
|
+
const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
|
|
1061
|
+
it('should return incoming identity when Neptune throws (fail-open fallback)', async () => {
|
|
1062
|
+
const incomingIdentity = {
|
|
1063
|
+
identityId: 'identity456',
|
|
1064
|
+
traits: { emails: ['test@email.com'] },
|
|
1065
|
+
};
|
|
1066
|
+
mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB down'));
|
|
1067
|
+
mockInvokeFunction.mockRejectedValueOnce(new Error('Lambda down'));
|
|
1068
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
|
|
1069
|
+
expect(result).toEqual(incomingIdentity);
|
|
1070
|
+
});
|
|
1071
|
+
it('should continue when cache write fails after Neptune success', async () => {
|
|
1072
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1073
|
+
mockInvokeFunction.mockResolvedValueOnce({
|
|
1074
|
+
statusCode: 200,
|
|
1075
|
+
body: JSON.stringify({
|
|
1076
|
+
identity: { identityId: 'resolved-id', traits: { emails: ['test@email.com'] } },
|
|
1077
|
+
}),
|
|
1078
|
+
});
|
|
1079
|
+
mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('Write failed'));
|
|
1080
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
|
|
1081
|
+
expect(result?.identityId).toBe('resolved-id');
|
|
1082
|
+
});
|
|
1083
|
+
it('should return incoming identity when Neptune returns non-200', async () => {
|
|
1084
|
+
const incomingIdentity = {
|
|
1085
|
+
identityId: 'incoming-id',
|
|
1086
|
+
traits: { emails: ['test@email.com'] },
|
|
1087
|
+
};
|
|
1088
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1089
|
+
mockInvokeFunction.mockResolvedValueOnce({
|
|
1090
|
+
statusCode: 500,
|
|
1091
|
+
body: 'Internal Server Error',
|
|
1092
|
+
});
|
|
1093
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
|
|
1094
|
+
expect(result).toEqual(incomingIdentity);
|
|
1095
|
+
});
|
|
1096
|
+
it('should return incoming identity when Neptune returns malformed JSON (fail-open)', async () => {
|
|
1097
|
+
const incomingIdentity = {
|
|
1098
|
+
identityId: 'test-id',
|
|
1099
|
+
traits: { emails: ['test@email.com'] },
|
|
1100
|
+
};
|
|
1101
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
1102
|
+
mockInvokeFunction.mockResolvedValueOnce({
|
|
1103
|
+
statusCode: 200,
|
|
1104
|
+
body: 'not valid json',
|
|
1105
|
+
});
|
|
1106
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
|
|
1107
|
+
expect(result).toEqual(incomingIdentity);
|
|
1108
|
+
});
|
|
1109
|
+
it('should return undefined only when both inputs are invalid', async () => {
|
|
1110
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityWithCaching(null, null, lambdaArn);
|
|
1111
|
+
expect(result).toBeUndefined();
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
describe('gsi1pk attribute on all records', () => {
|
|
1115
|
+
const pixelId = 'pixel123';
|
|
1116
|
+
it('should include gsi1pk on identity record', async () => {
|
|
1117
|
+
const identity = {
|
|
1118
|
+
identityId: 'identity456',
|
|
1119
|
+
traits: { emails: ['test@email.com'] },
|
|
1120
|
+
};
|
|
1121
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
1122
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
1123
|
+
const identityItem = writtenItems.find((item) => item.pk.startsWith('identity#'));
|
|
1124
|
+
expect(identityItem).toBeDefined();
|
|
1125
|
+
expect(identityItem.gsi1pk).toBe('identity456');
|
|
1126
|
+
});
|
|
1127
|
+
it('should include gsi1pk on all email pointer records', async () => {
|
|
1128
|
+
const identity = {
|
|
1129
|
+
identityId: 'identity456',
|
|
1130
|
+
traits: { emails: ['email1@test.com', 'email2@test.com'] },
|
|
1131
|
+
};
|
|
1132
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
1133
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
1134
|
+
const emailItems = writtenItems.filter((item) => item.pk.startsWith('email#'));
|
|
1135
|
+
for (const emailItem of emailItems) {
|
|
1136
|
+
expect(emailItem.gsi1pk).toBe('identity456');
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
784
1140
|
});
|
|
785
1141
|
//# sourceMappingURL=identity-cache-dynamodb-service.spec.js.map
|