@eresearchqut/ddb-repository 1.5.7 → 1.13.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -105
- package/dist/index.cjs +478 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +116 -0
- package/dist/index.d.mts +116 -0
- package/dist/index.mjs +474 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +33 -20
- package/.github/workflows/build.yml +0 -61
- package/.github/workflows/release.yml +0 -34
- package/.releaserc.json +0 -17
- package/CHANGELOG.md +0 -93
- package/dist/DynamoDbRepository.js +0 -244
- package/dist/consumed-capacity-middleware.js +0 -25
- package/dist/index.js +0 -8
- package/eslint.config.mjs +0 -13
- package/jest.config.ts +0 -28
- package/src/DynamoDbRepository.ts +0 -379
- package/src/consumed-capacity-middleware.ts +0 -41
- package/src/index.ts +0 -4
- package/test/DynamoDbRepository.test.ts +0 -1216
- package/tsconfig.json +0 -12
|
@@ -1,1216 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { DynamoDBClient, CreateTableCommand, DescribeTableCommand } from "@aws-sdk/client-dynamodb";
|
|
3
|
-
import { LocalstackContainer, StartedLocalStackContainer } from "@testcontainers/localstack";
|
|
4
|
-
import {ConsumedCapacityDetail, consumedCapacityMiddleware, DynamoDbRepository, FilterOperator} from "../src";
|
|
5
|
-
|
|
6
|
-
describe('DynamoDbRepository Integration Tests', () => {
|
|
7
|
-
let container: StartedLocalStackContainer;
|
|
8
|
-
let dynamoDBClient: DynamoDBClient;
|
|
9
|
-
let repository: DynamoDbRepository<{ id: string }, { id: string; name: string; email?: string; age?: number; status?: string, score?: number }>;
|
|
10
|
-
let compositeRepository: DynamoDbRepository<{ userId: string; itemId?: string }, { userId: string; itemId: string; name: string; category?: string; price?: number }>;
|
|
11
|
-
let gsiRepository: DynamoDbRepository<{ userId: string; itemId: string, status?: string }, { userId: string; itemId: string; name: string; category?: string; status?: string; createdAt?: string }>;
|
|
12
|
-
const tableName = 'test-table';
|
|
13
|
-
const compositeTableName = 'test-composite-table';
|
|
14
|
-
const gsiTableName = 'test-gsi-table';
|
|
15
|
-
const consumedCapacityRegister = new Array<ConsumedCapacityDetail>() ;
|
|
16
|
-
|
|
17
|
-
const getConsumedCapacity = (consumedCapacity: ConsumedCapacityDetail) => {
|
|
18
|
-
if (Array.isArray(consumedCapacity.ConsumedCapacity)) {
|
|
19
|
-
return consumedCapacity.ConsumedCapacity
|
|
20
|
-
.reduce((total, capacity) => total + (capacity?.CapacityUnits || 0), 0);
|
|
21
|
-
}
|
|
22
|
-
return consumedCapacity.ConsumedCapacity?.CapacityUnits || 0;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const sumConsumedCapacity = () =>
|
|
26
|
-
consumedCapacityRegister.reduce((total, value) =>
|
|
27
|
-
total + getConsumedCapacity(value), 0);
|
|
28
|
-
|
|
29
|
-
beforeAll(async () => {
|
|
30
|
-
// Start LocalStack container with DynamoDB
|
|
31
|
-
container = await new LocalstackContainer("localstack/localstack:latest")
|
|
32
|
-
.start();
|
|
33
|
-
|
|
34
|
-
// Create DynamoDB client pointing to the container
|
|
35
|
-
dynamoDBClient = new DynamoDBClient({
|
|
36
|
-
endpoint: container.getConnectionUri(),
|
|
37
|
-
region: "us-east-1",
|
|
38
|
-
credentials: {
|
|
39
|
-
accessKeyId: "test",
|
|
40
|
-
secretAccessKey: "test",
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
dynamoDBClient.middlewareStack
|
|
45
|
-
.add(consumedCapacityMiddleware({onConsumedCapacity: async (consumedCapacity) =>
|
|
46
|
-
consumedCapacityRegister.push(consumedCapacity)}));
|
|
47
|
-
|
|
48
|
-
// Create the test table with a simple key
|
|
49
|
-
await dynamoDBClient.send(
|
|
50
|
-
new CreateTableCommand({
|
|
51
|
-
TableName: tableName,
|
|
52
|
-
KeySchema: [
|
|
53
|
-
{ AttributeName: "id", KeyType: "HASH" },
|
|
54
|
-
],
|
|
55
|
-
AttributeDefinitions: [
|
|
56
|
-
{ AttributeName: "id", AttributeType: "S" },
|
|
57
|
-
],
|
|
58
|
-
BillingMode: "PAY_PER_REQUEST",
|
|
59
|
-
})
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// Create the test table with a composite key (partition key + sort key)
|
|
63
|
-
await dynamoDBClient.send(
|
|
64
|
-
new CreateTableCommand({
|
|
65
|
-
TableName: compositeTableName,
|
|
66
|
-
KeySchema: [
|
|
67
|
-
{ AttributeName: "userId", KeyType: "HASH" },
|
|
68
|
-
{ AttributeName: "itemId", KeyType: "RANGE" },
|
|
69
|
-
],
|
|
70
|
-
AttributeDefinitions: [
|
|
71
|
-
{ AttributeName: "userId", AttributeType: "S" },
|
|
72
|
-
{ AttributeName: "itemId", AttributeType: "S" },
|
|
73
|
-
],
|
|
74
|
-
BillingMode: "PAY_PER_REQUEST",
|
|
75
|
-
})
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
// Create the test table with GSI
|
|
79
|
-
await dynamoDBClient.send(
|
|
80
|
-
new CreateTableCommand({
|
|
81
|
-
TableName: gsiTableName,
|
|
82
|
-
KeySchema: [
|
|
83
|
-
{ AttributeName: "userId", KeyType: "HASH" },
|
|
84
|
-
{ AttributeName: "itemId", KeyType: "RANGE" },
|
|
85
|
-
],
|
|
86
|
-
AttributeDefinitions: [
|
|
87
|
-
{ AttributeName: "userId", AttributeType: "S" },
|
|
88
|
-
{ AttributeName: "itemId", AttributeType: "S" },
|
|
89
|
-
{ AttributeName: "status", AttributeType: "S" },
|
|
90
|
-
{ AttributeName: "createdAt", AttributeType: "S" },
|
|
91
|
-
],
|
|
92
|
-
GlobalSecondaryIndexes: [
|
|
93
|
-
{
|
|
94
|
-
IndexName: "StatusIndex",
|
|
95
|
-
KeySchema: [
|
|
96
|
-
{ AttributeName: "status", KeyType: "HASH" },
|
|
97
|
-
{ AttributeName: "createdAt", KeyType: "RANGE" },
|
|
98
|
-
],
|
|
99
|
-
Projection: {
|
|
100
|
-
ProjectionType: "ALL",
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
BillingMode: "PAY_PER_REQUEST",
|
|
105
|
-
})
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// Wait for tables to be active
|
|
109
|
-
for (const table of [tableName, compositeTableName, gsiTableName]) {
|
|
110
|
-
let tableActive = false;
|
|
111
|
-
while (!tableActive) {
|
|
112
|
-
const response = await dynamoDBClient.send(
|
|
113
|
-
new DescribeTableCommand({ TableName: table })
|
|
114
|
-
);
|
|
115
|
-
tableActive = response.Table?.TableStatus === "ACTIVE";
|
|
116
|
-
if (!tableActive) {
|
|
117
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Initialize repositories
|
|
123
|
-
repository = new DynamoDbRepository({
|
|
124
|
-
client: dynamoDBClient,
|
|
125
|
-
tableName: tableName,
|
|
126
|
-
hashKey: "id"
|
|
127
|
-
});
|
|
128
|
-
compositeRepository = new DynamoDbRepository({
|
|
129
|
-
client: dynamoDBClient,
|
|
130
|
-
tableName: compositeTableName,
|
|
131
|
-
hashKey: "userId",
|
|
132
|
-
rangeKey: "itemId"
|
|
133
|
-
});
|
|
134
|
-
gsiRepository = new DynamoDbRepository({
|
|
135
|
-
client: dynamoDBClient,
|
|
136
|
-
tableName: gsiTableName,
|
|
137
|
-
hashKey: "userId",
|
|
138
|
-
rangeKey: "itemId"
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
beforeEach(async () => {
|
|
143
|
-
consumedCapacityRegister.splice(0, consumedCapacityRegister.length);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
afterAll(async () => {
|
|
147
|
-
// Clean up
|
|
148
|
-
if (dynamoDBClient) {
|
|
149
|
-
dynamoDBClient.destroy();
|
|
150
|
-
}
|
|
151
|
-
if (container) {
|
|
152
|
-
await container.stop();
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
describe('getItem', () => {
|
|
157
|
-
it('should return undefined when item does not exist', async () => {
|
|
158
|
-
const result = await repository.getItem({ id: 'non-existent' });
|
|
159
|
-
expect(result).toBeUndefined();
|
|
160
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('should return the item when it exists', async () => {
|
|
164
|
-
const key = { id: 'test-123' };
|
|
165
|
-
const record = { id: 'test-123', name: 'Test User', email: 'test@example.com' };
|
|
166
|
-
|
|
167
|
-
await repository.putItem(key, record);
|
|
168
|
-
const result = await repository.getItem(key);
|
|
169
|
-
|
|
170
|
-
expect(result).toEqual(record);
|
|
171
|
-
expect(sumConsumedCapacity()).toEqual(2);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
describe('putItem', () => {
|
|
176
|
-
it('should create a new item', async () => {
|
|
177
|
-
const key = { id: 'new-item' };
|
|
178
|
-
const record = { id: 'new-item', name: 'New Item' };
|
|
179
|
-
|
|
180
|
-
const result = await repository.putItem(key, record);
|
|
181
|
-
|
|
182
|
-
expect(result).toEqual(record);
|
|
183
|
-
expect(sumConsumedCapacity()).toEqual(1.5);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe('deleteItem', () => {
|
|
188
|
-
it('should delete an existing item', async () => {
|
|
189
|
-
const key = { id: 'delete-test-1' };
|
|
190
|
-
const record = { id: 'delete-test-1', name: 'To Be Deleted' };
|
|
191
|
-
|
|
192
|
-
await repository.putItem(key, record);
|
|
193
|
-
await repository.deleteItem(key);
|
|
194
|
-
const afterDelete = await repository.getItem(key);
|
|
195
|
-
expect(afterDelete).toBeUndefined();
|
|
196
|
-
expect(sumConsumedCapacity()).toEqual(2);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe('updateItem', () => {
|
|
201
|
-
it('should update a single attribute', async () => {
|
|
202
|
-
const key = { id: 'update-single' };
|
|
203
|
-
const record = { id: 'update-single', name: 'Original Name', email: 'original@example.com' };
|
|
204
|
-
|
|
205
|
-
await repository.putItem(key, record);
|
|
206
|
-
const result = await repository.updateItem(key, { name: 'Updated Name' });
|
|
207
|
-
|
|
208
|
-
expect(result).toEqual({
|
|
209
|
-
id: 'update-single',
|
|
210
|
-
name: 'Updated Name',
|
|
211
|
-
email: 'original@example.com'
|
|
212
|
-
});
|
|
213
|
-
expect(sumConsumedCapacity()).toEqual(3);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe('batchGetItems', () => {
|
|
218
|
-
beforeEach(async () => {
|
|
219
|
-
// Create test items
|
|
220
|
-
for (let i = 1; i <= 10; i++) {
|
|
221
|
-
await repository.putItem(
|
|
222
|
-
{ id: `batch-item-${i}` },
|
|
223
|
-
{ id: `batch-item-${i}`, name: `Item ${i}`, age: i * 10 }
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
consumedCapacityRegister.splice(0, consumedCapacityRegister.length);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should retrieve multiple items by keys', async () => {
|
|
230
|
-
const keys = [
|
|
231
|
-
{ id: 'batch-item-1' },
|
|
232
|
-
{ id: 'batch-item-2' },
|
|
233
|
-
{ id: 'batch-item-3' },
|
|
234
|
-
];
|
|
235
|
-
|
|
236
|
-
const results = await repository.batchGetItems(keys);
|
|
237
|
-
|
|
238
|
-
expect(results).toBeDefined();
|
|
239
|
-
expect(results?.length).toBe(3);
|
|
240
|
-
expect(results).toEqual(
|
|
241
|
-
expect.arrayContaining([
|
|
242
|
-
expect.objectContaining({ id: 'batch-item-1', name: 'Item 1', age: 10 }),
|
|
243
|
-
expect.objectContaining({ id: 'batch-item-2', name: 'Item 2', age: 20 }),
|
|
244
|
-
expect.objectContaining({ id: 'batch-item-3', name: 'Item 3', age: 30 }),
|
|
245
|
-
])
|
|
246
|
-
);
|
|
247
|
-
expect(sumConsumedCapacity()).toEqual(1.5);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('should handle empty keys array', async () => {
|
|
251
|
-
const results = await repository.batchGetItems([]);
|
|
252
|
-
|
|
253
|
-
expect(results).toBeDefined();
|
|
254
|
-
expect(results?.length).toBe(0);
|
|
255
|
-
expect(sumConsumedCapacity()).toEqual(0);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should handle non-existent keys gracefully', async () => {
|
|
259
|
-
const keys = [
|
|
260
|
-
{ id: 'batch-item-1' },
|
|
261
|
-
{ id: 'non-existent-1' },
|
|
262
|
-
{ id: 'batch-item-2' },
|
|
263
|
-
{ id: 'non-existent-2' },
|
|
264
|
-
];
|
|
265
|
-
|
|
266
|
-
const results = await repository.batchGetItems(keys);
|
|
267
|
-
|
|
268
|
-
expect(results).toBeDefined();
|
|
269
|
-
// Only existing items should be returned
|
|
270
|
-
const existingItems = results?.filter(item => item !== undefined);
|
|
271
|
-
expect(existingItems?.length).toBeGreaterThanOrEqual(2);
|
|
272
|
-
|
|
273
|
-
const ids = existingItems?.map(item => item.id);
|
|
274
|
-
expect(ids).toContain('batch-item-1');
|
|
275
|
-
expect(ids).toContain('batch-item-2');
|
|
276
|
-
expect(sumConsumedCapacity()).toEqual(1);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('should handle batch size over 100 items (pagination)', async () => {
|
|
280
|
-
// Create 150 items
|
|
281
|
-
const keys = [];
|
|
282
|
-
for (let i = 1; i <= 150; i++) {
|
|
283
|
-
const key = { id: `batch-large-${i}` };
|
|
284
|
-
keys.push(key);
|
|
285
|
-
await repository.putItem(key, { id: `batch-large-${i}`, name: `Item ${i}`, age: i });
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const results = await repository.batchGetItems(keys);
|
|
289
|
-
|
|
290
|
-
expect(results).toBeDefined();
|
|
291
|
-
expect(results?.length).toBe(150);
|
|
292
|
-
|
|
293
|
-
// Verify some random items
|
|
294
|
-
const item50 = results?.find(r => r?.id === 'batch-large-50');
|
|
295
|
-
const item100 = results?.find(r => r?.id === 'batch-large-100');
|
|
296
|
-
const item150 = results?.find(r => r?.id === 'batch-large-150');
|
|
297
|
-
|
|
298
|
-
expect(item50).toBeDefined();
|
|
299
|
-
expect(item100).toBeDefined();
|
|
300
|
-
expect(item150).toBeDefined();
|
|
301
|
-
expect(sumConsumedCapacity()).toEqual(300);
|
|
302
|
-
}, 60000);
|
|
303
|
-
|
|
304
|
-
it('should retrieve composite key items', async () => {
|
|
305
|
-
// Create test items with composite keys
|
|
306
|
-
await compositeRepository.putItem(
|
|
307
|
-
{ userId: 'user-batch-1', itemId: 'item-a' },
|
|
308
|
-
{ userId: 'user-batch-1', itemId: 'item-a', name: 'Item A', category: 'test', price: 100 }
|
|
309
|
-
);
|
|
310
|
-
await compositeRepository.putItem(
|
|
311
|
-
{ userId: 'user-batch-1', itemId: 'item-b' },
|
|
312
|
-
{ userId: 'user-batch-1', itemId: 'item-b', name: 'Item B', category: 'test', price: 200 }
|
|
313
|
-
);
|
|
314
|
-
await compositeRepository.putItem(
|
|
315
|
-
{ userId: 'user-batch-2', itemId: 'item-c' },
|
|
316
|
-
{ userId: 'user-batch-2', itemId: 'item-c', name: 'Item C', category: 'test', price: 300 }
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
const keys = [
|
|
320
|
-
{ userId: 'user-batch-1', itemId: 'item-a' },
|
|
321
|
-
{ userId: 'user-batch-1', itemId: 'item-b' },
|
|
322
|
-
{ userId: 'user-batch-2', itemId: 'item-c' },
|
|
323
|
-
];
|
|
324
|
-
|
|
325
|
-
const results = await compositeRepository.batchGetItems(keys);
|
|
326
|
-
|
|
327
|
-
expect(results).toBeDefined();
|
|
328
|
-
expect(results?.length).toBe(3);
|
|
329
|
-
expect(results).toEqual(
|
|
330
|
-
expect.arrayContaining([
|
|
331
|
-
expect.objectContaining({ userId: 'user-batch-1', itemId: 'item-a', price: 100 }),
|
|
332
|
-
expect.objectContaining({ userId: 'user-batch-1', itemId: 'item-b', price: 200 }),
|
|
333
|
-
expect.objectContaining({ userId: 'user-batch-2', itemId: 'item-c', price: 300 }),
|
|
334
|
-
])
|
|
335
|
-
);
|
|
336
|
-
expect(sumConsumedCapacity()).toEqual(6);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('should maintain order independence', async () => {
|
|
340
|
-
const keys = [
|
|
341
|
-
{ id: 'batch-item-5' },
|
|
342
|
-
{ id: 'batch-item-1' },
|
|
343
|
-
{ id: 'batch-item-3' },
|
|
344
|
-
];
|
|
345
|
-
|
|
346
|
-
const results = await repository.batchGetItems(keys);
|
|
347
|
-
|
|
348
|
-
expect(results).toBeDefined();
|
|
349
|
-
expect(results?.length).toBe(3);
|
|
350
|
-
|
|
351
|
-
// Verify all items are present regardless of order
|
|
352
|
-
const ids = results?.map(item => item?.id);
|
|
353
|
-
expect(ids).toContain('batch-item-1');
|
|
354
|
-
expect(ids).toContain('batch-item-3');
|
|
355
|
-
expect(ids).toContain('batch-item-5');
|
|
356
|
-
expect(sumConsumedCapacity()).toEqual(1.5);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('should handle duplicate keys in input', async () => {
|
|
360
|
-
const keys = [
|
|
361
|
-
{ id: 'batch-item-1' },
|
|
362
|
-
{ id: 'batch-item-1' }, // duplicate
|
|
363
|
-
{ id: 'batch-item-2' },
|
|
364
|
-
{ id: 'batch-item-2' }, // duplicate
|
|
365
|
-
];
|
|
366
|
-
|
|
367
|
-
const results = await repository.batchGetItems(keys);
|
|
368
|
-
|
|
369
|
-
expect(results).toBeDefined();
|
|
370
|
-
// Results may contain duplicates depending on DynamoDB behavior
|
|
371
|
-
expect(results?.length).toBeGreaterThanOrEqual(2);
|
|
372
|
-
|
|
373
|
-
const item1Count = results?.filter(r => r?.id === 'batch-item-1').length;
|
|
374
|
-
const item2Count = results?.filter(r => r?.id === 'batch-item-2').length;
|
|
375
|
-
|
|
376
|
-
expect(item1Count).toBeGreaterThanOrEqual(1);
|
|
377
|
-
expect(item2Count).toBeGreaterThanOrEqual(1);
|
|
378
|
-
expect(sumConsumedCapacity()).toEqual(1);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('should retrieve all attributes for batched items', async () => {
|
|
382
|
-
await repository.putItem(
|
|
383
|
-
{ id: 'batch-full-attrs' },
|
|
384
|
-
{ id: 'batch-full-attrs', name: 'Full Item', email: 'test@example.com', age: 30, status: 'active' }
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
const results = await repository.batchGetItems([{ id: 'batch-full-attrs' }]);
|
|
388
|
-
|
|
389
|
-
expect(results).toBeDefined();
|
|
390
|
-
expect(results?.length).toBe(1);
|
|
391
|
-
expect(results?.[0]).toEqual({
|
|
392
|
-
id: 'batch-full-attrs',
|
|
393
|
-
name: 'Full Item',
|
|
394
|
-
email: 'test@example.com',
|
|
395
|
-
age: 30,
|
|
396
|
-
status: 'active'
|
|
397
|
-
});
|
|
398
|
-
expect(sumConsumedCapacity()).toEqual(2);
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
describe('getItems', () => {
|
|
403
|
-
describe('with simple partition key', () => {
|
|
404
|
-
it('should retrieve item by partition key', async () => {
|
|
405
|
-
const testId = 'query-test-simple';
|
|
406
|
-
await repository.putItem({ id: testId }, { id: testId, name: 'Test Item', age: 25 });
|
|
407
|
-
|
|
408
|
-
const results = await repository.getItems({ id: testId });
|
|
409
|
-
|
|
410
|
-
expect(results).toBeDefined();
|
|
411
|
-
expect(results?.length).toBe(1);
|
|
412
|
-
expect(results?.[0]).toEqual({
|
|
413
|
-
id: testId,
|
|
414
|
-
name: 'Test Item',
|
|
415
|
-
age: 25
|
|
416
|
-
});
|
|
417
|
-
expect(sumConsumedCapacity()).toEqual(2);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
describe('with composite key (partition + sort)', () => {
|
|
423
|
-
beforeEach(async () => {
|
|
424
|
-
const userId = 'user-456';
|
|
425
|
-
await compositeRepository.putItem(
|
|
426
|
-
{ userId, itemId: 'item-1' },
|
|
427
|
-
{ userId, itemId: 'item-1', name: 'Item One', category: 'electronics', price: 100 }
|
|
428
|
-
);
|
|
429
|
-
await compositeRepository.putItem(
|
|
430
|
-
{ userId, itemId: 'item-2' },
|
|
431
|
-
{ userId, itemId: 'item-2', name: 'Item Two', category: 'books', price: 20 }
|
|
432
|
-
);
|
|
433
|
-
await compositeRepository.putItem(
|
|
434
|
-
{ userId, itemId: 'item-3' },
|
|
435
|
-
{ userId, itemId: 'item-3', name: 'Item Three', category: 'electronics', price: 150 }
|
|
436
|
-
);
|
|
437
|
-
consumedCapacityRegister.splice(0, consumedCapacityRegister.length);
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it('should retrieve all items for a partition key', async () => {
|
|
441
|
-
const results = await compositeRepository.getItems({ userId: 'user-456' });
|
|
442
|
-
|
|
443
|
-
expect(results).toBeDefined();
|
|
444
|
-
expect(results?.length).toBe(3);
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
it('should filter results with operators', async () => {
|
|
448
|
-
const results = await compositeRepository.getItems({
|
|
449
|
-
userId: 'user-456',
|
|
450
|
-
filterExpressions: [
|
|
451
|
-
{ attribute: 'category', value: 'electronics', operator: FilterOperator.EQUALS }
|
|
452
|
-
]
|
|
453
|
-
});
|
|
454
|
-
expect(results).toBeDefined();
|
|
455
|
-
expect(results?.length).toBe(2);
|
|
456
|
-
expect(results?.every(item => item.category === 'electronics')).toBe(true);
|
|
457
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
describe('with GSI (Global Secondary Index)', () => {
|
|
463
|
-
beforeEach(async () => {
|
|
464
|
-
// Create items with different statuses and timestamps
|
|
465
|
-
const baseTime = new Date('2024-01-01T00:00:00Z').getTime();
|
|
466
|
-
|
|
467
|
-
await gsiRepository.putItem(
|
|
468
|
-
{ userId: 'user-gsi-1', itemId: 'gsi-item-1' },
|
|
469
|
-
{
|
|
470
|
-
userId: 'user-gsi-1',
|
|
471
|
-
itemId: 'gsi-item-1',
|
|
472
|
-
name: 'Active Item 1',
|
|
473
|
-
category: 'electronics',
|
|
474
|
-
status: 'active',
|
|
475
|
-
createdAt: new Date(baseTime).toISOString()
|
|
476
|
-
}
|
|
477
|
-
);
|
|
478
|
-
await gsiRepository.putItem(
|
|
479
|
-
{ userId: 'user-gsi-2', itemId: 'gsi-item-2' },
|
|
480
|
-
{
|
|
481
|
-
userId: 'user-gsi-2',
|
|
482
|
-
itemId: 'gsi-item-2',
|
|
483
|
-
name: 'Active Item 2',
|
|
484
|
-
category: 'books',
|
|
485
|
-
status: 'active',
|
|
486
|
-
createdAt: new Date(baseTime + 3600000).toISOString()
|
|
487
|
-
}
|
|
488
|
-
);
|
|
489
|
-
await gsiRepository.putItem(
|
|
490
|
-
{ userId: 'user-gsi-3', itemId: 'gsi-item-3' },
|
|
491
|
-
{
|
|
492
|
-
userId: 'user-gsi-3',
|
|
493
|
-
itemId: 'gsi-item-3',
|
|
494
|
-
name: 'Active Item 3',
|
|
495
|
-
category: 'electronics',
|
|
496
|
-
status: 'active',
|
|
497
|
-
createdAt: new Date(baseTime + 7200000).toISOString()
|
|
498
|
-
}
|
|
499
|
-
);
|
|
500
|
-
await gsiRepository.putItem(
|
|
501
|
-
{ userId: 'user-gsi-4', itemId: 'gsi-item-4' },
|
|
502
|
-
{
|
|
503
|
-
userId: 'user-gsi-4',
|
|
504
|
-
itemId: 'gsi-item-4',
|
|
505
|
-
name: 'Pending Item',
|
|
506
|
-
category: 'books',
|
|
507
|
-
status: 'pending',
|
|
508
|
-
createdAt: new Date(baseTime + 10800000).toISOString()
|
|
509
|
-
}
|
|
510
|
-
);
|
|
511
|
-
await gsiRepository.putItem(
|
|
512
|
-
{ userId: 'user-gsi-5', itemId: 'gsi-item-5' },
|
|
513
|
-
{
|
|
514
|
-
userId: 'user-gsi-5',
|
|
515
|
-
itemId: 'gsi-item-5',
|
|
516
|
-
name: 'Inactive Item',
|
|
517
|
-
category: 'clothing',
|
|
518
|
-
status: 'inactive',
|
|
519
|
-
createdAt: new Date(baseTime + 14400000).toISOString()
|
|
520
|
-
}
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
// Wait for GSI to be consistent
|
|
524
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
525
|
-
consumedCapacityRegister.splice(0, consumedCapacityRegister.length);
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
it('should query items using GSI and fetch full items via batchGetItems', async () => {
|
|
529
|
-
const results = await gsiRepository.getItems({
|
|
530
|
-
status: 'active',
|
|
531
|
-
index: 'StatusIndex'
|
|
532
|
-
});
|
|
533
|
-
expect(results).toBeDefined();
|
|
534
|
-
// When using index, it queries GSI then uses batchGetItems to fetch full items
|
|
535
|
-
expect(Array.isArray(results)).toBe(true);
|
|
536
|
-
expect(sumConsumedCapacity()).toEqual(2);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
it('should query all items with specific status using GSI', async () => {
|
|
540
|
-
const results = await gsiRepository.getItems({
|
|
541
|
-
status: 'active',
|
|
542
|
-
index: 'StatusIndex'
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
expect(results).toBeDefined();
|
|
546
|
-
if (results && results.length > 0) {
|
|
547
|
-
// All returned items should have the queried status
|
|
548
|
-
results.forEach(item => {
|
|
549
|
-
expect(item.status).toBe('active');
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
expect(sumConsumedCapacity()).toEqual(2);
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
it('should combine GSI query with filter expressions', async () => {
|
|
556
|
-
const results = await gsiRepository.getItems({
|
|
557
|
-
status: 'active',
|
|
558
|
-
index: 'StatusIndex',
|
|
559
|
-
filterExpressions: [
|
|
560
|
-
{ attribute: 'category', value: 'electronics', operator: FilterOperator.EQUALS }
|
|
561
|
-
]
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
expect(results).toBeDefined();
|
|
565
|
-
if (results && results.length > 0) {
|
|
566
|
-
results.forEach(item => {
|
|
567
|
-
expect(item.status).toBe('active');
|
|
568
|
-
expect(item.category).toBe('electronics');
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
expect(sumConsumedCapacity()).toEqual(1.5);
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it('should return full item attributes when querying via GSI', async () => {
|
|
575
|
-
const results = await gsiRepository.getItems({
|
|
576
|
-
status: 'active',
|
|
577
|
-
index: 'StatusIndex'
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
expect(results).toBeDefined();
|
|
581
|
-
if (results && results.length > 0) {
|
|
582
|
-
const item = results.find(r => r.itemId === 'gsi-item-2');
|
|
583
|
-
if (item) {
|
|
584
|
-
// Should have all attributes from main table
|
|
585
|
-
expect(item).toHaveProperty('userId');
|
|
586
|
-
expect(item).toHaveProperty('itemId');
|
|
587
|
-
expect(item).toHaveProperty('name');
|
|
588
|
-
expect(item).toHaveProperty('category');
|
|
589
|
-
expect(item).toHaveProperty('status');
|
|
590
|
-
expect(item).toHaveProperty('createdAt');
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
expect(sumConsumedCapacity()).toEqual(2);
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
it('should handle GSI query with multiple items', async () => {
|
|
597
|
-
const results = await gsiRepository.getItems({
|
|
598
|
-
status: 'active',
|
|
599
|
-
index: 'StatusIndex'
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
expect(results).toBeDefined();
|
|
603
|
-
expect(Array.isArray(results)).toBe(true);
|
|
604
|
-
|
|
605
|
-
// Should use batchGetItems internally to fetch full items
|
|
606
|
-
if (results && results.length > 0) {
|
|
607
|
-
results.forEach(item => {
|
|
608
|
-
expect(item).toHaveProperty('userId');
|
|
609
|
-
expect(item).toHaveProperty('itemId');
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
expect(sumConsumedCapacity()).toEqual(2);
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
it('should respect projection when querying GSI', async () => {
|
|
616
|
-
const results = await gsiRepository.getItems({
|
|
617
|
-
status: 'active',
|
|
618
|
-
index: 'StatusIndex',
|
|
619
|
-
projectedAttributes: ['name', 'status']
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
expect(results).toBeDefined();
|
|
623
|
-
if (results && results.length > 0) {
|
|
624
|
-
// Note: Since we use batchGetItems after GSI query,
|
|
625
|
-
// we get full items from main table
|
|
626
|
-
results.forEach(item => {
|
|
627
|
-
expect(item).toHaveProperty('name');
|
|
628
|
-
expect(item).toHaveProperty('status');
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
expect(sumConsumedCapacity()).toEqual(2);
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
it('should handle empty results from GSI query', async () => {
|
|
635
|
-
const results = await gsiRepository.getItems({
|
|
636
|
-
status: 'non-existent-status',
|
|
637
|
-
index: 'StatusIndex'
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
expect(results).toBeDefined();
|
|
641
|
-
expect(results?.length).toBe(0);
|
|
642
|
-
expect(sumConsumedCapacity()).toEqual(0);
|
|
643
|
-
});
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
describe('pagination with GSI', () => {
|
|
647
|
-
beforeEach(async () => {
|
|
648
|
-
// Create many items to test pagination via GSI
|
|
649
|
-
const baseTime = new Date('2024-01-01T00:00:00Z').getTime();
|
|
650
|
-
|
|
651
|
-
for (let i = 1; i <= 120; i++) {
|
|
652
|
-
await gsiRepository.putItem(
|
|
653
|
-
{ userId: `user-page-${i}`, itemId: `page-item-${i.toString().padStart(3, '0')}` },
|
|
654
|
-
{
|
|
655
|
-
userId: `user-page-${i}`,
|
|
656
|
-
itemId: `page-item-${i.toString().padStart(3, '0')}`,
|
|
657
|
-
name: `Paginated Item ${i}`,
|
|
658
|
-
category: i % 2 === 0 ? 'even' : 'odd',
|
|
659
|
-
status: 'paginated',
|
|
660
|
-
createdAt: new Date(baseTime + i * 60000).toISOString()
|
|
661
|
-
}
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
666
|
-
consumedCapacityRegister.splice(0, consumedCapacityRegister.length);
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it('should retrieve all items across multiple pages via GSI', async () => {
|
|
670
|
-
const results = await gsiRepository.getItems({
|
|
671
|
-
status: 'paginated',
|
|
672
|
-
index: 'StatusIndex'
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
expect(results).toBeDefined();
|
|
676
|
-
expect(Array.isArray(results)).toBe(true);
|
|
677
|
-
expect(sumConsumedCapacity()).toEqual(62);
|
|
678
|
-
// Should handle pagination internally via batchGetItems
|
|
679
|
-
}, 60000);
|
|
680
|
-
|
|
681
|
-
});
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
describe('multiple operations', () => {
|
|
685
|
-
it('should handle full lifecycle', async () => {
|
|
686
|
-
const key = { id: 'full-lifecycle-2' };
|
|
687
|
-
const record = { id: 'full-lifecycle-2', name: 'Initial', email: 'initial@example.com' };
|
|
688
|
-
|
|
689
|
-
await repository.putItem(key, record);
|
|
690
|
-
await repository.updateItem(key, { name: 'Modified' });
|
|
691
|
-
|
|
692
|
-
const afterUpdate = await repository.getItem(key);
|
|
693
|
-
expect(afterUpdate?.name).toBe('Modified');
|
|
694
|
-
|
|
695
|
-
await repository.deleteItem(key);
|
|
696
|
-
const afterDelete = await repository.getItem(key);
|
|
697
|
-
expect(afterDelete).toBeUndefined();
|
|
698
|
-
});
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
describe('FilterOperator Tests', () => {
|
|
702
|
-
beforeEach(async () => {
|
|
703
|
-
// Create test data with various values for filtering
|
|
704
|
-
const testData = [
|
|
705
|
-
{ id: 'filter-1', name: 'Alice', age: 25, score: 85.5, status: 'active', tags: ['new', 'premium'] },
|
|
706
|
-
{ id: 'filter-2', name: 'Bob', age: 30, score: 90.0, status: 'active', tags: ['premium'] },
|
|
707
|
-
{ id: 'filter-3', name: 'Charlie', age: 35, score: 75.0, status: 'inactive', tags: ['new'] },
|
|
708
|
-
{ id: 'filter-4', name: 'Diana', age: 28, score: 95.5, status: 'active', tags: ['vip', 'premium'] },
|
|
709
|
-
{ id: 'filter-5', name: 'Eve', age: 22, score: 80.0, status: 'pending', tags: ['new'] },
|
|
710
|
-
{ id: 'filter-6', name: 'Frank', age: 40, score: 70.0, status: 'inactive', tags: [] },
|
|
711
|
-
{ id: 'filter-7', name: 'Grace', age: 33, score: 88.0, status: 'active', tags: ['premium', 'vip'] },
|
|
712
|
-
];
|
|
713
|
-
|
|
714
|
-
for (const data of testData) {
|
|
715
|
-
await repository.putItem({ id: data.id }, data);
|
|
716
|
-
}
|
|
717
|
-
consumedCapacityRegister.splice(0, consumedCapacityRegister.length);
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
describe('EQUALS operator', () => {
|
|
721
|
-
it('should filter items with exact string match', async () => {
|
|
722
|
-
const results = await repository.getItems({
|
|
723
|
-
id: 'filter-1',
|
|
724
|
-
filterExpressions: [
|
|
725
|
-
{ attribute: 'status', value: 'active', operator: FilterOperator.EQUALS }
|
|
726
|
-
]
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
expect(results).toBeDefined();
|
|
730
|
-
expect(results?.every(item => item.status === 'active')).toBe(true);
|
|
731
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
it('should filter items with exact number match', async () => {
|
|
735
|
-
const results = await repository.getItems({
|
|
736
|
-
id: 'filter-2',
|
|
737
|
-
filterExpressions: [
|
|
738
|
-
{ attribute: 'age', value: 30, operator: FilterOperator.EQUALS }
|
|
739
|
-
]
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
expect(results).toBeDefined();
|
|
743
|
-
if (results && results.length > 0) {
|
|
744
|
-
expect(results[0].age).toBe(30);
|
|
745
|
-
}
|
|
746
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
it('should return empty array when no match found', async () => {
|
|
750
|
-
const results = await repository.getItems({
|
|
751
|
-
id: 'filter-1',
|
|
752
|
-
filterExpressions: [
|
|
753
|
-
{ attribute: 'status', value: 'non-existent', operator: FilterOperator.EQUALS }
|
|
754
|
-
]
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
expect(results).toBeDefined();
|
|
758
|
-
expect(results?.length).toBe(0);
|
|
759
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
760
|
-
});
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
describe('NOT_EQUALS operator', () => {
|
|
764
|
-
it('should filter items not matching string value', async () => {
|
|
765
|
-
const results = await repository.getItems({
|
|
766
|
-
id: 'filter-1',
|
|
767
|
-
filterExpressions: [
|
|
768
|
-
{ attribute: 'status', value: 'inactive', operator: FilterOperator.NOT_EQUALS }
|
|
769
|
-
]
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
expect(results).toBeDefined();
|
|
773
|
-
if (results && results.length > 0) {
|
|
774
|
-
expect(results[0].status).not.toBe('inactive');
|
|
775
|
-
}
|
|
776
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
it('should filter items not matching number value', async () => {
|
|
780
|
-
const results = await repository.getItems({
|
|
781
|
-
id: 'filter-3',
|
|
782
|
-
filterExpressions: [
|
|
783
|
-
{ attribute: 'age', value: 25, operator: FilterOperator.NOT_EQUALS }
|
|
784
|
-
]
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
expect(results).toBeDefined();
|
|
788
|
-
if (results && results.length > 0) {
|
|
789
|
-
expect(results[0].age).not.toBe(25);
|
|
790
|
-
}
|
|
791
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
792
|
-
});
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
describe('GREATER_THAN operator', () => {
|
|
796
|
-
it('should filter items greater than number value', async () => {
|
|
797
|
-
const results = await repository.getItems({
|
|
798
|
-
id: 'filter-3',
|
|
799
|
-
filterExpressions: [
|
|
800
|
-
{ attribute: 'age', value: 30, operator: FilterOperator.GREATER_THAN }
|
|
801
|
-
]
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
expect(results).toBeDefined();
|
|
805
|
-
if (results && results.length > 0) {
|
|
806
|
-
expect(results.every(item => item.age && item.age > 30)).toBe(true);
|
|
807
|
-
}
|
|
808
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
it('should filter items greater than decimal value', async () => {
|
|
812
|
-
const results = await repository.getItems({
|
|
813
|
-
id: 'filter-2',
|
|
814
|
-
filterExpressions: [
|
|
815
|
-
{ attribute: 'score', value: 89.0, operator: FilterOperator.GREATER_THAN }
|
|
816
|
-
]
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
expect(results).toBeDefined();
|
|
820
|
-
if (results && results.length > 0) {
|
|
821
|
-
expect(results.every(item => item.score && item.score > 89.0)).toBe(true);
|
|
822
|
-
}
|
|
823
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
it('should return empty array when no items are greater', async () => {
|
|
827
|
-
const results = await repository.getItems({
|
|
828
|
-
id: 'filter-5',
|
|
829
|
-
filterExpressions: [
|
|
830
|
-
{ attribute: 'age', value: 100, operator: FilterOperator.GREATER_THAN }
|
|
831
|
-
]
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
expect(results).toBeDefined();
|
|
835
|
-
expect(results?.length).toBe(0);
|
|
836
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
837
|
-
});
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
describe('GREATER_THAN_OR_EQUALS operator', () => {
|
|
841
|
-
it('should filter items greater than or equal to value', async () => {
|
|
842
|
-
const results = await repository.getItems({
|
|
843
|
-
id: 'filter-2',
|
|
844
|
-
filterExpressions: [
|
|
845
|
-
{ attribute: 'age', value: 30, operator: FilterOperator.GREATER_THAN_OR_EQUALS }
|
|
846
|
-
]
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
expect(results).toBeDefined();
|
|
850
|
-
if (results && results.length > 0) {
|
|
851
|
-
expect(results.every(item => item.age && item.age >= 30)).toBe(true);
|
|
852
|
-
}
|
|
853
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
854
|
-
});
|
|
855
|
-
|
|
856
|
-
it('should include items with exact value', async () => {
|
|
857
|
-
const results = await repository.getItems({
|
|
858
|
-
id: 'filter-4',
|
|
859
|
-
filterExpressions: [
|
|
860
|
-
{ attribute: 'score', value: 95.5, operator: FilterOperator.GREATER_THAN_OR_EQUALS }
|
|
861
|
-
]
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
expect(results).toBeDefined();
|
|
865
|
-
if (results && results.length > 0) {
|
|
866
|
-
const exactMatch = results.find(item => item.score === 95.5);
|
|
867
|
-
expect(exactMatch).toBeDefined();
|
|
868
|
-
}
|
|
869
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
870
|
-
});
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
describe('LESS_THAN operator', () => {
|
|
874
|
-
it('should filter items less than number value', async () => {
|
|
875
|
-
const results = await repository.getItems({
|
|
876
|
-
id: 'filter-5',
|
|
877
|
-
filterExpressions: [
|
|
878
|
-
{ attribute: 'age', value: 30, operator: FilterOperator.LESS_THAN }
|
|
879
|
-
]
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
expect(results).toBeDefined();
|
|
883
|
-
if (results && results.length > 0) {
|
|
884
|
-
expect(results.every(item => item.age && item.age < 30)).toBe(true);
|
|
885
|
-
}
|
|
886
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
it('should filter items less than decimal value', async () => {
|
|
890
|
-
const results = await repository.getItems({
|
|
891
|
-
id: 'filter-6',
|
|
892
|
-
filterExpressions: [
|
|
893
|
-
{ attribute: 'score', value: 75.0, operator: FilterOperator.LESS_THAN }
|
|
894
|
-
]
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
expect(results).toBeDefined();
|
|
898
|
-
if (results && results.length > 0) {
|
|
899
|
-
expect(results.every(item => item.score && item.score < 75.0)).toBe(true);
|
|
900
|
-
}
|
|
901
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
902
|
-
});
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
describe('LESS_THAN_OR_EQUALS operator', () => {
|
|
906
|
-
it('should filter items less than or equal to value', async () => {
|
|
907
|
-
const results = await repository.getItems({
|
|
908
|
-
id: 'filter-1',
|
|
909
|
-
filterExpressions: [
|
|
910
|
-
{ attribute: 'age', value: 25, operator: FilterOperator.LESS_THAN_OR_EQUALS }
|
|
911
|
-
]
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
expect(results).toBeDefined();
|
|
915
|
-
if (results && results.length > 0) {
|
|
916
|
-
expect(results.every(item => item.age && item.age <= 25)).toBe(true);
|
|
917
|
-
}
|
|
918
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
it('should include items with exact value', async () => {
|
|
922
|
-
const results = await repository.getItems({
|
|
923
|
-
id: 'filter-2',
|
|
924
|
-
filterExpressions: [
|
|
925
|
-
{ attribute: 'score', value: 90.0, operator: FilterOperator.LESS_THAN_OR_EQUALS }
|
|
926
|
-
]
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
expect(results).toBeDefined();
|
|
930
|
-
if (results && results.length > 0) {
|
|
931
|
-
const exactMatch = results.find(item => item.score === 90.0);
|
|
932
|
-
expect(exactMatch).toBeDefined();
|
|
933
|
-
}
|
|
934
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
935
|
-
});
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
describe('IN operator', () => {
|
|
939
|
-
it('should filter items with value in array of strings', async () => {
|
|
940
|
-
const results = await repository.getItems({
|
|
941
|
-
id: 'filter-1',
|
|
942
|
-
filterExpressions: [
|
|
943
|
-
{ attribute: 'status', value: ['active', 'pending'], operator: FilterOperator.IN }
|
|
944
|
-
]
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
expect(results).toBeDefined();
|
|
948
|
-
if (results && results.length > 0) {
|
|
949
|
-
expect(results.every(item => ['active', 'pending'].includes(item.status || ''))).toBe(true);
|
|
950
|
-
}
|
|
951
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
it('should filter items with value in array of numbers', async () => {
|
|
955
|
-
const results = await repository.getItems({
|
|
956
|
-
id: 'filter-2',
|
|
957
|
-
filterExpressions: [
|
|
958
|
-
{ attribute: 'age', value: [25, 30, 35], operator: FilterOperator.IN }
|
|
959
|
-
]
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
expect(results).toBeDefined();
|
|
963
|
-
if (results && results.length > 0) {
|
|
964
|
-
expect(results.every(item => [25, 30, 35].includes(item.age || 0))).toBe(true);
|
|
965
|
-
}
|
|
966
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
it('should return empty array when value not in list', async () => {
|
|
970
|
-
const results = await repository.getItems({
|
|
971
|
-
id: 'filter-1',
|
|
972
|
-
filterExpressions: [
|
|
973
|
-
{ attribute: 'status', value: ['deleted', 'archived'], operator: FilterOperator.IN }
|
|
974
|
-
]
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
expect(results).toBeDefined();
|
|
978
|
-
expect(results?.length).toBe(0);
|
|
979
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
it('should handle single value in array', async () => {
|
|
983
|
-
const results = await repository.getItems({
|
|
984
|
-
id: 'filter-4',
|
|
985
|
-
filterExpressions: [
|
|
986
|
-
{ attribute: 'status', value: ['active'], operator: FilterOperator.IN }
|
|
987
|
-
]
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
expect(results).toBeDefined();
|
|
991
|
-
if (results && results.length > 0) {
|
|
992
|
-
expect(results[0].status).toBe('active');
|
|
993
|
-
}
|
|
994
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
995
|
-
});
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
describe('BETWEEN operator', () => {
|
|
999
|
-
it('should filter items with number value between range', async () => {
|
|
1000
|
-
const results = await repository.getItems({
|
|
1001
|
-
id: 'filter-4',
|
|
1002
|
-
filterExpressions: [
|
|
1003
|
-
{ attribute: 'age', value: [25, 35], operator: FilterOperator.BETWEEN }
|
|
1004
|
-
]
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
expect(results).toBeDefined();
|
|
1008
|
-
if (results && results.length > 0) {
|
|
1009
|
-
expect(results.every(item => item.age && item.age >= 25 && item.age <= 35)).toBe(true);
|
|
1010
|
-
}
|
|
1011
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
it('should filter items with decimal value between range', async () => {
|
|
1015
|
-
const results = await repository.getItems({
|
|
1016
|
-
id: 'filter-1',
|
|
1017
|
-
filterExpressions: [
|
|
1018
|
-
{ attribute: 'score', value: [80.0, 90.0], operator: FilterOperator.BETWEEN }
|
|
1019
|
-
]
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
expect(results).toBeDefined();
|
|
1023
|
-
if (results && results.length > 0) {
|
|
1024
|
-
expect(results.every(item => item.score && item.score >= 80.0 && item.score <= 90.0)).toBe(true);
|
|
1025
|
-
}
|
|
1026
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
|
-
it('should include boundary values', async () => {
|
|
1030
|
-
const results = await repository.getItems({
|
|
1031
|
-
id: 'filter-2',
|
|
1032
|
-
filterExpressions: [
|
|
1033
|
-
{ attribute: 'age', value: [30, 40], operator: FilterOperator.BETWEEN }
|
|
1034
|
-
]
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
expect(results).toBeDefined();
|
|
1038
|
-
if (results && results.length > 0) {
|
|
1039
|
-
const hasLowerBound = results.some(item => item.age === 30);
|
|
1040
|
-
const hasUpperBound = results.some(item => item.age === 40);
|
|
1041
|
-
expect(hasLowerBound || hasUpperBound).toBe(true);
|
|
1042
|
-
}
|
|
1043
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
it('should return empty array when no values in range', async () => {
|
|
1047
|
-
const results = await repository.getItems({
|
|
1048
|
-
id: 'filter-1',
|
|
1049
|
-
filterExpressions: [
|
|
1050
|
-
{ attribute: 'age', value: [50, 60], operator: FilterOperator.BETWEEN }
|
|
1051
|
-
]
|
|
1052
|
-
});
|
|
1053
|
-
|
|
1054
|
-
expect(results).toBeDefined();
|
|
1055
|
-
expect(results?.length).toBe(0);
|
|
1056
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
it('should filter items with string value between range (lexicographical)', async () => {
|
|
1060
|
-
const results = await repository.getItems({
|
|
1061
|
-
id: 'filter-1',
|
|
1062
|
-
filterExpressions: [
|
|
1063
|
-
{ attribute: 'name', value: ['Alice', 'Diana'], operator: FilterOperator.BETWEEN }
|
|
1064
|
-
]
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
expect(results).toBeDefined();
|
|
1068
|
-
if (results && results.length > 0) {
|
|
1069
|
-
expect(results.every(item =>
|
|
1070
|
-
item.name && item.name >= 'Alice' && item.name <= 'Diana'
|
|
1071
|
-
)).toBe(true);
|
|
1072
|
-
}
|
|
1073
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
1074
|
-
});
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
describe('Multiple filter expressions', () => {
|
|
1078
|
-
it('should apply multiple filter conditions (AND logic)', async () => {
|
|
1079
|
-
const results = await repository.getItems({
|
|
1080
|
-
id: 'filter-1',
|
|
1081
|
-
filterExpressions: [
|
|
1082
|
-
{ attribute: 'status', value: 'active', operator: FilterOperator.EQUALS },
|
|
1083
|
-
{ attribute: 'age', value: 25, operator: FilterOperator.GREATER_THAN_OR_EQUALS }
|
|
1084
|
-
]
|
|
1085
|
-
});
|
|
1086
|
-
|
|
1087
|
-
expect(results).toBeDefined();
|
|
1088
|
-
if (results && results.length > 0) {
|
|
1089
|
-
expect(results.every(item =>
|
|
1090
|
-
item.status === 'active' && item.age && item.age >= 25
|
|
1091
|
-
)).toBe(true);
|
|
1092
|
-
}
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
it('should apply complex filtering with mixed operators', async () => {
|
|
1096
|
-
const results = await repository.getItems({
|
|
1097
|
-
id: 'filter-2',
|
|
1098
|
-
filterExpressions: [
|
|
1099
|
-
{ attribute: 'status', value: ['active', 'pending'], operator: FilterOperator.IN },
|
|
1100
|
-
{ attribute: 'age', value: [20, 35], operator: FilterOperator.BETWEEN },
|
|
1101
|
-
{ attribute: 'score', value: 80.0, operator: FilterOperator.GREATER_THAN_OR_EQUALS }
|
|
1102
|
-
]
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
expect(results).toBeDefined();
|
|
1106
|
-
if (results && results.length > 0) {
|
|
1107
|
-
expect(results.every(item =>
|
|
1108
|
-
['active', 'pending'].includes(item.status || '') &&
|
|
1109
|
-
item.age && item.age >= 20 && item.age <= 35 &&
|
|
1110
|
-
item.score && item.score >= 80.0
|
|
1111
|
-
)).toBe(true);
|
|
1112
|
-
}
|
|
1113
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
1114
|
-
});
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
describe('Negated filter expressions', () => {
|
|
1118
|
-
it('should negate EQUALS operator', async () => {
|
|
1119
|
-
const results = await repository.getItems({
|
|
1120
|
-
id: 'filter-1',
|
|
1121
|
-
filterExpressions: [
|
|
1122
|
-
{ attribute: 'status', value: 'inactive', operator: FilterOperator.EQUALS, negate: true }
|
|
1123
|
-
]
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
expect(results).toBeDefined();
|
|
1127
|
-
if (results && results.length > 0) {
|
|
1128
|
-
expect(results.every(item => item.status !== 'inactive')).toBe(true);
|
|
1129
|
-
}
|
|
1130
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
1131
|
-
});
|
|
1132
|
-
|
|
1133
|
-
it('should negate IN operator', async () => {
|
|
1134
|
-
const results = await repository.getItems({
|
|
1135
|
-
id: 'filter-6',
|
|
1136
|
-
filterExpressions: [
|
|
1137
|
-
{ attribute: 'status', value: ['active', 'pending'], operator: FilterOperator.IN, negate: true }
|
|
1138
|
-
]
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1141
|
-
expect(results).toBeDefined();
|
|
1142
|
-
if (results && results.length > 0) {
|
|
1143
|
-
expect(results.every(item =>
|
|
1144
|
-
!['active', 'pending'].includes(item.status || '')
|
|
1145
|
-
)).toBe(true);
|
|
1146
|
-
}
|
|
1147
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
it('should negate BETWEEN operator', async () => {
|
|
1151
|
-
const results = await repository.getItems({
|
|
1152
|
-
id: 'filter-6',
|
|
1153
|
-
filterExpressions: [
|
|
1154
|
-
{ attribute: 'age', value: [25, 35], operator: FilterOperator.BETWEEN, negate: true }
|
|
1155
|
-
]
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
expect(results).toBeDefined();
|
|
1159
|
-
if (results && results.length > 0) {
|
|
1160
|
-
expect(results.every(item =>
|
|
1161
|
-
item.age && (item.age < 25 || item.age > 35)
|
|
1162
|
-
)).toBe(true);
|
|
1163
|
-
}
|
|
1164
|
-
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
1165
|
-
});
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
describe('Edge cases', () => {
|
|
1169
|
-
it('should handle filtering on missing attributes', async () => {
|
|
1170
|
-
await repository.putItem(
|
|
1171
|
-
{ id: 'filter-missing' },
|
|
1172
|
-
{ id: 'filter-missing', name: 'No Age' }
|
|
1173
|
-
);
|
|
1174
|
-
|
|
1175
|
-
const results = await repository.getItems({
|
|
1176
|
-
id: 'filter-missing',
|
|
1177
|
-
filterExpressions: [
|
|
1178
|
-
{ attribute: 'age', value: 25, operator: FilterOperator.EQUALS }
|
|
1179
|
-
]
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
expect(results).toBeDefined();
|
|
1183
|
-
expect(results?.length).toBe(0);
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
it('should handle empty filter expressions array', async () => {
|
|
1187
|
-
const results = await repository.getItems({
|
|
1188
|
-
id: 'filter-1',
|
|
1189
|
-
filterExpressions: []
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
expect(results).toBeDefined();
|
|
1193
|
-
expect(results?.length).toBeGreaterThan(0);
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
it('should handle zero values in numeric comparisons', async () => {
|
|
1197
|
-
await repository.putItem(
|
|
1198
|
-
{ id: 'filter-zero' },
|
|
1199
|
-
{ id: 'filter-zero', name: 'Zero Score', score: 0 }
|
|
1200
|
-
);
|
|
1201
|
-
|
|
1202
|
-
const results = await repository.getItems({
|
|
1203
|
-
id: 'filter-zero',
|
|
1204
|
-
filterExpressions: [
|
|
1205
|
-
{ attribute: 'score', value: 0, operator: FilterOperator.EQUALS }
|
|
1206
|
-
]
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
expect(results).toBeDefined();
|
|
1210
|
-
if (results && results.length > 0) {
|
|
1211
|
-
expect(results[0].score).toBe(0);
|
|
1212
|
-
}
|
|
1213
|
-
});
|
|
1214
|
-
});
|
|
1215
|
-
});
|
|
1216
|
-
});
|