@adtrackify/at-service-common 4.0.5 → 4.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.d.ts +1 -0
  2. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js +646 -0
  3. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js.map +1 -0
  4. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.d.ts +1 -0
  5. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js +327 -0
  6. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js.map +1 -0
  7. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +277 -131
  8. package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +1 -1
  9. package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js +74 -80
  10. package/dist/cjs/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -1
  11. package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.d.ts +1 -0
  12. package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.js +567 -0
  13. package/dist/cjs/__tests__/identity-cache/identity-gateway-service.spec.js.map +1 -0
  14. package/dist/cjs/services/db/identity-cache-dynamodb-service/constants.d.ts +3 -0
  15. package/dist/cjs/services/db/identity-cache-dynamodb-service/constants.js +7 -0
  16. package/dist/cjs/services/db/identity-cache-dynamodb-service/constants.js.map +1 -0
  17. package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.d.ts +14 -0
  18. package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.js +251 -0
  19. package/dist/cjs/services/db/identity-cache-dynamodb-service/helpers.js.map +1 -0
  20. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.d.ts +24 -0
  21. package/dist/cjs/services/db/{identity-cache-dynamodb-service.js → identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js} +221 -343
  22. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js.map +1 -0
  23. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.d.ts +22 -0
  24. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.js +375 -0
  25. package/dist/cjs/services/db/identity-cache-dynamodb-service/identity-gateway-service.js.map +1 -0
  26. package/dist/cjs/services/db/identity-cache-dynamodb-service/index.d.ts +4 -0
  27. package/dist/cjs/services/db/identity-cache-dynamodb-service/index.js +21 -0
  28. package/dist/cjs/services/db/identity-cache-dynamodb-service/index.js.map +1 -0
  29. package/dist/cjs/services/db/identity-cache-dynamodb-service/key-builders.d.ts +12 -0
  30. package/dist/cjs/services/db/identity-cache-dynamodb-service/key-builders.js +32 -0
  31. package/dist/cjs/services/db/identity-cache-dynamodb-service/key-builders.js.map +1 -0
  32. package/dist/cjs/services/db/identity-cache-dynamodb-service/types.d.ts +205 -0
  33. package/dist/cjs/services/db/identity-cache-dynamodb-service/types.js +35 -0
  34. package/dist/cjs/services/db/identity-cache-dynamodb-service/types.js.map +1 -0
  35. package/dist/cjs/services/db/index.d.ts +4 -1
  36. package/dist/cjs/services/db/index.js +4 -1
  37. package/dist/cjs/services/db/index.js.map +1 -1
  38. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.d.ts +1 -0
  39. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js +644 -0
  40. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-db-service-v2-comparison.spec.js.map +1 -0
  41. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.d.ts +1 -0
  42. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js +325 -0
  43. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service-v2-comparison.spec.js.map +1 -0
  44. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +277 -131
  45. package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +1 -1
  46. package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js +74 -80
  47. package/dist/esm/__tests__/identity-cache/identity-cache-tier-routing.spec.js.map +1 -1
  48. package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.d.ts +1 -0
  49. package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.js +565 -0
  50. package/dist/esm/__tests__/identity-cache/identity-gateway-service.spec.js.map +1 -0
  51. package/dist/esm/services/db/identity-cache-dynamodb-service/constants.d.ts +3 -0
  52. package/dist/esm/services/db/identity-cache-dynamodb-service/constants.js +4 -0
  53. package/dist/esm/services/db/identity-cache-dynamodb-service/constants.js.map +1 -0
  54. package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.d.ts +14 -0
  55. package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.js +234 -0
  56. package/dist/esm/services/db/identity-cache-dynamodb-service/helpers.js.map +1 -0
  57. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.d.ts +24 -0
  58. package/dist/esm/services/db/{identity-cache-dynamodb-service.js → identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js} +199 -321
  59. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js.map +1 -0
  60. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.d.ts +22 -0
  61. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.js +368 -0
  62. package/dist/esm/services/db/identity-cache-dynamodb-service/identity-gateway-service.js.map +1 -0
  63. package/dist/esm/services/db/identity-cache-dynamodb-service/index.d.ts +4 -0
  64. package/dist/esm/services/db/identity-cache-dynamodb-service/index.js +5 -0
  65. package/dist/esm/services/db/identity-cache-dynamodb-service/index.js.map +1 -0
  66. package/dist/esm/services/db/identity-cache-dynamodb-service/key-builders.d.ts +12 -0
  67. package/dist/esm/services/db/identity-cache-dynamodb-service/key-builders.js +24 -0
  68. package/dist/esm/services/db/identity-cache-dynamodb-service/key-builders.js.map +1 -0
  69. package/dist/esm/services/db/identity-cache-dynamodb-service/types.d.ts +205 -0
  70. package/dist/esm/services/db/identity-cache-dynamodb-service/types.js +32 -0
  71. package/dist/esm/services/db/identity-cache-dynamodb-service/types.js.map +1 -0
  72. package/dist/esm/services/db/index.d.ts +4 -1
  73. package/dist/esm/services/db/index.js +4 -1
  74. package/dist/esm/services/db/index.js.map +1 -1
  75. package/package.json +2 -1
  76. package/dist/cjs/services/db/identity-cache-dynamodb-service.d.ts +0 -44
  77. package/dist/cjs/services/db/identity-cache-dynamodb-service.js.map +0 -1
  78. package/dist/esm/services/db/identity-cache-dynamodb-service.d.ts +0 -44
  79. package/dist/esm/services/db/identity-cache-dynamodb-service.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { buildEmailPk as buildTrackingTypesEmailPk, buildForcePurgePk as buildTrackingTypesForcePurgePk, buildIdentityMapPk as buildTrackingTypesIdentityMapPk, buildIdentityPk as buildTrackingTypesIdentityPk, buildUserIdPk as buildTrackingTypesUserIdPk, } from '@adtrackify/at-tracking-event-types';
1
2
  const mockInvokeFunction = jest.fn();
