@tomassabol/dynamodb-tools 1.0.19
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 +13 -0
- package/dist/common/logger-interface.d.ts +9 -0
- package/dist/common/logger-interface.js +3 -0
- package/dist/common/logger.d.ts +8 -0
- package/dist/common/logger.js +19 -0
- package/dist/dynamodb/command-builder.d.ts +22 -0
- package/dist/dynamodb/command-builder.js +46 -0
- package/dist/dynamodb/default-table-fields.d.ts +18 -0
- package/dist/dynamodb/default-table-fields.js +17 -0
- package/dist/dynamodb/get-many.d.ts +9 -0
- package/dist/dynamodb/get-many.js +30 -0
- package/dist/dynamodb/iterators.d.ts +12 -0
- package/dist/dynamodb/iterators.js +55 -0
- package/dist/dynamodb/query-command-builder.d.ts +92 -0
- package/dist/dynamodb/query-command-builder.js +161 -0
- package/dist/dynamodb/query-iterator.d.ts +41 -0
- package/dist/dynamodb/query-iterator.js +67 -0
- package/dist/dynamodb/update-command-builder.d.ts +64 -0
- package/dist/dynamodb/update-command-builder.js +141 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +27 -0
- package/dist/streams/stream-handler.d.ts +70 -0
- package/dist/streams/stream-handler.js +164 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/utils/compound-key.d.ts +10 -0
- package/dist/utils/compound-key.js +20 -0
- package/dist/utils/create-ttl.d.ts +10 -0
- package/dist/utils/create-ttl.js +14 -0
- package/dist/utils/date-time.d.ts +7 -0
- package/dist/utils/date-time.js +16 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger interface
|
|
3
|
+
*/
|
|
4
|
+
export interface ILogger {
|
|
5
|
+
debug(message?: any, ...optionalParams: any[]): void;
|
|
6
|
+
info(message?: any, ...optionalParams: any[]): void;
|
|
7
|
+
error(message?: any, ...optionalParams: any[]): void;
|
|
8
|
+
warn(message?: any, ...optionalParams: any[]): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Empty logger used to disable logging for unit tests.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logger = void 0;
|
|
7
|
+
const noLogger = {
|
|
8
|
+
log: () => undefined,
|
|
9
|
+
debug: () => undefined,
|
|
10
|
+
info: () => undefined,
|
|
11
|
+
error: () => undefined,
|
|
12
|
+
warn: () => undefined,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* CloudWatch logger.
|
|
16
|
+
*/
|
|
17
|
+
exports.logger = process.env.JEST_WORKER_ID === undefined || process.env.TEST_LOGGER
|
|
18
|
+
? console
|
|
19
|
+
: noLogger;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for DynamoDB command builders.
|
|
3
|
+
*/
|
|
4
|
+
export declare abstract class CommandBuilder<T extends Record<string, unknown>> {
|
|
5
|
+
protected expressionAttributeNames: Record<string, string>;
|
|
6
|
+
protected expressionAttributeValues: Record<string, unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Create new attribute name #attr = attr
|
|
9
|
+
*/
|
|
10
|
+
protected createAttributeName(attr: keyof T): string;
|
|
11
|
+
/**
|
|
12
|
+
* Create new attribute value :name = value
|
|
13
|
+
*/
|
|
14
|
+
protected createAttributeValue(name: string, value: unknown): string;
|
|
15
|
+
/**
|
|
16
|
+
* Create new attribute name and value: #attr = attr, :attr = value
|
|
17
|
+
*/
|
|
18
|
+
protected createAttributeNameAndValue(attr: keyof T, value: unknown): {
|
|
19
|
+
attrName: string;
|
|
20
|
+
valueName: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CommandBuilder = void 0;
|
|
7
|
+
const assert_1 = __importDefault(require("assert"));
|
|
8
|
+
/**
|
|
9
|
+
* Abstract base class for DynamoDB command builders.
|
|
10
|
+
*/
|
|
11
|
+
class CommandBuilder {
|
|
12
|
+
expressionAttributeNames = {};
|
|
13
|
+
expressionAttributeValues = {};
|
|
14
|
+
/**
|
|
15
|
+
* Create new attribute name #attr = attr
|
|
16
|
+
*/
|
|
17
|
+
createAttributeName(attr) {
|
|
18
|
+
(0, assert_1.default)(typeof attr === "string", "Invalid attribute");
|
|
19
|
+
const attrName = "#" + attr;
|
|
20
|
+
this.expressionAttributeNames[attrName] = attr;
|
|
21
|
+
return attrName;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create new attribute value :name = value
|
|
25
|
+
*/
|
|
26
|
+
createAttributeValue(name, value) {
|
|
27
|
+
let valueName = ":" + name;
|
|
28
|
+
if (Object.hasOwn(this.expressionAttributeValues, valueName) &&
|
|
29
|
+
this.expressionAttributeValues[valueName] !== value) {
|
|
30
|
+
const index = Object.keys(this.expressionAttributeValues).length; // Create unique index
|
|
31
|
+
valueName = `${valueName}_${index}`;
|
|
32
|
+
}
|
|
33
|
+
this.expressionAttributeValues[valueName] = value;
|
|
34
|
+
return valueName;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create new attribute name and value: #attr = attr, :attr = value
|
|
38
|
+
*/
|
|
39
|
+
createAttributeNameAndValue(attr, value) {
|
|
40
|
+
(0, assert_1.default)(typeof attr === "string", "Invalid attribute");
|
|
41
|
+
const attrName = this.createAttributeName(attr);
|
|
42
|
+
const valueName = this.createAttributeValue(attr, value);
|
|
43
|
+
return { attrName, valueName };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.CommandBuilder = CommandBuilder;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TTLValue } from "../utils/create-ttl";
|
|
2
|
+
/**
|
|
3
|
+
* Default fields added to tables
|
|
4
|
+
*/
|
|
5
|
+
export type DefaultTableFields = {
|
|
6
|
+
/** Date and time when record was created */
|
|
7
|
+
createdAt: string;
|
|
8
|
+
/** Date and time when record was last updated */
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
/** Time to live */
|
|
11
|
+
ttl?: number | null;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Create values for default table fields
|
|
15
|
+
*/
|
|
16
|
+
export declare function createDefaultTableFields(options?: {
|
|
17
|
+
ttl?: TTLValue | null;
|
|
18
|
+
}): DefaultTableFields;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDefaultTableFields = createDefaultTableFields;
|
|
4
|
+
const create_ttl_1 = require("../utils/create-ttl");
|
|
5
|
+
const date_time_1 = require("../utils/date-time");
|
|
6
|
+
/**
|
|
7
|
+
* Create values for default table fields
|
|
8
|
+
*/
|
|
9
|
+
function createDefaultTableFields(options = {}) {
|
|
10
|
+
const { ttl } = options;
|
|
11
|
+
const currentDate = (0, date_time_1.getCurrentDateTimeISO)();
|
|
12
|
+
return {
|
|
13
|
+
createdAt: currentDate,
|
|
14
|
+
updatedAt: currentDate,
|
|
15
|
+
...(ttl !== undefined && { ttl: (0, create_ttl_1.createTTL)(ttl) }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BatchGetCommandInput, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
/**
|
|
3
|
+
* Generator to split an array into smaller arrays containing max batchSize items
|
|
4
|
+
*/
|
|
5
|
+
export declare function splitToBatches<T>(arr: T[], batchSize?: number): Generator<T[], void, unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Async generator to perform GetBatchCommand and handle unprocessed keys with retries
|
|
8
|
+
*/
|
|
9
|
+
export declare function retryBatchGetCommand(client: DynamoDBDocumentClient, requestItems: BatchGetCommandInput["RequestItems"]): AsyncGenerator<import("@aws-sdk/lib-dynamodb").BatchGetCommandOutput, void, unknown>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.splitToBatches = splitToBatches;
|
|
4
|
+
exports.retryBatchGetCommand = retryBatchGetCommand;
|
|
5
|
+
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
6
|
+
/**
|
|
7
|
+
* Generator to split an array into smaller arrays containing max batchSize items
|
|
8
|
+
*/
|
|
9
|
+
function* splitToBatches(arr, batchSize = 25) {
|
|
10
|
+
let start = 0;
|
|
11
|
+
while (start < arr.length) {
|
|
12
|
+
const end = start + batchSize;
|
|
13
|
+
yield arr.slice(start, end);
|
|
14
|
+
start = end;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Async generator to perform GetBatchCommand and handle unprocessed keys with retries
|
|
19
|
+
*/
|
|
20
|
+
async function* retryBatchGetCommand(client, requestItems) {
|
|
21
|
+
let current = requestItems;
|
|
22
|
+
while (current && Object.keys(current).length > 0) {
|
|
23
|
+
const command = new lib_dynamodb_1.BatchGetCommand({
|
|
24
|
+
RequestItems: current,
|
|
25
|
+
});
|
|
26
|
+
const output = await client.send(command);
|
|
27
|
+
current = output.UnprocessedKeys;
|
|
28
|
+
yield output;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient, QueryCommandInput, ScanCommandInput } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
export type QueryTableIteratorOptions = Omit<QueryCommandInput, "TableName" | "ExclusiveStartKey">;
|
|
3
|
+
export type ScanTableIteratorOptions = Omit<ScanCommandInput, "TableName" | "ExclusiveStartKey">;
|
|
4
|
+
/**
|
|
5
|
+
* Create asynchronous iterator to query all items of a table
|
|
6
|
+
*/
|
|
7
|
+
export declare function queryTableIterator<T = unknown>(client: DynamoDBDocumentClient, tableName: string, startKey?: object, options?: QueryTableIteratorOptions): AsyncGenerator<Awaited<T>, void, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* Create asynchronous iterator to scan all items of a table
|
|
10
|
+
*/
|
|
11
|
+
export declare function scanTableIterator<T = unknown>(client: DynamoDBDocumentClient, tableName: string, startKey?: object, options?: ScanTableIteratorOptions): AsyncGenerator<Awaited<T>, void, unknown>;
|
|
12
|
+
export declare function collectItems<T>(iterator: AsyncGenerator<T>): Promise<Awaited<T>[]>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.queryTableIterator = queryTableIterator;
|
|
4
|
+
exports.scanTableIterator = scanTableIterator;
|
|
5
|
+
exports.collectItems = collectItems;
|
|
6
|
+
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
7
|
+
/**
|
|
8
|
+
* Create asynchronous iterator to query all items of a table
|
|
9
|
+
*/
|
|
10
|
+
async function* queryTableIterator(client, tableName, startKey, options = {}) {
|
|
11
|
+
let lastKey = startKey;
|
|
12
|
+
do {
|
|
13
|
+
const command = new lib_dynamodb_1.QueryCommand({
|
|
14
|
+
...options,
|
|
15
|
+
TableName: tableName,
|
|
16
|
+
ExclusiveStartKey: lastKey,
|
|
17
|
+
});
|
|
18
|
+
const output = await client.send(command);
|
|
19
|
+
const items = output.Items;
|
|
20
|
+
if (items) {
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
yield item;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
lastKey = output.LastEvaluatedKey;
|
|
26
|
+
} while (lastKey);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create asynchronous iterator to scan all items of a table
|
|
30
|
+
*/
|
|
31
|
+
async function* scanTableIterator(client, tableName, startKey, options = {}) {
|
|
32
|
+
let lastKey = startKey;
|
|
33
|
+
do {
|
|
34
|
+
const command = new lib_dynamodb_1.ScanCommand({
|
|
35
|
+
...options,
|
|
36
|
+
TableName: tableName,
|
|
37
|
+
ExclusiveStartKey: lastKey,
|
|
38
|
+
});
|
|
39
|
+
const output = await client.send(command);
|
|
40
|
+
const items = output.Items;
|
|
41
|
+
if (items) {
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
yield item;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
lastKey = output.LastEvaluatedKey;
|
|
47
|
+
} while (lastKey);
|
|
48
|
+
}
|
|
49
|
+
async function collectItems(iterator) {
|
|
50
|
+
const result = [];
|
|
51
|
+
for await (const item of iterator) {
|
|
52
|
+
result.push(item);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { QueryCommandInput } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { CommandBuilder } from "./command-builder";
|
|
3
|
+
type ArrayKeys<T> = {
|
|
4
|
+
[K in keyof T]: T[K] extends readonly any[] ? K : never;
|
|
5
|
+
}[keyof T];
|
|
6
|
+
/**
|
|
7
|
+
* Builder for update expression in DynamoDB update command
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const builder = new QueryCommandBuilder<MyTableRecord>("table")
|
|
12
|
+
*
|
|
13
|
+
* const input = builder
|
|
14
|
+
* .partitionKeyEquals("pk", "12345")
|
|
15
|
+
* .buildCommandInput()
|
|
16
|
+
* const command = new QueryCommand(input)
|
|
17
|
+
* await documentClient.client.send(command)
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare class QueryCommandBuilder<T extends Record<string, unknown>> extends CommandBuilder<T> {
|
|
21
|
+
private partitionKeyExpression;
|
|
22
|
+
private sortKeyExpression;
|
|
23
|
+
private projectionExpression;
|
|
24
|
+
private filterExpression;
|
|
25
|
+
private commandOptions;
|
|
26
|
+
constructor(tableName: string, options?: Partial<QueryCommandInput>);
|
|
27
|
+
/**
|
|
28
|
+
* Add key condition for partition key
|
|
29
|
+
*/
|
|
30
|
+
partitionKeyEquals<K extends keyof T>(attr: K, value: T[K]): this;
|
|
31
|
+
/**
|
|
32
|
+
* Add key condition for sort key
|
|
33
|
+
*/
|
|
34
|
+
sortKeyCondition<K extends keyof T>(attr: K, operator: "=" | "<" | "<=" | ">" | ">=" | "begins_with", value: T[K]): this;
|
|
35
|
+
/**
|
|
36
|
+
* Add filter expression condition
|
|
37
|
+
*
|
|
38
|
+
* @examples
|
|
39
|
+
* ```ts
|
|
40
|
+
* builder
|
|
41
|
+
* .addFilterCondition("attr1", ">", 10) // attr3 > 10
|
|
42
|
+
* .addFilterCondition("attr2", "IN", [1, 2, 3]) // attr2 IN (1, 2, 3)
|
|
43
|
+
* .addFilterCondition("attr3", "IN", [1, 2, 3], { not: true }) // NOT (attr3 IN (1, 2, 3))
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
addFilterCondition<K extends keyof T>(attr: K, operator: "IN", value: T[K][], options?: {
|
|
47
|
+
not?: boolean;
|
|
48
|
+
}): this;
|
|
49
|
+
addFilterCondition<K extends keyof T>(attr: K, operator: "=" | "<" | "<=" | ">" | ">=" | "<>", value: T[K], options?: {
|
|
50
|
+
not?: boolean;
|
|
51
|
+
}): this;
|
|
52
|
+
/**
|
|
53
|
+
* Add existential filter expression condition
|
|
54
|
+
*/
|
|
55
|
+
addExistentialFilterCondition<K extends keyof T>(attr: K, operator: "exists" | "not_exists"): this;
|
|
56
|
+
/**
|
|
57
|
+
* Add filter condition based on array size using DynamoDB's size() function.
|
|
58
|
+
*
|
|
59
|
+
* @param attr - The attribute to filter by size (should be an array)
|
|
60
|
+
* @param condition - The size condition with value and operator
|
|
61
|
+
* @param options - Optional settings like negation
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* // Filter by array size
|
|
66
|
+
* builder.addArraySizeFilterCondition("shipmentItems", { value: 5, operator: ">" })
|
|
67
|
+
* // Generates: size(#shipmentItems) > :shipmentItems
|
|
68
|
+
*
|
|
69
|
+
* // Works with all comparison operators
|
|
70
|
+
* builder.addArraySizeFilterCondition("tags", { value: 0, operator: "<>" })
|
|
71
|
+
* builder.addArraySizeFilterCondition("items", { value: 10, operator: "<=" })
|
|
72
|
+
*
|
|
73
|
+
* // Can be negated
|
|
74
|
+
* builder.addArraySizeFilterCondition("items", { value: 0, operator: "=" }, { not: true })
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
addArraySizeFilterCondition<K extends ArrayKeys<T>>(attr: K, condition: {
|
|
78
|
+
value: number;
|
|
79
|
+
operator: "=" | "<>" | "<" | "<=" | ">" | ">=";
|
|
80
|
+
}, options?: {
|
|
81
|
+
not?: boolean;
|
|
82
|
+
}): this;
|
|
83
|
+
/**
|
|
84
|
+
* Add attributes which should be returned in result (by default all attributes are returned).
|
|
85
|
+
*/
|
|
86
|
+
addProjectionAttributes(attrs: Array<keyof T>): this;
|
|
87
|
+
/**
|
|
88
|
+
* Build command input
|
|
89
|
+
*/
|
|
90
|
+
buildCommandInput(): QueryCommandInput;
|
|
91
|
+
}
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.QueryCommandBuilder = void 0;
|
|
7
|
+
const command_builder_1 = require("./command-builder");
|
|
8
|
+
const assert_1 = __importDefault(require("assert"));
|
|
9
|
+
/**
|
|
10
|
+
* Builder for update expression in DynamoDB update command
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const builder = new QueryCommandBuilder<MyTableRecord>("table")
|
|
15
|
+
*
|
|
16
|
+
* const input = builder
|
|
17
|
+
* .partitionKeyEquals("pk", "12345")
|
|
18
|
+
* .buildCommandInput()
|
|
19
|
+
* const command = new QueryCommand(input)
|
|
20
|
+
* await documentClient.client.send(command)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
class QueryCommandBuilder extends command_builder_1.CommandBuilder {
|
|
24
|
+
partitionKeyExpression = "";
|
|
25
|
+
sortKeyExpression = "";
|
|
26
|
+
projectionExpression = [];
|
|
27
|
+
filterExpression = [];
|
|
28
|
+
commandOptions;
|
|
29
|
+
constructor(tableName, options = {}) {
|
|
30
|
+
super();
|
|
31
|
+
this.commandOptions = {
|
|
32
|
+
...options,
|
|
33
|
+
TableName: tableName,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Add key condition for partition key
|
|
38
|
+
*/
|
|
39
|
+
partitionKeyEquals(attr, value) {
|
|
40
|
+
const { attrName, valueName } = this.createAttributeNameAndValue(attr, value);
|
|
41
|
+
if (this.partitionKeyExpression)
|
|
42
|
+
throw new Error("Max one partition key condition is allowed");
|
|
43
|
+
this.partitionKeyExpression = `${attrName} = ${valueName}`;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Add key condition for sort key
|
|
48
|
+
*/
|
|
49
|
+
sortKeyCondition(attr, operator, value) {
|
|
50
|
+
const { attrName, valueName } = this.createAttributeNameAndValue(attr, value);
|
|
51
|
+
if (this.sortKeyExpression)
|
|
52
|
+
throw new Error("Max one sort key condition is allowed");
|
|
53
|
+
this.sortKeyExpression =
|
|
54
|
+
operator === "begins_with"
|
|
55
|
+
? `begins_with(${attrName}, ${valueName})`
|
|
56
|
+
: `${attrName} ${operator} ${valueName}`;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
addFilterCondition(attr, operator, value, options = {}) {
|
|
60
|
+
// See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Syntax
|
|
61
|
+
(0, assert_1.default)(typeof attr === "string", "Invalid attribute");
|
|
62
|
+
const attrName = this.createAttributeName(attr);
|
|
63
|
+
const expectArrayOfValues = operator === "IN";
|
|
64
|
+
const valueArr = Array.isArray(value) ? value : [value];
|
|
65
|
+
if (valueArr.length > 1 && !expectArrayOfValues) {
|
|
66
|
+
throw new Error("QueryCommandBuilder.addFilterCondition: invalid value - array not expected for this operation");
|
|
67
|
+
}
|
|
68
|
+
const valueNames = valueArr.map((value) => this.createAttributeValue(attr, value));
|
|
69
|
+
const rightOperand = expectArrayOfValues
|
|
70
|
+
? `(${valueNames.join(", ")})`
|
|
71
|
+
: valueNames[0];
|
|
72
|
+
const condition = `${attrName} ${operator} ${rightOperand}`;
|
|
73
|
+
const expression = options.not ? `NOT (${condition})` : condition;
|
|
74
|
+
this.filterExpression.push(expression);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Add existential filter expression condition
|
|
79
|
+
*/
|
|
80
|
+
addExistentialFilterCondition(attr, operator) {
|
|
81
|
+
// See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Syntax
|
|
82
|
+
let existentialExpression;
|
|
83
|
+
const attributeName = this.createAttributeName(attr);
|
|
84
|
+
if (operator === "exists") {
|
|
85
|
+
existentialExpression = `attribute_exists(${attributeName})`;
|
|
86
|
+
}
|
|
87
|
+
if (operator === "not_exists") {
|
|
88
|
+
existentialExpression = `attribute_not_exists(${attributeName})`;
|
|
89
|
+
}
|
|
90
|
+
if (existentialExpression)
|
|
91
|
+
this.filterExpression.push(existentialExpression);
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Add filter condition based on array size using DynamoDB's size() function.
|
|
96
|
+
*
|
|
97
|
+
* @param attr - The attribute to filter by size (should be an array)
|
|
98
|
+
* @param condition - The size condition with value and operator
|
|
99
|
+
* @param options - Optional settings like negation
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* // Filter by array size
|
|
104
|
+
* builder.addArraySizeFilterCondition("shipmentItems", { value: 5, operator: ">" })
|
|
105
|
+
* // Generates: size(#shipmentItems) > :shipmentItems
|
|
106
|
+
*
|
|
107
|
+
* // Works with all comparison operators
|
|
108
|
+
* builder.addArraySizeFilterCondition("tags", { value: 0, operator: "<>" })
|
|
109
|
+
* builder.addArraySizeFilterCondition("items", { value: 10, operator: "<=" })
|
|
110
|
+
*
|
|
111
|
+
* // Can be negated
|
|
112
|
+
* builder.addArraySizeFilterCondition("items", { value: 0, operator: "=" }, { not: true })
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
addArraySizeFilterCondition(attr, condition, options = {}) {
|
|
116
|
+
(0, assert_1.default)(typeof attr === "string", "Invalid attribute");
|
|
117
|
+
const attrName = this.createAttributeName(attr);
|
|
118
|
+
const valueName = this.createAttributeValue(attr, condition.value);
|
|
119
|
+
const filterExpression = `size(${attrName}) ${condition.operator} ${valueName}`;
|
|
120
|
+
const expression = options.not
|
|
121
|
+
? `NOT (${filterExpression})`
|
|
122
|
+
: filterExpression;
|
|
123
|
+
this.filterExpression.push(expression);
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Add attributes which should be returned in result (by default all attributes are returned).
|
|
128
|
+
*/
|
|
129
|
+
addProjectionAttributes(attrs) {
|
|
130
|
+
attrs.forEach((attr) => {
|
|
131
|
+
const attrName = this.createAttributeName(attr);
|
|
132
|
+
this.projectionExpression.push(attrName);
|
|
133
|
+
});
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Build command input
|
|
138
|
+
*/
|
|
139
|
+
buildCommandInput() {
|
|
140
|
+
if (!this.partitionKeyExpression) {
|
|
141
|
+
throw new Error("Missing partition key condition");
|
|
142
|
+
}
|
|
143
|
+
const keyConditionExpression = this.sortKeyExpression
|
|
144
|
+
? `${this.partitionKeyExpression} AND ${this.sortKeyExpression}`
|
|
145
|
+
: this.partitionKeyExpression;
|
|
146
|
+
return {
|
|
147
|
+
...this.commandOptions,
|
|
148
|
+
KeyConditionExpression: keyConditionExpression,
|
|
149
|
+
ExpressionAttributeNames: this.expressionAttributeNames,
|
|
150
|
+
ExpressionAttributeValues: this.expressionAttributeValues,
|
|
151
|
+
...(this.projectionExpression.length > 0 && {
|
|
152
|
+
Select: "SPECIFIC_ATTRIBUTES",
|
|
153
|
+
ProjectionExpression: this.projectionExpression.join(", "),
|
|
154
|
+
}),
|
|
155
|
+
...(this.filterExpression.length > 0 && {
|
|
156
|
+
FilterExpression: this.filterExpression.join(" AND "),
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exports.QueryCommandBuilder = QueryCommandBuilder;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DynamoDBDocumentClient, QueryCommandInput, QueryCommandOutput } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
/**
|
|
3
|
+
* Async iterator for query items.
|
|
4
|
+
* Executes query and yields array of returned items. Repeats until all items are returned.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* for await (const items of queryIterator(exclusiveStartKey => doDynamoDBQuery(exclusiveStartKey))) {
|
|
9
|
+
* console.log(items)
|
|
10
|
+
* }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function queryIterator(query: (exclusiveStartKey?: object) => Promise<QueryCommandOutput>, options?: {
|
|
14
|
+
startKey?: object;
|
|
15
|
+
}): AsyncGenerator<Record<string, any>[], void, unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Async iterator for single query items.
|
|
18
|
+
* Executes query and yields each returned item. Repeats until all items are returned.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* for await (const item of queryItemIterator(exclusiveStartKey => doDynamoDBQuery(exclusiveStartKey))) {
|
|
23
|
+
* console.log(item)
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function queryItemIterator(query: (exclusiveStartKey?: object) => Promise<QueryCommandOutput>, options?: {
|
|
28
|
+
startKey?: object;
|
|
29
|
+
}): AsyncGenerator<Record<string, any>, void, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Concatenate all query results into an array of items
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const input = { TableName: "tableName", ... }
|
|
36
|
+
* const output = await concatQueryResults(client, input)
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function concatQueryResults<T>(client: DynamoDBDocumentClient, input: QueryCommandInput, options?: {
|
|
40
|
+
startKey?: object;
|
|
41
|
+
}): Promise<T[]>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.queryIterator = queryIterator;
|
|
4
|
+
exports.queryItemIterator = queryItemIterator;
|
|
5
|
+
exports.concatQueryResults = concatQueryResults;
|
|
6
|
+
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
7
|
+
/**
|
|
8
|
+
* Async iterator for query items.
|
|
9
|
+
* Executes query and yields array of returned items. Repeats until all items are returned.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* for await (const items of queryIterator(exclusiveStartKey => doDynamoDBQuery(exclusiveStartKey))) {
|
|
14
|
+
* console.log(items)
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
async function* queryIterator(query, options = {}) {
|
|
19
|
+
let { startKey: exclusiveStartKey } = options;
|
|
20
|
+
do {
|
|
21
|
+
const result = await query(exclusiveStartKey);
|
|
22
|
+
if (result.Items) {
|
|
23
|
+
yield result.Items;
|
|
24
|
+
}
|
|
25
|
+
exclusiveStartKey = result.LastEvaluatedKey;
|
|
26
|
+
} while (exclusiveStartKey);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Async iterator for single query items.
|
|
30
|
+
* Executes query and yields each returned item. Repeats until all items are returned.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* for await (const item of queryItemIterator(exclusiveStartKey => doDynamoDBQuery(exclusiveStartKey))) {
|
|
35
|
+
* console.log(item)
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
async function* queryItemIterator(query, options = {}) {
|
|
40
|
+
for await (const result of queryIterator(query, options)) {
|
|
41
|
+
for (const item of result) {
|
|
42
|
+
yield item;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Concatenate all query results into an array of items
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const input = { TableName: "tableName", ... }
|
|
52
|
+
* const output = await concatQueryResults(client, input)
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
async function concatQueryResults(client, input, options = {}) {
|
|
56
|
+
const result = [];
|
|
57
|
+
const asyncIterator = queryItemIterator((exclusiveStartKey) => {
|
|
58
|
+
const command = new lib_dynamodb_1.QueryCommand({
|
|
59
|
+
...input,
|
|
60
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
61
|
+
});
|
|
62
|
+
return client.send(command);
|
|
63
|
+
}, options);
|
|
64
|
+
for await (const item of asyncIterator)
|
|
65
|
+
result.push(item);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { UpdateCommandInput } from "@aws-sdk/lib-dynamodb";
|
|
2
|
+
import { CommandBuilder } from "./command-builder";
|
|
3
|
+
/**
|
|
4
|
+
* Update Command Options
|
|
5
|
+
*/
|
|
6
|
+
export type UpdateCommandOptions = {
|
|
7
|
+
ifNotExists?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type ConditionOperator = "=" | "<" | "<=" | ">" | ">=" | "<>" | "IN";
|
|
10
|
+
/**
|
|
11
|
+
* Builder for update expression in DynamoDB update command
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const builder = new UpdateCommandBuilder("table", { key: "value" })
|
|
16
|
+
*
|
|
17
|
+
* builder.setAttributeValue("message", "hello")
|
|
18
|
+
* builder.addValueToAttribute("count", 1)
|
|
19
|
+
*
|
|
20
|
+
* const command = new UpdateCommand(builder.buildCommandInput())
|
|
21
|
+
* await documentClient.client.send(command)
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare class UpdateCommandBuilder<T extends Record<string, unknown>> extends CommandBuilder<T> {
|
|
25
|
+
private setExpressions;
|
|
26
|
+
private removeExpressions;
|
|
27
|
+
private addExpressions;
|
|
28
|
+
private conditionExpressions;
|
|
29
|
+
private updateParams;
|
|
30
|
+
constructor(tableName: string, key: UpdateCommandInput["Key"], options?: Partial<UpdateCommandInput>);
|
|
31
|
+
/**
|
|
32
|
+
* Set value of an attribute
|
|
33
|
+
*/
|
|
34
|
+
setAttributeValue(attr: keyof T, value: unknown, options?: UpdateCommandOptions): this;
|
|
35
|
+
/**
|
|
36
|
+
* Delete attribute
|
|
37
|
+
*/
|
|
38
|
+
removeAttribute(attr: keyof T): this;
|
|
39
|
+
/**
|
|
40
|
+
* Set values for many attributes based on object with attributes as properties
|
|
41
|
+
*/
|
|
42
|
+
setAttributesValues(obj: Partial<Record<keyof T, unknown>>, options?: UpdateCommandOptions): this;
|
|
43
|
+
/**
|
|
44
|
+
* Add numeric value to attribute.
|
|
45
|
+
* Note: if attribute has no value, DynamoDB assumes value to be zero.
|
|
46
|
+
*/
|
|
47
|
+
addValueToAttribute(attr: keyof T, value: number): this;
|
|
48
|
+
/**
|
|
49
|
+
* Create condition that an attribute exist
|
|
50
|
+
*/
|
|
51
|
+
ifAttributeExists(attr: keyof T): this;
|
|
52
|
+
/**
|
|
53
|
+
* Create condition that an attribute does not exist
|
|
54
|
+
*/
|
|
55
|
+
ifAttributeNotExists(attr: keyof T): this;
|
|
56
|
+
addCondition<K extends keyof T, O extends ConditionOperator>(attr: K, operator: O, value: O extends "IN" ? T[K][] : T[K], options?: {
|
|
57
|
+
not?: boolean;
|
|
58
|
+
}): this;
|
|
59
|
+
/**
|
|
60
|
+
* Add listAppend method to UpdateCommandBuilder class that appends a value to a list attribute in DynamoDB.
|
|
61
|
+
*/
|
|
62
|
+
listAppend<K extends keyof T>(attr: K, value: T[K]): this;
|
|
63
|
+
buildCommandInput(): UpdateCommandInput;
|
|
64
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.UpdateCommandBuilder = void 0;
|
|
7
|
+
const command_builder_1 = require("./command-builder");
|
|
8
|
+
const assert_1 = __importDefault(require("assert"));
|
|
9
|
+
/**
|
|
10
|
+
* Builder for update expression in DynamoDB update command
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const builder = new UpdateCommandBuilder("table", { key: "value" })
|
|
15
|
+
*
|
|
16
|
+
* builder.setAttributeValue("message", "hello")
|
|
17
|
+
* builder.addValueToAttribute("count", 1)
|
|
18
|
+
*
|
|
19
|
+
* const command = new UpdateCommand(builder.buildCommandInput())
|
|
20
|
+
* await documentClient.client.send(command)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
class UpdateCommandBuilder extends command_builder_1.CommandBuilder {
|
|
24
|
+
setExpressions = [];
|
|
25
|
+
removeExpressions = [];
|
|
26
|
+
addExpressions = [];
|
|
27
|
+
conditionExpressions = [];
|
|
28
|
+
updateParams;
|
|
29
|
+
constructor(tableName, key, options = {}) {
|
|
30
|
+
super();
|
|
31
|
+
this.updateParams = {
|
|
32
|
+
...options,
|
|
33
|
+
TableName: tableName,
|
|
34
|
+
Key: key,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Set value of an attribute
|
|
39
|
+
*/
|
|
40
|
+
setAttributeValue(attr, value, options = {}) {
|
|
41
|
+
const { attrName, valueName } = this.createAttributeNameAndValue(attr, value);
|
|
42
|
+
const expression = options.ifNotExists
|
|
43
|
+
? `${attrName} = if_not_exists(${attrName}, ${valueName})`
|
|
44
|
+
: `${attrName} = ${valueName}`;
|
|
45
|
+
this.setExpressions.push(expression);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Delete attribute
|
|
50
|
+
*/
|
|
51
|
+
removeAttribute(attr) {
|
|
52
|
+
const attrName = this.createAttributeName(attr);
|
|
53
|
+
this.removeExpressions.push(attrName);
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Set values for many attributes based on object with attributes as properties
|
|
58
|
+
*/
|
|
59
|
+
setAttributesValues(obj, options = {}) {
|
|
60
|
+
Object.entries(obj).forEach(([key, value]) => this.setAttributeValue(key, value, options));
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Add numeric value to attribute.
|
|
65
|
+
* Note: if attribute has no value, DynamoDB assumes value to be zero.
|
|
66
|
+
*/
|
|
67
|
+
addValueToAttribute(attr, value) {
|
|
68
|
+
const { attrName, valueName } = this.createAttributeNameAndValue(attr, value);
|
|
69
|
+
const expression = `${attrName} ${valueName}`;
|
|
70
|
+
this.addExpressions.push(expression);
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create condition that an attribute exist
|
|
75
|
+
*/
|
|
76
|
+
ifAttributeExists(attr) {
|
|
77
|
+
const attrName = this.createAttributeName(attr);
|
|
78
|
+
this.conditionExpressions.push(`attribute_exists(${attrName})`);
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create condition that an attribute does not exist
|
|
83
|
+
*/
|
|
84
|
+
ifAttributeNotExists(attr) {
|
|
85
|
+
const attrName = this.createAttributeName(attr);
|
|
86
|
+
this.conditionExpressions.push(`attribute_not_exists(${attrName})`);
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
addCondition(attr, operator, value, options = {}) {
|
|
90
|
+
// See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html#Expressions.OperatorsAndFunctions.Syntax
|
|
91
|
+
if (operator === "IN") {
|
|
92
|
+
(0, assert_1.default)(typeof attr === "string", "Invalid condition attribute, expected string");
|
|
93
|
+
(0, assert_1.default)(Array.isArray(value), "Invalid condition value, expected array");
|
|
94
|
+
const attrName = this.createAttributeName(attr);
|
|
95
|
+
const valueNames = value.map((val) => this.createAttributeValue(attr, val));
|
|
96
|
+
const condition = `${attrName} IN (${valueNames.join(", ")})`;
|
|
97
|
+
const expression = options.not ? `NOT (${condition})` : condition;
|
|
98
|
+
this.conditionExpressions.push(expression);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const { attrName, valueName } = this.createAttributeNameAndValue(attr, value);
|
|
102
|
+
const condition = `${attrName} ${operator} ${valueName}`;
|
|
103
|
+
const expression = options.not ? `NOT (${condition})` : condition;
|
|
104
|
+
this.conditionExpressions.push(expression);
|
|
105
|
+
}
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Add listAppend method to UpdateCommandBuilder class that appends a value to a list attribute in DynamoDB.
|
|
110
|
+
*/
|
|
111
|
+
listAppend(attr, value) {
|
|
112
|
+
const { attrName, valueName } = this.createAttributeNameAndValue(attr, value);
|
|
113
|
+
const expression = `${attrName} = list_append(${attrName}, ${valueName})`;
|
|
114
|
+
this.setExpressions.push(expression);
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
buildCommandInput() {
|
|
118
|
+
let expression = "";
|
|
119
|
+
if (this.setExpressions.length > 0) {
|
|
120
|
+
expression += `SET ${this.setExpressions.join(", ")} `;
|
|
121
|
+
}
|
|
122
|
+
if (this.removeExpressions.length > 0) {
|
|
123
|
+
expression += `REMOVE ${this.removeExpressions.join(", ")} `;
|
|
124
|
+
}
|
|
125
|
+
if (this.addExpressions.length > 0) {
|
|
126
|
+
expression += `ADD ${this.addExpressions.join(", ")} `;
|
|
127
|
+
}
|
|
128
|
+
let condition;
|
|
129
|
+
if (this.conditionExpressions.length > 0) {
|
|
130
|
+
condition = this.conditionExpressions.join(" AND ");
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...this.updateParams,
|
|
134
|
+
UpdateExpression: expression,
|
|
135
|
+
ExpressionAttributeNames: this.expressionAttributeNames,
|
|
136
|
+
ExpressionAttributeValues: this.expressionAttributeValues,
|
|
137
|
+
...(condition && { ConditionExpression: condition }),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.UpdateCommandBuilder = UpdateCommandBuilder;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./common/logger-interface";
|
|
2
|
+
export * from "./dynamodb/default-table-fields";
|
|
3
|
+
export * from "./dynamodb/get-many";
|
|
4
|
+
export * from "./dynamodb/query-command-builder";
|
|
5
|
+
export * from "./dynamodb/query-iterator";
|
|
6
|
+
export * from "./dynamodb/iterators";
|
|
7
|
+
export * from "./dynamodb/update-command-builder";
|
|
8
|
+
export * from "./streams/stream-handler";
|
|
9
|
+
export * from "./utils/compound-key";
|
|
10
|
+
export * from "./utils/create-ttl";
|
|
11
|
+
export * from "./utils/date-time";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./common/logger-interface"), exports);
|
|
18
|
+
__exportStar(require("./dynamodb/default-table-fields"), exports);
|
|
19
|
+
__exportStar(require("./dynamodb/get-many"), exports);
|
|
20
|
+
__exportStar(require("./dynamodb/query-command-builder"), exports);
|
|
21
|
+
__exportStar(require("./dynamodb/query-iterator"), exports);
|
|
22
|
+
__exportStar(require("./dynamodb/iterators"), exports);
|
|
23
|
+
__exportStar(require("./dynamodb/update-command-builder"), exports);
|
|
24
|
+
__exportStar(require("./streams/stream-handler"), exports);
|
|
25
|
+
__exportStar(require("./utils/compound-key"), exports);
|
|
26
|
+
__exportStar(require("./utils/create-ttl"), exports);
|
|
27
|
+
__exportStar(require("./utils/date-time"), exports);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Context, DynamoDBRecord, DynamoDBStreamEvent } from "aws-lambda";
|
|
2
|
+
import { ILogger } from "../common/logger-interface";
|
|
3
|
+
import { DynamoDBPersistenceLayer } from "@aws-lambda-powertools/idempotency/dynamodb";
|
|
4
|
+
import { BasePersistenceLayer } from "@aws-lambda-powertools/idempotency/persistence";
|
|
5
|
+
import { DynamoDBPersistenceOptions, IdempotencyConfigOptions } from "@aws-lambda-powertools/idempotency/types";
|
|
6
|
+
/**
|
|
7
|
+
* Create standard Lambda handler for DynamoDB Stream events
|
|
8
|
+
*
|
|
9
|
+
* @examples
|
|
10
|
+
* ```ts
|
|
11
|
+
* const handler = createDynamoDbStreamHandler<TableRecord>(
|
|
12
|
+
* onInsert: (record) => handleInsert(record),
|
|
13
|
+
* // ...
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare function createDynamoDbStreamHandler<T>(options: DynamoDbStreamHandlerOptions<T>): (event: DynamoDBStreamEvent, context: Context) => Promise<void>;
|
|
18
|
+
export type DynamoDbStreamHandlerOptions<T> = {
|
|
19
|
+
/** Handle insertion of an item into DynamoDB table */
|
|
20
|
+
onInsert?: (item: T) => Promise<void>;
|
|
21
|
+
/** Handle update of an item into DynamoDB table */
|
|
22
|
+
onModify?: (oldItem: T, newItem: T) => Promise<void>;
|
|
23
|
+
/** Handle removal of an item into DynamoDB table */
|
|
24
|
+
onRemove?: (item: T) => Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Handle errors in onInsert, onModify and onRemove handlers.
|
|
27
|
+
* If not used then error is thrown
|
|
28
|
+
* Note: when used onError handler the idempotency management will prevent retry on the same record!
|
|
29
|
+
*/
|
|
30
|
+
onError?: (record: DynamoDBRecord, error: unknown) => Promise<void | unknown> | void | unknown;
|
|
31
|
+
/**
|
|
32
|
+
* Logger instance
|
|
33
|
+
*/
|
|
34
|
+
logger?: ILogger;
|
|
35
|
+
/**
|
|
36
|
+
* Optional store for idempotency handling using @aws-lambda-powertools/idempotency
|
|
37
|
+
* If not specified then idempotency is not managed.
|
|
38
|
+
*/
|
|
39
|
+
idempotencyPersistanceStore?: BasePersistenceLayer;
|
|
40
|
+
/**
|
|
41
|
+
* Optional configuration for idempotency
|
|
42
|
+
*/
|
|
43
|
+
idempotencyConfig?: IdempotencyConfigOptions;
|
|
44
|
+
/**
|
|
45
|
+
* Process batch items concurrently. Default is false, i.e. sequentially
|
|
46
|
+
* Should not be used if the processing order if items is important.
|
|
47
|
+
*/
|
|
48
|
+
concurrentBatchProcessing?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* If exception is handled by onError then the exception is
|
|
51
|
+
*/
|
|
52
|
+
consumeHandledExceptions?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Enable logging of incoming events, default is true
|
|
55
|
+
*/
|
|
56
|
+
logEvents?: boolean;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Helper to create DynamoDB persistance store to keep idempotency records.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* createDefaultIdempotencyPersistanceStore({
|
|
65
|
+
* tableName: "MyIdempotencyTable", // Default is process.env.IDEMPOTENCY_TABLE
|
|
66
|
+
* awsSdkV3Client: myDynamoDBClient, // Default is a new instance of DynamoDb client
|
|
67
|
+
* })
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function createDefaultIdempotencyPersistanceStore(config?: Partial<DynamoDBPersistenceOptions>): DynamoDBPersistenceLayer;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createDynamoDbStreamHandler = createDynamoDbStreamHandler;
|
|
7
|
+
exports.createDefaultIdempotencyPersistanceStore = createDefaultIdempotencyPersistanceStore;
|
|
8
|
+
const util_dynamodb_1 = require("@aws-sdk/util-dynamodb");
|
|
9
|
+
const logger_1 = require("../common/logger");
|
|
10
|
+
const dynamodb_1 = require("@aws-lambda-powertools/idempotency/dynamodb");
|
|
11
|
+
const idempotency_1 = require("@aws-lambda-powertools/idempotency");
|
|
12
|
+
const assert_1 = __importDefault(require("assert"));
|
|
13
|
+
/**
|
|
14
|
+
* Create standard Lambda handler for DynamoDB Stream events
|
|
15
|
+
*
|
|
16
|
+
* @examples
|
|
17
|
+
* ```ts
|
|
18
|
+
* const handler = createDynamoDbStreamHandler<TableRecord>(
|
|
19
|
+
* onInsert: (record) => handleInsert(record),
|
|
20
|
+
* // ...
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function createDynamoDbStreamHandler(options) {
|
|
25
|
+
const { logger = logger_1.logger, idempotencyPersistanceStore, idempotencyConfig = {}, logEvents = true, } = options;
|
|
26
|
+
/*
|
|
27
|
+
* Enable idempotency if persistance store is defined
|
|
28
|
+
*/
|
|
29
|
+
const enableIdempotency = idempotencyPersistanceStore !== undefined;
|
|
30
|
+
/*
|
|
31
|
+
* Configuration for powertools idempotency helper
|
|
32
|
+
*/
|
|
33
|
+
const config = new idempotency_1.IdempotencyConfig({
|
|
34
|
+
eventKeyJmesPath: "eventID", // JMES path to idempotency key, see https://jmespath.org/
|
|
35
|
+
throwOnNoIdempotencyKey: true,
|
|
36
|
+
...idempotencyConfig,
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Wrap handler with idempotency layer
|
|
40
|
+
*/
|
|
41
|
+
const idempotencyWrapper = (handler) => {
|
|
42
|
+
if (enableIdempotency) {
|
|
43
|
+
return (0, idempotency_1.makeIdempotent)(handler, {
|
|
44
|
+
persistenceStore: idempotencyPersistanceStore,
|
|
45
|
+
config,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return handler;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Handlers for insert / modify / remove events
|
|
52
|
+
*/
|
|
53
|
+
const handleInsert = idempotencyWrapper(async (record) => {
|
|
54
|
+
(0, assert_1.default)(options.onInsert);
|
|
55
|
+
assertImage(record.dynamodb?.NewImage);
|
|
56
|
+
await options.onInsert((0, util_dynamodb_1.unmarshall)(record.dynamodb?.NewImage));
|
|
57
|
+
return `INSERT eventID=${record.eventID}`; // Idempotency handler expect handler to return some value which is then stored in persistance store
|
|
58
|
+
});
|
|
59
|
+
const handleModify = idempotencyWrapper(async (record) => {
|
|
60
|
+
(0, assert_1.default)(options.onModify);
|
|
61
|
+
assertImage(record.dynamodb?.OldImage);
|
|
62
|
+
assertImage(record.dynamodb?.NewImage);
|
|
63
|
+
await options.onModify((0, util_dynamodb_1.unmarshall)(record.dynamodb?.OldImage), (0, util_dynamodb_1.unmarshall)(record.dynamodb?.NewImage));
|
|
64
|
+
return `MODIFY eventID=${record.eventID}`; // Idempotency handler expect handler to return some value which is then stored in persistance store
|
|
65
|
+
});
|
|
66
|
+
const handleRemove = idempotencyWrapper(async (record) => {
|
|
67
|
+
(0, assert_1.default)(options.onRemove);
|
|
68
|
+
assertImage(record.dynamodb?.OldImage);
|
|
69
|
+
await options.onRemove((0, util_dynamodb_1.unmarshall)(record.dynamodb?.OldImage));
|
|
70
|
+
return `REMOVE eventID=${record.eventID}`; // Idempotency handler expect handler to return some value which is then stored in persistance store
|
|
71
|
+
});
|
|
72
|
+
/*
|
|
73
|
+
* Dispatch event to appropriate handler
|
|
74
|
+
*/
|
|
75
|
+
const dispatchEventToHandler = async (record) => {
|
|
76
|
+
try {
|
|
77
|
+
switch (record.eventName) {
|
|
78
|
+
case "INSERT":
|
|
79
|
+
if (options.onInsert)
|
|
80
|
+
await handleInsert(record);
|
|
81
|
+
break;
|
|
82
|
+
case "MODIFY":
|
|
83
|
+
if (options.onModify)
|
|
84
|
+
await handleModify(record);
|
|
85
|
+
break;
|
|
86
|
+
case "REMOVE":
|
|
87
|
+
if (options.onRemove)
|
|
88
|
+
await handleRemove(record);
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
throw new Error(`Event ${record.eventName} not expected`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (options.onError) {
|
|
96
|
+
// onError can be either synchronous or asynchronous
|
|
97
|
+
await options.onError(record, error);
|
|
98
|
+
// Ignore error if consumeHandledExceptions is true
|
|
99
|
+
if (options.consumeHandledExceptions) {
|
|
100
|
+
return "ERROR"; // Idempotency handler from powertools expect that handler returns some result
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
/*
|
|
107
|
+
* Lambda function handler managing DynamoDB stream events
|
|
108
|
+
*/
|
|
109
|
+
const lambdaHandler = async (event, context) => {
|
|
110
|
+
if (logEvents)
|
|
111
|
+
logger.info("Incoming event", { batchSize: event?.Records.length, event });
|
|
112
|
+
/*
|
|
113
|
+
* Idempotency handler is using context to get Lambda timeout
|
|
114
|
+
*/
|
|
115
|
+
config.registerLambdaContext(context);
|
|
116
|
+
if (options.concurrentBatchProcessing) {
|
|
117
|
+
/*
|
|
118
|
+
* Process batch concurrently, try to process all items before reporting an error
|
|
119
|
+
*/
|
|
120
|
+
const settled = await Promise.allSettled(event.Records.map((record) => dispatchEventToHandler(record)));
|
|
121
|
+
if (settled.some((result) => result.status === "rejected")) {
|
|
122
|
+
throw new Error("Some batch items failed");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
/*
|
|
127
|
+
* Process batch sequentially, breaks on first error
|
|
128
|
+
*/
|
|
129
|
+
for (const record of event.Records) {
|
|
130
|
+
await dispatchEventToHandler(record);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return lambdaHandler;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Helper to create DynamoDB persistance store to keep idempotency records.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* createDefaultIdempotencyPersistanceStore({
|
|
143
|
+
* tableName: "MyIdempotencyTable", // Default is process.env.IDEMPOTENCY_TABLE
|
|
144
|
+
* awsSdkV3Client: myDynamoDBClient, // Default is a new instance of DynamoDb client
|
|
145
|
+
* })
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
function createDefaultIdempotencyPersistanceStore(config = {}) {
|
|
149
|
+
const { tableName = process.env.IDEMPOTENCY_TABLE } = config;
|
|
150
|
+
if (!tableName)
|
|
151
|
+
throw new Error("createDefaultIdempotencyPersistanceStore: invalid table name, use options or env IDEMPOTENCY_TABLE");
|
|
152
|
+
return new dynamodb_1.DynamoDBPersistenceLayer({
|
|
153
|
+
tableName,
|
|
154
|
+
...config,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check DynamoDB Stream record and cast it to proper type for unmarshall function
|
|
159
|
+
*/
|
|
160
|
+
function assertImage(value) {
|
|
161
|
+
if (value === null || typeof value !== "object") {
|
|
162
|
+
throw new Error("Invalid image in DynamoDB Stream record ");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/index.ts","../src/common/logger-interface.ts","../src/common/logger.ts","../src/dynamodb/command-builder.ts","../src/dynamodb/default-table-fields.ts","../src/dynamodb/get-many.ts","../src/dynamodb/iterators.ts","../src/dynamodb/query-command-builder.ts","../src/dynamodb/query-iterator.ts","../src/dynamodb/update-command-builder.ts","../src/streams/stream-handler.ts","../src/utils/compound-key.ts","../src/utils/create-ttl.ts","../src/utils/date-time.ts"],"version":"5.9.3"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create compound key from object
|
|
3
|
+
*/
|
|
4
|
+
export declare function createCompoundKey(key: Record<string, string>): string;
|
|
5
|
+
/**
|
|
6
|
+
* Parse compound key into an object
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseCompoundKey<K extends string>(compoundKey: string, keys: K[]): {
|
|
9
|
+
[index in K]: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Create compound key from object
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createCompoundKey = createCompoundKey;
|
|
7
|
+
exports.parseCompoundKey = parseCompoundKey;
|
|
8
|
+
function createCompoundKey(key) {
|
|
9
|
+
return Object.values(key).join("#");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse compound key into an object
|
|
13
|
+
*/
|
|
14
|
+
function parseCompoundKey(compoundKey, keys) {
|
|
15
|
+
const parts = compoundKey.split("#");
|
|
16
|
+
if (parts.length !== keys.length) {
|
|
17
|
+
throw new Error(`Cannot parse compound key "${compoundKey}", expected ${keys.length} elements got ${parts.length}.`);
|
|
18
|
+
}
|
|
19
|
+
return Object.fromEntries(keys.map((key, index) => [key, parts[index]]));
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Create AWS TTL value
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createTTL = createTTL;
|
|
7
|
+
function createTTL(ttl) {
|
|
8
|
+
if (ttl === null)
|
|
9
|
+
return null;
|
|
10
|
+
const ttlSeconds = ((ttl.days || 0) * 24 + (ttl.hours || 0)) * 3600 +
|
|
11
|
+
(ttl.minutes || 0) * 60 +
|
|
12
|
+
(ttl.seconds || 0);
|
|
13
|
+
return Math.floor(Date.now() / 1000) + ttlSeconds;
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Create current date and time in default ISO format.
|
|
4
|
+
* Default format is supposed to be used to store dates in DynamoDB tables.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.getCurrentDateTimeISO = getCurrentDateTimeISO;
|
|
8
|
+
function getCurrentDateTimeISO(options = {}) {
|
|
9
|
+
if (options.offsetMilliseconds) {
|
|
10
|
+
const now = Date.now() + options.offsetMilliseconds;
|
|
11
|
+
return new Date(now).toISOString();
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
return new Date().toISOString();
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tomassabol/dynamodb-tools",
|
|
3
|
+
"version": "1.0.19",
|
|
4
|
+
"description": "Tools for working with DynamoDB using AWS SDK v3",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/*"
|
|
9
|
+
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build-doc": "typedoc",
|
|
15
|
+
"build": "tsc -b tsconfig.build.json",
|
|
16
|
+
"clean": "rimraf dist/",
|
|
17
|
+
"format-check": "prettier --check .",
|
|
18
|
+
"husky-install": "husky install",
|
|
19
|
+
"lint": "eslint . --max-warnings 0",
|
|
20
|
+
"open-coverage": "open .coverage/lcov-report/index.html",
|
|
21
|
+
"prepare": "npm-run-all husky-install lint clean build",
|
|
22
|
+
"test": "jest"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@aws-lambda-powertools/idempotency": "^2.28.1",
|
|
26
|
+
"@aws-sdk/lib-dynamodb": "^3.914.0",
|
|
27
|
+
"@aws-sdk/util-dynamodb": "^3.914.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@tsconfig/node22": "^22.0.2",
|
|
31
|
+
"@types/aws-lambda": "^8.10.114",
|
|
32
|
+
"@types/jest": "^29.4.0",
|
|
33
|
+
"@types/node": "^22.18.12",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "^5.25.0",
|
|
35
|
+
"@typescript-eslint/parser": "^5.25.0",
|
|
36
|
+
"aws-lambda": "^1.0.7",
|
|
37
|
+
"esbuild-jest": "^0.4.0",
|
|
38
|
+
"eslint": "^8.11.0",
|
|
39
|
+
"eslint-config-prettier": "^8.3.0",
|
|
40
|
+
"eslint-plugin-import": "^2.25.4",
|
|
41
|
+
"eslint-plugin-json": "^3.1.0",
|
|
42
|
+
"eslint-plugin-markdown": "^3.0.0",
|
|
43
|
+
"husky": "^8.0.1",
|
|
44
|
+
"jest": "^29.4.3",
|
|
45
|
+
"jest-cli": "^29.4.3",
|
|
46
|
+
"jest-sonar-reporter": "^2.0.0",
|
|
47
|
+
"lint-staged": "^13.1.2",
|
|
48
|
+
"npm-run-all": "^4.1.5",
|
|
49
|
+
"prettier": "^2.6.2",
|
|
50
|
+
"prettier-plugin-sh": "^0.12.8",
|
|
51
|
+
"rimraf": "^4.1.2",
|
|
52
|
+
"typedoc": "^0.23.28",
|
|
53
|
+
"typedoc-plugin-markdown": "^3.14.0",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
55
|
+
},
|
|
56
|
+
"jestSonar": {
|
|
57
|
+
"reportPath": ".sonar",
|
|
58
|
+
"reportFile": "test-report.xml"
|
|
59
|
+
}
|
|
60
|
+
}
|