• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eresearchqut / ddb-repository / 26065874894

18 May 2026 11:12PM UTC coverage: 92.0% (-2.4%) from 94.357%
26065874894

push

github

web-flow
Merge pull request #41 from eresearchqut/repo-assist/feat-json-pointer-patch-document-2026-05-18-3441a27795bd5224

[Repo Assist] feat: add patchDocument to JsonPointerRepository

187 of 220 branches covered (85.0%)

Branch coverage included in aggregate %.

13 of 13 new or added lines in 1 file covered. (100.0%)

9 existing lines in 1 file now uncovered.

296 of 305 relevant lines covered (97.05%)

159.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.53
/src/DynamoDbRepository.ts
1
import {
2✔
2
    BatchGetItemCommand,
3
    BatchGetItemCommandInput,
4
    BatchWriteItemCommand,
5
    DeleteItemCommand,
6
    DynamoDBClient,
7
    GetItemCommand,
8
    paginateQuery,
9
    PutItemCommand,
10
    QueryCommand,
11
    QueryCommandInput,
12
    ReturnConsumedCapacity,
13
    ReturnValue,
14
    UpdateItemCommand,
15
    WriteRequest,
16
} from "@aws-sdk/client-dynamodb";
17
import {marshall, unmarshall, NativeAttributeValue} from "@aws-sdk/util-dynamodb";
2✔
18
import {replace, uniqWith, isEqual, pickBy} from "lodash";
2✔
19

20
export enum FilterOperator {
2✔
21
    EQUALS = "=",
2✔
22
    NOT_EQUALS = "<>",
2✔
23
    GREATER_THAN_OR_EQUALS = ">=",
2✔
24
    GREATER_THAN = ">",
2✔
25
    LESS_THAN = "<",
2✔
26
    LESS_THAN_OR_EQUALS = "<=",
2✔
27
    IN = "IN",
2✔
28
    BETWEEN = "BETWEEN",
2✔
29
    BEGINS_WITH = "BEGINS_WITH",
2✔
30
    CONTAINS = "CONTAINS",
2✔
31
}
32

33
export interface FilterExpression {
34
    attribute: string;
35
    value:
36
        | string
37
        | number
38
        | boolean
39
        | Array<string | number>
40
        | [string, string]
41
        | [number, number];
42
    operator: FilterOperator;
43
    negate?: boolean;
44
}
45

46
export interface FilterableQuery {
47
    filterExpressions: Array<FilterExpression>;
48
}
49

50
export interface ProjectedQuery {
51
    projectedAttributes: string[];
52
}
53

54
export interface IndexedQuery {
55
    index: string;
56
}
57

58
export interface Query extends Partial<FilterableQuery>, Partial<ProjectedQuery>, Partial<IndexedQuery> {
59
    [key: string]: unknown;
60
    filterExpressions?: Array<FilterExpression>;
61
    projectedAttributes?: string[];
62
    index?: string;
63
    sortOrder?: "ASC" | "DESC";
64
    limit?: number
65
}
66

67
const marshallKey = (key: unknown) =>
2✔
68
    marshall(key as Record<string, NativeAttributeValue>, {removeUndefinedValues: true});
429✔
69

70
const expressionAttributeKey = (key: string) => replace(key, /-/g, "_");
960✔
71

72
const mapInKeys = (filterExpression: FilterExpression) =>
2✔
73
    Array.isArray(filterExpression.value)
7!
74
        ? filterExpression.value.map(
75
            (_, index) => `:${expressionAttributeKey(filterExpression.attribute)}${index}`,
14✔
76
        )
77
        : `:${expressionAttributeKey(filterExpression.attribute)}`;
78