2
3
  jest.mock('../../clients/index.js', () => ({
3
4
  DynamoDbClient: {
@@ -21,10 +22,13 @@ jest.mock('../../helpers/index.js', () => ({
21
22
  error: jest.fn(),
22
23
  },
23
24
  }));
24
- import { IdentityCacheDynamoDbService, } from '../../services/db/identity-cache-dynamodb-service.js';
25
+ import { buildEmailPk, buildForcePurgePk, buildIdentityMapPk, buildIdentityPk, buildUserIdPk, } from '../../services/db/identity-cache-dynamodb-service/key-builders.js';
26
+ import { IdentityGatewayService, } from '../../services/db/identity-cache-dynamodb-service/identity-gateway-service.js';
27
+ import { IDENTITY_CACHE_WITH_CACHING_OUTCOME, } from '../../services/db/identity-cache-dynamodb-service/types.js';
28
+ import { IdentityCacheDynamoDbDbService } from '../../services/db/identity-cache-dynamodb-service/identity-cache-dynamodb-db-service.js';
25
29
  import { DynamoDbClient } from '../../clients/index.js';
26
30
  const mockedDynamoDbClient = DynamoDbClient;
27
- describe('IdentityCacheDynamoDbService', () => {
31
+ describe('IdentityGatewayService', () => {
28
32
  beforeEach(() => {
29
33
  jest.clearAllMocks();
30
34
  mockedDynamoDbClient.safeGet.mockReset();
@@ -35,9 +39,33 @@ describe('IdentityCacheDynamoDbService', () => {
35
39
  mockedDynamoDbClient.safeQueryByGSI.mockReset();
36
40
  mockedDynamoDbClient.batchWrite.mockReset();
37
41
  mockInvokeFunction.mockReset();
38
- IdentityCacheDynamoDbService.lambdaInvokeClient = undefined;
42
+ IdentityGatewayService.lambdaInvokeClient = undefined;
39
43
  });
40
- describe('getIdentityWithCaching', () => {
44
+ describe('identity cache key builders', () => {
45
+ it('should build primary identity cache keys', () => {
46
+ expect(buildIdentityPk('pixel123', 'identity456')).toBe('identity#pixel123#identity456');
47
+ });
48
+ it('should build lowercase email pointer keys', () => {
49
+ expect(buildEmailPk('pixel123', 'Test@Email.COM')).toBe('email#pixel123#test@email.com');
50
+ });
51
+ it('should build trimmed user id pointer keys', () => {
52
+ expect(buildUserIdPk('pixel123', ' user-1 ')).toBe('user_id#pixel123#user-1');
53
+ });
54
+ it('should build identity map keys', () => {
55
+ expect(buildIdentityMapPk('pixel123', 'identity456')).toBe('identity_map#pixel123#identity456');
56
+ });
57
+ it('should build force purge keys', () => {
58
+ expect(buildForcePurgePk('pixel123', 'identity456')).toBe('force_purge#pixel123#identity456');
59
+ });
60
+ it('should match the installed tracking-event-types key builder behavior during migration', () => {
61
+ expect(buildIdentityPk('pixel123', 'identity456')).toBe(buildTrackingTypesIdentityPk('pixel123', 'identity456'));
62
+ expect(buildEmailPk('pixel123', 'Test@Email.COM')).toBe(buildTrackingTypesEmailPk('pixel123', 'Test@Email.COM'));
63
+ expect(buildUserIdPk('pixel123', ' user-1 ')).toBe(buildTrackingTypesUserIdPk('pixel123', ' user-1 '));
64
+ expect(buildIdentityMapPk('pixel123', 'identity456')).toBe(buildTrackingTypesIdentityMapPk('pixel123', 'identity456'));
65
+ expect(buildForcePurgePk('pixel123', 'identity456')).toBe(buildTrackingTypesForcePurgePk('pixel123', 'identity456'));
66
+ });
67
+ });
68
+ describe('getIdentityWithCachingV2', () => {
41
69
  const pixelId = 'pixel123';
42
70
  const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
43
71
  describe('cache hit (fresh cache)', () => {
@@ -57,9 +85,9 @@ describe('IdentityCacheDynamoDbService', () => {
57
85
  updatedAt: new Date().toISOString(),
58
86
  };
59
87
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
60
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
61
- expect(result).toBeDefined();
62
- expect(result?.identityId).toBe('identity456');
88
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
89
+ expect(result.identity).toBeDefined();
90
+ expect(result.identity?.identityId).toBe('identity456');
63
91
  expect(mockInvokeFunction).not.toHaveBeenCalled();
64
92
  });
65
93
  });
@@ -79,12 +107,12 @@ describe('IdentityCacheDynamoDbService', () => {
79
107
  },
80
108
  }),
81
109
  });
82
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
110
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
83
111
  expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, {
84
112
  pixelId,
85
113
  context: { identity: incomingIdentity },
86
114
  });
87
- expect(result?.identityId).toBe('resolved-identity');
115
+ expect(result.identity?.identityId).toBe('resolved-identity');
88
116
  expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalled();
89
117
  });
90
118
  it('should return incomingIdentity when Neptune Lambda fails to resolve', async () => {
@@ -97,8 +125,8 @@ describe('IdentityCacheDynamoDbService', () => {
97
125
  statusCode: 200,
98
126
  body: JSON.stringify({ identity: undefined }),
99
127
  });
100
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
101
- expect(result).toEqual(incomingIdentity);
128
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
129
+ expect(result.identity).toEqual(incomingIdentity);
102
130
  });
103
131
  });
104
132
  describe('cache stale', () => {
@@ -127,10 +155,10 @@ describe('IdentityCacheDynamoDbService', () => {
127
155
  },
128
156
  }),
129
157
  });
130
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
158
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
131
159
  expect(mockInvokeFunction).toHaveBeenCalled();
132
- expect(result?.identityId).toBe('identity456');
133
- expect(result?.traits?.emails).toContain('new@email.com');
160
+ expect(result.identity?.identityId).toBe('identity456');
161
+ expect(result.identity?.traits?.emails).toContain('new@email.com');
134
162
  });
135
163
  it('should use merged cache identity when Neptune fails', async () => {
136
164
  const incomingIdentity = {
@@ -152,9 +180,9 @@ describe('IdentityCacheDynamoDbService', () => {
152
180
  statusCode: 500,
153
181
  body: 'Internal Server Error',
154
182
  });
155
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
156
- expect(result).toBeDefined();
157
- expect(result?.identityId).toBe('identity456');
183
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
184
+ expect(result.identity).toBeDefined();
185
+ expect(result.identity?.identityId).toBe('identity456');
158
186
  });
159
187
  });
160
188
  describe('Neptune Lambda invocation', () => {
@@ -172,7 +200,7 @@ describe('IdentityCacheDynamoDbService', () => {
172
200
  },
173
201
  }),
174
202
  });
175
- await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
203
+ await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
176
204
  expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalled();
177
205
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
178
206
  const identityPks = writtenItems.map((item) => item.pk);
@@ -187,30 +215,33 @@ describe('IdentityCacheDynamoDbService', () => {
187
215
  statusCode: 200,
188
216
  body: JSON.stringify({ identity: undefined }),
189
217
  });
190
- await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
218
+ await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
191
219
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
192
220
  });
193
221
  });
