@moicky/dynamodb 2.6.4 → 3.0.1

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.
@@ -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;
@@ -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?: number): T[][];
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;
@@ -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 fixes_1 = require("./fixes");
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, fixes_1.marshallWithOptions)({
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 = 25) {
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, fixes_1.marshallWithOptions)((attributesToGet || Object.keys(key)).reduce((acc, keyName) => {
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, fixes_1.marshallWithOptions)(this.ExpressionAttributeValues);
80
+ const marshalled = (0, config_1.marshallWithOptions)(this.ExpressionAttributeValues);
81
81
  return {
82
82
  ExpressionAttributeNames: this.ExpressionAttributeNames,
83
83
  ...(Object.keys(marshalled).length > 0 && {
@@ -1,5 +1,5 @@
1
1
  export * from "./client";
2
+ export * from "./config";
2
3
  export * from "./defaultArguments";
3
- export * from "./fixes";
4
4
  export * from "./helpers";
5
5
  export * from "./schemas";
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);
@@ -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)();
@@ -142,7 +142,7 @@ exports.getItems = getItems;
142
142
  * ```
143
143
  */
144
144
  async function getAllItems(args = {}) {
145
- args = (0, lib_1.withFixes)((0, lib_1.withDefaults)(args, "getAllItems"));
145
+ args = (0, lib_1.withConfig)((0, lib_1.withDefaults)(args, "getAllItems"));
146
146
  let items = [];
147
147
  let lastEvaluatedKey;
148
148
  do {
@@ -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: Date.now() };
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 now = Date.now();
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 ?? now,
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) {
@@ -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.withFixes)(args);
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 populatedItems = transactItems.map((item) => {
59
- if (item.ConditionCheck) {
60
- return handleConditionCheck(item.ConditionCheck, { now, table });
61
- }
62
- else if (item.Put) {
63
- return handlePutItem(item.Put, { now, table });
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, { now, table });
74
+ return handleDeleteItem(item.Delete, { table });
67
75
  }
68
76
  else if (item.Update) {
69
- return handleUpdateItem(item.Update, { now, table });
77
+ updatedAt ??= (0, lib_1.getItemModificationTimestamp)("updatedAt");
78
+ return handleUpdateItem(item.Update, { updatedAt, table });
70
79
  }
71
80
  else {
72
- throw new Error("[@moicky/dynamodb]: Invalid TransactItem: " + JSON.stringify(item));
81
+ reject(new Error("[@moicky/dynamodb]: Invalid TransactItem: " +
82
+ JSON.stringify(item)));
73
83
  }
74
84
  });
75
- return (0, lib_1.getClient)()
76
- .send(new client_dynamodb_1.TransactWriteItemsCommand({
77
- TransactItems: populatedItems,
78
- ...args,
79
- }))
80
- .then((res) => {
81
- const results = {};
82
- Object.entries(res.ItemCollectionMetrics || {}).forEach(([tableName, metrics]) => {
83
- const unmarshalledMetrics = metrics.map((metric) => ({
84
- Key: (0, lib_1.unmarshallWithOptions)(metric.ItemCollectionKey || {}),
85
- SizeEstimateRangeGB: metric.SizeEstimateRangeGB,
86
- }));
87
- if (!results[tableName]) {
88
- results[tableName] = [];
89
- }
90
- results[tableName].push(...unmarshalledMetrics);
91
- });
92
- resolve(results);
93
- })
94
- .catch(reject);
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 ??= args.now;
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 ??= args.now;
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: Date.now() };
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,21 @@
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 timestamp;
10
+ private createdAt;
11
+ private updatedAt;
8
12
  private operations;
9
- constructor({ tableName, timestamp, }?: {
13
+ private shouldSplitTransactions;
14
+ constructor({ tableName, createdAt, updatedAt, shouldSplitTransactions, }?: {
10
15
  tableName?: string;
11
- timestamp?: number;
16
+ createdAt?: any;
17
+ updatedAt?: any;
18
+ shouldSplitTransactions?: boolean;
12
19
  });
13
20
  private getItemKey;
14
21
  create<T extends ItemWithKey>(item: WithoutReferences<T>, args?: CreateOperation["args"]): CreateOperations<T>;
@@ -16,5 +23,6 @@ export declare class Transaction {
16
23
  delete(item: DynamoDBItemKey, args?: DeleteOperation["args"]): void;
17
24
  addConditionFor<T extends DynamoDBItemKey>(item: T, args?: Partial<ConditionOperation["args"]>): ConditionOperations<T>;
18
25
  private handleOperation;
19
- execute(args?: Partial<Omit<TransactWriteItemsCommandInput, "TransactItems">>): Promise<import("@aws-sdk/client-dynamodb").TransactWriteItemsCommandOutput>;
26
+ execute(args?: Partial<Omit<TransactWriteItemsCommandInput, "TransactItems">>): Promise<Record<string, ResponseItem[]>>;
20
27
  }
28
+ export {};
@@ -8,11 +8,18 @@ const operations_1 = require("./operations");
8
8
  const OPERATIONS_LIMIT = 100;
9
9
  class Transaction {
10
10
  tableName;
11
- timestamp = Date.now();
11
+ createdAt;
12
+ updatedAt;
12
13
  operations = {};
13
- constructor({ tableName = (0, __1.getDefaultTable)(), timestamp = Date.now(), } = {}) {
14
- this.tableName = tableName;
15
- this.timestamp = timestamp;
14
+ shouldSplitTransactions;
15
+ constructor({ tableName, createdAt, updatedAt, shouldSplitTransactions, } = {}) {
16
+ this.tableName = tableName ?? (0, __1.getDefaultTable)();
17
+ this.createdAt = createdAt ?? (0, __1.getItemModificationTimestamp)("createdAt");
18
+ this.updatedAt = updatedAt ?? (0, __1.getItemModificationTimestamp)("updatedAt");
19
+ this.shouldSplitTransactions =
20
+ shouldSplitTransactions ??
21
+ (0, __1.getConfig)().splitTransactionsIfAboveLimit ??
22
+ false;
16
23
  }
17
24
  getItemKey(item, tableName) {
18
25
  return (0, __1.getItemKey)(item, { TableName: tableName || this.tableName });
@@ -32,7 +39,7 @@ class Transaction {
32
39
  const updateOperation = {
33
40
  _type: "update",
34
41
  item,
35
- actions: [{ _type: "set", values: { updatedAt: this.timestamp } }],
42
+ actions: [{ _type: "set", values: { updatedAt: this.updatedAt } }],
36
43
  args: { TableName: this.tableName, ...args },
37
44
  };
38
45
  this.operations[itemKey] = updateOperation;
@@ -58,7 +65,7 @@ class Transaction {
58
65
  const { item, args } = operation;
59
66
  return {
60
67
  Put: {
61
- Item: (0, __1.marshallWithOptions)({ createdAt: this.timestamp, ...item }),
68
+ Item: (0, __1.marshallWithOptions)({ createdAt: this.createdAt, ...item }),
62
69
  ...args,
63
70
  },
64
71
  };
@@ -123,15 +130,44 @@ class Transaction {
123
130
  }
124
131
  }
125
132
  async execute(args) {
126
- const operations = Object.values(this.operations).map((op) => this.handleOperation(op));
127
- if (operations.length === 0 || operations.length > OPERATIONS_LIMIT) {
128
- throw new Error("Invalid number of operations");
129
- }
130
- const input = {
131
- TransactItems: operations,
132
- ...args,
133
- };
134
- return (0, __1.getClient)().send(new client_dynamodb_1.TransactWriteItemsCommand(input));
133
+ args = (0, __1.withDefaults)(args, "transactWriteItems");
134
+ return new Promise(async (resolve, reject) => {
135
+ const operations = Object.values(this.operations);
136
+ if (operations.length === 0 ||
137
+ (operations.length > OPERATIONS_LIMIT &&
138
+ !this.shouldSplitTransactions)) {
139
+ reject(new Error("[@moicky/dynamodb]: Invalid number of operations"));
140
+ }
141
+ const conditionCheckItems = operations
142
+ .filter((op) => op._type === "condition")
143
+ .map((op) => this.handleOperation(op));
144
+ const operationItems = operations
145
+ .filter((op) => op._type !== "condition")
146
+ .map((op) => this.handleOperation(op));
147
+ const availableSlots = OPERATIONS_LIMIT - conditionCheckItems.length;
148
+ const batches = (0, __1.splitEvery)(operationItems, availableSlots);
149
+ const results = {};
150
+ for (const batch of operationItems?.length ? batches : [[]]) {
151
+ const populatedItems = [...conditionCheckItems, ...batch];
152
+ await (0, __1.getClient)()
153
+ .send(new client_dynamodb_1.TransactWriteItemsCommand({
154
+ TransactItems: populatedItems,
155
+ ...args,
156
+ }))
157
+ .then((res) => {
158
+ Object.entries(res.ItemCollectionMetrics || {}).forEach(([tableName, metrics]) => {
159
+ const unmarshalledMetrics = metrics.map((metric) => ({
160
+ Key: (0, __1.unmarshallWithOptions)(metric.ItemCollectionKey || {}),
161
+ SizeEstimateRangeGB: metric.SizeEstimateRangeGB,
162
+ }));
163
+ results[tableName] ??= [];
164
+ results[tableName].push(...unmarshalledMetrics);
165
+ });
166
+ })
167
+ .catch(reject);
168
+ }
169
+ return resolve(results);
170
+ });
135
171
  }
136
172
  }
137
173
  exports.Transaction = Transaction;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export type DynamoDBItem = Record<string, any> & {
2
- createdAt?: number;
3
- updatedAt?: number;
2
+ createdAt?: any;
3
+ updatedAt?: any;
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moicky/dynamodb",
3
- "version": "2.6.4",
3
+ "version": "3.0.1",
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",
@@ -25,12 +25,12 @@
25
25
  "dependencies": {
26
26
  "@aws-sdk/client-dynamodb": "^3.338.0",
27
27
  "@aws-sdk/util-dynamodb": "^3.338.0",
28
- "aws-crt": "^1.15.20"
28
+ "aws-crt": "^1.15.20",
29
+ "@vercel/functions": "^2.0.0"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/jest": "^29.5.1",
32
33
  "@types/node": "^20.2.4",
33
- "@vercel/functions": "^2.0.0",
34
34
  "dotenv": "^16.0.3",
35
35
  "jest": "^29.5.0",
36
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 fixes for several issues with dynamodb ([example](#applying-fixes))
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
- ## Applying fixes
376
+ ## Configuration
372
377
 
373
- 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
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 { initFixes } from "@moicky/dynamodb";
385
+ import { initConfig } from "@moicky/dynamodb";
377
386
 
378
- initFixes({
387
+ initConfig({
379
388
  marshallOptions: {
380
389
  removeUndefinedValues: true,
381
390
  },
@@ -385,22 +394,55 @@ initFixes({
385
394
  });
386
395
  ```
387
396
 
388
- When using `GlobalSecondaryIndexes`, DynamoDb does not support using `ConsistantRead`. This is fixed by default (`ConsistantRead` is turned off) and can be configured using:
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 { initFixes } from "@moicky/dynamodb";
402
+ import { initConfig } from "@moicky/dynamodb";
392
403
 
393
- initFixes({
404
+ initConfig({
394
405
  disableConsistantReadWhenUsingIndexes: {
395
- enabled: true, // default,
406
+ enabled: true, // default
396
407
 
397
- // Won't disable ConsistantRead if IndexName is specified here.
398
- // This works because DynamoDB supports ConsistantRead on LocalSecondaryIndexes
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: