Skip to content

Commit

Permalink
fix(format-item): resolve wrap number issues (dynamodb-toolbox#590)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam McCarthy <5650580+amccarthy1@users.noreply.github.com>
  • Loading branch information
naorpeled and amccarthy1 authored Oct 5, 2023
1 parent a59efdf commit 90f7013
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 81 deletions.
46 changes: 25 additions & 21 deletions src/__tests__/entity.parse.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { DocumentClientWithWrappedNumbers } from './bootstrap.test'

import Table from '../classes/Table'
import Entity from '../classes/Entity'
import { toDynamoBigInt } from '../lib/utils'
import { unmarshall } from '@aws-sdk/util-dynamodb'

const TestTable = new Table({
Expand Down Expand Up @@ -206,7 +205,8 @@ describe('parse', () => {

it('parses wrapped numbers', () => {
const wrap = (value: number) =>
unmarshall({ valueToUnmarshall: {N: value.toString()} }, { wrapNumbers: true }).valueToUnmarshall.value
unmarshall({ valueToUnmarshall: { N: value.toString() } }, { wrapNumbers: true })
.valueToUnmarshall.value

const item = TestEntity.parse({
pk: 'test@test.com',
Expand All @@ -226,35 +226,39 @@ describe('parse', () => {
const item = TestEntity.parse({
pk: 'test@test.com',
sk: 'bigint',
test_bigint: toDynamoBigInt(BigInt('90071992547409911234')),
test_bigint: { value: '99999999999999999999999999999999999999' },
test_bigint_coerce: '12345'
})
expect(item).toEqual({
email: 'test@test.com',
test_type: 'bigint',
test_bigint: BigInt('90071992547409911234'),
test_bigint_coerce: BigInt('12345')
})
expect(item).toMatchInlineSnapshot(`
{
"email": "test@test.com",
"test_bigint": 99999999999999999999999999999999999999n,
"test_bigint_coerce": 12345n,
"test_type": "bigint",
}
`)
})

it('parses bigint sets', () => {
const item = TestEntity.parse({
pk: 'test@test.com',
sk: 'bigint',
test_bigint_set_type: new Set([
toDynamoBigInt(BigInt('90071992547409911234')),
toDynamoBigInt(BigInt('-90071992547409911234')),
{ value: '90071992547409911234' },
{ value: '-90071992547409911234' },
1234
]),
})
expect(item).toEqual({
email: 'test@test.com',
test_type: 'bigint',
test_bigint_set_type: [
BigInt('90071992547409911234'),
BigInt('-90071992547409911234'),
BigInt(1234),
]
])
})
expect(item).toMatchInlineSnapshot(`
{
"email": "test@test.com",
"test_bigint_set_type": [
90071992547409911234n,
-90071992547409911234n,
1234n,
],
"test_type": "bigint",
}
`)
})
})
131 changes: 79 additions & 52 deletions src/__tests__/formatItem.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ DefaultTable.addEntity(
set: { type: 'set', setType: 'string', alias: 'set_alias' },
set_alias2: { type: 'set', setType: 'string', map: 'set2' },
number: 'number',
numberSet: { type: 'set', setType: 'number' },
bigint: 'bigint',
bigintSet: { type: 'set', setType: 'bigint' },
list: { type: 'list', alias: 'list_alias' },
list_alias2: { type: 'list', map: 'list2' },
test: 'map',
Expand All @@ -38,63 +41,95 @@ DefaultTable.addEntity(
} as const)
)

// console.log(DefaultTable.User);

describe('formatItem', () => {
it('formats item with no alias', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ pk: 'test' }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
pk: 'test'
})
expect(result).toEqual({ pk: 'test' })
})

it('formats item with alias', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ list: ['test'] }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
list: ['test']
})
expect(result).toEqual({ list_alias: ['test'] })
})

it('formats item with mapping', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ list2: ['test'] }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
list2: ['test']
})
expect(result).toEqual({ list_alias2: ['test'] })
})