194
222
  describe('error handling (fail-open)', () => {
195
- it('should return undefined when pixelId is missing', async () => {
196
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(null, { identityId: 'test-id' }, lambdaArn);
197
- expect(result).toBeUndefined();
223
+ it('should return undefined identity when pixelId is missing', async () => {
224
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(null, { identityId: 'test-id' }, lambdaArn);
225
+ expect(result.identity).toBeUndefined();
226
+ expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
198
227
  expect(mockInvokeFunction).not.toHaveBeenCalled();
199
228
  });
200
- it('should return undefined when incomingIdentity is missing', async () => {
201
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, null, lambdaArn);
202
- expect(result).toBeUndefined();
229
+ it('should return undefined identity when incomingIdentity is missing', async () => {
230
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, null, lambdaArn);
231
+ expect(result.identity).toBeUndefined();
232
+ expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
203
233
  expect(mockInvokeFunction).not.toHaveBeenCalled();
204
234
  });
205
- it('should return undefined when Neptune Lambda throws', async () => {
235
+ it('should return incomingIdentity when Neptune Lambda throws', async () => {
206
236
  const incomingIdentity = {
207
237
  identityId: 'identity456',
208
238
  traits: { emails: ['test@email.com'] },
209
239
  };
210
240
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
211
241
  mockInvokeFunction.mockRejectedValueOnce(new Error('Lambda invocation failed'));
212
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
213
- expect(result).toEqual(incomingIdentity);
242
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
243
+ expect(result.identity).toEqual(incomingIdentity);
244
+ expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.FALLBACK_INCOMING);
214
245
  });
215
246
  it('should not throw when cache write fails after Neptune success', async () => {
216
247
  const incomingIdentity = {
@@ -227,8 +258,8 @@ describe('IdentityCacheDynamoDbService', () => {
227
258
  }),
228
259
  });
229
260
  mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('DynamoDB write failed'));
230
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
231
- expect(result?.identityId).toBe('neptune-resolved-id');
261
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
262
+ expect(result.identity?.identityId).toBe('neptune-resolved-id');
232
263
  });
233
264
  });
234
265
  });
@@ -238,7 +269,7 @@ describe('IdentityCacheDynamoDbService', () => {
238
269
  const pixelId = 'pixel123';
239
270
  const identityId = 'identity456';
240
271
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
241
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, { identityId });
272
+ await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, { identityId });
242
273
  expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `identity#${pixelId}#${identityId}`);
243
274
  });
244
275
  });
@@ -247,7 +278,7 @@ describe('IdentityCacheDynamoDbService', () => {
247
278
  const pixelId = 'pixel123';
248
279
  const email = 'Test@Email.COM';
249
280
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
250
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
281
+ await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
251
282
  traits: { emails: [email] },
252
283
  });
253
284
  expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#test@email.com`);
@@ -258,7 +289,7 @@ describe('IdentityCacheDynamoDbService', () => {
258
289
  const pixelId = 'pixel123';
259
290
  const userId = ' user789 ';
260
291
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
261
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
292
+ await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
262
293
  traits: { userIds: [userId] },
263
294
  });
264
295
  expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
@@ -281,7 +312,7 @@ describe('IdentityCacheDynamoDbService', () => {
281
312
  updatedAt: new Date().toISOString(),
282
313
  };
283
314
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
284
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
315
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
285
316
  identityId,
286
317
  traits: { emails: ['test@email.com'] },
287
318
  });
@@ -291,7 +322,7 @@ describe('IdentityCacheDynamoDbService', () => {
291
322
  });
292
323
  it('should return cache miss when identity not found', async () => {
293
324
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
294
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
325
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
295
326
  identityId: 'unknown-id',
296
327
  });
297
328
  expect(result.resolvedIdentity).toBeUndefined();
@@ -299,7 +330,7 @@ describe('IdentityCacheDynamoDbService', () => {
299
330
  });
300
331
  it('should NOT fallback to secondary keys when identityId lookup misses', async () => {
301
332
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
302
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
333
+ await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
303
334
  identityId: 'unknown-id',
304
335
  traits: { emails: ['test@email.com'] },
305
336
  });
@@ -318,7 +349,7 @@ describe('IdentityCacheDynamoDbService', () => {
318
349
  updatedAt: new Date().toISOString(),
319
350
  };
320
351
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
321
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
352
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
322
353
  identityId,
323
354
  traits: { emails: ['old@email.com', 'new@email.com'] },
324
355
  });
@@ -336,7 +367,7 @@ describe('IdentityCacheDynamoDbService', () => {
336
367
  updatedAt: new Date().toISOString(),
337
368
  };
338
369
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
339
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
370
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
340
371
  traits: { emails: ['test@email.com'], userIds: ['user123'] },
341
372
  });
342
373
  expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `email#${pixelId}#test@email.com`);
@@ -354,7 +385,7 @@ describe('IdentityCacheDynamoDbService', () => {
354
385
  updatedAt: new Date().toISOString(),
355
386
  };
356
387
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(pointerRecord);
357
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
388
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
358
389
  traits: { emails: ['test@email.com'], userIds: ['user123'] },
359
390
  });
360
391
  expect(result.resolvedIdentity).toBeDefined();
@@ -363,14 +394,14 @@ describe('IdentityCacheDynamoDbService', () => {
363
394
  });
364
395
  it('should return cache miss when no secondary keys match', async () => {
365
396
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
366
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
397
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
367
398
  traits: { emails: ['unknown@email.com'] },
368
399
  });
369
400
  expect(result.resolvedIdentity).toBeUndefined();
370
401
  expect(result.isCacheStale).toBe(true);
371
402
  });
372
403
  it('should return cache miss when no emails to lookup', async () => {
373
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
404
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
374
405
  traits: { userIds: ['user123'] },
375
406
  });
376
407
  expect(result.resolvedIdentity).toBeUndefined();
@@ -378,7 +409,7 @@ describe('IdentityCacheDynamoDbService', () => {
378
409
  expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
379
410
  });
380
411
  it('should return cache miss when empty traits', async () => {
381
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
412
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
382
413
  traits: {},
383
414
  });
384
415
  expect(result.resolvedIdentity).toBeUndefined();