79
const mapFilterExpression = (filterExpression: FilterExpression) => {
2✔
80
    switch (filterExpression.operator) {
47✔
81
        case FilterOperator.IN:
82
            return (
7✔
83
                `#${expressionAttributeKey(filterExpression.attribute)} ${filterExpression.operator} ` +
84
                `(${mapInKeys(filterExpression)})`
85
            );
86
        case FilterOperator.BETWEEN:
87
            return (
7✔
88
                `#${expressionAttributeKey(filterExpression.attribute)} ${filterExpression.operator} ` +
89
                `:${expressionAttributeKey(filterExpression.attribute)}0 AND :${expressionAttributeKey(filterExpression.attribute)}1`
90
            );
91
        case FilterOperator.BEGINS_WITH:
92
        case FilterOperator.CONTAINS:
93
            return (
6✔
94
                `${filterExpression.operator.toLowerCase()}(#${expressionAttributeKey(filterExpression.attribute)}, ` +
95
                `:${expressionAttributeKey(filterExpression.attribute)})`
96
            );
97
        default:
98
            return (
27✔
99
                `#${expressionAttributeKey(filterExpression.attribute)} ${filterExpression.operator} ` +
100
                `:${expressionAttributeKey(filterExpression.attribute)}`
101
            );
102
    }
103
};
104

105
const mapFilterExpressions = (
2✔
106
    filterExpressions: Array<FilterExpression>,
107
) =>
108
    filterExpressions
44✔
109
        .map((filterExpression) =>
110
            filterExpression.negate
47✔
111
                ? `NOT ${mapFilterExpression(filterExpression)}`
112
                : mapFilterExpression(filterExpression),
113
        )
114
        .join(" AND ");
115

116
const mapFilterExpressionValues = (
2✔
117
    filterExpression: FilterExpression,
118
): Record<string, string | number | boolean> =>
119
    Array.isArray(filterExpression.value)
47✔
120
        ? filterExpression.value.reduce(
121
            (reduction, value, index) => ({
28✔
122
                ...reduction,
123
                [`:${expressionAttributeKey(filterExpression.attribute)}${index}`]: value,
124
            }),
125
            Object.assign({}),
126
        )
127
        : {
128
            [`:${expressionAttributeKey(filterExpression.attribute)}`]:
129
            filterExpression.value,
130
        };
131

132
const paginate = <T>(array: Array<T>, pageSize: number) => {
2✔
133
    return array.reduce((acc, val, i) => {
81✔
134
        const idx = Math.floor(i / pageSize)
566✔
135
        const page = acc[idx] || (acc[idx] = [])
566✔
136
        page.push(val)
566✔
137
        return acc
566✔
138
    }, [] as Array<Array<T>>);
139
}
140

141
export interface DynamoDbRepositoryOptions {
142
    client: DynamoDBClient;
143
    tableName: string;
144
    hashKey: string;
145
    rangeKey?: string;
146
    returnConsumedCapacity?: ReturnConsumedCapacity;
147
}
148

149
export interface PageResult<T> {
150
    items: Array<T>;
151
    cursor?: string;
152
}
153

