@moicky/dynamodb 2.6.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/client.js +5 -29
- package/dist/lib/config.d.ts +78 -0
- package/dist/lib/config.js +77 -0
- package/dist/lib/helpers.d.ts +1 -1
- package/dist/lib/helpers.js +5 -5
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/lib/schemas.js +3 -3
- package/dist/operations/delete.js +1 -1
- package/dist/operations/get.js +1 -1
- package/dist/operations/put.js +4 -4
- package/dist/operations/query.js +1 -1
- package/dist/operations/transactWriteItems.js +52 -35
- package/dist/operations/update.js +1 -1
- package/dist/transactions/index.d.ts +11 -5
- package/dist/transactions/index.js +45 -14
- package/dist/types.d.ts +2 -2
- package/package.json +2 -3
- package/readme.md +53 -11
package/dist/lib/client.js
CHANGED
|
@@ -2,38 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getClient = exports.initClient = void 0;
|
|
4
4
|
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
5
|
+
const oidc_1 = require("@vercel/functions/oidc");
|
|
5
6
|
const schemas_1 = require("./schemas");
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
if (!process.env.AWS_ROLE_ARN)
|
|
9
|
-
return {};
|
|
10
|
-
return {
|
|
11
|
-
credentials: import("@vercel/functions/oidc").then(({ awsCredentialsProvider }) => ({
|
|
12
|
-
credentials: awsCredentialsProvider({
|
|
13
|
-
roleArn: process.env.AWS_ROLE_ARN,
|
|
14
|
-
roleSessionName: "moicky-dynamodb",
|
|
15
|
-
}),
|
|
16
|
-
})),
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
assumeRole: () => {
|
|
20
|
-
if (!process.env.DYNAMODB_ASSUME_ROLE)
|
|
21
|
-
return {};
|
|
22
|
-
return {
|
|
23
|
-
credentials: import("@aws-sdk/credential-providers").then(({ fromTemporaryCredentials }) => fromTemporaryCredentials({
|
|
24
|
-
params: {
|
|
25
|
-
RoleArn: process.env.DYNAMODB_ASSUME_ROLE,
|
|
26
|
-
RoleSessionName: "moicky-dynamodb",
|
|
27
|
-
},
|
|
28
|
-
})),
|
|
29
|
-
};
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
const _authType = process.env.MOICKY_DYNAMODB_AWS_ROLE;
|
|
33
|
-
const authType = authTypes[_authType];
|
|
7
|
+
const authType = process.env.MOICKY_DYNAMODB_AWS_ROLE;
|
|
8
|
+
const roleArn = process.env.AWS_ROLE_ARN;
|
|
34
9
|
let client = new client_dynamodb_1.DynamoDBClient({
|
|
35
10
|
region: process.env.AWS_REGION || "eu-central-1",
|
|
36
|
-
...(authType &&
|
|
11
|
+
...(authType === "vercel" &&
|
|
12
|
+
roleArn && { credentials: (0, oidc_1.awsCredentialsProvider)({ roleArn }) }),
|
|
37
13
|
});
|
|
38
14
|
const defaultTable = process.env.DYNAMODB_TABLE;
|
|
39
15
|
if (defaultTable) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { QueryCommandInput, ScanCommandInput } from "@aws-sdk/client-dynamodb";
|
|
2
|
+
import { marshall, marshallOptions, unmarshall, unmarshallOptions } from "@aws-sdk/util-dynamodb";
|
|
3
|
+
import { DynamoDBItem } from "../types";
|
|
4
|
+
/**
|
|
5
|
+
* DynamoDBConfig is a collection of fixes or configurations for DynamoDB and this package.
|
|
6
|
+
* @property disableConsistantReadWhenUsingIndexes - Disables ConsistentRead when using indexes.
|
|
7
|
+
* @property marshallOptions - Options to pass to the [marshall](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/interfaces/_aws_sdk_util_dynamodb.marshallOptions.html) function.
|
|
8
|
+
* @property unmarshallOptions - Options to pass to the [unmarshall](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/interfaces/_aws_sdk_util_dynamodb.unmarshallOptions.html) function.
|
|
9
|
+
* @property splitTransactionsIfAboveLimit - Splits a transaction into multiple, if more than [100 operations](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html) are provided.
|
|
10
|
+
* @property itemModificationTimestamp - A function that returns the value of the `createdAt` and `updatedAt` attributes for an item when it is created or updated.
|
|
11
|
+
* @example
|
|
12
|
+
* ```javascript
|
|
13
|
+
* initConfig({
|
|
14
|
+
* disableConsistantReadWhenUsingIndexes: {
|
|
15
|
+
* enabled: true, // default,
|
|
16
|
+
*
|
|
17
|
+
* // Won't disable ConsistantRead if IndexName is specified here.
|
|
18
|
+
* stillUseOnLocalIndexes: ["localIndexName1", "localIndexName1"],
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare interface DynamoDBConfig {
|
|
24
|
+
disableConsistantReadWhenUsingIndexes?: {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
stillUseOnLocalIndexes?: string[];
|
|
27
|
+
};
|
|
28
|
+
marshallOptions?: marshallOptions;
|
|
29
|
+
unmarshallOptions?: unmarshallOptions;
|
|
30
|
+
splitTransactionsIfAboveLimit?: boolean;
|
|
31
|
+
itemModificationTimestamp?: (field: "createdAt" | "updatedAt") => any;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Initializes the {@link DynamoDBConfig} to use for all operations.
|
|
35
|
+
* @param newConfig - The new {@link DynamoDBConfig} to use, will be merged with the default config.
|
|
36
|
+
* @returns void
|
|
37
|
+
*/
|
|
38
|
+
export declare const initConfig: (newConfig: DynamoDBConfig) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Returns the current {@link DynamoDBConfig} used for all operations.
|
|
41
|
+
* @returns The current {@link DynamoDBConfig}
|
|
42
|
+
* @private
|
|
43
|
+
*/
|
|
44
|
+
export declare const getConfig: () => DynamoDBConfig;
|
|
45
|
+
/**
|
|
46
|
+
* Applies fixes & configurations for the arguments for Query/Scan operations.
|
|
47
|
+
* @param args - The arguments to override the default arguments with
|
|
48
|
+
* @returns The merged arguments
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
export declare const withConfig: (args: Partial<QueryCommandInput> | Partial<ScanCommandInput>) => Partial<QueryCommandInput> | Partial<ScanCommandInput>;
|
|
52
|
+
/**
|
|
53
|
+
* Returns the default {@link DynamoDBConfig} used for all operations.
|
|
54
|
+
* @returns The current {@link DynamoDBConfig}
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
export declare const getDefaultConfig: () => DynamoDBConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Marshalls the input using {@link marshall} with the global options.
|
|
60
|
+
* @param input - The input to marshall
|
|
61
|
+
* @returns The marshalled input
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
export declare const marshallWithOptions: (input: Parameters<typeof marshall>[0]) => Record<string, import("@aws-sdk/client-dynamodb").AttributeValue>;
|
|
65
|
+
/**
|
|
66
|
+
* Unmarshalls the input using {@link unmarshall} with the global options.
|
|
67
|
+
* @param input - The input to unmarshall
|
|
68
|
+
* @returns The unmarshalled input
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
export declare const unmarshallWithOptions: <T extends DynamoDBItem = DynamoDBItem>(input: Parameters<typeof unmarshall>[0]) => T;
|
|
72
|
+
/**
|
|
73
|
+
* Returns the timestamp for the item modification field.
|
|
74
|
+
* @param field - The field to get the timestamp for
|
|
75
|
+
* @returns The timestamp
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
export declare const getItemModificationTimestamp: (field: "createdAt" | "updatedAt") => any;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getItemModificationTimestamp = exports.unmarshallWithOptions = exports.marshallWithOptions = exports.getDefaultConfig = exports.withConfig = exports.getConfig = exports.initConfig = void 0;
|
|
4
|
+
const util_dynamodb_1 = require("@aws-sdk/util-dynamodb");
|
|
5
|
+
const defaultConfig = Object.freeze({
|
|
6
|
+
disableConsistantReadWhenUsingIndexes: {
|
|
7
|
+
enabled: true,
|
|
8
|
+
},
|
|
9
|
+
itemModificationTimestamp: (_) => Date.now(),
|
|
10
|
+
});
|
|
11
|
+
let config = defaultConfig;
|
|
12
|
+
/**
|
|
13
|
+
* Initializes the {@link DynamoDBConfig} to use for all operations.
|
|
14
|
+
* @param newConfig - The new {@link DynamoDBConfig} to use, will be merged with the default config.
|
|
15
|
+
* @returns void
|
|
16
|
+
*/
|
|
17
|
+
const initConfig = (newConfig) => {
|
|
18
|
+
config = { ...defaultConfig, ...newConfig };
|
|
19
|
+
};
|
|
20
|
+
exports.initConfig = initConfig;
|
|
21
|
+
/**
|
|
22
|
+
* Returns the current {@link DynamoDBConfig} used for all operations.
|
|
23
|
+
* @returns The current {@link DynamoDBConfig}
|
|
24
|
+
* @private
|
|
25
|
+
*/
|
|
26
|
+
const getConfig = () => config;
|
|
27
|
+
exports.getConfig = getConfig;
|
|
28
|
+
const handleIndex = (args) => {
|
|
29
|
+
if (config?.disableConsistantReadWhenUsingIndexes?.enabled &&
|
|
30
|
+
args?.IndexName &&
|
|
31
|
+
args?.ConsistentRead &&
|
|
32
|
+
!config?.disableConsistantReadWhenUsingIndexes?.stillUseOnLocalIndexes?.includes(args?.IndexName)) {
|
|
33
|
+
args.ConsistentRead = false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Applies fixes & configurations for the arguments for Query/Scan operations.
|
|
38
|
+
* @param args - The arguments to override the default arguments with
|
|
39
|
+
* @returns The merged arguments
|
|
40
|
+
* @private
|
|
41
|
+
*/
|
|
42
|
+
const withConfig = (args) => {
|
|
43
|
+
handleIndex(args);
|
|
44
|
+
return args;
|
|
45
|
+
};
|
|
46
|
+
exports.withConfig = withConfig;
|
|
47
|
+
/**
|
|
48
|
+
* Returns the default {@link DynamoDBConfig} used for all operations.
|
|
49
|
+
* @returns The current {@link DynamoDBConfig}
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
const getDefaultConfig = () => defaultConfig;
|
|
53
|
+
exports.getDefaultConfig = getDefaultConfig;
|
|
54
|
+
/**
|
|
55
|
+
* Marshalls the input using {@link marshall} with the global options.
|
|
56
|
+
* @param input - The input to marshall
|
|
57
|
+
* @returns The marshalled input
|
|
58
|
+
* @private
|
|
59
|
+
*/
|
|
60
|
+
const marshallWithOptions = (input) => (0, util_dynamodb_1.marshall)(input, config.marshallOptions);
|
|
61
|
+
exports.marshallWithOptions = marshallWithOptions;
|
|
62
|
+
/**
|
|
63
|
+
* Unmarshalls the input using {@link unmarshall} with the global options.
|
|
64
|
+
* @param input - The input to unmarshall
|
|
65
|
+
* @returns The unmarshalled input
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
const unmarshallWithOptions = (input) => (0, util_dynamodb_1.unmarshall)(input, config.unmarshallOptions);
|
|
69
|
+
exports.unmarshallWithOptions = unmarshallWithOptions;
|
|
70
|
+
/**
|
|
71
|
+
* Returns the timestamp for the item modification field.
|
|
72
|
+
* @param field - The field to get the timestamp for
|
|
73
|
+
* @returns The timestamp
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
const getItemModificationTimestamp = (field) => config.itemModificationTimestamp?.(field) ?? Date.now();
|
|
77
|
+
exports.getItemModificationTimestamp = getItemModificationTimestamp;
|
package/dist/lib/helpers.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare function stripKey(key: Record<string, any>, args?: {
|
|
2
2
|
TableName?: string;
|
|
3
3
|
}): Record<string, import("@aws-sdk/client-dynamodb").AttributeValue>;
|
|
4
|
-
export declare function splitEvery<T>(items: T[], limit
|
|
4
|
+
export declare function splitEvery<T>(items: T[], limit: number): T[][];
|
|
5
5
|
export declare function getAttributeValues(key: Record<string, any>, { attributesToGet, prefix, }?: {
|
|
6
6
|
attributesToGet?: string[];
|
|
7
7
|
prefix?: string;
|
package/dist/lib/helpers.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getItemKey = exports.ExpressionAttributes = exports.getAttributesFromExpression = exports.getAttributeNames = exports.getAttributeValues = exports.splitEvery = exports.stripKey = void 0;
|
|
4
|
-
const
|
|
4
|
+
const config_1 = require("./config");
|
|
5
5
|
const schemas_1 = require("./schemas");
|
|
6
6
|
// Since dynamo only accepts key atrtributes which are described in table schema
|
|
7
7
|
// we remove any other attributes from the item if they are present and passed as
|
|
8
8
|
// key parameter to any of the functions below (getItem, updateItem, deleteItem...)
|
|
9
9
|
function stripKey(key, args) {
|
|
10
10
|
const { hash, range } = (0, schemas_1.getTableSchema)(args?.TableName);
|
|
11
|
-
return (0,
|
|
11
|
+
return (0, config_1.marshallWithOptions)({
|
|
12
12
|
[hash]: key[hash],
|
|
13
13
|
...(range && { [range]: key[range] }),
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
exports.stripKey = stripKey;
|
|
17
|
-
function splitEvery(items, limit
|
|
17
|
+
function splitEvery(items, limit) {
|
|
18
18
|
const batches = [];
|
|
19
19
|
for (let i = 0; i < items.length; i += limit) {
|
|
20
20
|
batches.push(items.slice(i, i + limit));
|
|
@@ -23,7 +23,7 @@ function splitEvery(items, limit = 25) {
|
|
|
23
23
|
}
|
|
24
24
|
exports.splitEvery = splitEvery;
|
|
25
25
|
function getAttributeValues(key, { attributesToGet, prefix = ":", } = {}) {
|
|
26
|
-
return (0,
|
|
26
|
+
return (0, config_1.marshallWithOptions)((attributesToGet || Object.keys(key)).reduce((acc, keyName) => {
|
|
27
27
|
acc[`${prefix}${keyName}`] = key[keyName];
|
|
28
28
|
return acc;
|
|
29
29
|
}, {}));
|
|
@@ -77,7 +77,7 @@ class ExpressionAttributes {
|
|
|
77
77
|
this.appendValues(values);
|
|
78
78
|
}
|
|
79
79
|
getAttributes() {
|
|
80
|
-
const marshalled = (0,
|
|
80
|
+
const marshalled = (0, config_1.marshallWithOptions)(this.ExpressionAttributeValues);
|
|
81
81
|
return {
|
|
82
82
|
ExpressionAttributeNames: this.ExpressionAttributeNames,
|
|
83
83
|
...(Object.keys(marshalled).length > 0 && {
|
package/dist/lib/index.d.ts
CHANGED
package/dist/lib/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./client"), exports);
|
|
18
|
+
__exportStar(require("./config"), exports);
|
|
18
19
|
__exportStar(require("./defaultArguments"), exports);
|
|
19
|
-
__exportStar(require("./fixes"), exports);
|
|
20
20
|
__exportStar(require("./helpers"), exports);
|
|
21
21
|
__exportStar(require("./schemas"), exports);
|
package/dist/lib/schemas.js
CHANGED
|
@@ -37,14 +37,14 @@ const validateSchema = (schema) => {
|
|
|
37
37
|
tables.forEach((table) => {
|
|
38
38
|
const { hash } = schema[table];
|
|
39
39
|
if (!hash) {
|
|
40
|
-
throw new Error(`No hash key provided for table ${table}`);
|
|
40
|
+
throw new Error(`[@moicky/dynamodb]: No hash key provided for table ${table}`);
|
|
41
41
|
}
|
|
42
42
|
if (typeof hash !== "string") {
|
|
43
|
-
throw new Error(`Invalid hash key provided for table ${table}`);
|
|
43
|
+
throw new Error(`[@moicky/dynamodb]: Invalid hash key provided for table ${table}`);
|
|
44
44
|
}
|
|
45
45
|
const { range } = schema[table];
|
|
46
46
|
if (range && typeof range !== "string") {
|
|
47
|
-
throw new Error(`Invalid range key provided for table ${table}`);
|
|
47
|
+
throw new Error(`[@moicky/dynamodb]: Invalid range key provided for table ${table}`);
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
return true;
|
|
@@ -81,7 +81,7 @@ async function deleteItems(keys, args = {}, retry = 0) {
|
|
|
81
81
|
return acc;
|
|
82
82
|
}, {}));
|
|
83
83
|
return new Promise(async (resolve, reject) => {
|
|
84
|
-
const batches = (0, lib_1.splitEvery)(uniqueKeys);
|
|
84
|
+
const batches = (0, lib_1.splitEvery)(uniqueKeys, 25);
|
|
85
85
|
if (retry > 3)
|
|
86
86
|
return;
|
|
87
87
|
const table = args?.TableName || (0, lib_1.getDefaultTable)();
|
package/dist/operations/get.js
CHANGED
|
@@ -142,7 +142,7 @@ exports.getItems = getItems;
|
|
|
142
142
|
* ```
|
|
143
143
|
*/
|
|
144
144
|
async function getAllItems(args = {}) {
|
|
145
|
-
args = (0, lib_1.
|
|
145
|
+
args = (0, lib_1.withConfig)((0, lib_1.withDefaults)(args, "getAllItems"));
|
|
146
146
|
let items = [];
|
|
147
147
|
let lastEvaluatedKey;
|
|
148
148
|
do {
|
package/dist/operations/put.js
CHANGED
|
@@ -6,7 +6,7 @@ const lib_1 = require("../lib");
|
|
|
6
6
|
async function putItem(item, args) {
|
|
7
7
|
const argsWithDefaults = (0, lib_1.withDefaults)(args || {}, "putItem");
|
|
8
8
|
if (!Object.keys(item).includes("createdAt")) {
|
|
9
|
-
item = { ...item, createdAt:
|
|
9
|
+
item = { ...item, createdAt: (0, lib_1.getItemModificationTimestamp)("createdAt") };
|
|
10
10
|
}
|
|
11
11
|
const { ReturnValues, ...otherArgs } = argsWithDefaults;
|
|
12
12
|
return (0, lib_1.getClient)()
|
|
@@ -49,11 +49,11 @@ async function putItems(items, args = {}, retry = 0) {
|
|
|
49
49
|
return;
|
|
50
50
|
args = (0, lib_1.withDefaults)(args, "putItems");
|
|
51
51
|
return new Promise(async (resolve, reject) => {
|
|
52
|
-
const
|
|
52
|
+
const createdAt = (0, lib_1.getItemModificationTimestamp)("createdAt");
|
|
53
53
|
const batches = (0, lib_1.splitEvery)(items.map((item) => ({
|
|
54
54
|
...item,
|
|
55
|
-
createdAt: item?.createdAt ??
|
|
56
|
-
})));
|
|
55
|
+
createdAt: item?.createdAt ?? createdAt,
|
|
56
|
+
})), 25);
|
|
57
57
|
const results = [];
|
|
58
58
|
const table = args?.TableName || (0, lib_1.getDefaultTable)();
|
|
59
59
|
for (const batch of batches) {
|
package/dist/operations/query.js
CHANGED
|
@@ -12,7 +12,7 @@ const lib_1 = require("../lib");
|
|
|
12
12
|
* @private
|
|
13
13
|
*/
|
|
14
14
|
async function _query(keyCondition, key, args = {}) {
|
|
15
|
-
args = (0, lib_1.
|
|
15
|
+
args = (0, lib_1.withConfig)(args);
|
|
16
16
|
return (0, lib_1.getClient)().send(new client_dynamodb_1.QueryCommand({
|
|
17
17
|
KeyConditionExpression: keyCondition,
|
|
18
18
|
ExpressionAttributeValues: (0, lib_1.getAttributeValues)(key, {
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.transactWriteItems = void 0;
|
|
4
4
|
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
5
5
|
const lib_1 = require("../lib");
|
|
6
|
+
// https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html
|
|
7
|
+
const OPERATIONS_LIMIT = 100;
|
|
6
8
|
/**
|
|
7
9
|
* Performs a TransactWriteItems operation against DynamoDB. This allows you to perform multiple write operations in a single transaction.
|
|
8
10
|
* @param transactItems - Array of items to transact. Each item can be a Put, Update, Delete, or ConditionCheck operation.
|
|
@@ -50,48 +52,59 @@ const lib_1 = require("../lib");
|
|
|
50
52
|
async function transactWriteItems(transactItems, args = {}) {
|
|
51
53
|
return new Promise(async (resolve, reject) => {
|
|
52
54
|
args = (0, lib_1.withDefaults)(args, "transactWriteItems");
|
|
53
|
-
if (transactItems.length > 100) {
|
|
54
|
-
throw new Error("[@moicky/dynamodb]: TransactWriteItems can only handle up to 100 items");
|
|
55
|
-
}
|
|
56
|
-
const now = Date.now();
|
|
57
55
|
const table = args.TableName || (0, lib_1.getDefaultTable)();
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
const shouldSplitTransactions = (0, lib_1.getConfig)().splitTransactionsIfAboveLimit ?? false;
|
|
57
|
+
if (transactItems.length === 0 ||
|
|
58
|
+
(transactItems.length > OPERATIONS_LIMIT && !shouldSplitTransactions)) {
|
|
59
|
+
reject(new Error("[@moicky/dynamodb]: Invalid number of operations"));
|
|
60
|
+
}
|
|
61
|
+
const conditionCheckItems = transactItems
|
|
62
|
+
.filter((item) => item.ConditionCheck)
|
|
63
|
+
.map((item) => handleConditionCheck(item.ConditionCheck, { table }));
|
|
64
|
+
let createdAt = null;
|
|
65
|
+
let updatedAt = null;
|
|
66
|
+
const operationItems = transactItems
|
|
67
|
+
.filter((item) => !item.ConditionCheck)
|
|
68
|
+
.map((item) => {
|
|
69
|
+
if (item.Put) {
|
|
70
|
+
createdAt ??= (0, lib_1.getItemModificationTimestamp)("createdAt");
|
|
71
|
+
return handlePutItem(item.Put, { createdAt, table });
|
|
64
72
|
}
|
|
65
73
|
else if (item.Delete) {
|
|
66
|
-
return handleDeleteItem(item.Delete, {
|
|
74
|
+
return handleDeleteItem(item.Delete, { table });
|
|
67
75
|
}
|
|
68
76
|
else if (item.Update) {
|
|
69
|
-
|
|
77
|
+
updatedAt ??= (0, lib_1.getItemModificationTimestamp)("updatedAt");
|
|
78
|
+
return handleUpdateItem(item.Update, { updatedAt, table });
|
|
70
79
|
}
|
|
71
80
|
else {
|
|
72
|
-
|
|
81
|
+
reject(new Error("[@moicky/dynamodb]: Invalid TransactItem: " +
|
|
82
|
+
JSON.stringify(item)));
|
|
73
83
|
}
|
|
74
84
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}))
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
const availableSlots = OPERATIONS_LIMIT - conditionCheckItems.length;
|
|
86
|
+
const batches = (0, lib_1.splitEvery)(operationItems, availableSlots);
|
|
87
|
+
const results = {};
|
|
88
|
+
for (const batch of operationItems?.length ? batches : [[]]) {
|
|
89
|
+
const populatedItems = [...conditionCheckItems, ...batch];
|
|
90
|
+
await (0, lib_1.getClient)()
|
|
91
|
+
.send(new client_dynamodb_1.TransactWriteItemsCommand({
|
|
92
|
+
TransactItems: populatedItems,
|
|
93
|
+
...args,
|
|
94
|
+
}))
|
|
95
|
+
.then((res) => {
|
|
96
|
+
Object.entries(res.ItemCollectionMetrics || {}).forEach(([tableName, metrics]) => {
|
|
97
|
+
const unmarshalledMetrics = metrics.map((metric) => ({
|
|
98
|
+
Key: (0, lib_1.unmarshallWithOptions)(metric.ItemCollectionKey || {}),
|
|
99
|
+
SizeEstimateRangeGB: metric.SizeEstimateRangeGB,
|
|
100
|
+
}));
|
|
101
|
+
results[tableName] ??= [];
|
|
102
|
+
results[tableName].push(...unmarshalledMetrics);
|
|
103
|
+
});
|
|
104
|
+
})
|
|
105
|
+
.catch(reject);
|
|
106
|
+
}
|
|
107
|
+
return resolve(results);
|
|
95
108
|
});
|
|
96
109
|
}
|
|
97
110
|
exports.transactWriteItems = transactWriteItems;
|
|
@@ -118,7 +131,9 @@ function handleConditionCheck(params, args) {
|
|
|
118
131
|
}
|
|
119
132
|
function handlePutItem(params, args) {
|
|
120
133
|
const populatedData = structuredClone(params.item);
|
|
121
|
-
populatedData.createdAt
|
|
134
|
+
if (!Object.keys(populatedData).includes("createdAt")) {
|
|
135
|
+
populatedData.createdAt = args.createdAt;
|
|
136
|
+
}
|
|
122
137
|
const { item, conditionData, ...rest } = params;
|
|
123
138
|
return {
|
|
124
139
|
Put: {
|
|
@@ -143,7 +158,9 @@ function handleDeleteItem(params, args) {
|
|
|
143
158
|
function handleUpdateItem(params, args) {
|
|
144
159
|
const { key, updateData, conditionData, ...rest } = params;
|
|
145
160
|
const populatedData = structuredClone(updateData);
|
|
146
|
-
populatedData.updatedAt
|
|
161
|
+
if (!Object.keys(populatedData).includes("updatedAt")) {
|
|
162
|
+
populatedData.updatedAt = args.updatedAt;
|
|
163
|
+
}
|
|
147
164
|
const mergedData = { ...populatedData, ...conditionData };
|
|
148
165
|
const UpdateExpression = "SET " +
|
|
149
166
|
Object.keys(populatedData)
|
|
@@ -6,7 +6,7 @@ const lib_1 = require("../lib");
|
|
|
6
6
|
async function updateItem(key, data, args) {
|
|
7
7
|
const argsWithDefaults = (0, lib_1.withDefaults)(args || {}, "updateItem");
|
|
8
8
|
if (!Object.keys(data).includes("updatedAt")) {
|
|
9
|
-
data = { ...data, updatedAt:
|
|
9
|
+
data = { ...data, updatedAt: (0, lib_1.getItemModificationTimestamp)("updatedAt") };
|
|
10
10
|
}
|
|
11
11
|
const valuesInCondition = (0, lib_1.getAttributesFromExpression)(argsWithDefaults?.ConditionExpression || "", ":");
|
|
12
12
|
const namesInCondition = (0, lib_1.getAttributesFromExpression)(argsWithDefaults?.ConditionExpression || "");
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { TransactWriteItemsCommandInput } from "@aws-sdk/client-dynamodb";
|
|
1
|
+
import { ItemCollectionMetrics, TransactWriteItemsCommandInput } from "@aws-sdk/client-dynamodb";
|
|
2
2
|
import { ConditionOperations, CreateOperations, UpdateOperations } from "./operations";
|
|
3
3
|
import { WithoutReferences } from "./references/types";
|
|
4
4
|
import { ConditionOperation, CreateOperation, DeleteOperation, DynamoDBItemKey, ItemWithKey, OnlyKey, UpdateOperation } from "./types";
|
|
5
|
+
type ResponseItem = Pick<ItemCollectionMetrics, "SizeEstimateRangeGB"> & {
|
|
6
|
+
Key: Record<string, any>;
|
|
7
|
+
};
|
|
5
8
|
export declare class Transaction {
|
|
6
9
|
private tableName;
|
|
7
|
-
private
|
|
10
|
+
private createdAt;
|
|
11
|
+
private updatedAt;
|
|
8
12
|
private operations;
|
|
9
|
-
constructor({ tableName,
|
|
13
|
+
constructor({ tableName, createdAt, updatedAt, }?: {
|
|
10
14
|
tableName?: string;
|
|
11
|
-
|
|
15
|
+
createdAt?: any;
|
|
16
|
+
updatedAt?: any;
|
|
12
17
|
});
|
|
13
18
|
private getItemKey;
|
|
14
19
|
create<T extends ItemWithKey>(item: WithoutReferences<T>, args?: CreateOperation["args"]): CreateOperations<T>;
|
|
@@ -16,5 +21,6 @@ export declare class Transaction {
|
|
|
16
21
|
delete(item: DynamoDBItemKey, args?: DeleteOperation["args"]): void;
|
|
17
22
|
addConditionFor<T extends DynamoDBItemKey>(item: T, args?: Partial<ConditionOperation["args"]>): ConditionOperations<T>;
|
|
18
23
|
private handleOperation;
|
|
19
|
-
execute(args?: Partial<Omit<TransactWriteItemsCommandInput, "TransactItems">>): Promise<
|
|
24
|
+
execute(args?: Partial<Omit<TransactWriteItemsCommandInput, "TransactItems">>): Promise<Record<string, ResponseItem[]>>;
|
|
20
25
|
}
|
|
26
|
+
export {};
|
|
@@ -8,11 +8,13 @@ const operations_1 = require("./operations");
|
|
|
8
8
|
const OPERATIONS_LIMIT = 100;
|
|
9
9
|
class Transaction {
|
|
10
10
|
tableName;
|
|
11
|
-
|
|
11
|
+
createdAt;
|
|
12
|
+
updatedAt;
|
|
12
13
|
operations = {};
|
|
13
|
-
constructor({ tableName = (0, __1.getDefaultTable)(),
|
|
14
|
+
constructor({ tableName = (0, __1.getDefaultTable)(), createdAt = (0, __1.getItemModificationTimestamp)("createdAt"), updatedAt = (0, __1.getItemModificationTimestamp)("updatedAt"), } = {}) {
|
|
14
15
|
this.tableName = tableName;
|
|
15
|
-
this.
|
|
16
|
+
this.createdAt = createdAt ?? (0, __1.getItemModificationTimestamp)("createdAt");
|
|
17
|
+
this.updatedAt = updatedAt ?? (0, __1.getItemModificationTimestamp)("updatedAt");
|
|
16
18
|
}
|
|
17
19
|
getItemKey(item, tableName) {
|
|
18
20
|
return (0, __1.getItemKey)(item, { TableName: tableName || this.tableName });
|
|
@@ -32,7 +34,7 @@ class Transaction {
|
|
|
32
34
|
const updateOperation = {
|
|
33
35
|
_type: "update",
|
|
34
36
|
item,
|
|
35
|
-
actions: [{ _type: "set", values: { updatedAt: this.
|
|
37
|
+
actions: [{ _type: "set", values: { updatedAt: this.updatedAt } }],
|
|
36
38
|
args: { TableName: this.tableName, ...args },
|
|
37
39
|
};
|
|
38
40
|
this.operations[itemKey] = updateOperation;
|
|
@@ -58,7 +60,7 @@ class Transaction {
|
|
|
58
60
|
const { item, args } = operation;
|
|
59
61
|
return {
|
|
60
62
|
Put: {
|
|
61
|
-
Item: (0, __1.marshallWithOptions)({ createdAt: this.
|
|
63
|
+
Item: (0, __1.marshallWithOptions)({ createdAt: this.createdAt, ...item }),
|
|
62
64
|
...args,
|
|
63
65
|
},
|
|
64
66
|
};
|
|
@@ -123,15 +125,44 @@ class Transaction {
|
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
async execute(args) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
args = (0, __1.withDefaults)(args, "transactWriteItems");
|
|
129
|
+
return new Promise(async (resolve, reject) => {
|
|
130
|
+
const shouldSplitTransactions = (0, __1.getConfig)().splitTransactionsIfAboveLimit ?? false;
|
|
131
|
+
const operations = Object.values(this.operations);
|
|
132
|
+
if (operations.length === 0 ||
|
|
133
|
+
(operations.length > OPERATIONS_LIMIT && !shouldSplitTransactions)) {
|
|
134
|
+
reject(new Error("[@moicky/dynamodb]: Invalid number of operations"));
|
|
135
|
+
}
|
|
136
|
+
const conditionCheckItems = operations
|
|
137
|
+
.filter((op) => op._type === "condition")
|
|
138
|
+
.map((op) => this.handleOperation(op));
|
|
139
|
+
const operationItems = operations
|
|
140
|
+
.filter((op) => op._type !== "condition")
|
|
141
|
+
.map((op) => this.handleOperation(op));
|
|
142
|
+
const availableSlots = OPERATIONS_LIMIT - conditionCheckItems.length;
|
|
143
|
+
const batches = (0, __1.splitEvery)(operationItems, availableSlots);
|
|
144
|
+
const results = {};
|
|
145
|
+
for (const batch of operationItems?.length ? batches : [[]]) {
|
|
146
|
+
const populatedItems = [...conditionCheckItems, ...batch];
|
|
147
|
+
await (0, __1.getClient)()
|
|
148
|
+
.send(new client_dynamodb_1.TransactWriteItemsCommand({
|
|
149
|
+
TransactItems: populatedItems,
|
|
150
|
+
...args,
|
|
151
|
+
}))
|
|
152
|
+
.then((res) => {
|
|
153
|
+
Object.entries(res.ItemCollectionMetrics || {}).forEach(([tableName, metrics]) => {
|
|
154
|
+
const unmarshalledMetrics = metrics.map((metric) => ({
|
|
155
|
+
Key: (0, __1.unmarshallWithOptions)(metric.ItemCollectionKey || {}),
|
|
156
|
+
SizeEstimateRangeGB: metric.SizeEstimateRangeGB,
|
|
157
|
+
}));
|
|
158
|
+
results[tableName] ??= [];
|
|
159
|
+
results[tableName].push(...unmarshalledMetrics);
|
|
160
|
+
});
|
|
161
|
+
})
|
|
162
|
+
.catch(reject);
|
|
163
|
+
}
|
|
164
|
+
return resolve(results);
|
|
165
|
+
});
|
|
135
166
|
}
|
|
136
167
|
}
|
|
137
168
|
exports.Transaction = Transaction;
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moicky/dynamodb",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"description": "Contains a collection of convenience functions for working with AWS DynamoDB",
|
|
@@ -28,10 +28,9 @@
|
|
|
28
28
|
"aws-crt": "^1.15.20"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@aws-sdk/credential-providers": "^3.744.0",
|
|
32
|
-
"@vercel/functions": "^2.0.0",
|
|
33
31
|
"@types/jest": "^29.5.1",
|
|
34
32
|
"@types/node": "^20.2.4",
|
|
33
|
+
"@vercel/functions": "^2.0.0",
|
|
35
34
|
"dotenv": "^16.0.3",
|
|
36
35
|
"jest": "^29.5.0",
|
|
37
36
|
"typescript": "^5.0.4"
|
package/readme.md
CHANGED
|
@@ -14,9 +14,11 @@ Contains convenience functions for all major dynamodb operations. Requires very
|
|
|
14
14
|
- 🔒 When specifying an item using its keySchema, all additional attributes (apart from keySchema attributes from `initSchema` or `PK` & `SK` as default) will be removed to avoid errors
|
|
15
15
|
- 👻 Will **use placeholders** to avoid colliding with [reserved words](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html) if applicable
|
|
16
16
|
- 🌎 Supports globally defined default arguments for each operation ([example](#configuring-global-defaults))
|
|
17
|
-
- 🔨 Supports
|
|
17
|
+
- 🔨 Supports configuration for several dynamodb behaviors and fixes ([example](#configuration))
|
|
18
18
|
- 📖 Offers a convenient way to use pagination with queries ([example](#paginated-items))
|
|
19
19
|
- 🗂️ Supports **transactGetItems** & **transactWriteItems**
|
|
20
|
+
- ⚙️ Customizable timestamp generation for `createdAt` and `updatedAt` attributes
|
|
21
|
+
- 🔄 Automatic transaction splitting for operations exceeding the 100-item limit (disabled by default)
|
|
20
22
|
|
|
21
23
|
## Installation
|
|
22
24
|
|
|
@@ -290,6 +292,9 @@ console.log(id4); // "00000010"
|
|
|
290
292
|
import { transactWriteItems } from "@moicky/dynamodb";
|
|
291
293
|
|
|
292
294
|
// Perform a TransactWriteItems operation
|
|
295
|
+
// Note: By default, transactions are limited to 100 operations (AWS limit).
|
|
296
|
+
// Enable automatic splitting via initConfig({ splitTransactionsIfAboveLimit: true })
|
|
297
|
+
// to handle larger transactions automatically. (note that this is not recommended for transactional updates)
|
|
293
298
|
const response = await transactWriteItems([
|
|
294
299
|
{
|
|
295
300
|
Put: {
|
|
@@ -368,14 +373,18 @@ const itemWithoutConsistentRead = await getItem(
|
|
|
368
373
|
);
|
|
369
374
|
```
|
|
370
375
|
|
|
371
|
-
##
|
|
376
|
+
## Configuration
|
|
372
377
|
|
|
373
|
-
|
|
378
|
+
The package provides a unified configuration system through `initConfig` that allows you to customize various behaviors and fixes for DynamoDB operations.
|
|
379
|
+
|
|
380
|
+
### Marshall/Unmarshall Options
|
|
381
|
+
|
|
382
|
+
Arguments which are passed to [marshall](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/interfaces/_aws_sdk_util_dynamodb.marshallOptions.html) and [unmarshall](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/interfaces/_aws_sdk_util_dynamodb.unmarshallOptions.html) from `@aws-sdk/util-dynamodb` can be configured using:
|
|
374
383
|
|
|
375
384
|
```ts
|
|
376
|
-
import {
|
|
385
|
+
import { initConfig } from "@moicky/dynamodb";
|
|
377
386
|
|
|
378
|
-
|
|
387
|
+
initConfig({
|
|
379
388
|
marshallOptions: {
|
|
380
389
|
removeUndefinedValues: true,
|
|
381
390
|
},
|
|
@@ -385,22 +394,55 @@ initFixes({
|
|
|
385
394
|
});
|
|
386
395
|
```
|
|
387
396
|
|
|
388
|
-
|
|
397
|
+
### Consistent Read with Indexes
|
|
398
|
+
|
|
399
|
+
When using `GlobalSecondaryIndexes`, DynamoDB does not support using `ConsistentRead`. This is fixed by default (`ConsistentRead` is turned off) and can be configured using:
|
|
389
400
|
|
|
390
401
|
```ts
|
|
391
|
-
import {
|
|
402
|
+
import { initConfig } from "@moicky/dynamodb";
|
|
392
403
|
|
|
393
|
-
|
|
404
|
+
initConfig({
|
|
394
405
|
disableConsistantReadWhenUsingIndexes: {
|
|
395
|
-
enabled: true, // default
|
|
406
|
+
enabled: true, // default
|
|
396
407
|
|
|
397
|
-
// Won't disable
|
|
398
|
-
// This works because DynamoDB supports
|
|
408
|
+
// Won't disable ConsistentRead if IndexName is specified here.
|
|
409
|
+
// This works because DynamoDB supports ConsistentRead on LocalSecondaryIndexes
|
|
399
410
|
stillUseOnLocalIndexes: ["localIndexName1", "localIndexName2"],
|
|
400
411
|
},
|
|
401
412
|
});
|
|
402
413
|
```
|
|
403
414
|
|
|
415
|
+
### Custom Timestamp Generation
|
|
416
|
+
|
|
417
|
+
You can customize how `createdAt` and `updatedAt` timestamps are generated for items:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { initConfig } from "@moicky/dynamodb";
|
|
421
|
+
|
|
422
|
+
initConfig({
|
|
423
|
+
itemModificationTimestamp: (field) => {
|
|
424
|
+
if (field === "createdAt") {
|
|
425
|
+
return new Date().toISOString();
|
|
426
|
+
}
|
|
427
|
+
return Date.now();
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Transaction Splitting
|
|
433
|
+
|
|
434
|
+
By default, `transactWriteItems` will reject transactions with more than 100 operations (AWS limit). You can enable automatic splitting to handle larger transactions:
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import { initConfig } from "@moicky/dynamodb";
|
|
438
|
+
|
|
439
|
+
initConfig({
|
|
440
|
+
splitTransactionsIfAboveLimit: true,
|
|
441
|
+
});
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
When enabled, transactions exceeding 100 operations will be automatically split into multiple batches, with condition checks preserved across all batches.
|
|
445
|
+
|
|
404
446
|
## What are the benefits and why should I use it?
|
|
405
447
|
|
|
406
448
|
Generally it makes it easier to interact with the dynamodb from AWS. Here are some before and after examples using the new aws-sdk v3:
|