@@ -388,7 +419,7 @@ describe('IdentityCacheDynamoDbService', () => {
388
419
  describe('error handling (fail-open)', () => {
389
420
  it('should return cache miss on safeGet error', async () => {
390
421
  mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
391
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
422
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
392
423
  identityId: 'test-id',
393
424
  });
394
425
  expect(result.resolvedIdentity).toBeUndefined();
@@ -396,21 +427,21 @@ describe('IdentityCacheDynamoDbService', () => {
396
427
  });
397
428
  it('should return cache miss on safeGet error for email lookup', async () => {
398
429
  mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
399
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
430
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
400
431
  traits: { emails: ['test@email.com'] },
401
432
  });
402
433
  expect(result.resolvedIdentity).toBeUndefined();
403
434
  expect(result.isCacheStale).toBe(true);
404
435
  });
405
436
  it('should return cache miss for null pixelId', async () => {
406
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(null, {
437
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(null, {
407
438
  identityId: 'test-id',
408
439
  });
409
440
  expect(result.resolvedIdentity).toBeUndefined();
410
441
  expect(result.isCacheStale).toBe(true);
411
442
  });
412
443
  it('should return cache miss for null incomingIdentity', async () => {
413
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, null);
444
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, null);
414
445
  expect(result.resolvedIdentity).toBeUndefined();
415
446
  expect(result.isCacheStale).toBe(true);
416
447
  });
@@ -426,7 +457,7 @@ describe('IdentityCacheDynamoDbService', () => {
426
457
  userIds: ['user1', 'user2'],
427
458
  },
428
459
  };
429
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
460
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
430
461
  expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalledTimes(1);
431
462
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
432
463
  expect(writtenItems).toHaveLength(3);
@@ -438,12 +469,12 @@ describe('IdentityCacheDynamoDbService', () => {
438
469
  expect(pks).not.toContain(`user_id#${pixelId}#user2`);
439
470
  const identityItem = writtenItems.find((item) => item.pk.startsWith('identity#'));
440
471
  expect(identityItem).toBeDefined();
441
- expect(identityItem.response).toBeDefined();
442
- expect(identityItem.response.identityId).toBe('identity456');
472
+ expect(identityItem?.response).toBeDefined();
473
+ expect(identityItem?.response.identityId).toBe('identity456');
443
474
  const emailItem = writtenItems.find((item) => item.pk.startsWith('email#'));
444
475
  expect(emailItem).toBeDefined();
445
- expect(emailItem.response).toBeUndefined();
446
- expect(emailItem.identityId).toBe('identity456');
476
+ expect(emailItem?.response).toBeUndefined();
477
+ expect(emailItem?.identityId).toBe('identity456');
447
478
  });
448
479
  it('should not throw on safeBatchWrite error (fail-open)', async () => {
449
480
  mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('DynamoDB error'));
@@ -451,14 +482,14 @@ describe('IdentityCacheDynamoDbService', () => {
451
482
  identityId: 'identity456',
452
483
  traits: { emails: ['test@email.com'] },
453
484
  };
454
- await expect(IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity)).resolves.toBeUndefined();
485
+ await expect(IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity)).resolves.toBeUndefined();
455
486
  });
456
487
  it('should return early for missing identityId', async () => {
457
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, {});
488
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, {});
458
489
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
459
490
  });