154
export class DynamoDbRepository<K, T> {
2✔
155
    private readonly dynamoDBClient: DynamoDBClient;
156
    private readonly tableName: string;
157
    private readonly hashKey: string;
158
    private readonly rangKey?: string;
159
    private readonly returnConsumedCapacity: ReturnConsumedCapacity | undefined;
160

161
    constructor(options: DynamoDbRepositoryOptions) {
162
        this.dynamoDBClient = options.client;
5✔
163
        this.tableName = options.tableName;
5✔
164
        this.hashKey = options.hashKey;
5✔
165
        this.rangKey = options.rangeKey;
5✔
166
        this.returnConsumedCapacity = options.returnConsumedCapacity ?? ReturnConsumedCapacity.TOTAL;
5!
167
    }
168

169
    getItem = async (key: K): Promise<T | undefined> => {
24✔
170
        return this.dynamoDBClient
24✔
171
            .send(
172
                new GetItemCommand({
173
                    TableName: this.tableName,
174
                    Key: marshallKey(key),
175
                    ReturnConsumedCapacity: this.returnConsumedCapacity,
176
                }),
177
            )
178
            .then((result) =>
179
                result.Item ? unmarshall(result.Item) as T : undefined,
24✔
180
            )
181
    };
182

183
    putItem = async (key: K, record: T): Promise<T> => {
826✔
184
        const Item = marshall({...record, ...key} as Record<string, NativeAttributeValue>, {removeUndefinedValues: true});
826✔
185
        return this.dynamoDBClient
826✔
186
            .send(
187
                new PutItemCommand({
188
                    TableName: this.tableName,
189
                    ReturnConsumedCapacity: this.returnConsumedCapacity,
190
                    Item,
191
                }),
192
            )
193
            .then(() => unmarshall(Item) as T);
826✔
194
    };
195

196
    deleteItem = async (key: K): Promise<T | undefined> => {
8✔
197
        return this.dynamoDBClient.send(new DeleteItemCommand({
8✔
198
            TableName: this.tableName,
199
            Key: marshallKey(key),
200
            ReturnValues: ReturnValue.ALL_OLD,
201
            ReturnConsumedCapacity: this.returnConsumedCapacity,
202
        })).then((result) => result.Attributes ?
8✔
203
            unmarshall(result.Attributes) as T : undefined);
204
    };
205

206

207
    updateItem = async (
5✔
208
        key: K,
209
        updates: Partial<T>,
210
        remove?: string[],
211
    ): Promise<T | undefined> => {
9✔
212
        const filteredUpdateEntries = Object.entries(updates).filter(([, value]) => value !== undefined);
9✔
213
        const hasUpdates = filteredUpdateEntries.length > 0;
9✔
214
        if (!hasUpdates && !remove?.length) {
9✔
215
            return this.getItem(key);
1✔
216
        }
217
        const setAttributesExpression = hasUpdates ? `SET ${filteredUpdateEntries
8✔
218
            .map(
219
                ([key]) =>
220
                    `#${expressionAttributeKey(key)} = :${expressionAttributeKey(key)}`,
6✔
221
            )
222
            .join(", ")}` : '';
223
        const removeAttributesExpression = remove?.length
8✔
224
            ? ` REMOVE ${remove.map((key) => `#${expressionAttributeKey(key)}`).join(", ")}`
4✔
225
            : "";
226
        const removeAttributeNames = remove?.length
8✔
227
            ? remove.reduce(
228
                (acc, key) => ({
4✔
229
                    ...acc,
230
                    [`#${expressionAttributeKey(key)}`]: key,
231
                }),
232
                {} as Record<string, string>,
233
            )
234
            : {};
235
        const updateItemCommandInput = {
8✔
236
            TableName: this.tableName,
237
            Key: marshallKey(key),
238
            UpdateExpression: `${setAttributesExpression}${removeAttributesExpression}`,
239
            ExpressionAttributeNames: filteredUpdateEntries
240
                .reduce(
241
                    (acc, [key]) => ({
6✔
242
                        ...acc,
243
                        [`#${expressionAttributeKey(key)}`]: key,
244
                    }),
245
                    Object.assign(
246
                        removeAttributeNames,
247
                    ),
248
                ) as Record<string, string>,
249
            ExpressionAttributeValues: hasUpdates ? marshall(
8✔
250
                filteredUpdateEntries.reduce(
251
                    (acc, [key, value]) => ({
6✔
252
                        ...acc,
253
                        [`:${expressionAttributeKey(key)}`]: value,
254
                    }),
255
                    {} as Record<string, NativeAttributeValue>,
256
                ),
257
                {removeUndefinedValues: true},
258
            ) : undefined,
259
            ReturnConsumedCapacity: this.returnConsumedCapacity,
260
        };
261
        return this.dynamoDBClient
8✔
262
            .send(new UpdateItemCommand({...updateItemCommandInput, ReturnValues: 'ALL_NEW'}))
