@adtrackify/at-service-common 3.19.9 → 3.19.10
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__/clients/dynamodb-client.spec.d.ts +1 -0
- package/dist/cjs/__tests__/clients/dynamodb-client.spec.js +160 -0
- package/dist/cjs/__tests__/clients/dynamodb-client.spec.js.map +1 -0
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +1 -0
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +480 -0
- package/dist/cjs/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +1 -0
- package/dist/cjs/services/db/identity-cache-dynamodb-service.js +1 -3
- package/dist/cjs/services/db/identity-cache-dynamodb-service.js.map +1 -1
- package/dist/esm/__tests__/clients/dynamodb-client.spec.d.ts +1 -0
- package/dist/esm/__tests__/clients/dynamodb-client.spec.js +158 -0
- package/dist/esm/__tests__/clients/dynamodb-client.spec.js.map +1 -0
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.d.ts +1 -0
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js +478 -0
- package/dist/esm/__tests__/identity-cache/identity-cache-dynamodb-service.spec.js.map +1 -0
- package/dist/esm/services/db/identity-cache-dynamodb-service.js +1 -3
- package/dist/esm/services/db/identity-cache-dynamodb-service.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const dynamodb_client_js_1 = require("../../clients/generic/dynamodb-client.js");
|
|
4
|
+
jest.mock('@aws-sdk/lib-dynamodb', () => {
|
|
5
|
+
const mockBatchGet = jest.fn();
|
|
6
|
+
const mockBatchWrite = jest.fn();
|
|
7
|
+
return {
|
|
8
|
+
DynamoDBDocument: {
|
|
9
|
+
from: () => ({
|
|
10
|
+
batchGet: mockBatchGet,
|
|
11
|
+
batchWrite: mockBatchWrite,
|
|
12
|
+
get: jest.fn(),
|
|
13
|
+
put: jest.fn(),
|
|
14
|
+
delete: jest.fn(),
|
|
15
|
+
query: jest.fn(),
|
|
16
|
+
scan: jest.fn(),
|
|
17
|
+
update: jest.fn(),
|
|
18
|
+
}),
|
|
19
|
+
},
|
|
20
|
+
__mockBatchGet: mockBatchGet,
|
|
21
|
+
__mockBatchWrite: mockBatchWrite,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
jest.mock('../../helpers/index.js', () => ({
|
|
25
|
+
Logger: {
|
|
26
|
+
debug: jest.fn(),
|
|
27
|
+
info: jest.fn(),
|
|
28
|
+
warn: jest.fn(),
|
|
29
|
+
error: jest.fn(),
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
const { __mockBatchGet, __mockBatchWrite } = jest.requireMock('@aws-sdk/lib-dynamodb');
|
|
33
|
+
describe('DynamoDbClient', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
describe('safeBatchGet', () => {
|
|
38
|
+
const tableName = 'test-table';
|
|
39
|
+
it('should return empty array for empty keys', async () => {
|
|
40
|
+
const result = await dynamodb_client_js_1.DynamoDbClient.safeBatchGet(tableName, []);
|
|
41
|
+
expect(result).toEqual([]);
|
|
42
|
+
expect(__mockBatchGet).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
it('should return items from DynamoDB response', async () => {
|
|
45
|
+
const mockItems = [
|
|
46
|
+
{ pk: 'key1', data: 'value1' },
|
|
47
|
+
{ pk: 'key2', data: 'value2' },
|
|
48
|
+
];
|
|
49
|
+
__mockBatchGet.mockResolvedValueOnce({
|
|
50
|
+
Responses: { [tableName]: mockItems },
|
|
51
|
+
});
|
|
52
|
+
const keys = [{ pk: 'key1' }, { pk: 'key2' }];
|
|
53
|
+
const result = await dynamodb_client_js_1.DynamoDbClient.safeBatchGet(tableName, keys);
|
|
54
|
+
expect(result).toEqual(mockItems);
|
|
55
|
+
expect(__mockBatchGet).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(__mockBatchGet).toHaveBeenCalledWith({
|
|
57
|
+
RequestItems: { [tableName]: { Keys: keys } },
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it('should return empty array on error (fail-open behavior)', async () => {
|
|
61
|
+
__mockBatchGet.mockRejectedValueOnce(new Error('DynamoDB error'));
|
|
62
|
+
const keys = [{ pk: 'key1' }];
|
|
63
|
+
const result = await dynamodb_client_js_1.DynamoDbClient.safeBatchGet(tableName, keys);
|
|
64
|
+
expect(result).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
it('should handle partial results with UnprocessedKeys (fail-open)', async () => {
|
|
67
|
+
const mockItems = [{ pk: 'key1', data: 'value1' }];
|
|
68
|
+
__mockBatchGet.mockResolvedValueOnce({
|
|
69
|
+
Responses: { [tableName]: mockItems },
|
|
70
|
+
UnprocessedKeys: { [tableName]: { Keys: [{ pk: 'key2' }] } },
|
|
71
|
+
});
|
|
72
|
+
const keys = [{ pk: 'key1' }, { pk: 'key2' }];
|
|
73
|
+
const result = await dynamodb_client_js_1.DynamoDbClient.safeBatchGet(tableName, keys);
|
|
74
|
+
expect(result).toEqual(mockItems);
|
|
75
|
+
expect(__mockBatchGet).toHaveBeenCalledTimes(1);
|
|
76
|
+
});
|
|
77
|
+
it('should chunk keys into batches of 100', async () => {
|
|
78
|
+
const mockItems1 = Array.from({ length: 100 }, (_, i) => ({ pk: `key${i}`, data: `value${i}` }));
|
|
79
|
+
const mockItems2 = Array.from({ length: 50 }, (_, i) => ({ pk: `key${100 + i}`, data: `value${100 + i}` }));
|
|
80
|
+
__mockBatchGet
|
|
81
|
+
.mockResolvedValueOnce({ Responses: { [tableName]: mockItems1 } })
|
|
82
|
+
.mockResolvedValueOnce({ Responses: { [tableName]: mockItems2 } });
|
|
83
|
+
const keys = Array.from({ length: 150 }, (_, i) => ({ pk: `key${i}` }));
|
|
84
|
+
const result = await dynamodb_client_js_1.DynamoDbClient.safeBatchGet(tableName, keys);
|
|
85
|
+
expect(result).toHaveLength(150);
|
|
86
|
+
expect(__mockBatchGet).toHaveBeenCalledTimes(2);
|
|
87
|
+
});
|
|
88
|
+
it('should handle missing Responses gracefully', async () => {
|
|
89
|
+
__mockBatchGet.mockResolvedValueOnce({});
|
|
90
|
+
const keys = [{ pk: 'key1' }];
|
|
91
|
+
const result = await dynamodb_client_js_1.DynamoDbClient.safeBatchGet(tableName, keys);
|
|
92
|
+
expect(result).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
it('should continue processing other chunks if one fails', async () => {
|
|
95
|
+
const mockItems = [{ pk: 'key100', data: 'value100' }];
|
|
96
|
+
__mockBatchGet
|
|
97
|
+
.mockRejectedValueOnce(new Error('First chunk failed'))
|
|
98
|
+
.mockResolvedValueOnce({ Responses: { [tableName]: mockItems } });
|
|
99
|
+
const keys = Array.from({ length: 150 }, (_, i) => ({ pk: `key${i}` }));
|
|
100
|
+
const result = await dynamodb_client_js_1.DynamoDbClient.safeBatchGet(tableName, keys);
|
|
101
|
+
expect(result).toEqual(mockItems);
|
|
102
|
+
expect(__mockBatchGet).toHaveBeenCalledTimes(2);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('safeBatchWrite', () => {
|
|
106
|
+
const tableName = 'test-table';
|
|
107
|
+
it('should return early for empty items', async () => {
|
|
108
|
+
await dynamodb_client_js_1.DynamoDbClient.safeBatchWrite(tableName, []);
|
|
109
|
+
expect(__mockBatchWrite).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
it('should write items to DynamoDB', async () => {
|
|
112
|
+
__mockBatchWrite.mockResolvedValueOnce({});
|
|
113
|
+
const items = [
|
|
114
|
+
{ pk: 'key1', data: 'value1' },
|
|
115
|
+
{ pk: 'key2', data: 'value2' },
|
|
116
|
+
];
|
|
117
|
+
await dynamodb_client_js_1.DynamoDbClient.safeBatchWrite(tableName, items);
|
|
118
|
+
expect(__mockBatchWrite).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(__mockBatchWrite).toHaveBeenCalledWith({
|
|
120
|
+
RequestItems: {
|
|
121
|
+
[tableName]: [
|
|
122
|
+
{ PutRequest: { Item: items[0] } },
|
|
123
|
+
{ PutRequest: { Item: items[1] } },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
it('should not throw on error (fail-open behavior)', async () => {
|
|
129
|
+
__mockBatchWrite.mockRejectedValueOnce(new Error('DynamoDB error'));
|
|
130
|
+
const items = [{ pk: 'key1', data: 'value1' }];
|
|
131
|
+
await expect(dynamodb_client_js_1.DynamoDbClient.safeBatchWrite(tableName, items)).resolves.toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
it('should handle partial writes with UnprocessedItems (no retry)', async () => {
|
|
134
|
+
__mockBatchWrite.mockResolvedValueOnce({
|
|
135
|
+
UnprocessedItems: { [tableName]: [{ PutRequest: { Item: { pk: 'key2' } } }] },
|
|
136
|
+
});
|
|
137
|
+
const items = [
|
|
138
|
+
{ pk: 'key1', data: 'value1' },
|
|
139
|
+
{ pk: 'key2', data: 'value2' },
|
|
140
|
+
];
|
|
141
|
+
await dynamodb_client_js_1.DynamoDbClient.safeBatchWrite(tableName, items);
|
|
142
|
+
expect(__mockBatchWrite).toHaveBeenCalledTimes(1);
|
|
143
|
+
});
|
|
144
|
+
it('should chunk items into batches of 25', async () => {
|
|
145
|
+
__mockBatchWrite.mockResolvedValue({});
|
|
146
|
+
const items = Array.from({ length: 60 }, (_, i) => ({ pk: `key${i}`, data: `value${i}` }));
|
|
147
|
+
await dynamodb_client_js_1.DynamoDbClient.safeBatchWrite(tableName, items);
|
|
148
|
+
expect(__mockBatchWrite).toHaveBeenCalledTimes(3);
|
|
149
|
+
});
|
|
150
|
+
it('should continue processing other chunks if one fails', async () => {
|
|
151
|
+
__mockBatchWrite
|
|
152
|
+
.mockRejectedValueOnce(new Error('First chunk failed'))
|
|
153
|
+
.mockResolvedValueOnce({});
|
|
154
|
+
const items = Array.from({ length: 30 }, (_, i) => ({ pk: `key${i}`, data: `value${i}` }));
|
|
155
|
+
await dynamodb_client_js_1.DynamoDbClient.safeBatchWrite(tableName, items);
|
|
156
|
+
expect(__mockBatchWrite).toHaveBeenCalledTimes(2);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
//# sourceMappingURL=dynamodb-client.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamodb-client.spec.js","sourceRoot":"","sources":["../../../../src/__tests__/clients/dynamodb-client.spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
jest.mock('../../clients/index.js', () => ({
|
|
4
|
+
DynamoDbClient: {
|
|
5
|
+
safeGet: jest.fn(),
|
|
6
|
+
safePut: jest.fn(),
|
|
7
|
+
safeDelete: jest.fn(),
|
|
8
|
+
safeBatchGet: jest.fn(),
|
|
9
|
+
safeBatchWrite: jest.fn(),
|
|
10
|
+
safeQueryByGSI: jest.fn(),
|
|
11
|
+
batchWrite: jest.fn(),
|
|
12
|
+
},
|
|
13
|
+
LambdaInvokeClient: jest.fn().mockImplementation(() => ({
|
|
14
|
+
invokeFunction: jest.fn(),
|
|
15
|
+
})),
|
|
16
|
+
}));
|
|
17
|
+
jest.mock('../../helpers/index.js', () => ({
|
|
18
|
+
Logger: {
|
|
19
|
+
debug: jest.fn(),
|
|
20
|
+
info: jest.fn(),
|
|
21
|
+
warn: jest.fn(),
|
|
22
|
+
error: jest.fn(),
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
const identity_cache_dynamodb_service_js_1 = require("../../services/db/identity-cache-dynamodb-service.js");
|
|
26
|
+
const index_js_1 = require("../../clients/index.js");
|
|
27
|
+
const mockedDynamoDbClient = index_js_1.DynamoDbClient;
|
|
28
|
+
describe('IdentityCacheDynamoDbService', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
describe('Key Builders', () => {
|
|
33
|
+
describe('buildIdentityPk', () => {
|
|
34
|
+
it('should build correct pk for identity lookup', async () => {
|
|
35
|
+
const pixelId = 'pixel123';
|
|
36
|
+
const identityId = 'identity456';
|
|
37
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
38
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, { identityId });
|
|
39
|
+
expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', `identity#${pixelId}#${identityId}`);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('buildEmailPk', () => {
|
|
43
|
+
it('should build correct pk for email lookup (lowercase)', async () => {
|
|
44
|
+
const pixelId = 'pixel123';
|
|
45
|
+
const email = 'Test@Email.COM';
|
|
46
|
+
mockedDynamoDbClient.safeBatchGet.mockResolvedValueOnce([]);
|
|
47
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
48
|
+
traits: { emails: [email] },
|
|
49
|
+
});
|
|
50
|
+
expect(mockedDynamoDbClient.safeBatchGet).toHaveBeenCalledWith(expect.any(String), [{ pk: `email#${pixelId}#test@email.com` }]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('buildUserIdPk', () => {
|
|
54
|
+
it('should build correct pk for userId lookup (trimmed)', async () => {
|
|
55
|
+
const pixelId = 'pixel123';
|
|
56
|
+
const userId = ' user789 ';
|
|
57
|
+
mockedDynamoDbClient.safeBatchGet.mockResolvedValueOnce([]);
|
|
58
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
59
|
+
traits: { userIds: [userId] },
|
|
60
|
+
});
|
|
61
|
+
expect(mockedDynamoDbClient.safeBatchGet).toHaveBeenCalledWith(expect.any(String), [{ pk: `user_id#${pixelId}#user789` }]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('getIdentityFromCache', () => {
|
|
66
|
+
const pixelId = 'pixel123';
|
|
67
|
+
describe('with identityId (primary lookup)', () => {
|
|
68
|
+
it('should return cache hit when identity found', async () => {
|
|
69
|
+
const identityId = 'identity456';
|
|
70
|
+
const cachedResponse = {
|
|
71
|
+
pk: `identity#${pixelId}#${identityId}`,
|
|
72
|
+
pixelId,
|
|
73
|
+
identityId,
|
|
74
|
+
response: {
|
|
75
|
+
identityId,
|
|
76
|
+
traits: { emails: ['test@email.com'] },
|
|
77
|
+
},
|
|
78
|
+
updatedAt: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
81
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
82
|
+
identityId,
|
|
83
|
+
traits: { emails: ['test@email.com'] },
|
|
84
|
+
});
|
|
85
|
+
expect(result.resolvedIdentity).toBeDefined();
|
|
86
|
+
expect(result.resolvedIdentity?.identityId).toBe(identityId);
|
|
87
|
+
expect(result.isCacheStale).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
it('should return cache miss when identity not found', async () => {
|
|
90
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
91
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
92
|
+
identityId: 'unknown-id',
|
|
93
|
+
});
|
|
94
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
95
|
+
expect(result.isCacheStale).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it('should NOT fallback to secondary keys when identityId lookup misses', async () => {
|
|
98
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
99
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
100
|
+
identityId: 'unknown-id',
|
|
101
|
+
traits: { emails: ['test@email.com'] },
|
|
102
|
+
});
|
|
103
|
+
expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
it('should detect stale cache when new traits present', async () => {
|
|
106
|
+
const identityId = 'identity456';
|
|
107
|
+
const cachedResponse = {
|
|
108
|
+
pk: `identity#${pixelId}#${identityId}`,
|
|
109
|
+
pixelId,
|
|
110
|
+
identityId,
|
|
111
|
+
response: {
|
|
112
|
+
identityId,
|
|
113
|
+
traits: { emails: ['old@email.com'] },
|
|
114
|
+
},
|
|
115
|
+
updatedAt: new Date().toISOString(),
|
|
116
|
+
};
|
|
117
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
118
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
119
|
+
identityId,
|
|
120
|
+
traits: { emails: ['old@email.com', 'new@email.com'] },
|
|
121
|
+
});
|
|
122
|
+
expect(result.resolvedIdentity).toBeDefined();
|
|
123
|
+
expect(result.isCacheStale).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('without identityId (secondary lookup)', () => {
|
|
127
|
+
it('should use batch get for email and userId lookups', async () => {
|
|
128
|
+
const cachedResponse = {
|
|
129
|
+
pk: `email#${pixelId}#test@email.com`,
|
|
130
|
+
pixelId,
|
|
131
|
+
identityId: 'resolved-id',
|
|
132
|
+
response: {
|
|
133
|
+
identityId: 'resolved-id',
|
|
134
|
+
traits: { emails: ['test@email.com'] },
|
|
135
|
+
},
|
|
136
|
+
updatedAt: new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
mockedDynamoDbClient.safeBatchGet.mockResolvedValueOnce([cachedResponse]);
|
|
139
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
140
|
+
traits: { emails: ['test@email.com'], userIds: ['user123'] },
|
|
141
|
+
});
|
|
142
|
+
expect(mockedDynamoDbClient.safeBatchGet).toHaveBeenCalledWith(expect.any(String), [
|
|
143
|
+
{ pk: `email#${pixelId}#test@email.com` },
|
|
144
|
+
{ pk: `user_id#${pixelId}#user123` },
|
|
145
|
+
]);
|
|
146
|
+
expect(result.resolvedIdentity).toBeDefined();
|
|
147
|
+
expect(result.resolvedIdentity?.traits?.emails).toContain('test@email.com');
|
|
148
|
+
expect(result.isCacheStale).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
it('should detect conflict when multiple identityIds found', async () => {
|
|
151
|
+
const response1 = {
|
|
152
|
+
pk: `email#${pixelId}#test@email.com`,
|
|
153
|
+
pixelId,
|
|
154
|
+
identityId: 'identity-1',
|
|
155
|
+
response: { identityId: 'identity-1' },
|
|
156
|
+
updatedAt: new Date().toISOString(),
|
|
157
|
+
};
|
|
158
|
+
const response2 = {
|
|
159
|
+
pk: `user_id#${pixelId}#user123`,
|
|
160
|
+
pixelId,
|
|
161
|
+
identityId: 'identity-2',
|
|
162
|
+
response: { identityId: 'identity-2' },
|
|
163
|
+
updatedAt: new Date().toISOString(),
|
|
164
|
+
};
|
|
165
|
+
mockedDynamoDbClient.safeBatchGet.mockResolvedValueOnce([response1, response2]);
|
|
166
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
167
|
+
traits: { emails: ['test@email.com'], userIds: ['user123'] },
|
|
168
|
+
});
|
|
169
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
170
|
+
expect(result.isCacheStale).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
it('should return cache miss when no secondary keys match', async () => {
|
|
173
|
+
mockedDynamoDbClient.safeBatchGet.mockResolvedValueOnce([]);
|
|
174
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
175
|
+
traits: { emails: ['unknown@email.com'] },
|
|
176
|
+
});
|
|
177
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
178
|
+
expect(result.isCacheStale).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
it('should return cache miss when no secondary keys to lookup', async () => {
|
|
181
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
182
|
+
traits: {},
|
|
183
|
+
});
|
|
184
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
185
|
+
expect(result.isCacheStale).toBe(true);
|
|
186
|
+
expect(mockedDynamoDbClient.safeBatchGet).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('error handling (fail-open)', () => {
|
|
190
|
+
it('should return cache miss on safeGet error', async () => {
|
|
191
|
+
mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
|
|
192
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
193
|
+
identityId: 'test-id',
|
|
194
|
+
});
|
|
195
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
196
|
+
expect(result.isCacheStale).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
it('should return cache miss on safeBatchGet error', async () => {
|
|
199
|
+
mockedDynamoDbClient.safeBatchGet.mockRejectedValueOnce(new Error('DynamoDB error'));
|
|
200
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
201
|
+
traits: { emails: ['test@email.com'] },
|
|
202
|
+
});
|
|
203
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
204
|
+
expect(result.isCacheStale).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
it('should return cache miss for null pixelId', async () => {
|
|
207
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(null, {
|
|
208
|
+
identityId: 'test-id',
|
|
209
|
+
});
|
|
210
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
211
|
+
expect(result.isCacheStale).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
it('should return cache miss for null incomingIdentity', async () => {
|
|
214
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, null);
|
|
215
|
+
expect(result.resolvedIdentity).toBeUndefined();
|
|
216
|
+
expect(result.isCacheStale).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
describe('updateIdentityCache', () => {
|
|
221
|
+
const pixelId = 'pixel123';
|
|
222
|
+
it('should write identity with all secondary keys', async () => {
|
|
223
|
+
const identity = {
|
|
224
|
+
identityId: 'identity456',
|
|
225
|
+
traits: {
|
|
226
|
+
emails: ['test@email.com', 'other@email.com'],
|
|
227
|
+
userIds: ['user1', 'user2'],
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
231
|
+
expect(mockedDynamoDbClient.safeBatchWrite).toHaveBeenCalledTimes(1);
|
|
232
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
233
|
+
expect(writtenItems).toHaveLength(5);
|
|
234
|
+
const pks = writtenItems.map((item) => item.pk);
|
|
235
|
+
expect(pks).toContain(`identity#${pixelId}#identity456`);
|
|
236
|
+
expect(pks).toContain(`email#${pixelId}#test@email.com`);
|
|
237
|
+
expect(pks).toContain(`email#${pixelId}#other@email.com`);
|
|
238
|
+
expect(pks).toContain(`user_id#${pixelId}#user1`);
|
|
239
|
+
expect(pks).toContain(`user_id#${pixelId}#user2`);
|
|
240
|
+
});
|
|
241
|
+
it('should not throw on safeBatchWrite error (fail-open)', async () => {
|
|
242
|
+
mockedDynamoDbClient.safeBatchWrite.mockRejectedValueOnce(new Error('DynamoDB error'));
|
|
243
|
+
const identity = {
|
|
244
|
+
identityId: 'identity456',
|
|
245
|
+
traits: { emails: ['test@email.com'] },
|
|
246
|
+
};
|
|
247
|
+
await expect(identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity)).resolves.toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
it('should return early for missing identityId', async () => {
|
|
250
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, {});
|
|
251
|
+
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
it('should return early for missing pixelId', async () => {
|
|
254
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(null, {
|
|
255
|
+
identityId: 'test-id',
|
|
256
|
+
});
|
|
257
|
+
expect(mockedDynamoDbClient.safeBatchWrite).not.toHaveBeenCalled();
|
|
258
|
+
});
|
|
259
|
+
it('should handle identity with no traits', async () => {
|
|
260
|
+
const identity = {
|
|
261
|
+
identityId: 'identity456',
|
|
262
|
+
};
|
|
263
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
264
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
265
|
+
expect(writtenItems).toHaveLength(1);
|
|
266
|
+
});
|
|
267
|
+
it('should include gsi1pk for reverse lookups', async () => {
|
|
268
|
+
const identity = {
|
|
269
|
+
identityId: 'identity456',
|
|
270
|
+
traits: { emails: ['test@email.com'] },
|
|
271
|
+
};
|
|
272
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.updateIdentityCache(pixelId, identity);
|
|
273
|
+
const writtenItems = mockedDynamoDbClient.safeBatchWrite.mock.calls[0][1];
|
|
274
|
+
for (const item of writtenItems) {
|
|
275
|
+
expect(item.gsi1pk).toBe('identity456');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
describe('getIdentityMap', () => {
|
|
280
|
+
it('should return identity map when found', async () => {
|
|
281
|
+
const mockMap = {
|
|
282
|
+
pk: 'identity_map#pixel123#identity456',
|
|
283
|
+
pixelId: 'pixel123',
|
|
284
|
+
identityId: 'identity456',
|
|
285
|
+
linkedIdentities: ['identity789'],
|
|
286
|
+
updatedAt: new Date().toISOString(),
|
|
287
|
+
};
|
|
288
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(mockMap);
|
|
289
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
|
|
290
|
+
expect(result).toEqual(mockMap);
|
|
291
|
+
expect(mockedDynamoDbClient.safeGet).toHaveBeenCalledWith(expect.any(String), 'pk', 'identity_map#pixel123#identity456');
|
|
292
|
+
});
|
|
293
|
+
it('should return undefined when not found', async () => {
|
|
294
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
295
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityMap('pixel123', 'identity456');
|
|
296
|
+
expect(result).toBeUndefined();
|
|
297
|
+
});
|
|
298
|
+
it('should return undefined for missing params (fail-open)', async () => {
|
|
299
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityMap('', '');
|
|
300
|
+
expect(result).toBeUndefined();
|
|
301
|
+
expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
describe('getForcePurgeFlag', () => {
|
|
305
|
+
it('should return true when flag exists', async () => {
|
|
306
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce({
|
|
307
|
+
pk: 'force_purge#pixel123#identity456',
|
|
308
|
+
});
|
|
309
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
|
|
310
|
+
expect(result).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
it('should return false when flag not found', async () => {
|
|
313
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(null);
|
|
314
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
|
|
315
|
+
expect(result).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
it('should return false on error (fail-open)', async () => {
|
|
318
|
+
mockedDynamoDbClient.safeGet.mockRejectedValueOnce(new Error('DynamoDB error'));
|
|
319
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getForcePurgeFlag('pixel123', 'identity456');
|
|
320
|
+
expect(result).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
it('should return false for missing params', async () => {
|
|
323
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getForcePurgeFlag('', '');
|
|
324
|
+
expect(result).toBe(false);
|
|
325
|
+
expect(mockedDynamoDbClient.safeGet).not.toHaveBeenCalled();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
describe('setForcePurgeFlag', () => {
|
|
329
|
+
it('should set flag with TTL', async () => {
|
|
330
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456');
|
|
331
|
+
expect(mockedDynamoDbClient.safePut).toHaveBeenCalledTimes(1);
|
|
332
|
+
const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
|
|
333
|
+
expect(putItem.pk).toBe('force_purge#pixel123#identity456');
|
|
334
|
+
expect(putItem.pixelId).toBe('pixel123');
|
|
335
|
+
expect(putItem.identityId).toBe('identity456');
|
|
336
|
+
expect(putItem.ttl).toBeGreaterThan(0);
|
|
337
|
+
});
|
|
338
|
+
it('should accept custom TTL', async () => {
|
|
339
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456', 3600);
|
|
340
|
+
const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
|
|
341
|
+
expect(putItem.ttl).toBeGreaterThan(0);
|
|
342
|
+
});
|
|
343
|
+
it('should not throw on error (fail-open)', async () => {
|
|
344
|
+
mockedDynamoDbClient.safePut.mockRejectedValueOnce(new Error('DynamoDB error'));
|
|
345
|
+
await expect(identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.setForcePurgeFlag('pixel123', 'identity456')).resolves.toBeUndefined();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
describe('putIdentityMap', () => {
|
|
349
|
+
it('should write identity map', async () => {
|
|
350
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.putIdentityMap('pixel123', 'identity456', ['linked1', 'linked2']);
|
|
351
|
+
expect(mockedDynamoDbClient.safePut).toHaveBeenCalledTimes(1);
|
|
352
|
+
const putItem = mockedDynamoDbClient.safePut.mock.calls[0][1];
|
|
353
|
+
expect(putItem.pk).toBe('identity_map#pixel123#identity456');
|
|
354
|
+
expect(putItem.linkedIdentities).toEqual(['linked1', 'linked2']);
|
|
355
|
+
});
|
|
356
|
+
it('should return early for missing params', async () => {
|
|
357
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.putIdentityMap('', '', []);
|
|
358
|
+
expect(mockedDynamoDbClient.safePut).not.toHaveBeenCalled();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
describe('deleteIdentityMap', () => {
|
|
362
|
+
it('should delete identity map', async () => {
|
|
363
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityMap('pixel123', 'identity456');
|
|
364
|
+
expect(mockedDynamoDbClient.safeDelete).toHaveBeenCalledWith(expect.any(String), 'pk', 'identity_map#pixel123#identity456');
|
|
365
|
+
});
|
|
366
|
+
it('should return early for missing params', async () => {
|
|
367
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityMap('', '');
|
|
368
|
+
expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
describe('deleteForcePurgeFlag', () => {
|
|
372
|
+
it('should delete force purge flag', async () => {
|
|
373
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteForcePurgeFlag('pixel123', 'identity456');
|
|
374
|
+
expect(mockedDynamoDbClient.safeDelete).toHaveBeenCalledWith(expect.any(String), 'pk', 'force_purge#pixel123#identity456');
|
|
375
|
+
});
|
|
376
|
+
it('should return early for missing params', async () => {
|
|
377
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteForcePurgeFlag('', '');
|
|
378
|
+
expect(mockedDynamoDbClient.safeDelete).not.toHaveBeenCalled();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
describe('deleteIdentityCache', () => {
|
|
382
|
+
const pixelId = 'pixel123';
|
|
383
|
+
it('should delete all related cache items', async () => {
|
|
384
|
+
mockedDynamoDbClient.safeQueryByGSI.mockResolvedValue([
|
|
385
|
+
{ pk: `identity#${pixelId}#identity456`, pixelId },
|
|
386
|
+
{ pk: `email#${pixelId}#test@email.com`, pixelId },
|
|
387
|
+
]);
|
|
388
|
+
mockedDynamoDbClient.batchWrite.mockResolvedValue({ $metadata: {} });
|
|
389
|
+
const incomingIdentity = {
|
|
390
|
+
identityId: 'identity456',
|
|
391
|
+
traits: { emails: ['test@email.com'] },
|
|
392
|
+
};
|
|
393
|
+
const resolvedIdentity = {
|
|
394
|
+
identityId: 'identity456',
|
|
395
|
+
traits: { emails: ['test@email.com', 'other@email.com'] },
|
|
396
|
+
};
|
|
397
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityCache(pixelId, incomingIdentity, resolvedIdentity);
|
|
398
|
+
expect(mockedDynamoDbClient.safeQueryByGSI).toHaveBeenCalled();
|
|
399
|
+
expect(mockedDynamoDbClient.batchWrite).toHaveBeenCalled();
|
|
400
|
+
});
|
|
401
|
+
it('should return early for missing pixelId', async () => {
|
|
402
|
+
await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.deleteIdentityCache(null, {}, {});
|
|
403
|
+
expect(mockedDynamoDbClient.safeQueryByGSI).not.toHaveBeenCalled();
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
describe('merge and staleness logic', () => {
|
|
407
|
+
const pixelId = 'pixel123';
|
|
408
|
+
it('should merge incoming traits with cached traits', async () => {
|
|
409
|
+
const cachedResponse = {
|
|
410
|
+
pk: `identity#${pixelId}#identity456`,
|
|
411
|
+
pixelId,
|
|
412
|
+
identityId: 'identity456',
|
|
413
|
+
response: {
|
|
414
|
+
identityId: 'identity456',
|
|
415
|
+
traits: {
|
|
416
|
+
emails: ['cached@email.com'],
|
|
417
|
+
userIds: ['cached-user'],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
updatedAt: new Date().toISOString(),
|
|
421
|
+
};
|
|
422
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
423
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
424
|
+
identityId: 'identity456',
|
|
425
|
+
traits: {
|
|
426
|
+
emails: ['incoming@email.com'],
|
|
427
|
+
phones: ['+1234567890'],
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
expect(result.resolvedIdentity?.traits?.emails).toContain('incoming@email.com');
|
|
431
|
+
expect(result.resolvedIdentity?.traits?.emails).toContain('cached@email.com');
|
|
432
|
+
expect(result.resolvedIdentity?.traits?.userIds).toContain('cached-user');
|
|
433
|
+
expect(result.resolvedIdentity?.traits?.phones).toContain('+1234567890');
|
|
434
|
+
});
|
|
435
|
+
it('should mark cache stale when new data present', async () => {
|
|
436
|
+
const cachedResponse = {
|
|
437
|
+
pk: `identity#${pixelId}#identity456`,
|
|
438
|
+
pixelId,
|
|
439
|
+
identityId: 'identity456',
|
|
440
|
+
response: {
|
|
441
|
+
identityId: 'identity456',
|
|
442
|
+
traits: { emails: ['cached@email.com'] },
|
|
443
|
+
},
|
|
444
|
+
updatedAt: new Date().toISOString(),
|
|
445
|
+
};
|
|
446
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
447
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
448
|
+
identityId: 'identity456',
|
|
449
|
+
traits: {
|
|
450
|
+
emails: ['cached@email.com', 'new@email.com'],
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
expect(result.isCacheStale).toBe(true);
|
|
454
|
+
});
|
|
455
|
+
it('should not mark cache stale when incoming is subset of cached', async () => {
|
|
456
|
+
const cachedResponse = {
|
|
457
|
+
pk: `identity#${pixelId}#identity456`,
|
|
458
|
+
pixelId,
|
|
459
|
+
identityId: 'identity456',
|
|
460
|
+
response: {
|
|
461
|
+
identityId: 'identity456',
|
|
462
|
+
traits: {
|
|
463
|
+
emails: ['a@email.com', 'b@email.com'],
|
|
464
|
+
userIds: ['user1', 'user2'],
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
updatedAt: new Date().toISOString(),
|
|
468
|
+
};
|
|
469
|
+
mockedDynamoDbClient.safeGet.mockResolvedValueOnce(cachedResponse);
|
|
470
|
+
const result = await identity_cache_dynamodb_service_js_1.IdentityCacheDynamoDbService.getIdentityFromCache(pixelId, {
|
|
471
|
+
identityId: 'identity456',
|
|
472
|
+
traits: {
|
|
473
|
+
emails: ['a@email.com'],
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
expect(result.isCacheStale).toBe(false);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
//# sourceMappingURL=identity-cache-dynamodb-service.spec.js.map
|