Repository: typedorm/typedorm (ts)

Issue:
Allow customizing translate config used by DocumentClientV3  
Not sure if this is pure stupidity on my behalf, but I've spent some time battling the AWS SDK V3 on undefined values for dynamoDB.
(A problem I didn't run into with V2)

And unfortunately, we don't yet expose the marshal options as part of SDK V3. 
(Manually changing removeUndefinedValues to true seems to fix all my issues)

Thoughts on allowing the options to be passed as part of constructing the document client?

Happy to put forward a PR for this.

Pull request:
Expose translate config options to Document client V3
closes #319 

Patch:
diff --git a/packages/common/private-api.ts b/packages/common/private-api.ts
index 399245a..03ed8d8 100644
--- a/packages/common/private-api.ts
+++ b/packages/common/private-api.ts
@@ -3,4 +3,8 @@ export * from './src/metadata/metadata-storage';
 export * from './src/logger/debug-logger';
 export * from './src/error';
 export * from './src/index-options';
+
+// Helpers
 export * from './src/helpers/load-package';
+export * from './src/helpers/is-empty-object';
+export * from './src/helpers/is-object';
diff --git a/packages/common/public-api.ts b/packages/common/public-api.ts
index ec0c60a..e9c722f 100644
--- a/packages/common/public-api.ts
+++ b/packages/common/public-api.ts
@@ -10,10 +10,12 @@ export * from './src/helpers/common-types';
 export * from './src/constants';
 export * from './src/helpers/dynamo-entity-type';
 export * from './src/enums';
+export * from './src/operator-types';
+
+// Helpers
 export * from './src/helpers/non-key-attributes-type';
 export * from './src/helpers/non-key-attribute-with-return-type';
 export * from './src/helpers/object-type';
-export * from './src/operator-types';
 export * from './src/helpers/require-at-least-one-type';
 export * from './src/helpers/require-at-least-two-type';
 export * from './src/helpers/require-only-one-type';
diff --git a/packages/core/src/helpers/is-empty-object.ts b/packages/common/src/helpers/is-empty-object.ts
similarity index 100%
rename from packages/core/src/helpers/is-empty-object.ts
rename to packages/common/src/helpers/is-empty-object.ts
diff --git a/packages/core/src/helpers/is-object.ts b/packages/common/src/helpers/is-object.ts
similarity index 100%
rename from packages/core/src/helpers/is-object.ts
rename to packages/common/src/helpers/is-object.ts
diff --git a/packages/core/src/classes/batch/type-guards.ts b/packages/core/src/classes/batch/type-guards.ts
index 730eb0e..f7443cc 100644
--- a/packages/core/src/classes/batch/type-guards.ts
+++ b/packages/core/src/classes/batch/type-guards.ts
@@ -1,4 +1,4 @@
-import {isEmptyObject} from '../../helpers/is-empty-object';
+import {isEmptyObject} from '@typedorm/common';
 import {WriteBatchCreate, WriteBatchDelete} from './write-batch';
 
 export function isBatchAddCreateItem<Entity>(
diff --git a/packages/core/src/classes/expression/expression-builder.ts b/packages/core/src/classes/expression/expression-builder.ts
index 2643666..33af6bd 100644
--- a/packages/core/src/classes/expression/expression-builder.ts
+++ b/packages/core/src/classes/expression/expression-builder.ts
@@ -1,5 +1,4 @@
-import {Table} from '@typedorm/common';
-import {isEmptyObject} from '../../helpers/is-empty-object';
+import {Table, isEmptyObject} from '@typedorm/common';
 import {MERGE_STRATEGY} from './base-expression-input';
 import {Condition} from './condition';
 import {Filter} from './filter';
diff --git a/packages/core/src/classes/expression/expression-input-parser.ts b/packages/core/src/classes/expression/expression-input-parser.ts
index 85db85d..26349c3 100644
--- a/packages/core/src/classes/expression/expression-input-parser.ts
+++ b/packages/core/src/classes/expression/expression-input-parser.ts
@@ -4,8 +4,9 @@ import {
   ScalarType,
   SimpleOperator,
   UpdateType,
+  isEmptyObject,
+  isObject,
 } from '@typedorm/common';
-import {isEmptyObject} from '../../helpers/is-empty-object';
 import {KeyCondition} from './key-condition';
 import {Filter} from './filter';
 import {BaseExpressionInput, MERGE_STRATEGY} from './base-expression-input';
@@ -23,7 +24,6 @@ import {Update} from './update/update';
 
 import {DeleteUpdate} from './update/delete-update';
 import {RemoveUpdate} from './update/remove-update';
-import {isObject} from '../../helpers/is-object';
 import {nestedKeyAccessRegex} from '../../helpers/constants';
 
 /**
diff --git a/packages/core/src/classes/expression/update-body-type.ts b/packages/core/src/classes/expression/update-body-type.ts
index 223079e..efa40a4 100644
--- a/packages/core/src/classes/expression/update-body-type.ts
+++ b/packages/core/src/classes/expression/update-body-type.ts
@@ -1,5 +1,5 @@
 import {UpdateType, RequireOnlyOne, InvalidType} from '@typedorm/common';
-import {isEmptyObject} from '../../helpers/is-empty-object';
+import {isEmptyObject} from '@typedorm/common';
 
 /**
  * Type Guards
diff --git a/packages/core/src/classes/manager/batch-manager.ts b/packages/core/src/classes/manager/batch-manager.ts
index 8df3ed2..d797c70 100644
--- a/packages/core/src/classes/manager/batch-manager.ts
+++ b/packages/core/src/classes/manager/batch-manager.ts
@@ -10,8 +10,8 @@ import {
   INTERNAL_ENTITY_ATTRIBUTE,
   MANAGER_NAME,
   STATS_TYPE,
+  isEmptyObject,
 } from '@typedorm/common';
-import {isEmptyObject} from '../../helpers/is-empty-object';
 import {ReadBatch} from '../batch/read-batch';
 import {MetadataOptions} from '../transformer/base-transformer';
 import {getUniqueRequestId} from '../../helpers/get-unique-request-id';
diff --git a/packages/core/src/classes/manager/entity-manager.ts b/packages/core/src/classes/manager/entity-manager.ts
index dd87304..7cd11eb 100644
--- a/packages/core/src/classes/manager/entity-manager.ts
+++ b/packages/core/src/classes/manager/entity-manager.ts
@@ -7,9 +7,9 @@ import {
   MANAGER_NAME,
   QUERY_ORDER,
   STATS_TYPE,
+  isEmptyObject,
 } from '@typedorm/common';
 import {getDynamoQueryItemsLimit} from '../../helpers/get-dynamo-query-items-limit';
-import {isEmptyObject} from '../../helpers/is-empty-object';
 import {Connection} from '../connection/connection';
 import {DocumentClientRequestTransformer} from '../transformer/document-client-request-transformer';
 import {EntityTransformer} from '../transformer/entity-transformer';
diff --git a/packages/core/src/classes/transformer/base-transformer.ts b/packages/core/src/classes/transformer/base-transformer.ts
index 9e28b97..9c6ebc0 100644
--- a/packages/core/src/classes/transformer/base-transformer.ts
+++ b/packages/core/src/classes/transformer/base-transformer.ts
@@ -9,9 +9,9 @@ import {
   SparseIndexParseError,
   CONSUMED_CAPACITY_TYPE,
   InvalidDynamicUpdateAttributeValueError,
+  isEmptyObject,
 } from '@typedorm/common';
 import {getConstructorForInstance} from '../../helpers/get-constructor-for-instance';
-import {isEmptyObject} from '../../helpers/is-empty-object';
 import {isScalarType} from '../../helpers/is-scalar-type';
 import {parseKey} from '../../helpers/parse-key';
 import {Connection} from '../connection/connection';
diff --git a/packages/core/src/classes/transformer/document-client-request-transformer.ts b/packages/core/src/classes/transformer/document-client-request-transformer.ts
index af84892..bdd0b73 100644
--- a/packages/core/src/classes/transformer/document-client-request-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-request-transformer.ts
@@ -13,10 +13,11 @@ import {
   InvalidSelectInputError,
   InvalidUniqueAttributeUpdateError,
   InvalidDynamicUpdateAttributeValueError,
+  isEmptyObject,
+  isObject,
 } from '@typedorm/common';
 import {dropProp} from '../../helpers/drop-prop';
 import {getConstructorForInstance} from '../../helpers/get-constructor-for-instance';
-import {isEmptyObject} from '../../helpers/is-empty-object';
 import {parseKey} from '../../helpers/parse-key';
 import {KeyCondition} from '../expression/key-condition';
 import {Connection} from '../connection/connection';
@@ -27,7 +28,6 @@ import {BaseTransformer, MetadataOptions} from './base-transformer';
 import {LazyTransactionWriteItemListLoader} from './is-lazy-transaction-write-item-list-loader';
 import {KeyConditionOptions} from '../expression/key-condition-options-type';
 import {UpdateBody} from '../expression/update-body-type';
-import {isObject} from '../../helpers/is-object';
 import {DocumentClientTypes} from '@typedorm/document-client';
 import {autoGenerateValue} from '../../helpers/auto-generate-attribute-value';
 
diff --git a/packages/core/src/classes/transformer/document-client-scan-transformer.ts b/packages/core/src/classes/transformer/document-client-scan-transformer.ts
index 5a9ce67..f7885ed 100644
--- a/packages/core/src/classes/transformer/document-client-scan-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-scan-transformer.ts
@@ -8,8 +8,8 @@ import {
   NoSuchIndexFoundError,
   QUERY_SELECT_TYPE,
   TRANSFORM_SCAN_TYPE,
+  isEmptyObject,
 } from '@typedorm/common';
-import {isEmptyObject} from '../../helpers/is-empty-object';
 import {Connection} from '../connection/connection';
 import {MERGE_STRATEGY} from '../expression/base-expression-input';
 import {Filter} from '../expression/filter';
diff --git a/packages/core/src/helpers/deep-sort-object.ts b/packages/core/src/helpers/deep-sort-object.ts
index aa8fc71..09f63d7 100644
--- a/packages/core/src/helpers/deep-sort-object.ts
+++ b/packages/core/src/helpers/deep-sort-object.ts
@@ -1,4 +1,4 @@
-import {isObject} from './is-object';
+import {isObject} from '@typedorm/common';
 
 export function deepSortObject(item: object) {
   if (!isObject(item)) {
diff --git a/packages/document-client/BUILD b/packages/document-client/BUILD
index f8ded9a..c9dbf7c 100644
--- a/packages/document-client/BUILD
+++ b/packages/document-client/BUILD
@@ -9,6 +9,7 @@ setup_ts_build(
     tsconfig_cjs = "//:tsconfig_cjs",
     tsconfig_esm = "//:tsconfig_esm",    
     deps = [
+        "//packages/common:library",
         "@npm//@aws-sdk/client-dynamodb",
         "@npm//@aws-sdk/lib-dynamodb",
         "@npm//@types/jest",
diff --git a/packages/document-client/src/classes/document-client-v3.ts b/packages/document-client/src/classes/document-client-v3.ts
index 156d171..9789e04 100644
--- a/packages/document-client/src/classes/document-client-v3.ts
+++ b/packages/document-client/src/classes/document-client-v3.ts
@@ -33,8 +33,11 @@ import {
   ScanCommand,
   TransactGetCommand,
   TransactWriteCommand,
+  TranslateConfig,
   UpdateCommand,
 } from '@aws-sdk/lib-dynamodb';
+import {isEmptyObject} from '@typedorm/common';
+import {DEFAULT_TRANSLATE_CONFIG_V3} from '../constants/translate-config';
 import {TransactionCancelledException} from '../exceptions';
 import {DocumentClient} from './base-document-client';
 
@@ -44,23 +47,29 @@ export class DocumentClientV3<
   readonly documentClient: DynamoDBDocumentClientType;
   readonly version = 3;
 
-  constructor(dynamoDBClient: DynamoDBClient) {
+  constructor(
+    dynamoDBClient: DynamoDBClient,
+    customTranslateConfig?: TranslateConfig
+  ) {
     super();
-    const marshallOptions = {
-      // Whether to automatically convert empty strings, blobs, and sets to `null`.
-      convertEmptyValues: false, // false, by default.
-      // Whether to remove undefined values while marshalling.
-      removeUndefinedValues: false, // false, by default.
-      // Whether to convert typeof object to map attribute.
-      convertClassInstanceToMap: false, // false, by default.
-    };
 
-    const unmarshallOptions = {
-      // Whether to return numbers as a string instead of converting them to native JavaScript numbers.
-      wrapNumbers: false, // false, by default.
+    const translateConfig = {
+      marshallOptions:
+        (customTranslateConfig &&
+          !isEmptyObject(customTranslateConfig.marshallOptions) && {
+            ...DEFAULT_TRANSLATE_CONFIG_V3.marshallOptions,
+            ...customTranslateConfig.marshallOptions,
+          }) ||
+        DEFAULT_TRANSLATE_CONFIG_V3.marshallOptions,
+      unmarshallOptions:
+        (customTranslateConfig &&
+          !isEmptyObject(customTranslateConfig.marshallOptions) && {
+            ...DEFAULT_TRANSLATE_CONFIG_V3.unmarshallOptions,
+            ...customTranslateConfig.unmarshallOptions,
+          }) ||
+        DEFAULT_TRANSLATE_CONFIG_V3.unmarshallOptions,
     };
 
-    const translateConfig = {marshallOptions, unmarshallOptions};
     this.documentClient = DynamoDBDocumentClient.from(
       dynamoDBClient,
       translateConfig
diff --git a/packages/document-client/src/constants/translate-config.ts b/packages/document-client/src/constants/translate-config.ts
new file mode 100644
index 0000000..0d7cd9a
--- /dev/null
+++ b/packages/document-client/src/constants/translate-config.ts
@@ -0,0 +1,14 @@
+export const DEFAULT_TRANSLATE_CONFIG_V3 = {
+  marshallOptions: {
+    // Whether to automatically convert empty strings, blobs, and sets to `null`.
+    convertEmptyValues: false, // false, by default.
+    // Whether to remove undefined values while marshalling.
+    removeUndefinedValues: false, // false, by default.
+    // Whether to convert typeof object to map attribute.
+    convertClassInstanceToMap: false, // false, by default.
+  },
+  unmarshallOptions: {
+    // Whether to return numbers as a string instead of converting them to native JavaScript numbers.
+    wrapNumbers: false, // false, by default.
+  },
+};


Test patch:
diff --git a/packages/document-client/src/classes/__tests__/document-client-v3.spec.ts b/packages/document-client/src/classes/__tests__/document-client-v3.spec.ts
index e2da280..78b8625 100644
--- a/packages/document-client/src/classes/__tests__/document-client-v3.spec.ts
+++ b/packages/document-client/src/classes/__tests__/document-client-v3.spec.ts
@@ -1,4 +1,5 @@
 import {DynamoDBClient} from '@aws-sdk/client-dynamodb';
+import {DEFAULT_TRANSLATE_CONFIG_V3} from '../../constants/translate-config';
 import {DocumentClient} from '../base-document-client';
 import {DocumentClientV3} from '../document-client-v3';
 
@@ -16,3 +17,44 @@ beforeEach(() => {
 test('registers a valid documentClient instance', async () => {
   expect(dc.version).toEqual(3);
 });
+
+it('uses default documentClientV3 translate config', () => {
+  const documentClientInstance = new DocumentClientV3(new DynamoDBClient({}));
+
+  expect(documentClientInstance.documentClient.config.translateConfig).toEqual(
+    DEFAULT_TRANSLATE_CONFIG_V3
+  );
+});
+
+it('overrides documentClientV3 translate config with custom values', () => {
+  const documentClientInstance = new DocumentClientV3(new DynamoDBClient({}), {
+    marshallOptions: {
+      convertEmptyValues: true,
+    },
+    unmarshallOptions: {
+      wrapNumbers: true,
+    },
+  });
+
+  expect(documentClientInstance.documentClient.config.translateConfig).toEqual({
+    marshallOptions: {
+      convertClassInstanceToMap: false,
+      convertEmptyValues: true,
+      removeUndefinedValues: false,
+    },
+    unmarshallOptions: {
+      wrapNumbers: true,
+    },
+  });
+});
+
+it('correctly merges documentClientV3 translate config with custom values when all values are not provided', () => {
+  const documentClientInstance = new DocumentClientV3(new DynamoDBClient({}), {
+    marshallOptions: {},
+    unmarshallOptions: undefined,
+  });
+
+  expect(documentClientInstance.documentClient.config.translateConfig).toEqual(
+    DEFAULT_TRANSLATE_CONFIG_V3
+  );
+});