263
            .then((result) => result.Attributes ? unmarshall(result.Attributes) as T : undefined);
8!
264
    };
265

266

267
    getItems = async (
5✔
268
        query: Query
269
    ): Promise<Array<T> | undefined> => {
153✔
270
        const {index, filterExpressions, projectedAttributes, limit, sortOrder,...keys} = query;
153✔
271
        const KeyConditionExpression = Object.keys(keys)
153✔
272
            .map((key) => `#${expressionAttributeKey(key)} = :${expressionAttributeKey(key)}`).join(' AND ');
153✔
273
        const keyExpressionAttributeNames = Object.keys(keys)
153✔
274
            .reduce((acc, key) => ({...acc, [`#${expressionAttributeKey(key)}`]: key}), Object.assign({}));
153✔
275
        const keyExpressionAttributeValues = Object.entries(keys)
153✔
276
            .reduce((acc, [key, value]) => ({...acc, [`:${expressionAttributeKey(key)}`]: value}), Object.assign({}));
153✔
277

278
        const gsiKeyAttributes = index
153✔
279
            ? [this.hashKey, ...(this.rangKey ? [this.rangKey] : [])]
10!
280
            : [];
281

282
        const ProjectionExpression = index
153✔
283
            ? gsiKeyAttributes.map((attr) => `#${expressionAttributeKey(attr)}`).join(',')
20✔
284
            : (projectedAttributes
143✔
285
                ? projectedAttributes.map((attribute) => `#${expressionAttributeKey(attribute)}`).join(',')
6✔
286
                : undefined);
287

288
        const projectionAttributeNames: Record<string, string> = index
153✔
289
            ? gsiKeyAttributes.reduce(
290
                (acc: Record<string, string>, attr: string) => ({
20✔
291
                    ...acc,
292
                    [`#${expressionAttributeKey(attr)}`]: attr,
293
                }),
294
                {},
295
            )
296
            : (projectedAttributes ? projectedAttributes.reduce(
143✔
297
                (
298
                    reduction: Record<string, string>,
299
                    attribute: string,
300
                ) => ({
6✔
301
                    ...reduction,
302
                    [`#${expressionAttributeKey(attribute)}`]:
303
                    attribute,
304
                }),
305
                Object.assign({}),
306
            ) : {})
307
        const hasFilterExpressions = Array.isArray(filterExpressions) && filterExpressions.length > 0;
153✔
308
        const FilterExpression = hasFilterExpressions
153✔
309
            ? mapFilterExpressions(filterExpressions!)
310
            : undefined;
311
        const filterAttributeNames: Record<string, string> = hasFilterExpressions
153✔
312
            ? filterExpressions!.reduce(
313
                (
314
                    reduction: Record<string, string>,
315
                    filterExpression: FilterExpression,
316
                ) => ({
46✔
317
                    ...reduction,
318
                    [`#${expressionAttributeKey(filterExpression.attribute)}`]:
319
                    filterExpression.attribute,
320
                }),
321
                Object.assign({}),
322
            )
323
            : {};
324
        const filterAttributeValues = filterExpressions
153✔
325
            ? filterExpressions.reduce(
326
                (reduction, filterExpression) => ({
46✔
327
                    ...reduction,
328
                    ...mapFilterExpressionValues(filterExpression),
329
                }),
330
                Object.assign({}),
331
            )
332
            : {};
333

334
        const Limit = limit;
153✔
335
        const ScanIndexForward = sortOrder === "DESC" ? false : undefined;
153✔
336
        const queryCommandInput: QueryCommandInput = {
153✔
337
            TableName: this.tableName,
338
            ReturnConsumedCapacity: this.returnConsumedCapacity,
339
            IndexName: index,
340
            KeyConditionExpression,
341
            FilterExpression,
342
            ProjectionExpression,
343
            ExpressionAttributeNames: {
344
                ...keyExpressionAttributeNames,
345
                ...filterAttributeNames,
346
                ...projectionAttributeNames
347
            },
348
            ExpressionAttributeValues: marshall(
349
                {...keyExpressionAttributeValues, ...filterAttributeValues} as Record<string, NativeAttributeValue>,
350
                {removeUndefinedValues: true},
351
            ),
352
            Limit,
353
            ScanIndexForward
354
        };
355
        const paginator = paginateQuery(
153✔
356
            {client: this.dynamoDBClient, pageSize: 100},
357
            queryCommandInput,
358
        );
359

360
        if (index) {
153✔
361
            const collectedKeys: Array<K> = [];
10✔
362
            for await (const page of paginator) {
32✔
363
                if (page.Items) {
11!
364
                    collectedKeys.push(
11✔
365
                        ...(page.Items.map((item) => unmarshall(item) as T)
143✔
366
                            .map((item: T) =>
367
                                pickBy(item as object, (_, key) => (key === this.hashKey || key === this.rangKey)) as K)),
286✔
368
                    )
369
                }
370
                if (limit && collectedKeys.length >= limit) break;
11!
371
            }
372
            const keysBatch = limit ? collectedKeys.slice(0, limit) : collectedKeys;
10!
373
            const keyAttrs = [this.hashKey, ...(this.rangKey ? [this.rangKey] : [])];
10!
374
            const batchProjectedQuery = projectedAttributes
10✔
375
                ? { ...query, projectedAttributes: [...new Set([...projectedAttributes, ...keyAttrs])] } as ProjectedQuery
376
                : query as ProjectedQuery;
377
            const items = await this.batchGetItems(keysBatch, batchProjectedQuery);
10✔
378
            const orderedItems = keysBatch.flatMap((key) => {
10✔
379
                const k = key as Record<string, unknown>;
143✔
380
                const match = (items as Array<T | undefined>).find((item) => {
143✔
381
                    if (!item) return false;
7,305!
382
                    const t = item as Record<string, unknown>;
7,305✔
383
                    return t[this.hashKey] === k[this.hashKey] &&
7,305✔
384
                        (!this.rangKey || t[this.rangKey] === k[this.rangKey]);
385
                });
386
                return match ? [match] : [];
143!
387
            });
388
            if (projectedAttributes) {
10✔
389
                const projSet = new Set(projectedAttributes);
1✔
390
                return orderedItems.map(item =>
1✔
391
                    pickBy(item as object, (_, key) => projSet.has(key)) as T
12✔
392
                ) as Array<T>;
393
            }
394
            return orderedItems as Array<T>;
9✔
395
        }
396

397
        const items: Array<T> = [];
143✔
398
        for await (const page of paginator) {
429✔
399
            if (page.Items) {
143!
400
                items.push(
143✔
401
                    ...(page.Items?.map((item) => unmarshall(item) as T) || []),
221!
402
                )
403
            }
404
            if (limit && items.length >= limit) break;
143✔
405
        }
406
        return limit ? items.slice(0, limit) : items;
143✔
407
    };