460
491
  it('should return early for missing pixelId', async () => {
461
- await IdentityCacheDynamoDbService.updateIdentityCache(null, {
492
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(null, {
462
493
  identityId: 'test-id',
463
494
  });
464
495
  expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
@@ -467,7 +498,7 @@ describe('IdentityCacheDynamoDbService', () => {
467
498
  const identity = {
468
499
  identityId: 'identity456',
469
500
  };
470
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
501
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
471
502
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
472
503
  expect(writtenItems).toHaveLength(1);
473
504
  });
@@ -476,7 +507,7 @@ describe('IdentityCacheDynamoDbService', () => {
476
507
  identityId: 'identity456',
477
508
  traits: { emails: ['test@email.com'] },
478
509
  };
479
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
510
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
480
511
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
481
512
  for (const item of writtenItems) {
482
513
  expect(item.gsi1pk).toBe('identity456');
@@ -493,33 +524,33 @@ describe('IdentityCacheDynamoDbService', () => {
493
524
  updatedAt: new Date().toISOString(),
494
525
  };
495
526
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(mockMap);
496
- const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
527
+ const result = await IdentityCacheDynamoDbDbService.getIdentityMap('pixel123', 'identity456');
497
528
  expect(result).toEqual(mockMap);
498
529
  expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', 'identity_map#pixel123#identity456');
499
530
  });
500
531
  it('should return undefined when not found', async () => {
501
532
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
502
- const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
533
+ const result = await IdentityCacheDynamoDbDbService.getIdentityMap('pixel123', 'identity456');
503
534
  expect(result).toBeUndefined();
504
535
  });
505
536
  it('should return undefined for missing params (fail-open)', async () => {
506
- const result = await IdentityCacheDynamoDbService.getIdentityMap('', '');
537
+ const result = await IdentityCacheDynamoDbDbService.getIdentityMap('', '');
507
538
  expect(result).toBeUndefined();
508
539
  expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
509
540
  });
510
541
  it('should return undefined when only pixelId is missing', async () => {
511
- const result = await IdentityCacheDynamoDbService.getIdentityMap('', 'identity456');
542
+ const result = await IdentityCacheDynamoDbDbService.getIdentityMap('', 'identity456');
512
543
  expect(result).toBeUndefined();
513
544
  expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
514
545
  });
515
546
  it('should return undefined when only identityId is missing', async () => {
516
- const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', '');
547
+ const result = await IdentityCacheDynamoDbDbService.getIdentityMap('pixel123', '');
517
548
  expect(result).toBeUndefined();
518
549
  expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
519
550
  });
520
551
  it('should return undefined on safeGet error (fail-open)', async () => {
521
552
  mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
522
- const result = await IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
553
+ const result = await IdentityCacheDynamoDbDbService.getIdentityMap('pixel123', 'identity456');
523
554
  expect(result).toBeUndefined();
524
555
  });
525
556
  });
@@ -528,28 +559,28 @@ describe('IdentityCacheDynamoDbService', () => {
528
559
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce({
529
560
  pk: 'force_purge#pixel123#identity456',
530
561
  });
531
- const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
562
+ const result = await IdentityCacheDynamoDbDbService.getForcePurgeFlag('pixel123', 'identity456');
532
563
  expect(result).toBe(true);
533
564
  });
534
565
  it('should return false when flag not found', async () => {
535
566
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
536
- const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
567
+ const result = await IdentityCacheDynamoDbDbService.getForcePurgeFlag('pixel123', 'identity456');
537
568
  expect(result).toBe(false);
538
569
  });
539
570
  it('should return false on error (fail-open)', async () => {
540
571
  mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
541
- const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
572
+ const result = await IdentityCacheDynamoDbDbService.getForcePurgeFlag('pixel123', 'identity456');
542
573
  expect(result).toBe(false);
543
574
  });
544
575
  it('should return false for missing params', async () => {
545
- const result = await IdentityCacheDynamoDbService.getForcePurgeFlag('', '');
576
+ const result = await IdentityCacheDynamoDbDbService.getForcePurgeFlag('', '');
546
577
  expect(result).toBe(false);
547
578
  expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
548
579
  });
549
580
  });
550
581
  describe('setForcePurgeFlag', () => {
551
582
  it('should set flag with TTL', async () => {
552
- await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456');
583
+ await IdentityCacheDynamoDbDbService.setForcePurgeFlag('pixel123', 'identity456');
553
584
  expect(mockedDynamoDbClient.safePut).toHaveBeenCalledTimes(1);
554
585
  const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
555
586
  expect(putItem.pk).toBe('force_purge#pixel123#identity456');
@@ -558,13 +589,13 @@ describe('IdentityCacheDynamoDbService', () => {
558
589
  expect(putItem.ttl).toBeGreaterThan(0);
559
590
  });
560
591
  it('should accept custom TTL', async () => {
561
- await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456', 3600);
592
+ await IdentityCacheDynamoDbDbService.setForcePurgeFlag('pixel123', 'identity456', 3600);
562
593
  const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
563
594
  expect(putItem.ttl).toBeGreaterThan(0);
564
595
  });
565
596
  it('should include createdAt timestamp', async () => {
566
597
  const before = new Date().toISOString();
567
- await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456');
598
+ await IdentityCacheDynamoDbDbService.setForcePurgeFlag('pixel123', 'identity456');
568
599
  const after = new Date().toISOString();
569
600
  const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
570
601
  expect(putItem.createdAt).toBeDefined();
@@ -572,41 +603,41 @@ describe('IdentityCacheDynamoDbService', () => {
572
603
  expect(putItem.createdAt <= after).toBe(true);
573
604
  });
574
605
  it('should return early when pixelId is missing', async () => {
575
- await IdentityCacheDynamoDbService.setForcePurgeFlag('', 'identity456');
606
+ await IdentityCacheDynamoDbDbService.setForcePurgeFlag('', 'identity456');
576
607
  expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
577
608
  });
578
609
  it('should return early when identityId is missing', async () => {
579
- await IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', '');
610
+ await IdentityCacheDynamoDbDbService.setForcePurgeFlag('pixel123', '');
580
611
  expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
581
612
  });
582
613
  it('should not throw on error (fail-open)', async () => {
583
614
  mockedDynamoDbClient.safePut.mockRejectedValueOnce(new Error('DynamoDB error'));
584
- await expect(IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456')).resolves.toBeUndefined();
615
+ await expect(IdentityCacheDynamoDbDbService.setForcePurgeFlag('pixel123', 'identity456')).resolves.toBeUndefined();
585
616
  });
586
617
  });
587
618
  describe('putIdentityMap', () => {
588
619
  it('should write identity map', async () => {
589
- await IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1', 'linked2']);
620
+ await IdentityCacheDynamoDbDbService.putIdentityMap('pixel123', 'identity456', ['linked1', 'linked2']);
590
621
  expect(mockedDynamoDbClient.safePut).toHaveBeenCalledTimes(1);
591
622
  const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
592
623
  expect(putItem.pk).toBe('identity_map#pixel123#identity456');
593
624
  expect(putItem.linkedIdentities).toEqual(['linked1', 'linked2']);
594
625
  });
595
626
  it('should return early for missing params', async () => {
596
- await IdentityCacheDynamoDbService.putIdentityMap('', '', []);
627
+ await IdentityCacheDynamoDbDbService.putIdentityMap('', '', []);
597
628
  expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
598
629
  });
599
630
  it('should return early when only pixelId is missing', async () => {
600
- await IdentityCacheDynamoDbService.putIdentityMap('', 'identity456', ['linked1']);
631
+ await IdentityCacheDynamoDbDbService.putIdentityMap('', 'identity456', ['linked1']);
601
632
  expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
602
633
  });
603
634
  it('should return early when only identityId is missing', async () => {
604
- await IdentityCacheDynamoDbService.putIdentityMap('pixel123', '', ['linked1']);
635
+ await IdentityCacheDynamoDbDbService.putIdentityMap('pixel123', '', ['linked1']);
605
636
  expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
606
637
  });
607
638
  it('should include updatedAt timestamp', async () => {
608
639
  const before = new Date().toISOString();
609
- await IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1']);
640
+ await IdentityCacheDynamoDbDbService.putIdentityMap('pixel123', 'identity456', ['linked1']);
610
641
  const after = new Date().toISOString();
611
642
  const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
612
643
  expect(putItem.updatedAt).toBeDefined();
@@ -615,51 +646,51 @@ describe('IdentityCacheDynamoDbService', () => {
615
646
  });
616
647
  it('should not throw on safePut error (fail-open)', async () => {
617
648
  mockedDynamoDbClient.safePut.mockRejectedValueOnce(new Error('DynamoDB error'));
618
- await expect(IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1'])).resolves.toBeUndefined();
649
+ await expect(IdentityCacheDynamoDbDbService.putIdentityMap('pixel123', 'identity456', ['linked1'])).resolves.toBeUndefined();
619
650
  });
620
651
  });
621
652
  describe('deleteIdentityMap', () => {
622
653
  it('should delete identity map', async () => {
623
- await IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', 'identity456');
654
+ await IdentityCacheDynamoDbDbService.deleteIdentityMap('pixel123', 'identity456');
624
655
  expect(mockedDynamoDbClient.safeDelete).toHaveBeenCalledWith(expect.any(String), 'pk', 'identity_map#pixel123#identity456');
625
656
  });
626
657
  it('should return early for missing params', async () => {
627
- await IdentityCacheDynamoDbService.deleteIdentityMap('', '');
658
+ await IdentityCacheDynamoDbDbService.deleteIdentityMap('', '');
628
659
  expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
629
660
  });
630
661
  it('should return early when only pixelId is missing', async () => {
631
- await IdentityCacheDynamoDbService.deleteIdentityMap('', 'identity456');
662
+ await IdentityCacheDynamoDbDbService.deleteIdentityMap('', 'identity456');
632
663
  expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
633
664
  });
634
665
  it('should return early when only identityId is missing', async () => {
635
- await IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', '');
666
+ await IdentityCacheDynamoDbDbService.deleteIdentityMap('pixel123', '');
636
667
  expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
637
668
  });
638
669
  it('should not throw on safeDelete error (fail-open)', async () => {
639
670
  mockedDynamoDbClient.safeDelete.mockRejectedValueOnce(new Error('DynamoDB error'));
640
- await expect(IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', 'identity456')).resolves.toBeUndefined();
671
+ await expect(IdentityCacheDynamoDbDbService.deleteIdentityMap('pixel123', 'identity456')).resolves.toBeUndefined();
641
672
  });
642
673
  });
643
674
  describe('deleteForcePurgeFlag', () => {
644
675
  it('should delete force purge flag', async () => {
645
- await IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', 'identity456');
676
+ await IdentityCacheDynamoDbDbService.deleteForcePurgeFlag('pixel123', 'identity456');
646
677
  expect(mockedDynamoDbClient.safeDelete).toHaveBeenCalledWith(expect.any(String), 'pk', 'force_purge#pixel123#identity456');
647
678
  });
648
679
  it('should return early for missing params', async () => {
649
- await IdentityCacheDynamoDbService.deleteForcePurgeFlag('', '');
680
+ await IdentityCacheDynamoDbDbService.deleteForcePurgeFlag('', '');
650
681
  expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
651
682
  });
652
683
  it('should return early when only pixelId is missing', async () => {
653
- await IdentityCacheDynamoDbService.deleteForcePurgeFlag('', 'identity456');
684
+ await IdentityCacheDynamoDbDbService.deleteForcePurgeFlag('', 'identity456');
654
685
  expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
655
686
  });
656
687
  it('should return early when only identityId is missing', async () => {
657
- await IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', '');
688
+ await IdentityCacheDynamoDbDbService.deleteForcePurgeFlag('pixel123', '');
658
689
  expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
659
690
  });
660
691
  it('should not throw on safeDelete error (fail-open)', async () => {
661
692
  mockedDynamoDbClient.safeDelete.mockRejectedValueOnce(new Error('DynamoDB error'));
662
- await expect(IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', 'identity456')).resolves.toBeUndefined();
693
+ await expect(IdentityCacheDynamoDbDbService.deleteForcePurgeFlag('pixel123', 'identity456')).resolves.toBeUndefined();
663
694
  });
664
695
  });
665
696
  describe('deleteIdentityCache', () => {
@@ -678,12 +709,12 @@ describe('IdentityCacheDynamoDbService', () => {
678
709
  identityId: 'identity456',
679
710
  traits: { emails: ['test@email.com', 'other@email.com'] },
680
711
  };
681
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
712
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
682
713
  expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalled();
683
714
  expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
684
715
  });
685
716
  it('should return early for missing pixelId', async () => {
686
- await IdentityCacheDynamoDbService.deleteIdentityCache(null, {}, {});
717
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(null, {}, {});
687
718
  expect(mockedDynamoDbClient.safeQueryByGSI).not.toHaveBeenCalled();
688
719
  });
689
720
  it('should handle different identityIds in incoming vs resolved', async () => {
@@ -697,19 +728,19 @@ describe('IdentityCacheDynamoDbService', () => {
697
728
  identityId: 'resolved-id',
698
729
  traits: { emails: ['test@email.com'] },
699
730
  };
700
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
731
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
701
732
  expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalledTimes(2);
702
733
  });
703
734
  it('should not throw on batchWrite error (fail-open)', async () => {
704
735
  mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([{ pk: `identity#${pixelId}#identity456`, pixelId }]);
705
736
  mockedDynamoDbClient.batchWrite.mockRejectedValueOnce(new Error('DynamoDB error'));
706
- await expect(IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' })).resolves.toBeUndefined();
737
+ await expect(IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' })).resolves.toBeUndefined();
707
738
  });
708
739
  it('should deduplicate keys when same items returned from GSI query', async () => {
709
740
  const duplicateItem = { pk: `identity#${pixelId}#identity456`, pixelId };
710
741
  mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([duplicateItem, duplicateItem]);
711
742
  mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
712
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
743
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
713
744
  expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
714
745
  const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0];
715
746
  expect(batchWriteCall).toBeDefined();
@@ -732,7 +763,7 @@ describe('IdentityCacheDynamoDbService', () => {
732
763
  updatedAt: new Date().toISOString(),
733
764
  };
734
765
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
735
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
766
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
736
767
  identityId: 'identity456',
737
768
  traits: {
738
769
  emails: ['incoming@email.com'],
@@ -756,7 +787,7 @@ describe('IdentityCacheDynamoDbService', () => {
756
787
  updatedAt: new Date().toISOString(),
757
788
  };
758
789
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
759
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
790
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
760
791
  identityId: 'identity456',
761
792
  traits: {
762
793
  emails: ['cached@email.com', 'new@email.com'],
@@ -779,7 +810,7 @@ describe('IdentityCacheDynamoDbService', () => {
779
810
  updatedAt: new Date().toISOString(),
780
811
  };
781
812
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
782
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
813
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
783
814
  identityId: 'identity456',
784
815
  traits: {
785
816
  emails: ['a@email.com'],
@@ -797,7 +828,7 @@ describe('IdentityCacheDynamoDbService', () => {
797
828
  emails: ['first@email.com', 'second@email.com', 'third@email.com', 'fourth@email.com'],
798
829
  },
799
830
  };
800
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
831
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
801
832
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
802
833
  expect(writtenItems).toHaveLength(5);
803
834
  const emailPks = writtenItems
@@ -815,7 +846,7 @@ describe('IdentityCacheDynamoDbService', () => {
815
846
  emails: ['valid@email.com', null, undefined, '', 'another@email.com'],
816
847
  },
817
848
  };
818
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
849
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
819
850
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
820
851
  const emailItems = writtenItems.filter((item) => item.pk.startsWith('email#'));
821
852
  expect(emailItems).toHaveLength(2);
@@ -827,7 +858,7 @@ describe('IdentityCacheDynamoDbService', () => {
827
858
  identityId: 'identity456',
828
859
  traits: { emails: [] },
829
860
  };
830
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
861
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
831
862
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
832
863
  expect(writtenItems).toHaveLength(1);
833
864
  expect(writtenItems[0].pk).toBe(`identity#${pixelId}#identity456`);
@@ -839,7 +870,7 @@ describe('IdentityCacheDynamoDbService', () => {
839
870
  emails: ['UPPERCASE@EMAIL.COM', 'MixedCase@Email.Com'],
840
871
  },
841
872
  };
842
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
873
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
843
874
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
844
875
  const emailPks = writtenItems
845
876
  .filter((item) => item.pk.startsWith('email#'))
@@ -855,7 +886,7 @@ describe('IdentityCacheDynamoDbService', () => {
855
886
  identityId: 'identity456',
856
887
  traits: null,
857
888
  };
858
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
889
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
859
890
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
860
891
  expect(writtenItems).toHaveLength(1);
861
892
  });
@@ -864,12 +895,12 @@ describe('IdentityCacheDynamoDbService', () => {
864
895
  identityId: 'identity456',
865
896
  traits: undefined,
866
897
  };
867
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
898
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
868
899
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
869
900
  expect(writtenItems).toHaveLength(1);
870
901
  });
871
902
  it('should return cache miss for identity with null traits in lookup', async () => {
872
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
903
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
873
904
  traits: null,
874
905
  });
875
906
  expect(result.resolvedIdentity).toBeUndefined();
@@ -877,7 +908,7 @@ describe('IdentityCacheDynamoDbService', () => {
877
908
  expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
878
909
  });
879
910
  it('should handle identity with undefined emails in traits', async () => {
880
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
911
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
881
912
  traits: { userIds: ['user123'] },
882
913
  });
883
914
  expect(result.resolvedIdentity).toBeUndefined();
@@ -899,7 +930,7 @@ describe('IdentityCacheDynamoDbService', () => {
899
930
  updatedAt: new Date().toISOString(),
900
931
  };
901
932
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(oldFormatEmailRecord);
902
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
933
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
903
934
  traits: { emails: ['test@email.com'] },
904
935
  });
905
936
  expect(result.resolvedIdentity).toBeDefined();
@@ -915,7 +946,7 @@ describe('IdentityCacheDynamoDbService', () => {
915
946
  updatedAt: new Date().toISOString(),
916
947
  };
917
948
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(newFormatPointerRecord);
918
- const result = await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
949
+ const result = await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
919
950
  traits: { emails: ['test@email.com'] },
920
951
  });
921
952
  expect(result.resolvedIdentity).toBeDefined();
@@ -941,7 +972,7 @@ describe('IdentityCacheDynamoDbService', () => {
941
972
  },
942
973
  }),
943
974
  });
944
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
975
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
945
976
  expect(mockInvokeFunction).toHaveBeenCalledWith(lambdaArn, expect.objectContaining({
946
977
  pixelId,
947
978
  context: {
@@ -950,7 +981,7 @@ describe('IdentityCacheDynamoDbService', () => {
950
981
  }),
951
982
  },
952
983
  }));
953
- expect(result?.identityId).toBe('discovered-id');
984
+ expect(result.identity?.identityId).toBe('discovered-id');
954
985
  });
955
986
  });
956
987
  describe('deleteIdentityCache with multiple email pointers', () => {
@@ -968,7 +999,7 @@ describe('IdentityCacheDynamoDbService', () => {
968
999
  identityId: 'identity456',
969
1000
  traits: { emails: ['resolved1@email.com', 'resolved2@email.com', 'resolved3@email.com'] },
970
1001
  };
971
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
1002
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
972
1003
  expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
973
1004
  const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0][0];
974
1005
  const deleteRequests = batchWriteCall.RequestItems[Object.keys(batchWriteCall.RequestItems)[0]];
@@ -985,7 +1016,7 @@ describe('IdentityCacheDynamoDbService', () => {
985
1016
  { pk: `identity#${pixelId}#identity456`, pixelId },
986
1017
  ]);
987
1018
  mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
988
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456', traits: { emails: [] } }, { identityId: 'identity456' });
1019
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, { identityId: 'identity456', traits: { emails: [] } }, { identityId: 'identity456' });
989
1020
  expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
990
1021
  });
991
1022
  it('should handle missing traits in delete', async () => {
@@ -993,7 +1024,7 @@ describe('IdentityCacheDynamoDbService', () => {
993
1024
  { pk: `identity#${pixelId}#identity456`, pixelId },
994
1025
  ]);
995
1026
  mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
996
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
1027
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
997
1028
  expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
998
1029
  });
999
1030
  it('should not delete items from other pixelIds', async () => {
@@ -1003,7 +1034,7 @@ describe('IdentityCacheDynamoDbService', () => {
1003
1034
  { pk: `identity#${otherPixelId}#identity456`, pixelId: otherPixelId },
1004
1035
  ]);
1005
1036
  mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
1006
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
1037
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' });
1007
1038
  const batchWriteCall = mockedDynamoDbClient.batchWrite.mock.calls[0][0];
1008
1039
  const deleteRequests = batchWriteCall.RequestItems[Object.keys(batchWriteCall.RequestItems)[0]];
1009
1040
  const deletedPks = deleteRequests.map((req) => req.DeleteRequest.Key.pk);
@@ -1022,24 +1053,24 @@ describe('IdentityCacheDynamoDbService', () => {
1022
1053
  ];
1023
1054
  mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue(mockResults);
1024
1055
  mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
1025
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId }, { identityId });
1056
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, { identityId }, { identityId });
1026
1057
  expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalledWith(expect.any(String), expect.any(String), 'gsi1pk', identityId);
1027
1058
  });
1028
1059
  it('should handle GSI1 query returning empty results', async () => {
1029
1060
  mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([]);
1030
- await IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'unknown-id' }, { identityId: 'unknown-id' });
1061
+ await IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, { identityId: 'unknown-id' }, { identityId: 'unknown-id' });
1031
1062
  expect(mockedDynamoDbClient.batchWrite).not.toHaveBeenCalled();
1032
1063
  });
1033
1064
  it('should handle GSI1 query error gracefully (fail-open)', async () => {
1034
1065
  mockedDynamoDbClient.safeQueryByGSI.mockRejectedValueOnce(new Error('GSI query failed'));
1035
- await expect(IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' })).resolves.toBeUndefined();
1066
+ await expect(IdentityCacheDynamoDbDbService.deleteIdentityCache(pixelId, { identityId: 'identity456' }, { identityId: 'identity456' })).resolves.toBeUndefined();
1036
1067
  });
1037
1068
  });
1038
1069
  describe('email pointer lookups', () => {
1039
1070
  const pixelId = 'pixel123';
1040
1071
  it('should only lookup first email (optimization)', async () => {
1041
1072
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1042
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
1073
+ await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
1043
1074
  traits: { emails: ['first@email.com', 'second@email.com', 'third@email.com'] },
1044
1075
  });
1045
1076
  expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledTimes(1);
@@ -1047,7 +1078,7 @@ describe('IdentityCacheDynamoDbService', () => {
1047
1078
  });
1048
1079
  it('should handle whitespace in email during lookup', async () => {
1049
1080
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1050
- await IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
1081
+ await IdentityCacheDynamoDbDbService.getIdentityFromCache(pixelId, {
1051
1082
  traits: { emails: [' test@email.com '] },
1052
1083
  });
1053
1084
  expect(mockedDynamoDbClient.safeGet).toHaveBeenCalled();
@@ -1063,8 +1094,8 @@ describe('IdentityCacheDynamoDbService', () => {
1063
1094
  };
1064
1095
  mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB down'));
1065
1096
  mockInvokeFunction.mockRejectedValueOnce(new Error('Lambda down'));
1066
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
1067
- expect(result).toEqual(incomingIdentity);
1097
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1098
+ expect(result.identity).toEqual(incomingIdentity);
1068
1099
  });
1069
1100
  it('should continue when cache write fails after Neptune success', async () => {
1070
1101
  mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
@@ -1075,8 +1106,8 @@ describe('IdentityCacheDynamoDbService', () => {
1075
1106
  }),
1076
1107
  });
1077
1108
  mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('Write failed'));
1078
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
1079
- expect(result?.identityId).toBe('resolved-id');
1109
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, { traits: { emails: ['test@email.com'] } }, lambdaArn);
1110
+ expect(result.identity?.identityId).toBe('resolved-id');
1080
1111
  });
1081
1112
  it('should return incoming identity when Neptune returns non-200', async () => {
1082
1113
  const incomingIdentity = {
@@ -1088,8 +1119,8 @@ describe('IdentityCacheDynamoDbService', () => {
1088
1119
  statusCode: 500,
1089
1120
  body: 'Internal Server Error',
1090
1121
  });
1091
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
1092
- expect(result).toEqual(incomingIdentity);
1122
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1123
+ expect(result.identity).toEqual(incomingIdentity);
1093
1124
  });
1094
1125
  it('should return incoming identity when Neptune returns malformed JSON (fail-open)', async () => {
1095
1126
  const incomingIdentity = {
@@ -1101,12 +1132,127 @@ describe('IdentityCacheDynamoDbService', () => {
1101
1132
  statusCode: 200,
1102
1133
  body: 'not valid json',
1103
1134
  });
1104
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(pixelId, incomingIdentity, lambdaArn);
1105
- expect(result).toEqual(incomingIdentity);
1135
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1136
+ expect(result.identity).toEqual(incomingIdentity);
1106
1137
  });
1107
- it('should return undefined only when both inputs are invalid', async () => {
1108
- const result = await IdentityCacheDynamoDbService.getIdentityWithCaching(null, null, lambdaArn);
1109
- expect(result).toBeUndefined();
1138
+ it('should return undefined identity only when both inputs are invalid', async () => {
1139
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(null, null, lambdaArn);
1140
+ expect(result.identity).toBeUndefined();
1141
+ expect(result.summary.outcome).toBe(IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT);
1142
+ });
1143
+ });
1144
+ describe('getIdentityWithCachingV2', () => {
1145
+ const pixelId = 'pixel123';
1146
+ const lambdaArn = 'arn:aws:lambda:us-east-1:123456789:function:identity-private';
1147
+ it('should return invalid input summary without resolving', async () => {
1148
+ const result = await IdentityGatewayService.getIdentityWithCachingV2('', { identityId: 'identity456' }, lambdaArn);
1149
+ expect(result.identity).toBeUndefined();
1150
+ expect(result.summary).toEqual(expect.objectContaining({
1151
+ outcome: IDENTITY_CACHE_WITH_CACHING_OUTCOME.INVALID_INPUT,
1152
+ didReadCache: false,
1153
+ didCallNeptune: false,
1154
+ didWriteCache: false,
1155
+ }));
1156
+ expect(mockInvokeFunction).not.toHaveBeenCalled();
1157
+ expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
1158
+ });
1159
+ it('should return fresh cache hit summary without invoking Neptune', async () => {
1160
+ const incomingIdentity = {
1161
+ identityId: 'identity456',
1162
+ traits: { emails: ['test@email.com'] },
1163
+ };
1164
+ const cachedResponse = {
1165
+ pk: `identity#${pixelId}#identity456`,
1166
+ pixelId,
1167
+ identityId: 'identity456',
1168
+ response: {
1169
+ identityId: 'identity456',
1170
+ traits: { emails: ['test@email.com'] },
1171
+ },
1172
+ updatedAt: new Date().toISOString(),
1173
+ };
1174
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1175
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1176
+ expect(result.identity?.identityId).toBe('identity456');
1177
+ expect(result.summary).toEqual(expect.objectContaining({
1178
+ outcome: IDENTITY_CACHE_WITH_CACHING_OUTCOME.FRESH_CACHE_HIT,
1179
+ pixelId,
1180
+ identityId: 'identity456',
1181
+ didReadCache: true,
1182
+ didCallNeptune: false,
1183
+ didWriteCache: false,
1184
+ cachedIdentity: cachedResponse.response,
1185
+ incomingIdentity,
1186
+ resolvedIdentity: expect.objectContaining({ identityId: 'identity456' }),
1187
+ }));
1188
+ expect(mockInvokeFunction).not.toHaveBeenCalled();
1189
+ });
1190
+ it('should return Neptune write-back summary on cache miss', async () => {
1191
+ const incomingIdentity = {
1192
+ identityId: 'identity456',
1193
+ traits: { emails: ['test@email.com'] },
1194
+ };
1195
+ const neptuneIdentity = {
1196
+ identityId: 'resolved-identity',
1197
+ traits: { emails: ['test@email.com', 'resolved@email.com'] },
1198
+ };
1199
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
1200
+ mockInvokeFunction.mockResolvedValueOnce({
1201
+ statusCode: 200,
1202
+ body: JSON.stringify({ identity: neptuneIdentity }),
1203
+ });
1204
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1205
+ expect(result.identity).toEqual(neptuneIdentity);
1206
+ expect(result.summary).toEqual(expect.objectContaining({
1207
+ outcome: IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_WRITE_BACK,
1208
+ pixelId,
1209
+ identityId: 'resolved-identity',
1210
+ didReadCache: true,
1211
+ didCallNeptune: true,
1212
+ didWriteCache: true,
1213
+ incomingIdentity,
1214
+ identityForNeptune: incomingIdentity,
1215
+ neptuneIdentity,
1216
+ traitChanges: { reason: { resolved: 'no cached identity' } },
1217
+ }));
1218
+ expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalled();
1219
+ });
1220
+ it('should return Neptune skip-write summary when Neptune matches cached identity', async () => {
1221
+ const incomingIdentity = {
1222
+ identityId: 'identity456',
1223
+ traits: { emails: ['old@email.com', 'new@email.com'] },
1224
+ };
1225
+ const cachedIdentity = {
1226
+ identityId: 'identity456',
1227
+ traits: { emails: ['old@email.com'] },
1228
+ };
1229
+ const cachedResponse = {
1230
+ pk: `identity#${pixelId}#identity456`,
1231
+ pixelId,
1232
+ identityId: 'identity456',
1233
+ response: cachedIdentity,
1234
+ updatedAt: new Date().toISOString(),
1235
+ };
1236
+ mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
1237
+ mockInvokeFunction.mockResolvedValueOnce({
1238
+ statusCode: 200,
1239
+ body: JSON.stringify({ identity: cachedIdentity }),
1240
+ });
1241
+ const result = await IdentityGatewayService.getIdentityWithCachingV2(pixelId, incomingIdentity, lambdaArn);
1242
+ expect(result.identity).toEqual(cachedIdentity);
1243
+ expect(result.summary).toEqual(expect.objectContaining({
1244
+ outcome: IDENTITY_CACHE_WITH_CACHING_OUTCOME.NEPTUNE_SKIP_WRITE,
1245
+ pixelId,
1246
+ identityId: 'identity456',
1247
+ didReadCache: true,
1248
+ didCallNeptune: true,
1249
+ didWriteCache: false,
1250
+ cachedIdentity,
1251
+ incomingIdentity,
1252
+ identityForNeptune: expect.objectContaining({ identityId: 'identity456' }),
1253
+ neptuneIdentity: cachedIdentity,
1254
+ }));
1255
+ expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
1110
1256
  });
1111
1257
  });
1112
1258
  describe('gsi1pk attribute on all records', () => {
@@ -1116,18 +1262,18 @@ describe('IdentityCacheDynamoDbService', () => {
1116
1262
  identityId: 'identity456',
1117
1263
  traits: { emails: ['test@email.com'] },
1118
1264
  };
1119
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
1265
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
1120
1266
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
1121
1267
  const identityItem = writtenItems.find((item) => item.pk.startsWith('identity#'));
1122
1268
  expect(identityItem).toBeDefined();
1123
- expect(identityItem.gsi1pk).toBe('identity456');
1269
+ expect(identityItem?.gsi1pk).toBe('identity456');
1124
1270
  });
1125
1271
  it('should include gsi1pk on all email pointer records', async () => {
1126
1272
  const identity = {
1127
1273
  identityId: 'identity456',
1128
1274
  traits: { emails: ['email1@test.com', 'email2@test.com'] },
1129
1275
  };
1130
- await IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
1276
+ await IdentityCacheDynamoDbDbService.updateIdentityCache(pixelId, identity);
1131
1277
  const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
1132
1278
  const emailItems = writtenItems.filter((item) => item.pk.startsWith('email#'));
1133
1279
  for (const emailItem of emailItems) {