Repository: jeremydaly/dynamodb-toolbox (ts)

Issue:
`NULL` converted to `0` when fetching data
My table is configured to NOT remove null attributes
```
removeNullAttributes: false,
```

I have two attributes defined as
```
attr1: { type: 'number' },
attr2: { type: 'string' },
```

Saved them both as `null`, checking on Dynamo as expected, we have:

```
"attr1": {
    "NULL": true
  },
"attr2": {
    "NULL": true
  },
```

However, when getting the item back from Dynamo, I get

```
attr1: 0,
attr2: null,
```

I don´t see why any conversion took place for the number
But even if a conversion (null to 0) were to take place, it should have also taken place when saving the data to dynamoDB and saved 0, not NULL.




Pull request description:
fix: resolve null attributes being wrapped when parsed
resolves #514

Patch:
diff --git a/src/lib/formatItem.ts b/src/lib/formatItem.ts
index 2b8bf02e..94418990 100644
--- a/src/lib/formatItem.ts
+++ b/src/lib/formatItem.ts
@@ -10,24 +10,28 @@ import { Linked } from './parseEntity'
 
 // Convert from DocumentClient values, which may be wrapped sets or numbers,
 // into normal TS values.
-const convertDynamoValues = (value: any, attr?: PureAttributeDefinition) => {
+const convertDynamoValues = (value: unknown, attr?: PureAttributeDefinition): unknown => {
+  if (value === null) {
+    return value
+  }
+
   // Unwrap bigint/number sets to regular numbers/bigints
   if (attr && attr.type === 'set') {
     if (attr.setType === 'bigint') {
-      value = Array.from(value).map((n: any) => BigInt(n))
+      value = Array.from(value as Set<bigint>).map((n: any) => BigInt(n))
     } else if (attr.setType === 'number') {
-      value = Array.from(value).map((n: any) => Number(n))
+      value = Array.from(value as Set<number>).map((n: any) => Number(n))
     } else {
-      value = Array.from(value)
+      value = Array.from(value as Set<unknown>)
     }
   }
 
   // Convert wrapped number values to bigints
   if (attr && attr.type === 'bigint') {
-    value = BigInt(value)
+    value = BigInt(value as number)
   }
   if (attr && attr.type === 'number') {
-    value = Number(value)
+    value = Number(value as number)
   }
 
   return value
@@ -38,7 +42,7 @@ export default () => (
   attributes: { [key: string]: PureAttributeDefinition },
   linked: Linked,
   item: any,
-  include: string[] = []
+  include: string[] = [],
 ) => {
   // TODO: Support nested maps?
   // TODO: include alias support?
@@ -59,8 +63,9 @@ export default () => (
             attributes[f].save ||
             attributes[f].hidden ||
             (include.length > 0 && !include.includes(f))
-          )
+          ) {
             return acc
+          }
           return Object.assign(acc, {
             [attributes[f].alias || f]: validateType(
               attributes[f],
@@ -68,18 +73,19 @@ export default () => (
               item[field]
                 .replace(new RegExp(`^${escapeRegExp(attributes[field].prefix!)}`), '')
                 .replace(new RegExp(`${escapeRegExp(attributes[field].suffix!)}$`), '')
-                .split(attributes[field].delimiter || '#')[i]
-            )
+                .split(attributes[field].delimiter || '#')[i],
+            ),
           })
-        }, {})
+        }, {}),
       )
     }
 
     if (
       (attributes[field] && attributes[field].hidden) ||
       (include.length > 0 && !include.includes(field))
-    )
+    ) {
       return acc
+    }
 
     item[field] = convertDynamoValues(item[field], attributes[field])
 
@@ -96,7 +102,7 @@ export default () => (
         : fieldValue
 
     return Object.assign(acc, {
-      [(attributes[field] && attributes[field].alias) || field]: transformedValue
+      [(attributes[field] && attributes[field].alias) || field]: transformedValue,
     })
   }, {})
 }


Test patch:
diff --git a/src/__tests__/formatItem.unit.test.ts b/src/__tests__/formatItem.unit.test.ts
index 86a0e799..1124cf18 100644
--- a/src/__tests__/formatItem.unit.test.ts
+++ b/src/__tests__/formatItem.unit.test.ts
@@ -159,4 +159,13 @@ describe('formatItem', () => {
     )
     expect(result).toEqual({ unspecified: 'value' })
   })
+
+  it('passes through null attribute', () => {
+    const result = formatItem()(
+      DefaultTable.User.schema.attributes,
+      DefaultTable.User.linked,
+      { number: null }
+    )
+    expect(result).toEqual({ number: null })
+  })
 })