408

409

410
    getItemsPage = async (
5✔
411
        query: Query & { cursor?: string }
412
    ): Promise<PageResult<T>> => {
8✔
413
        const {index, filterExpressions, projectedAttributes, limit, sortOrder, cursor, ...keys} = query;
8✔
414
        const KeyConditionExpression = Object.keys(keys)
8✔
415
            .map((key) => `#${expressionAttributeKey(key)} = :${expressionAttributeKey(key)}`).join(' AND ');
8✔
416
        const keyExpressionAttributeNames = Object.keys(keys)
8✔
417
            .reduce((acc, key) => ({...acc, [`#${expressionAttributeKey(key)}`]: key}), Object.assign({}));
8✔
418
        const keyExpressionAttributeValues = Object.entries(keys)
8✔
419
            .reduce((acc, [key, value]) => ({...acc, [`:${expressionAttributeKey(key)}`]: value}), Object.assign({}));
8✔
420

421
        const ProjectionExpression = !index && projectedAttributes
8!
UNCOV
422
            ? projectedAttributes.map((attribute) => `#${expressionAttributeKey(attribute)}`).join(',')
×
423
            : undefined;
424
        const projectionAttributeNames: Record<string, string> = !index && projectedAttributes ? projectedAttributes.reduce(
8!
UNCOV
425
            (reduction: Record<string, string>, attribute: string) => ({
×
426
                ...reduction,
427
                [`#${expressionAttributeKey(attribute)}`]: attribute,
428
            }),
429
            Object.assign({}),
430
        ) : {};
431
        const hasFilterExpressions = Array.isArray(filterExpressions) && filterExpressions.length > 0;
8✔
432
        const FilterExpression = hasFilterExpressions
8✔
433
            ? mapFilterExpressions(filterExpressions!)
434
            : undefined;
435
        const filterAttributeNames: Record<string, string> = hasFilterExpressions
8✔
436
            ? filterExpressions!.reduce(
437
                (reduction: Record<string, string>, filterExpression: FilterExpression) => ({
1✔
438
                    ...reduction,
439
                    [`#${expressionAttributeKey(filterExpression.attribute)}`]: filterExpression.attribute,
440
                }),
441
                Object.assign({}),
442
            )
443
            : {};
444
        const filterAttributeValues = filterExpressions
8✔
445
            ? filterExpressions.reduce(
446
                (reduction, filterExpression) => ({
1✔
447
                    ...reduction,
448
                    ...mapFilterExpressionValues(filterExpression),
449
                }),
450
                Object.assign({}),
451
            )
452
            : {};
453

454
        const ExclusiveStartKey = cursor
8✔
455
            ? JSON.parse(Buffer.from(cursor, 'base64').toString('utf-8')) as Record<string, unknown>
456
            : undefined;
457

458
        const queryCommandInput: QueryCommandInput = {
8✔
459
            TableName: this.tableName,
460
            ReturnConsumedCapacity: this.returnConsumedCapacity,
461
            IndexName: index,
462
            KeyConditionExpression,
463
            FilterExpression,
464
            ProjectionExpression,
465
            ExpressionAttributeNames: {
466
                ...keyExpressionAttributeNames,
467
                ...filterAttributeNames,
468
                ...projectionAttributeNames
469
            },
470
            ExpressionAttributeValues: marshall(
471
                {...keyExpressionAttributeValues, ...filterAttributeValues} as Record<string, NativeAttributeValue>,
472
                {removeUndefinedValues: true},
473
            ),
474
            Limit: limit,
475
            ScanIndexForward: sortOrder === "DESC" ? false : undefined,
8✔
476
            ExclusiveStartKey: ExclusiveStartKey
8✔
477
                ? marshall(ExclusiveStartKey as Record<string, NativeAttributeValue>, {removeUndefinedValues: true})
478
                : undefined,
479
        };
480

481
        const result = await this.dynamoDBClient.send(new QueryCommand(queryCommandInput));
8✔
482
        const nextCursor = result.LastEvaluatedKey
8✔
483
            ? Buffer.from(JSON.stringify(unmarshall(result.LastEvaluatedKey))).toString('base64')
484
            : undefined;
485

486
        if (index) {
8!
UNCOV
487
            const collectedKeys = (result.Items ?? [])
×
UNCOV
488
                .map((item) => unmarshall(item) as T)
×
489
                .map((item: T) =>
UNCOV
490
                    pickBy(item as object, (_, key) => (key === this.hashKey || key === this.rangKey)) as K
×
491
                );
UNCOV
492
            const items = await this.batchGetItems(collectedKeys, query as ProjectedQuery);
×
UNCOV
493
            return {items: items.filter((item): item is T => item !== undefined), cursor: nextCursor};
×
494
        }
495

496
        const items = (result.Items ?? []).map((item) => unmarshall(item) as T);
43!
497
        return {items, cursor: nextCursor};
8✔
498
    };
