@squiz/db-lib 1.77.0 → 1.77.2
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/CHANGELOG.md +12 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +7 -0
- package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.js +29 -5
- package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +162 -1
- package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -1
- package/lib/externalized/ExternalizedDynamoDbRepository.d.ts +15 -0
- package/lib/externalized/ExternalizedDynamoDbRepository.d.ts.map +1 -1
- package/lib/externalized/ExternalizedDynamoDbRepository.js +93 -12
- package/lib/externalized/ExternalizedDynamoDbRepository.js.map +1 -1
- package/lib/externalized/ExternalizedDynamoDbRepository.spec.js +372 -133
- package/lib/externalized/ExternalizedDynamoDbRepository.spec.js.map +1 -1
- package/package.json +1 -1
- package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +179 -1
- package/src/dynamodb/AbstractDynamoDbRepository.ts +39 -5
- package/src/externalized/ExternalizedDynamoDbRepository.spec.ts +453 -151
- package/src/externalized/ExternalizedDynamoDbRepository.ts +104 -12
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -43,11 +43,34 @@ const testEntityDefinition: EntityDefinition = {
|
|
|
43
43
|
// Concrete test repository
|
|
44
44
|
class TestRepo extends ExternalizedDynamoDbRepository<TestItemShape, TestItem> {
|
|
45
45
|
public mockSuperUpdateItem?: jest.Mock;
|
|
46
|
+
public mockSuperGetItem?: jest.Mock;
|
|
47
|
+
public mockSuperCreateItem?: jest.Mock;
|
|
46
48
|
|
|
47
49
|
constructor(dbManager: DynamoDbManager<any>, storage: S3ExternalStorage, overrides: Partial<EntityDefinition> = {}) {
|
|
48
50
|
super('test-table', dbManager, 'test_item', { ...testEntityDefinition, ...overrides }, TestItem, storage);
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
// Expose protected methods for testing
|
|
54
|
+
public async testPrepareValueForStorage(value: TestItem): Promise<TestItem> {
|
|
55
|
+
return await (this as any).prepareValueForStorage(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public async testHydrateFromExternalStorage(record?: TestItem): Promise<TestItem | undefined> {
|
|
59
|
+
return await (this as any).hydrateFromExternalStorage(record);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public testIsDynamoItemSizeLimitError(error: unknown): boolean {
|
|
63
|
+
return (this as any).isDynamoItemSizeLimitError(error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public testIsContentInS3(item: TestItem): boolean {
|
|
67
|
+
return (this as any).isContentInS3(item);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public testGetKeyFieldsValues(obj: Record<string, unknown>): Record<string, unknown> {
|
|
71
|
+
return (this as any).getKeyFieldsValues(obj);
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
// Override updateItem to allow mocking super.updateItem
|
|
52
75
|
public async updateItem(value: Partial<TestItemShape>): Promise<TestItem | undefined> {
|
|
53
76
|
if (this.mockSuperUpdateItem) {
|
|
@@ -74,6 +97,7 @@ const createDbManager = (clientOverrides: Record<string, jest.Mock> = {}) =>
|
|
|
74
97
|
put: jest.fn(),
|
|
75
98
|
delete: jest.fn(),
|
|
76
99
|
update: jest.fn(),
|
|
100
|
+
query: jest.fn(),
|
|
77
101
|
...clientOverrides,
|
|
78
102
|
},
|
|
79
103
|
repositories: {} as any,
|
|
@@ -99,176 +123,454 @@ const createTestItem = (overrides: Partial<TestItem> = {}): TestItem => {
|
|
|
99
123
|
};
|
|
100
124
|
|
|
101
125
|
describe('ExternalizedDynamoDbRepository', () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
describe('prepareValueForStorage', () => {
|
|
127
|
+
it('externalizes data to S3 when prepareValueForStorage is called', async () => {
|
|
128
|
+
const storage = {
|
|
129
|
+
...createStorage(),
|
|
130
|
+
save: jest.fn().mockResolvedValue({ location: { type: 's3', key: 'item-key' }, size: 500 }),
|
|
131
|
+
} as unknown as S3ExternalStorage;
|
|
132
|
+
const repo = new TestRepo(createDbManager(), storage);
|
|
133
|
+
const item = createTestItem();
|
|
134
|
+
|
|
135
|
+
const storedValue = await repo.testPrepareValueForStorage(item);
|
|
136
|
+
|
|
137
|
+
expect(storage.save).toHaveBeenCalled();
|
|
138
|
+
// storedValue is a minimal payload - original item is NOT mutated
|
|
139
|
+
expect(storedValue.data).toEqual([]);
|
|
140
|
+
expect(storedValue.storageLocation).toEqual({ type: 's3', key: 'item-key' });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('removes existing storageLocation before saving to S3', async () => {
|
|
144
|
+
const storage = {
|
|
145
|
+
...createStorage(),
|
|
146
|
+
save: jest.fn().mockResolvedValue({ location: { type: 's3', key: 'new-key' }, size: 500 }),
|
|
147
|
+
} as unknown as S3ExternalStorage;
|
|
148
|
+
const repo = new TestRepo(createDbManager(), storage);
|
|
149
|
+
const item = createTestItem();
|
|
150
|
+
(item as any).storageLocation = { type: 's3', key: 'old-key' };
|
|
151
|
+
|
|
152
|
+
await repo.testPrepareValueForStorage(item);
|
|
153
|
+
|
|
154
|
+
// Verify save was called without storageLocation in the payload
|
|
155
|
+
const savedPayload = (storage.save as jest.Mock).mock.calls[0][2];
|
|
156
|
+
expect(savedPayload.storageLocation).toBeUndefined();
|
|
157
|
+
});
|
|
116
158
|
});
|
|
117
159
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
describe('createItem', () => {
|
|
161
|
+
it('creates item with storageLocation preserved', async () => {
|
|
162
|
+
const client = createDbManager().client as any;
|
|
163
|
+
(client.put as jest.Mock).mockResolvedValue({});
|
|
164
|
+
const storage = {
|
|
165
|
+
...createStorage(),
|
|
166
|
+
save: jest.fn().mockResolvedValue({ location: { type: 's3', key: 'item-key' }, size: 500 }),
|
|
167
|
+
load: jest.fn().mockResolvedValue(createTestItem()),
|
|
168
|
+
} as unknown as S3ExternalStorage;
|
|
169
|
+
|
|
170
|
+
const repo = new TestRepo({ client } as any, storage);
|
|
171
|
+
const item = createTestItem();
|
|
172
|
+
|
|
173
|
+
// Use prepareValueForStorage to externalize data and set storageLocation
|
|
174
|
+
const preparedItem = await repo.testPrepareValueForStorage(item);
|
|
125
175
|
|
|
126
|
-
|
|
127
|
-
|
|
176
|
+
// Verify the prepared value has minimal payload (empty data)
|
|
177
|
+
expect(preparedItem.data).toEqual([]);
|
|
178
|
+
expect(preparedItem.storageLocation).toEqual({ type: 's3', key: 'item-key' });
|
|
128
179
|
|
|
129
|
-
|
|
130
|
-
|
|
180
|
+
// Original item should NOT be mutated
|
|
181
|
+
expect(item.storageLocation).toBeUndefined();
|
|
182
|
+
});
|
|
131
183
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
184
|
+
it('automatically externalizes to S3 when createItem exceeds DynamoDB size limit', async () => {
|
|
185
|
+
const client = createDbManager().client as any;
|
|
186
|
+
let callCount = 0;
|
|
187
|
+
(client.put as jest.Mock).mockImplementation(() => {
|
|
188
|
+
callCount++;
|
|
189
|
+
if (callCount === 1) {
|
|
190
|
+
// First call fails with size limit error
|
|
191
|
+
const error = new Error('Item size has exceeded the maximum allowed size');
|
|
192
|
+
(error as any).code = 'ValidationException';
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
// Second call succeeds
|
|
196
|
+
return Promise.resolve({});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const storage = {
|
|
200
|
+
...createStorage(),
|
|
201
|
+
save: jest.fn().mockResolvedValue({ location: { type: 's3', key: 'auto-key' }, size: 500 }),
|
|
202
|
+
load: jest.fn().mockResolvedValue(createTestItem()),
|
|
203
|
+
} as unknown as S3ExternalStorage;
|
|
204
|
+
|
|
205
|
+
const repo = new TestRepo({ client } as any, storage);
|
|
206
|
+
const item = createTestItem({ data: [{ key: 'large', value: { content: 'x'.repeat(500000) } }] });
|
|
207
|
+
|
|
208
|
+
await repo.createItem(item);
|
|
209
|
+
|
|
210
|
+
// Should have called save to externalize
|
|
211
|
+
expect(storage.save).toHaveBeenCalled();
|
|
212
|
+
// Should have retried the put
|
|
213
|
+
expect(client.put).toHaveBeenCalledTimes(2);
|
|
214
|
+
});
|
|
135
215
|
|
|
136
|
-
|
|
137
|
-
|
|
216
|
+
it('cleans up old S3 file when overrideExisting is true', async () => {
|
|
217
|
+
const oldLocation = { type: 's3' as const, key: 'old-key' };
|
|
218
|
+
const newLocation = { type: 's3' as const, key: 'new-key' };
|
|
219
|
+
|
|
220
|
+
const client = createDbManager({
|
|
221
|
+
get: jest.fn().mockResolvedValue({
|
|
222
|
+
Item: {
|
|
223
|
+
id: 'item-1',
|
|
224
|
+
name: 'Old',
|
|
225
|
+
storageLocation: oldLocation,
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
put: jest.fn().mockResolvedValue({}),
|
|
229
|
+
}).client as any;
|
|
230
|
+
|
|
231
|
+
const storage = {
|
|
232
|
+
...createStorage(),
|
|
233
|
+
save: jest.fn().mockResolvedValue({ location: newLocation, size: 500 }),
|
|
234
|
+
load: jest.fn().mockResolvedValue(createTestItem()),
|
|
235
|
+
} as unknown as S3ExternalStorage;
|
|
236
|
+
|
|
237
|
+
const repo = new TestRepo({ client } as any, storage);
|
|
238
|
+
const item = createTestItem();
|
|
239
|
+
const preparedItem = await repo.testPrepareValueForStorage(item);
|
|
240
|
+
|
|
241
|
+
await repo.createItem(preparedItem, {}, {}, { overrideExisting: true });
|
|
242
|
+
|
|
243
|
+
expect(storage.delete).toHaveBeenCalledWith(oldLocation);
|
|
244
|
+
});
|
|
138
245
|
});
|
|
139
246
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
247
|
+
describe('updateItem', () => {
|
|
248
|
+
it('cleans up old S3 file when updating with new storageLocation', async () => {
|
|
249
|
+
const oldLocation = { type: 's3' as const, key: 'old-key' };
|
|
250
|
+
const newLocation = { type: 's3' as const, key: 'new-key' };
|
|
251
|
+
|
|
252
|
+
const client = createDbManager({
|
|
253
|
+
get: jest.fn().mockResolvedValue({
|
|
254
|
+
Item: {
|
|
255
|
+
id: 'item-1',
|
|
256
|
+
name: 'Test',
|
|
257
|
+
storageLocation: oldLocation,
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
update: jest.fn().mockResolvedValue({
|
|
261
|
+
Attributes: {
|
|
262
|
+
id: 'item-1',
|
|
263
|
+
name: 'Updated',
|
|
264
|
+
storageLocation: newLocation,
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
}).client as any;
|
|
268
|
+
|
|
269
|
+
const storage = createStorage();
|
|
270
|
+
const repo = new TestRepo({ client } as any, storage);
|
|
271
|
+
|
|
272
|
+
const updateValue = {
|
|
273
|
+
id: 'item-1',
|
|
274
|
+
name: 'Updated',
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const updatedItem = createTestItem({ id: 'item-1', name: 'Updated' });
|
|
278
|
+
(updatedItem as any).storageLocation = newLocation;
|
|
279
|
+
|
|
280
|
+
repo.mockSuperUpdateItem = jest.fn().mockResolvedValue(updatedItem as any);
|
|
281
|
+
|
|
282
|
+
await repo.updateItem(updateValue);
|
|
283
|
+
|
|
284
|
+
expect(storage.delete).toHaveBeenCalledWith(oldLocation);
|
|
155
285
|
});
|
|
156
286
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
287
|
+
it('does not delete S3 file when storageLocation is unchanged', async () => {
|
|
288
|
+
const location = { type: 's3' as const, key: 'same-key' };
|
|
289
|
+
|
|
290
|
+
const client = createDbManager({
|
|
291
|
+
get: jest.fn().mockResolvedValue({
|
|
292
|
+
Item: {
|
|
293
|
+
id: 'item-1',
|
|
294
|
+
name: 'Test',
|
|
295
|
+
storageLocation: location,
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
update: jest.fn().mockResolvedValue({
|
|
299
|
+
Attributes: {
|
|
300
|
+
id: 'item-1',
|
|
301
|
+
name: 'Updated',
|
|
302
|
+
storageLocation: location,
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
}).client as any;
|
|
306
|
+
|
|
307
|
+
const storage = createStorage();
|
|
308
|
+
const repo = new TestRepo({ client } as any, storage);
|
|
309
|
+
|
|
310
|
+
const updateValue = {
|
|
311
|
+
id: 'item-1',
|
|
312
|
+
name: 'Updated',
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const updatedItem = createTestItem({ id: 'item-1', name: 'Updated' });
|
|
316
|
+
(updatedItem as any).storageLocation = location;
|
|
317
|
+
|
|
318
|
+
repo.mockSuperUpdateItem = jest.fn().mockResolvedValue(updatedItem as any);
|
|
319
|
+
|
|
320
|
+
await repo.updateItem(updateValue);
|
|
321
|
+
|
|
322
|
+
expect(storage.delete).not.toHaveBeenCalled();
|
|
323
|
+
});
|
|
160
324
|
});
|
|
161
325
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
326
|
+
describe('getItem', () => {
|
|
327
|
+
it('loads payload from external storage when storageLocation is set', async () => {
|
|
328
|
+
const storage = createStorage();
|
|
329
|
+
const fullData = {
|
|
330
|
+
id: 'item-1',
|
|
331
|
+
name: 'From S3',
|
|
332
|
+
data: [{ key: 'sample', value: { main: [] } }],
|
|
333
|
+
};
|
|
334
|
+
(storage.load as jest.Mock).mockResolvedValue(fullData);
|
|
335
|
+
|
|
336
|
+
const repo = new TestRepo(createDbManager({ get: jest.fn() as any }), storage);
|
|
337
|
+
const result = await repo.testHydrateFromExternalStorage({
|
|
338
|
+
id: 'item-1',
|
|
339
|
+
name: 'Placeholder',
|
|
340
|
+
data: [],
|
|
341
|
+
storageLocation: { type: 's3', key: 'item-key' },
|
|
342
|
+
} as TestItem);
|
|
343
|
+
|
|
344
|
+
expect(storage.load).toHaveBeenCalledWith({ type: 's3', key: 'item-key' });
|
|
345
|
+
expect(result?.name).toBe('From S3');
|
|
346
|
+
expect(result?.data).toEqual([{ key: 'sample', value: { main: [] } }]);
|
|
347
|
+
});
|
|
169
348
|
|
|
170
|
-
|
|
349
|
+
it('returns inline data when no storageLocation is present', async () => {
|
|
350
|
+
const storage = createStorage();
|
|
351
|
+
const repo = new TestRepo(createDbManager({ get: jest.fn() as any }), storage);
|
|
352
|
+
const inlineRecord = {
|
|
353
|
+
id: 'item-1',
|
|
354
|
+
name: 'Inline Item',
|
|
355
|
+
data: [{ key: 'sample', value: { main: [] } }],
|
|
356
|
+
} as TestItem;
|
|
357
|
+
|
|
358
|
+
const result = await repo.testHydrateFromExternalStorage(inlineRecord);
|
|
359
|
+
|
|
360
|
+
expect(storage.load).not.toHaveBeenCalled();
|
|
361
|
+
expect(result).toBeDefined();
|
|
362
|
+
expect(result?.name).toBe('Inline Item');
|
|
363
|
+
expect(result?.data).toEqual([{ key: 'sample', value: { main: [] } }]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('never includes storageLocation in response', async () => {
|
|
367
|
+
const storage = createStorage();
|
|
368
|
+
const fullData = {
|
|
369
|
+
id: 'item-1',
|
|
370
|
+
name: 'From S3',
|
|
371
|
+
data: [{ key: 'sample', value: { main: [] } }],
|
|
372
|
+
};
|
|
373
|
+
(storage.load as jest.Mock).mockResolvedValue(fullData);
|
|
374
|
+
|
|
375
|
+
const repo = new TestRepo(createDbManager(), storage);
|
|
376
|
+
const result = await repo.testHydrateFromExternalStorage({
|
|
377
|
+
id: 'item-1',
|
|
378
|
+
name: 'Placeholder',
|
|
379
|
+
data: [],
|
|
380
|
+
storageLocation: { type: 's3', key: 'item-key' },
|
|
381
|
+
} as TestItem);
|
|
382
|
+
|
|
383
|
+
// storageLocation is an internal implementation detail and should never be exposed
|
|
384
|
+
expect(result?.storageLocation).toBeUndefined();
|
|
385
|
+
expect(result?.name).toBe('From S3');
|
|
386
|
+
expect(result?.data).toEqual([{ key: 'sample', value: { main: [] } }]);
|
|
387
|
+
});
|
|
171
388
|
|
|
172
|
-
|
|
389
|
+
it('returns empty content when S3 storage is missing or fails to load', async () => {
|
|
390
|
+
const storage = createStorage();
|
|
391
|
+
const s3Error = new Error('The specified key does not exist');
|
|
392
|
+
(s3Error as any).code = 'NoSuchKey';
|
|
393
|
+
(storage.load as jest.Mock).mockRejectedValue(s3Error);
|
|
394
|
+
|
|
395
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
396
|
+
|
|
397
|
+
const repo = new TestRepo(createDbManager(), storage);
|
|
398
|
+
const result = await repo.testHydrateFromExternalStorage({
|
|
399
|
+
id: 'item-1',
|
|
400
|
+
name: 'Test Item',
|
|
401
|
+
data: [],
|
|
402
|
+
storageLocation: { type: 's3', key: 'missing-key' },
|
|
403
|
+
} as TestItem);
|
|
404
|
+
|
|
405
|
+
// Should attempt to load from S3
|
|
406
|
+
expect(storage.load).toHaveBeenCalledWith({ type: 's3', key: 'missing-key' });
|
|
407
|
+
|
|
408
|
+
// Should log warning about failed load
|
|
409
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
410
|
+
'Failed to load content from S3 for test_item:',
|
|
411
|
+
'The specified key does not exist',
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Should return item with empty content (graceful degradation)
|
|
415
|
+
expect(result).toBeDefined();
|
|
416
|
+
expect(result?.id).toBe('item-1');
|
|
417
|
+
expect(result?.name).toBe('Test Item');
|
|
418
|
+
expect(result?.data).toEqual([]);
|
|
419
|
+
|
|
420
|
+
// storageLocation should be removed to avoid confusion
|
|
421
|
+
expect(result?.storageLocation).toBeUndefined();
|
|
422
|
+
|
|
423
|
+
consoleWarnSpy.mockRestore();
|
|
424
|
+
});
|
|
173
425
|
});
|
|
174
426
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
427
|
+
describe('deleteItem', () => {
|
|
428
|
+
it('deletes externalized payloads when deleting items', async () => {
|
|
429
|
+
const client = createDbManager({
|
|
430
|
+
get: jest.fn().mockResolvedValue({ Item: { storageLocation: { type: 's3', key: 'item-key' } } }),
|
|
431
|
+
delete: jest.fn().mockResolvedValue({}),
|
|
432
|
+
}).client as any;
|
|
433
|
+
const storage = createStorage();
|
|
434
|
+
const repo = new TestRepo({ client } as any, storage);
|
|
435
|
+
|
|
436
|
+
await repo.deleteItem({ id: 'item-1' });
|
|
437
|
+
|
|
438
|
+
expect(storage.delete).toHaveBeenCalledWith({ type: 's3', key: 'item-key' });
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('does not attempt to delete S3 when item has no storageLocation', async () => {
|
|
442
|
+
const client = createDbManager({
|
|
443
|
+
get: jest.fn().mockResolvedValue({ Item: { id: 'item-1', name: 'Test' } }),
|
|
444
|
+
delete: jest.fn().mockResolvedValue({}),
|
|
445
|
+
}).client as any;
|
|
446
|
+
const storage = createStorage();
|
|
447
|
+
const repo = new TestRepo({ client } as any, storage);
|
|
448
|
+
|
|
449
|
+
await repo.deleteItem({ id: 'item-1' });
|
|
450
|
+
|
|
451
|
+
expect(storage.delete).not.toHaveBeenCalled();
|
|
452
|
+
});
|
|
190
453
|
});
|
|
191
454
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
455
|
+
describe('isDynamoItemSizeLimitError', () => {
|
|
456
|
+
it('detects ValidationException with size message', () => {
|
|
457
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
458
|
+
const error = {
|
|
459
|
+
code: 'ValidationException',
|
|
460
|
+
message: 'Item size has exceeded the maximum allowed size',
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
expect(repo.testIsDynamoItemSizeLimitError(error)).toBe(true);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('detects TransactionCanceledException with size in cancellation reasons', () => {
|
|
467
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
468
|
+
const error = {
|
|
469
|
+
code: 'TransactionCanceledException',
|
|
470
|
+
CancellationReasons: [
|
|
471
|
+
{
|
|
472
|
+
Code: 'ValidationException',
|
|
473
|
+
Message: 'Item size exceeded 400 KB limit',
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
expect(repo.testIsDynamoItemSizeLimitError(error)).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('detects size keywords in error message', () => {
|
|
482
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
483
|
+
const error = {
|
|
484
|
+
message: 'The item size exceeds the maximum allowed size',
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
expect(repo.testIsDynamoItemSizeLimitError(error)).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('returns false for non-size-related errors', () => {
|
|
491
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
492
|
+
const error = {
|
|
493
|
+
code: 'ResourceNotFoundException',
|
|
494
|
+
message: 'Table not found',
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
expect(repo.testIsDynamoItemSizeLimitError(error)).toBe(false);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('returns false for null/undefined errors', () => {
|
|
501
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
502
|
+
|
|
503
|
+
expect(repo.testIsDynamoItemSizeLimitError(null)).toBe(false);
|
|
504
|
+
expect(repo.testIsDynamoItemSizeLimitError(undefined)).toBe(false);
|
|
505
|
+
});
|
|
232
506
|
});
|
|
233
507
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
508
|
+
describe('isContentInS3', () => {
|
|
509
|
+
it('returns true when storageLocation exists and large content fields are empty', () => {
|
|
510
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
511
|
+
const item = createTestItem({ data: [] });
|
|
512
|
+
(item as any).storageLocation = { type: 's3', key: 'item-key' };
|
|
513
|
+
|
|
514
|
+
expect(repo.testIsContentInS3(item)).toBe(true);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('returns false when storageLocation exists but large content fields have data', () => {
|
|
518
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
519
|
+
const item = createTestItem({ data: [{ key: 'sample', value: { main: [] } }] });
|
|
520
|
+
(item as any).storageLocation = { type: 's3', key: 'item-key' };
|
|
521
|
+
|
|
522
|
+
expect(repo.testIsContentInS3(item)).toBe(false);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('returns false when no storageLocation', () => {
|
|
526
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
527
|
+
const item = createTestItem();
|
|
528
|
+
|
|
529
|
+
expect(repo.testIsContentInS3(item)).toBe(false);
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe('getKeyFieldsValues', () => {
|
|
534
|
+
it('preserves key fields and small metadata', () => {
|
|
535
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
536
|
+
const obj = {
|
|
537
|
+
id: 'item-1',
|
|
538
|
+
name: 'Test',
|
|
539
|
+
data: [{ key: 'large', value: { content: 'big' } }],
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const result = repo.testGetKeyFieldsValues(obj);
|
|
543
|
+
|
|
544
|
+
expect(result.id).toBe('item-1');
|
|
545
|
+
expect(result.name).toBe('Test');
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('empties large content fields (fieldsAsJsonString)', () => {
|
|
549
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
550
|
+
const obj = {
|
|
551
|
+
id: 'item-1',
|
|
552
|
+
name: 'Test',
|
|
553
|
+
data: [{ key: 'large', value: { content: 'big' } }],
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const result = repo.testGetKeyFieldsValues(obj);
|
|
557
|
+
|
|
558
|
+
// data is in fieldsAsJsonString, so should be empty array
|
|
559
|
+
expect(result.data).toEqual([]);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('removes storageLocation from result', () => {
|
|
563
|
+
const repo = new TestRepo(createDbManager(), createStorage());
|
|
564
|
+
const obj = {
|
|
565
|
+
id: 'item-1',
|
|
566
|
+
name: 'Test',
|
|
567
|
+
data: [],
|
|
568
|
+
storageLocation: { type: 's3', key: 'item-key' },
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const result = repo.testGetKeyFieldsValues(obj);
|
|
572
|
+
|
|
573
|
+
expect(result.storageLocation).toBeUndefined();
|
|
574
|
+
});
|
|
273
575
|
});
|
|
274
576
|
});
|