it('formats item with set (alias)', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ set: new Set([1, 2, 3]) }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
set: new Set([1, 2, 3])
})
expect(result).toEqual({ set_alias: [1, 2, 3] })
})

it('formats item with set (map)', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ set2: new Set([1, 2, 3]) }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
set2: new Set([1, 2, 3])
})
expect(result).toEqual({ set_alias2: [1, 2, 3] })
})

it('formats item with linked fields', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ sk: 'test1#test2' }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
sk: 'test1#test2'
})
expect(result).toEqual({ sk: 'test1#test2', linked1: 'test1', linked2: 'test2' })
})

it('formats item with wrapped numbers', () => {
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
number: { value: '1' },
numberSet: new Set([
{
value: '1'
},
{
value: '2'
},
{
value: '3'
}
]),
bigint: { value: '1' },
bigintSet: new Set([
{
value: '11234567899999999'
},
{
value: '11234567899999999'
},
{
value: '11234567899999999'
}
])
})

expect(result).toMatchInlineSnapshot(`
{
"bigint": 1n,
"bigintSet": [
11234567899999999n,
11234567899999999n,
11234567899999999n,
],
"number": 1,
"numberSet": [
1,
2,
3,
],
}
`)
})

it('specifies attributes to include', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
Expand Down Expand Up @@ -126,11 +161,9 @@ describe('formatItem', () => {
})

it('formats item with linked aliased composite field', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ composite1: 'test1#test2' }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
composite1: 'test1#test2'
})
expect(result).toEqual({
composite1_alias: 'test1#test2',
linked3: 'test1',
Expand All @@ -139,11 +172,9 @@ describe('formatItem', () => {
})

it('formats item with linked mapped composite field', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ composite2: 'test1#test2' }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
composite2: 'test1#test2'
})
expect(result).toEqual({
composite2_alias: 'test1#test2',
linked5: 'test1',
Expand All @@ -152,20 +183,16 @@ describe('formatItem', () => {
})

it('passes through attribute not specified in entity', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ unspecified: 'value' }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
unspecified: 'value'
})
expect(result).toEqual({ unspecified: 'value' })
})

it('passes through null attribute', () => {
const result = formatItem()(
DefaultTable.User.schema.attributes,
DefaultTable.User.linked,
{ number: null }
)
const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, {
number: null
})
expect(result).toEqual({ number: null })
})
})
25 changes: 17 additions & 8 deletions src/lib/formatItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,43 @@
* @license MIT
*/

import { PureAttributeDefinition } from '../classes/Entity'
import type { NativeAttributeValue, NativeScalarAttributeValue, NumberValue } from '@aws-sdk/util-dynamodb'
import type { PureAttributeDefinition } from '../classes/Entity'
import validateTypes from './validateTypes'
import { Linked } from './parseEntity'
import type { Linked } from './parseEntity'

// Convert from DocumentClient values, which may be wrapped sets or numbers,
// into normal TS values.
const convertDynamoValues = (value: unknown, attr?: PureAttributeDefinition): unknown => {
const convertDynamoValues = (value: NativeAttributeValue, 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 as Set<bigint>).map((n: any) => BigInt(n))
value = Array.from(value as Set<bigint>).map((n: any) => BigInt(unwrapAttributeValue(n)))
} else if (attr.setType === 'number') {
value = Array.from(value as Set<number>).map((n: any) => Number(n))
value = Array.from(value as Set<number>).map((n: any) => Number(unwrapAttributeValue(n)))
} else {
value = Array.from(value as Set<unknown>)
}
}

// Convert wrapped number values to bigints
if (attr && attr.type === 'bigint') {
value = BigInt(value as number)
value = BigInt(unwrapAttributeValue(value))
}

if (attr && attr.type === 'number') {
value = Number(value as number)
value = Number(unwrapAttributeValue(value))
}

return value
}

const unwrapAttributeValue = (value: NativeAttributeValue): boolean | number | bigint | string => {
if( value?.value !== undefined ) {
return value.value
}

return value
Expand Down

0 comments on commit 90f7013

Please sign in to comment.