499

500

501
    batchGetItems = async (
5✔
502
        keys: K[], projectedQuery?: ProjectedQuery
503
    ): Promise<Array<T | undefined>> => {
20✔
504
        const uniqueKeys = uniqWith(keys, isEqual);
20✔
505
        const keyPages = paginate(uniqueKeys, 100);
20✔
506
        const {projectedAttributes} = projectedQuery || {};
20✔
507
        const ProjectionExpression = projectedAttributes
20✔
508
            ? projectedAttributes.map((attribute) => `#${expressionAttributeKey(attribute)}`).join(',')
8✔
509
            : undefined;
510
        const ExpressionAttributeNames = projectedAttributes ?
20✔
511
            projectedAttributes.reduce(
512
            (
513
                reduction: Record<string, string>,
514
                attribute: string,
515
            ) => ({
8✔
516
                ...reduction,
517
                [`#${expressionAttributeKey(attribute)}`]:
518
                attribute,
519
            }),
520
            Object.assign({}),
521
        ) : undefined;
522
        return Promise.all((keyPages.map(async (keyPage) => {
20✔
523
            const batchRequest: BatchGetItemCommandInput = {
20✔
524
                RequestItems: {
525
                    [this.tableName]: {
526
                        Keys: keyPage.map((key) => marshallKey(key)),
312✔
527
                        ProjectionExpression,
528
                        ExpressionAttributeNames,
529
                    }
530
                },
531
                ReturnConsumedCapacity: this.returnConsumedCapacity,
532
            }
533

534
            const fetchPage = async (request: BatchGetItemCommandInput): Promise<T[]> => {
20✔
535
                const result = await this.dynamoDBClient.send(new BatchGetItemCommand(request));
20✔
536
                const retrieved = result.Responses?.[this.tableName]?.map((item) => unmarshall(item) as T) ?? [];
310!
537
                const unprocessed = result.UnprocessedKeys;
20✔
538
                if (!unprocessed || Object.keys(unprocessed).length === 0) return retrieved;
20!
UNCOV
539
                return [...retrieved, ...await fetchPage({
×
540
                    RequestItems: unprocessed,
541
                    ReturnConsumedCapacity: this.returnConsumedCapacity,
542
                })];
543
            };
544

545
            return fetchPage(batchRequest);
20✔
546
        })))
547
            .then((itemSets) => itemSets.flat());
20✔
548
    };
549

550
    batchWriteItems = async (
5✔
551
        puts: { key: K; item: T }[],
552
        deletes: K[],
553
    ): Promise<void> => {
61✔
554
        const requests: WriteRequest[] = [
61✔
555
            ...puts.map(({ key, item }) => ({
177✔
556
                PutRequest: {
557
                    Item: marshall(
558
                        { ...item, ...key } as Record<string, NativeAttributeValue>,
559
                        { removeUndefinedValues: true },
560
                    ),
561
                },
562
            })),
563
            ...deletes.map((key) => ({
77✔
564
                DeleteRequest: { Key: marshallKey(key) },
565
            })),
566
        ];
567

568
        const sendBatch = async (requestItems: Record<string, WriteRequest[]>): Promise<void> => {
66✔
569
            const result = await this.dynamoDBClient.send(
66✔
570
                new BatchWriteItemCommand({
571
                    RequestItems: requestItems,
572
                    ReturnConsumedCapacity: this.returnConsumedCapacity,
573
                }),
574
            );
575
            const unprocessed = result.UnprocessedItems;
66✔
576
            if (unprocessed && Object.keys(unprocessed).length > 0) {
66!
UNCOV
577
                await sendBatch(unprocessed as Record<string, WriteRequest[]>);
×
578
            }
579
        };
580

581
        await Promise.all(
61✔
582
            paginate(requests, 25).map(batch => sendBatch({ [this.tableName]: batch })),
66✔
583
        );
584
    };
585
}
586

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc