From a00be0449f857bc4c67d69014210a885b0d11d99 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 19 Oct 2018 08:03:24 +0200 Subject: [PATCH 1/7] fix(mapper): proper validation if attribute is HASH or RANGE key --- src/mapper/mapper.ts | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/mapper/mapper.ts b/src/mapper/mapper.ts index 8b9ba60f6..e9b9549dd 100644 --- a/src/mapper/mapper.ts +++ b/src/mapper/mapper.ts @@ -134,38 +134,30 @@ export class Mapper { : null const type: AttributeModelType = explicitType || Util.typeOf(propertyValue) + const mapper = propertyMetadata && propertyMetadata.mapper ? new propertyMetadata.mapper() : Mapper.forType(type) + + const attrValue: AttributeValue | null = explicitType + ? mapper.toDb(propertyValue, propertyMetadata) + : mapper.toDb(propertyValue) + // some basic validation + // todo: null is probably not ok !? if ( propertyMetadata && propertyMetadata.key && - propertyMetadata.key.type === 'HASH' && - !propertyMetadata.mapper && - type !== String && - type !== Number && - type !== Binary + attrValue !== null && + !('S' in attrValue) && + !('N' in attrValue) && + !('B' in attrValue) ) { throw new Error( - `make sure to define a custom mapper which returns a string or number value for partition key, type ${type} cannot be used as partition key, value = ${JSON.stringify( - propertyValue - )}` + `\ +DynamoDb only allows string, number or binary type for RANGE and HASH key. \ +Make sure to define a custom mapper which returns a string, number or binary value for partition key, \ +type ${type} cannot be used as partition key, value = ${JSON.stringify(propertyValue)}` ) } - - if (propertyMetadata && propertyMetadata.mapper) { - // custom mapper - if (explicitType) { - return new propertyMetadata.mapper().toDb(propertyValue, propertyMetadata) - } else { - return new propertyMetadata.mapper().toDb(propertyValue) - } - } else { - // mapper by type - if (explicitType) { - return Mapper.forType(type).toDb(propertyValue, propertyMetadata) - } else { - return Mapper.forType(type).toDb(propertyValue) - } - } + return attrValue } static fromDb(attributeMap: AttributeMap, modelClass?: ModelConstructor): T { From 30cdedd9200c7532fefc7eb7c4718cf2ec854c3d Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 19 Oct 2018 08:05:34 +0200 Subject: [PATCH 2/7] test(mapper): add tests for complex types as HASH or RANGE key --- test/mapper.spec.ts | 260 ++++++++++-------- test/models/model-with-moment-as-key.model.ts | 41 +++ .../model-without-custom-mapper.model.ts | 31 +++ 3 files changed, 215 insertions(+), 117 deletions(-) create mode 100644 test/models/model-with-moment-as-key.model.ts create mode 100644 test/models/model-without-custom-mapper.model.ts diff --git a/test/mapper.spec.ts b/test/mapper.spec.ts index 4a84b3cd6..0d01b651a 100644 --- a/test/mapper.spec.ts +++ b/test/mapper.spec.ts @@ -22,6 +22,8 @@ import { productFromDb } from './data/product-dynamodb.data' import { Employee } from './models/employee.model' import { ModelWithAutogeneratedId } from './models/model-with-autogenerated-id.model' import { Id, ModelWithCustomMapperModel } from './models/model-with-custom-mapper.model' +import { ModelWithMomentAsHashKey, ModelWithMomentAsIndexHashKey } from './models/model-with-moment-as-key.model' +import { ModelWithoutCustomMapper, ModelWithoutCustomMapperOnIndex } from './models/model-without-custom-mapper.model' import { Birthday, Organization, OrganizationEvent } from './models/organization.model' import { Product } from './models/product.model' import { Type } from './models/types.enum' @@ -30,44 +32,44 @@ describe('Mapper', () => { describe('should map single values', () => { describe('to db', () => { it('string', () => { - const attrValue: AttributeValue = Mapper.toDbOne('foo') + const attrValue: AttributeValue = Mapper.toDbOne('foo')! expect(attrValue).toBeDefined() - expect(attrValue.S).toBeDefined() - expect(attrValue.S).toBe('foo') + expect(attrValue!.S).toBeDefined() + expect(attrValue!.S).toBe('foo') }) it('string (empty)', () => { - const attrValue: AttributeValue = Mapper.toDbOne('') + const attrValue: AttributeValue = Mapper.toDbOne('')! expect(attrValue).toBe(null) }) it('number', () => { - const attrValue: AttributeValue = Mapper.toDbOne(3) + const attrValue: AttributeValue = Mapper.toDbOne(3)! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('N') - expect(attrValue.N).toBe('3') + expect(keyOf(attrValue!)).toBe('N') + expect(attrValue!.N).toBe('3') }) it('boolean', () => { - const attrValue: AttributeValue = Mapper.toDbOne(false) + const attrValue: AttributeValue = Mapper.toDbOne(false)! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('BOOL') - expect(attrValue.BOOL).toBe(false) + expect(keyOf(attrValue!)).toBe('BOOL') + expect(attrValue!.BOOL).toBe(false) }) it('null', () => { - const attrValue: AttributeValue = Mapper.toDbOne(null) + const attrValue: AttributeValue = Mapper.toDbOne(null)! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('NULL') - expect(attrValue.NULL).toBe(true) + expect(keyOf(attrValue!)).toBe('NULL') + expect(attrValue!.NULL).toBe(true) }) it('date (moment)', () => { const m = moment() - const attrValue: AttributeValue = Mapper.toDbOne(m) + const attrValue: AttributeValue = Mapper.toDbOne(m)! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('S') - expect(attrValue.S).toBe( + expect(keyOf(attrValue!)).toBe('S') + expect(attrValue!.S).toBe( m .clone() .utc() @@ -76,71 +78,71 @@ describe('Mapper', () => { }) it('enum (no enum decorator)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(Type.FirstType) + const attrValue: AttributeValue = Mapper.toDbOne(Type.FirstType)! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('N') - expect(attrValue.N).toBe('0') + expect(keyOf(attrValue!)).toBe('N') + expect(attrValue!.N).toBe('0') }) it('enum (propertyMetadata -> no enum decorator)', () => { const attrValue: AttributeValue = Mapper.toDbOne(Type.FirstType, { typeInfo: { type: Object, isCustom: true }, - }) + })! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('M') - expect(attrValue.M).toEqual({}) + expect(keyOf(attrValue!)).toBe('M') + expect(attrValue!.M).toEqual({}) }) it('enum (with decorator)', () => { const attrValue: AttributeValue = Mapper.toDbOne(Type.FirstType, { typeInfo: { type: EnumType, isCustom: true }, - }) + })! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('N') - expect(attrValue.N).toBe('0') + expect(keyOf(attrValue!)).toBe('N') + expect(attrValue!.N).toBe('0') }) it('array -> SS (homogen, no duplicates)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(['foo', 'bar']) + const attrValue: AttributeValue = Mapper.toDbOne(['foo', 'bar'])! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('SS') - expect(attrValue.SS[0]).toBe('foo') - expect(attrValue.SS[1]).toBe('bar') + expect(keyOf(attrValue!)).toBe('SS') + expect(attrValue!.SS![0]).toBe('foo') + expect(attrValue!.SS![1]).toBe('bar') }) it('array -> L (homogen, no duplicates, explicit type)', () => { const propertyMetadata = >>{ typeInfo: { type: Array, isCustom: true }, } - const attrValue: AttributeValue = Mapper.toDbOne(['foo', 'bar'], propertyMetadata) + const attrValue: AttributeValue = Mapper.toDbOne(['foo', 'bar'], propertyMetadata)! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('L') + expect(keyOf(attrValue!)).toBe('L') - expect(keyOf(attrValue.L[0])).toBe('S') - expect(attrValue.L[0].S).toBe('foo') + expect(keyOf(attrValue!.L![0])).toBe('S') + expect(attrValue!.L![0].S).toBe('foo') - expect(keyOf(attrValue.L[1])).toBe('S') - expect(attrValue.L[1].S).toBe('bar') + expect(keyOf(attrValue!.L![1])).toBe('S') + expect(attrValue!.L![1].S).toBe('bar') }) it('array -> L (heterogen, no duplicates)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(['foo', 56, true]) + const attrValue: AttributeValue = Mapper.toDbOne(['foo', 56, true])! expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('L') expect(attrValue.L).toBeDefined() - expect(attrValue.L.length).toBe(3) + expect(attrValue.L!.length).toBe(3) - const foo: AttributeValue = attrValue.L[0] + const foo: AttributeValue = attrValue.L![0] expect(foo).toBeDefined() expect(keyOf(foo)).toBe('S') expect(foo.S).toBe('foo') - const no: AttributeValue = attrValue.L[1] + const no: AttributeValue = attrValue.L![1] expect(no).toBeDefined() expect(keyOf(no)).toBe('N') expect(no.N).toBe('56') - const bool: AttributeValue = attrValue.L[2] + const bool: AttributeValue = attrValue.L![2] expect(bool).toBeDefined() expect(keyOf(bool)).toBe('BOOL') expect(bool.BOOL).toBe(true) @@ -151,24 +153,24 @@ describe('Mapper', () => { const attrValue: AttributeValue = Mapper.toDbOne([ new Employee('max', 25, now, null), new Employee('anna', 65, now, null), - ]) + ])! expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('L') - const employee1 = attrValue.L[0] + const employee1 = attrValue.L![0] expect(employee1).toBeDefined() expect(keyOf(employee1)).toBe('M') - expect(Object.keys(employee1.M).length).toBe(3) - expect(employee1.M['name']).toBeDefined() - expect(keyOf(employee1.M['name'])).toBe('S') - expect(employee1.M['name']['S']).toBe('max') + expect(Object.keys(employee1.M!).length).toBe(3) + expect(employee1.M!['name']).toBeDefined() + expect(keyOf(employee1.M!['name'])).toBe('S') + expect(employee1.M!['name']['S']).toBe('max') - expect(employee1.M['age']).toBeDefined() - expect(keyOf(employee1.M['age'])).toBe('N') - expect(employee1.M['age']['N']).toBe('25') + expect(employee1.M!['age']).toBeDefined() + expect(keyOf(employee1.M!['age'])).toBe('N') + expect(employee1.M!['age']['N']).toBe('25') - expect(employee1.M['createdAt']).toEqual({ + expect(employee1.M!['createdAt']).toEqual({ S: now .clone() .utc() @@ -177,16 +179,16 @@ describe('Mapper', () => { }) it('set', () => { - const attrValue: AttributeValue = Mapper.toDbOne(new Set(['foo', 'bar', 25])) + const attrValue: AttributeValue = Mapper.toDbOne(new Set(['foo', 'bar', 25]))! expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('L') - expect(attrValue.L[0]).toEqual({ S: 'foo' }) - expect(attrValue.L[1]).toEqual({ S: 'bar' }) - expect(attrValue.L[2]).toEqual({ N: '25' }) + expect(attrValue.L![0]).toEqual({ S: 'foo' }) + expect(attrValue.L![1]).toEqual({ S: 'bar' }) + expect(attrValue.L![2]).toEqual({ N: '25' }) }) it('set (empty)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(new Set()) + const attrValue: AttributeValue = Mapper.toDbOne(new Set())! expect(attrValue).toBe(null) }) @@ -198,37 +200,37 @@ describe('Mapper', () => { { name: 'foo', age: 56, createdAt: cd }, { name: 'anna', age: 26, createdAt: cd2 }, ]) - ) + )! expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('L') - expect(attrValue.L.length).toBe(2) - expect(attrValue.L[0].M).toBeDefined() - expect(attrValue.L[0].M['name']).toBeDefined() - expect(keyOf(attrValue.L[0].M['name'])).toBe('S') - expect(attrValue.L[0].M['name'].S).toBe('foo') + expect(attrValue.L!.length).toBe(2) + expect(attrValue.L![0].M).toBeDefined() + expect(attrValue.L![0].M!['name']).toBeDefined() + expect(keyOf(attrValue.L![0].M!['name'])).toBe('S') + expect(attrValue.L![0].M!['name'].S).toBe('foo') }) it('object (Employee created using Object literal)', () => { const cr: moment.Moment = moment('2017-03-03', 'YYYY-MM-DD') - const attrValue: AttributeValue = Mapper.toDbOne({ name: 'foo', age: 56, createdAt: cr }) + const attrValue: AttributeValue = Mapper.toDbOne({ name: 'foo', age: 56, createdAt: cr })! expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('M') // name - expect(attrValue.M['name']).toBeDefined() - expect(keyOf(attrValue.M['name'])).toBe('S') - expect(attrValue.M['name'].S).toBe('foo') + expect(attrValue.M!['name']).toBeDefined() + expect(keyOf(attrValue.M!['name'])).toBe('S') + expect(attrValue.M!['name'].S).toBe('foo') // age - expect(attrValue.M['age']).toBeDefined() - expect(keyOf(attrValue.M['age'])).toBe('N') - expect(attrValue.M['age'].N).toBe('56') + expect(attrValue.M!['age']).toBeDefined() + expect(keyOf(attrValue.M!['age'])).toBe('N') + expect(attrValue.M!['age'].N).toBe('56') // createdAt - expect(attrValue.M['createdAt']).toBeDefined() - expect(keyOf(attrValue.M['createdAt'])).toBe('S') - expect(attrValue.M['createdAt'].S).toBe( + expect(attrValue.M!['createdAt']).toBeDefined() + expect(keyOf(attrValue.M!['createdAt'])).toBe('S') + expect(attrValue.M!['createdAt'].S).toBe( cr .clone() .utc() @@ -238,24 +240,24 @@ describe('Mapper', () => { it('object (Employee created using constructor)', () => { const cr: moment.Moment = moment('2017-05-03', 'YYYY-MM-DD') - const attrValue: AttributeValue = Mapper.toDbOne(new Employee('foo', 56, cr, [])) + const attrValue: AttributeValue = Mapper.toDbOne(new Employee('foo', 56, cr, []))! expect(attrValue).toBeDefined() - expect(keyOf(attrValue)).toBe('M') + expect(keyOf(attrValue!)).toBe('M') // name - expect(attrValue.M['name']).toBeDefined() - expect(keyOf(attrValue.M['name'])).toBe('S') - expect(attrValue.M['name'].S).toBe('foo') + expect(attrValue!.M!['name']).toBeDefined() + expect(keyOf(attrValue!.M!['name'])).toBe('S') + expect(attrValue!.M!['name'].S).toBe('foo') // age - expect(attrValue.M['age']).toBeDefined() - expect(keyOf(attrValue.M['age'])).toBe('N') - expect(attrValue.M['age'].N).toBe('56') + expect(attrValue!.M!['age']).toBeDefined() + expect(keyOf(attrValue!.M!['age'])).toBe('N') + expect(attrValue!.M!['age'].N).toBe('56') // createdAt - expect(attrValue.M['createdAt']).toBeDefined() - expect(keyOf(attrValue.M['createdAt'])).toBe('S') - expect(attrValue.M['createdAt'].S).toBe( + expect(attrValue!.M!['createdAt']).toBeDefined() + expect(keyOf(attrValue!.M!['createdAt'])).toBe('S') + expect(attrValue!.M!['createdAt'].S).toBe( cr .clone() .utc() @@ -387,7 +389,7 @@ describe('Mapper', () => { it('M', () => { const createdAt: moment.Moment = moment('2017-05-02', 'YYYY-MM-DD') - const lastUpdatedDate: moment.Moment = moment('2017-07-05', 'YYYY-MM-DD') + // const lastUpdatedDate: moment.Moment = moment('2017-07-05', 'YYYY-MM-DD') const attrValue = { M: { name: { S: 'name' }, @@ -520,7 +522,7 @@ describe('Mapper', () => { it('domains', () => { expect(organizationAttrMap.domains).toBeDefined() - const domains: StringSetAttributeValue = organizationAttrMap.domains.SS + const domains: StringSetAttributeValue = organizationAttrMap.domains.SS! expect(domains).toBeDefined() expect(domains.length).toBe(3) @@ -532,7 +534,7 @@ describe('Mapper', () => { it('random details', () => { expect(organizationAttrMap.randomDetails).toBeDefined() - const randomDetails: ListAttributeValue = organizationAttrMap.randomDetails.L + const randomDetails: ListAttributeValue = organizationAttrMap.randomDetails.L! expect(randomDetails).toBeDefined() expect(randomDetails.length).toBe(3) @@ -548,14 +550,14 @@ describe('Mapper', () => { it('employees', () => { expect(organizationAttrMap.employees).toBeDefined() - const employeesL: AttributeValueList = organizationAttrMap.employees.L + const employeesL: AttributeValueList = organizationAttrMap.employees.L! expect(employeesL).toBeDefined() expect(employeesL.length).toBe(2) expect(employeesL[0]).toBeDefined() expect(employeesL[0].M).toBeDefined() // test employee1 - const employee1: MapAttributeValue = employeesL[0].M + const employee1: MapAttributeValue = employeesL[0].M! expect(employee1['name']).toBeDefined() expect(employee1['name'].S).toBeDefined() expect(employee1['name'].S).toBe('max') @@ -572,7 +574,7 @@ describe('Mapper', () => { ) // test employee2 - const employee2: MapAttributeValue = employeesL[1].M + const employee2: MapAttributeValue = employeesL[1].M! expect(employee2['name']).toBeDefined() expect(employee2['name'].S).toBeDefined() expect(employee2['name'].S).toBe('anna') @@ -592,7 +594,7 @@ describe('Mapper', () => { it('cities', () => { expect(organizationAttrMap.cities).toBeDefined() - const citiesSS: StringSetAttributeValue = organizationAttrMap.cities.SS + const citiesSS: StringSetAttributeValue = organizationAttrMap.cities.SS! expect(citiesSS).toBeDefined() expect(citiesSS.length).toBe(2) expect(citiesSS[0]).toBe('zürich') @@ -602,14 +604,14 @@ describe('Mapper', () => { it('birthdays', () => { expect(organizationAttrMap.birthdays).toBeDefined() - const birthdays: ListAttributeValue = organizationAttrMap.birthdays.L + const birthdays: ListAttributeValue = organizationAttrMap.birthdays.L! expect(birthdays).toBeDefined() expect(birthdays.length).toBe(2) expect(keyOf(birthdays[0])).toBe('M') // birthday 1 - const birthday1: MapAttributeValue = birthdays[0]['M'] + const birthday1: MapAttributeValue = birthdays[0]['M']! expect(birthday1['date']).toBeDefined() expect(keyOf(birthday1['date'])).toBe('S') expect(birthday1['date']['S']).toBe( @@ -621,23 +623,23 @@ describe('Mapper', () => { expect(birthday1['presents']).toBeDefined() expect(keyOf(birthday1['presents'])).toBe('L') - expect(birthday1['presents']['L'].length).toBe(2) - expect(keyOf(birthday1['presents']['L'][0])).toBe('M') + expect(birthday1['presents']['L']!.length).toBe(2) + expect(keyOf(birthday1['presents']['L']![0])).toBe('M') - expect(keyOf(birthday1['presents']['L'][0])).toBe('M') + expect(keyOf(birthday1['presents']['L']![0])).toBe('M') - const birthday1gift1 = birthday1['presents']['L'][0]['M'] + const birthday1gift1 = birthday1['presents']['L']![0]['M']! expect(birthday1gift1['description']).toBeDefined() expect(keyOf(birthday1gift1['description'])).toBe('S') expect(birthday1gift1['description']['S']).toBe('ticket to rome') - const birthday1gift2 = birthday1['presents']['L'][1]['M'] + const birthday1gift2 = birthday1['presents']['L']![1]['M']! expect(birthday1gift2['description']).toBeDefined() expect(keyOf(birthday1gift2['description'])).toBe('S') expect(birthday1gift2['description']['S']).toBe('camper van') // birthday 2 - const birthday2: MapAttributeValue = birthdays[1]['M'] + const birthday2: MapAttributeValue = birthdays[1]['M']! expect(birthday2['date']).toBeDefined() expect(keyOf(birthday2['date'])).toBe('S') expect(birthday2['date']['S']).toBe( @@ -649,17 +651,17 @@ describe('Mapper', () => { expect(birthday2['presents']).toBeDefined() expect(keyOf(birthday2['presents'])).toBe('L') - expect(birthday2['presents']['L'].length).toBe(2) - expect(keyOf(birthday2['presents']['L'][0])).toBe('M') + expect(birthday2['presents']['L']!.length).toBe(2) + expect(keyOf(birthday2['presents']['L']![0])).toBe('M') - expect(keyOf(birthday2['presents']['L'][0])).toBe('M') + expect(keyOf(birthday2['presents']['L']![0])).toBe('M') - const birthday2gift1 = birthday2['presents']['L'][0]['M'] + const birthday2gift1 = birthday2['presents']['L']![0]['M']! expect(birthday2gift1['description']).toBeDefined() expect(keyOf(birthday2gift1['description'])).toBe('S') expect(birthday2gift1['description']['S']).toBe('car') - const birthday2gift2 = birthday2['presents']['L'][1]['M'] + const birthday2gift2 = birthday2['presents']['L']![1]['M']! expect(birthday2gift2['description']).toBeDefined() expect(keyOf(birthday2gift2['description'])).toBe('S') expect(birthday2gift2['description']['S']).toBe('gin') @@ -667,7 +669,7 @@ describe('Mapper', () => { it('awards', () => { expect(organizationAttrMap.awards).toBeDefined() - const awards: ListAttributeValue = organizationAttrMap.awards.L + const awards: ListAttributeValue = organizationAttrMap.awards.L! expect(awards).toBeDefined() expect(awards.length).toBe(2) @@ -680,18 +682,18 @@ describe('Mapper', () => { it('events', () => { expect(organizationAttrMap.events).toBeDefined() - const events: ListAttributeValue = organizationAttrMap.events.L + const events: ListAttributeValue = organizationAttrMap.events.L! expect(events).toBeDefined() expect(events.length).toBe(1) expect(keyOf(events[0])).toBe('M') - expect(events[0]['M']['name']).toBeDefined() - expect(keyOf(events[0]['M']['name'])).toBe('S') - expect(events[0]['M']['name']['S']).toBe('shift the web') + expect(events[0]['M']!['name']).toBeDefined() + expect(keyOf(events[0]['M']!['name'])).toBe('S') + expect(events[0]['M']!['name']['S']).toBe('shift the web') - expect(events[0]['M']['participantCount']).toBeDefined() - expect(keyOf(events[0]['M']['participantCount'])).toBe('N') - expect(events[0]['M']['participantCount']['N']).toBe('1520') + expect(events[0]['M']!['participantCount']).toBeDefined() + expect(keyOf(events[0]['M']!['participantCount'])).toBe('N') + expect(events[0]['M']!['participantCount']['N']).toBe('1520') }) it('transient', () => { @@ -727,6 +729,30 @@ describe('Mapper', () => { }) }) + describe('model with non string/number/binary keys', () => { + it('should accept date/moment as HASH or RANGE key', () => { + const now = moment() + const toDb: AttributeMap = Mapper.toDb(new ModelWithMomentAsHashKey(now), ModelWithMomentAsHashKey) + expect(toDb.startDate.S).toBeDefined() + expect(toDb.startDate.S).toEqual(now.utc().format()) + }) + it('should accept date/moment as HASH or RANGE key on GSI', () => { + const now = moment() + const toDb: AttributeMap = Mapper.toDb(new ModelWithMomentAsIndexHashKey(0, now)) + expect(toDb.creationDate.S).toBeDefined() + expect(toDb.creationDate.S).toEqual(now.utc().format()) + }) + it('should throw error when no custom mapper was defined', () => { + expect(() => { + Mapper.toDb(new ModelWithoutCustomMapper('key', 'value', 'otherValue'), ModelWithoutCustomMapper) + }).toThrow() + + expect(() => { + Mapper.toDb(new ModelWithoutCustomMapperOnIndex('id', 'key', 'value'), ModelWithoutCustomMapperOnIndex) + }).toThrow() + }) + }) + // FIXME TEST fix this test xdescribe('model with complex property values (decorators)', () => { let toDb: AttributeMap @@ -738,19 +764,19 @@ describe('Mapper', () => { it('nested value', () => { expect(toDb.nestedValue).toBeDefined() expect(toDb.nestedValue.M).toBeDefined() - expect(Object.keys(toDb.nestedValue.M).length).toBe(1) - expect(toDb.nestedValue.M['sortedSet']).toBeDefined() - expect(keyOf(toDb.nestedValue.M['sortedSet'])).toBe('L') + expect(Object.keys(toDb.nestedValue.M!).length).toBe(1) + expect(toDb.nestedValue.M!['sortedSet']).toBeDefined() + expect(keyOf(toDb.nestedValue.M!['sortedSet'])).toBe('L') }) it('list', () => { expect(toDb.list).toBeDefined() expect(keyOf(toDb.list)).toBe('L') - expect(toDb.list.L.length).toBe(1) - expect(keyOf(toDb.list.L[0])).toBe('M') + expect(toDb.list.L!.length).toBe(1) + expect(keyOf(toDb.list.L![0])).toBe('M') // expect(Object.keys(toDb.list.L[0].M).length).toBe(1); - expect(toDb.list.L[0].M.collection).toBeDefined() - expect(keyOf(toDb.list.L[0].M.collection)).toBe('L') + expect(toDb.list.L![0].M!.collection).toBeDefined() + expect(keyOf(toDb.list.L![0].M!.collection)).toBe('L') }) }) }) diff --git a/test/models/model-with-moment-as-key.model.ts b/test/models/model-with-moment-as-key.model.ts new file mode 100644 index 000000000..956dba3b0 --- /dev/null +++ b/test/models/model-with-moment-as-key.model.ts @@ -0,0 +1,41 @@ +// tslint:disable:max-classes-per-file +import * as moment from 'moment' +import { GSIPartitionKey, Model, PartitionKey, SortKey } from '../../src/decorator' + +@Model() +export class ModelWithMomentAsHashKey { + @PartitionKey() + startDate: moment.Moment + + constructor(startDate: moment.Moment) { + this.startDate = startDate + } +} + +@Model() +export class ModelWithMomentAsRangeKey { + @PartitionKey() + id: number + + @SortKey() + creationDate: moment.Moment + + constructor(id: number, creationDate: moment.Moment) { + this.id = id + this.creationDate = creationDate + } +} + +@Model() +export class ModelWithMomentAsIndexHashKey { + @PartitionKey() + id: number + + @GSIPartitionKey('anyGSI') + creationDate: moment.Moment + + constructor(id: number, creationDate: moment.Moment) { + this.id = id + this.creationDate = creationDate + } +} diff --git a/test/models/model-without-custom-mapper.model.ts b/test/models/model-without-custom-mapper.model.ts new file mode 100644 index 000000000..0c12e2f37 --- /dev/null +++ b/test/models/model-without-custom-mapper.model.ts @@ -0,0 +1,31 @@ +// tslint:disable:max-classes-per-file +import { GSIPartitionKey } from '../../src/decorator' +import { PartitionKey } from '../../src/decorator/impl/key/partition-key.decorator' +import { Model } from '../../src/decorator/impl/model/model.decorator' + +@Model() +export class ModelWithoutCustomMapper { + @PartitionKey() + id: { key: string; value: string } + + otherVal: string + + constructor(key: string, value: string, otherValue: string) { + this.id = { key, value } + this.otherVal = otherValue + } +} + +@Model() +export class ModelWithoutCustomMapperOnIndex { + @PartitionKey() + id: string + + @GSIPartitionKey('anyGSI') + gsiPk: { key: string; value: string } + + constructor(id: string, key: string, value: string) { + this.id = id + this.gsiPk = { key, value } + } +} From 1147be049bd90d41a5b44e28ea2d9833e7542a39 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Sat, 20 Oct 2018 09:02:02 +0200 Subject: [PATCH 3/7] fix(mapper): disallowe null value for HASH/RANGE key --- src/mapper/mapper.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/mapper/mapper.ts b/src/mapper/mapper.ts index e9b9549dd..6781a24ce 100644 --- a/src/mapper/mapper.ts +++ b/src/mapper/mapper.ts @@ -141,21 +141,18 @@ export class Mapper { : mapper.toDb(propertyValue) // some basic validation - // todo: null is probably not ok !? - if ( - propertyMetadata && - propertyMetadata.key && - attrValue !== null && - !('S' in attrValue) && - !('N' in attrValue) && - !('B' in attrValue) - ) { - throw new Error( - `\ + if (propertyMetadata && propertyMetadata.key) { + if (attrValue === null) { + throw new Error(`null but is key`) + } + if (!('S' in attrValue) && !('N' in attrValue) && !('B' in attrValue)) { + throw new Error( + `\ DynamoDb only allows string, number or binary type for RANGE and HASH key. \ Make sure to define a custom mapper which returns a string, number or binary value for partition key, \ type ${type} cannot be used as partition key, value = ${JSON.stringify(propertyValue)}` - ) + ) + } } return attrValue } From e0cf2af1bc302cc0632498650eca3190e4180569 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Wed, 24 Oct 2018 17:09:16 +0200 Subject: [PATCH 4/7] feat(mapper): print out property name on error --- src/mapper/mapper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mapper/mapper.ts b/src/mapper/mapper.ts index 6781a24ce..df9424073 100644 --- a/src/mapper/mapper.ts +++ b/src/mapper/mapper.ts @@ -143,13 +143,13 @@ export class Mapper { // some basic validation if (propertyMetadata && propertyMetadata.key) { if (attrValue === null) { - throw new Error(`null but is key`) + throw new Error(`${propertyMetadata.name.toString()} is null but is a ${propertyMetadata.key.type} key`) } if (!('S' in attrValue) && !('N' in attrValue) && !('B' in attrValue)) { throw new Error( `\ DynamoDb only allows string, number or binary type for RANGE and HASH key. \ -Make sure to define a custom mapper which returns a string, number or binary value for partition key, \ +Make sure to define a custom mapper for '${propertyMetadata.name.toString()}' which returns a string, number or binary value for partition key, \ type ${type} cannot be used as partition key, value = ${JSON.stringify(propertyValue)}` ) } From 807ef7ebba65f57bdd6218fa2c1fe0deb1d4bf51 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Thu, 25 Oct 2018 13:01:09 +0200 Subject: [PATCH 5/7] refactor(mapper): specific attribute types for a strictly typed mapper now dynamo-easy is using own types for attributes to provide strict arguments and return types for mappers BREAKING CHANGE: CustomMappers need to define their AttributeType --- .../impl/mapper/custom-mapper.decorator.ts | 5 +- .../impl/property/property.decorator.ts | 14 +-- .../metadata/property-metadata.model.ts | 7 +- src/dynamo/batchget/batch-get.request.ts | 5 +- .../type/condition-expression-chain.ts | 2 +- .../condition-expression-definition-chain.ts | 2 +- .../type/request-condition-function.ts | 2 +- .../batch-get-single-table.request.ts | 7 +- src/dynamo/request/get/get.request.ts | 5 +- src/dynamo/request/query/query.request.ts | 7 +- src/dynamo/request/scan/scan.request.ts | 9 +- src/helper/index.ts | 1 + src/helper/not-null.function.ts | 3 + src/mapper/for-type/base.mapper.ts | 32 ++++++- src/mapper/for-type/boolean.mapper.spec.ts | 4 +- src/mapper/for-type/boolean.mapper.ts | 10 +-- src/mapper/for-type/collection.mapper.ts | 76 ++++++++++------ src/mapper/for-type/date.mapper.spec.ts | 2 +- src/mapper/for-type/date.mapper.ts | 9 +- src/mapper/for-type/enum.mapper.ts | 8 +- src/mapper/for-type/moment.mapper.ts | 8 +- src/mapper/for-type/null.mapper.spec.ts | 2 +- src/mapper/for-type/null.mapper.ts | 9 +- src/mapper/for-type/number.mapper.spec.ts | 2 +- src/mapper/for-type/number.mapper.ts | 9 +- src/mapper/for-type/object.mapper.ts | 12 +-- src/mapper/for-type/string.mapper.spec.ts | 4 +- src/mapper/for-type/string.mapper.ts | 8 +- src/mapper/index.ts | 5 +- src/mapper/mapper.ts | 38 ++++---- src/mapper/type/attribute-collection.type.ts | 1 - src/mapper/type/attribute-type.type.ts | 3 + ...l.type.ts => attribute-value-type.type.ts} | 2 +- src/mapper/type/attribute.type.ts | 88 ++++++++++++++++++- src/mapper/util.ts | 9 +- ...l-with-custom-mapper-for-sort-key.model.ts | 10 +-- test/models/model-with-custom-mapper.model.ts | 16 ++-- test/models/real-world/form-id.mapper.ts | 31 ++++--- test/models/real-world/number-enum.mapper.ts | 10 +-- test/models/real-world/order-id.mapper.ts | 8 +- test/models/real-world/order.model.ts | 6 +- 41 files changed, 321 insertions(+), 170 deletions(-) create mode 100644 src/helper/index.ts create mode 100644 src/helper/not-null.function.ts delete mode 100644 src/mapper/type/attribute-collection.type.ts create mode 100644 src/mapper/type/attribute-type.type.ts rename src/mapper/type/{attribute-model.type.ts => attribute-value-type.type.ts} (90%) diff --git a/src/decorator/impl/mapper/custom-mapper.decorator.ts b/src/decorator/impl/mapper/custom-mapper.decorator.ts index b9675e97a..8bb1e04e8 100644 --- a/src/decorator/impl/mapper/custom-mapper.decorator.ts +++ b/src/decorator/impl/mapper/custom-mapper.decorator.ts @@ -1,8 +1,11 @@ import { MapperForType } from '../../../mapper/for-type/base.mapper' +import { Attribute } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' import { initOrUpdateProperty } from '../property/property.decorator' -export function CustomMapper(mapperClazz: ModelConstructor>): PropertyDecorator { +export function CustomMapper( + mapperClazz: ModelConstructor> +): PropertyDecorator { return (target: any, propertyKey: string | symbol) => { if (typeof propertyKey === 'string') { initOrUpdateProperty({ mapper: mapperClazz }, target, propertyKey) diff --git a/src/decorator/impl/property/property.decorator.ts b/src/decorator/impl/property/property.decorator.ts index ba9073d93..b3fa984ae 100644 --- a/src/decorator/impl/property/property.decorator.ts +++ b/src/decorator/impl/property/property.decorator.ts @@ -1,6 +1,7 @@ import { KeyType } from 'aws-sdk/clients/dynamodb' import { DynamoEasyConfig } from '../../../config/dynamo-easy-config' -import { AttributeModelType } from '../../../mapper/type/attribute-model.type' +import { AttributeValueType } from '../../../mapper/type/attribute-value-type.type' +import { Attribute } from '../../../mapper/type/attribute.type' import { MomentType } from '../../../mapper/type/moment.type' import { Util } from '../../../mapper/util' import { PropertyMetadata, TypeInfo } from '../../metadata/property-metadata.model' @@ -74,7 +75,7 @@ function initOrUpdateLSI(indexes: string[], indexData: IndexData): Partial> = {}, + propertyMetadata: Partial> = {}, target: any, propertyKey: string ): void { @@ -86,7 +87,10 @@ export function initOrUpdateProperty( if (existingProperty) { // merge property options // console.log('merge into existing property', existingProperty, propertyMetadata); - Object.assign, Partial>>(existingProperty, propertyMetadata) + Object.assign, Partial>>( + existingProperty, + propertyMetadata + ) } else { // add new options const newProperty: PropertyMetadata = createNewProperty(propertyMetadata, target, propertyKey) @@ -98,11 +102,11 @@ export function initOrUpdateProperty( } function createNewProperty( - propertyOptions: Partial> = {}, + propertyOptions: Partial> = {}, target: any, propertyKey: string ): PropertyMetadata { - let propertyType: AttributeModelType = getMetadataType(target, propertyKey) + let propertyType: AttributeValueType = getMetadataType(target, propertyKey) let customType = isCustomType(propertyType) const typeByConvention = Util.typeByConvention(propertyKey) diff --git a/src/decorator/metadata/property-metadata.model.ts b/src/decorator/metadata/property-metadata.model.ts index a9c6534c8..8fd8b4c1d 100644 --- a/src/decorator/metadata/property-metadata.model.ts +++ b/src/decorator/metadata/property-metadata.model.ts @@ -1,5 +1,6 @@ import { KeyType } from 'aws-sdk/clients/dynamodb' import { MapperForType } from '../../mapper/for-type/base.mapper' +import { Attribute } from '../../mapper/type/attribute.type' import { ModelConstructor } from '../../model/model-constructor' export interface TypeInfo { @@ -14,7 +15,7 @@ export interface Key { uuid?: boolean } -export interface PropertyMetadata { +export interface PropertyMetadata { // this property desribes a key attribute (either partition or sort) for the table key?: Key @@ -35,7 +36,7 @@ export interface PropertyMetadata { */ isSortedCollection?: boolean - mapper?: ModelConstructor> + mapper?: ModelConstructor> // maps the index name to the key type to describe for which GSI this property describes a key attribute keyForGSI?: { [key: string]: KeyType } @@ -47,6 +48,6 @@ export interface PropertyMetadata { transient?: boolean } -export function hasGenericType(propertyMetadata?: PropertyMetadata): boolean { +export function hasGenericType(propertyMetadata?: PropertyMetadata): boolean { return !!(propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.genericType) } diff --git a/src/dynamo/batchget/batch-get.request.ts b/src/dynamo/batchget/batch-get.request.ts index 87a6898be..cffd7d7bc 100644 --- a/src/dynamo/batchget/batch-get.request.ts +++ b/src/dynamo/batchget/batch-get.request.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { MetadataHelper } from '../../decorator/metadata/metadata-helper' import { Mapper } from '../../mapper/mapper' +import { Attributes } from '../../mapper/type/attribute.type' import { ModelConstructor } from '../../model/model-constructor' import { DEFAULT_SESSION_VALIDITY_ENSURER } from '../default-session-validity-ensurer.const' import { DEFAULT_TABLE_NAME_RESOLVER } from '../default-table-name-resolver.const' @@ -101,7 +102,7 @@ export class BatchGetRequest { if (response.Responses && Object.keys(response.Responses).length) { Object.keys(response.Responses).forEach(tableName => { const mapped = response.Responses![tableName].map(attributeMap => - Mapper.fromDb(attributeMap, this.tables.get(tableName)) + Mapper.fromDb(attributeMap, this.tables.get(tableName)) ) r.Responses![tableName] = mapped }) @@ -119,7 +120,7 @@ export class BatchGetRequest { if (response.Responses && Object.keys(response.Responses).length) { Object.keys(response.Responses).forEach(tableName => { const mapped = response.Responses![tableName].map(attributeMap => - Mapper.fromDb(attributeMap, this.tables.get(tableName)) + Mapper.fromDb(attributeMap, this.tables.get(tableName)) ) r[tableName] = mapped }) diff --git a/src/dynamo/expression/type/condition-expression-chain.ts b/src/dynamo/expression/type/condition-expression-chain.ts index f1bb9f6af..ce7b996e1 100644 --- a/src/dynamo/expression/type/condition-expression-chain.ts +++ b/src/dynamo/expression/type/condition-expression-chain.ts @@ -1,4 +1,4 @@ -import { AttributeType } from '../../../mapper/type/attribute.type' +import { AttributeType } from '../../../mapper/type/attribute-type.type' import { Expression } from './expression.type' export interface ConditionExpressionChain { diff --git a/src/dynamo/expression/type/condition-expression-definition-chain.ts b/src/dynamo/expression/type/condition-expression-definition-chain.ts index 2ddefac98..41d888572 100644 --- a/src/dynamo/expression/type/condition-expression-definition-chain.ts +++ b/src/dynamo/expression/type/condition-expression-definition-chain.ts @@ -1,4 +1,4 @@ -import { AttributeType } from '../../../mapper/type/attribute.type' +import { AttributeType } from '../../../mapper/type/attribute-type.type' import { ConditionExpressionDefinitionFunction } from './condition-expression-definition-function' export interface ConditionExpressionDefinitionChain { diff --git a/src/dynamo/expression/type/request-condition-function.ts b/src/dynamo/expression/type/request-condition-function.ts index 0b5543520..0ed9be095 100644 --- a/src/dynamo/expression/type/request-condition-function.ts +++ b/src/dynamo/expression/type/request-condition-function.ts @@ -1,4 +1,4 @@ -import { AttributeType } from '../../../mapper/type/attribute.type' +import { AttributeType } from '../../../mapper/type/attribute-type.type' import { BaseRequest } from '../../request/base.request' export interface RequestConditionFunction> { diff --git a/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts b/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts index 6f1d6abb3..e7ef597af 100644 --- a/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts +++ b/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts @@ -5,6 +5,7 @@ import { map } from 'rxjs/operators' import { Metadata } from '../../../decorator/metadata/metadata' import { MetadataHelper } from '../../../decorator/metadata/metadata-helper' import { Mapper } from '../../../mapper/mapper' +import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' import { DynamoRx } from '../../dynamo-rx' import { BatchGetSingleTableResponse } from './batch-get-single-table.response' @@ -49,7 +50,7 @@ export class BatchGetSingleTableRequest { let items: T[] if (response.Responses && Object.keys(response.Responses).length && response.Responses[this.tableName]) { const mapped: T[] = response.Responses![this.tableName].map(attributeMap => - Mapper.fromDb(attributeMap, this.modelClazz) + Mapper.fromDb(attributeMap, this.modelClazz) ) items = mapped } else { @@ -69,7 +70,9 @@ export class BatchGetSingleTableRequest { return this.dynamoRx.batchGetItems(this.params).pipe( map(response => { if (response.Responses && Object.keys(response.Responses).length && response.Responses[this.tableName]) { - return response.Responses![this.tableName].map(attributeMap => Mapper.fromDb(attributeMap, this.modelClazz)) + return response.Responses![this.tableName].map(attributeMap => + Mapper.fromDb(attributeMap, this.modelClazz) + ) } else { return [] } diff --git a/src/dynamo/request/get/get.request.ts b/src/dynamo/request/get/get.request.ts index 217fcb5bc..0b5b6b1af 100644 --- a/src/dynamo/request/get/get.request.ts +++ b/src/dynamo/request/get/get.request.ts @@ -3,6 +3,7 @@ import { values as objValues } from 'lodash' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { Mapper } from '../../../mapper/mapper' +import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' import { DynamoRx } from '../../dynamo-rx' import { resolveAttributeNames } from '../../expression/functions/attribute-names.function' @@ -75,7 +76,7 @@ export class GetRequest extends BaseRequest { const response: GetResponse = { ...getItemResponse } if (getItemResponse.Item) { - response.Item = Mapper.fromDb(getItemResponse.Item, this.modelClazz) + response.Item = Mapper.fromDb(getItemResponse.Item, this.modelClazz) } else { response.Item = null } @@ -89,7 +90,7 @@ export class GetRequest extends BaseRequest { return this.dynamoRx.getItem(this.params).pipe( map(response => { if (response.Item) { - return Mapper.fromDb(response.Item, this.modelClazz) + return Mapper.fromDb(response.Item, this.modelClazz) } else { return null } diff --git a/src/dynamo/request/query/query.request.ts b/src/dynamo/request/query/query.request.ts index caec09ea3..d0852f3f4 100644 --- a/src/dynamo/request/query/query.request.ts +++ b/src/dynamo/request/query/query.request.ts @@ -2,6 +2,7 @@ import { QueryInput, QueryOutput } from 'aws-sdk/clients/dynamodb' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { Mapper } from '../../../mapper/mapper' +import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' import { DynamoRx } from '../../dynamo-rx' import { and } from '../../expression/logical-operator/and.function' @@ -102,7 +103,7 @@ export class QueryRequest extends Request, QueryInput, Que return this.dynamoRx.query(this.params).pipe( map(queryResponse => { const response: QueryResponse = { ...queryResponse } - response.Items = queryResponse.Items!.map(item => Mapper.fromDb(item, this.modelClazz)) + response.Items = queryResponse.Items!.map(item => Mapper.fromDb(item, this.modelClazz)) return response }) @@ -112,7 +113,7 @@ export class QueryRequest extends Request, QueryInput, Que exec(): Observable { return this.dynamoRx .query(this.params) - .pipe(map(response => response.Items!.map(item => Mapper.fromDb(item, this.modelClazz)))) + .pipe(map(response => response.Items!.map(item => Mapper.fromDb(item, this.modelClazz)))) } execNoMap(): Observable { @@ -125,7 +126,7 @@ export class QueryRequest extends Request, QueryInput, Que return this.dynamoRx.query(this.params).pipe( map(response => { if (response.Count) { - return Mapper.fromDb(response.Items![0], this.modelClazz) + return Mapper.fromDb(response.Items![0], this.modelClazz) } else { return null } diff --git a/src/dynamo/request/scan/scan.request.ts b/src/dynamo/request/scan/scan.request.ts index d9b608e9a..25af612ee 100644 --- a/src/dynamo/request/scan/scan.request.ts +++ b/src/dynamo/request/scan/scan.request.ts @@ -2,6 +2,7 @@ import { ScanInput, ScanOutput } from 'aws-sdk/clients/dynamodb' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { Mapper } from '../../../mapper/mapper' +import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' import { DynamoRx } from '../../dynamo-rx' import { and } from '../../expression/logical-operator/and.function' @@ -35,7 +36,7 @@ export class ScanRequest extends Request, ScanInput, ScanRe return this.dynamoRx.scan(this.params).pipe( map(queryResponse => { const response: ScanResponse = { ...queryResponse } - response.Items = queryResponse.Items!.map(item => Mapper.fromDb(item, this.modelClazz)) + response.Items = queryResponse.Items!.map(item => Mapper.fromDb(item, this.modelClazz)) return response }) @@ -51,13 +52,15 @@ export class ScanRequest extends Request, ScanInput, ScanRe return this.dynamoRx .scan(this.params) - .pipe(map(response => response.Items!.map(item => Mapper.fromDb(item, this.modelClazz)))) + .pipe(map(response => response.Items!.map(item => Mapper.fromDb(item, this.modelClazz)))) } execSingle(): Observable { delete this.params.Select - return this.dynamoRx.scan(this.params).pipe(map(response => Mapper.fromDb(response.Items![0], this.modelClazz))) + return this.dynamoRx + .scan(this.params) + .pipe(map(response => Mapper.fromDb(response.Items![0], this.modelClazz))) } execCount(): Observable { diff --git a/src/helper/index.ts b/src/helper/index.ts new file mode 100644 index 000000000..5a1a3bdfe --- /dev/null +++ b/src/helper/index.ts @@ -0,0 +1 @@ +export * from './not-null.function' diff --git a/src/helper/not-null.function.ts b/src/helper/not-null.function.ts new file mode 100644 index 000000000..92ef5223a --- /dev/null +++ b/src/helper/not-null.function.ts @@ -0,0 +1,3 @@ +export function notNull(value: TValue | null): value is TValue { + return value !== null +} diff --git a/src/mapper/for-type/base.mapper.ts b/src/mapper/for-type/base.mapper.ts index 28e64f0b9..b566e13dd 100644 --- a/src/mapper/for-type/base.mapper.ts +++ b/src/mapper/for-type/base.mapper.ts @@ -1,15 +1,39 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' import { PropertyMetadata } from '../../decorator/metadata/property-metadata.model' +import { + BinaryAttribute, + BinarySetAttribute, + BooleanAttribute, + ListAttribute, + MapAttribute, + NullAttribute, + NumberAttribute, + NumberSetAttribute, + StringAttribute, + StringSetAttribute, +} from '../type/attribute.type' /** * A Mapper is responsible to define how a specific type is mapped to an attribute value which can be stored in dynamodb and how to parse the value from * dynamodb back into the specific type */ -export interface MapperForType { +export interface MapperForType< + T, + R extends + | StringAttribute + | NumberAttribute + | BinaryAttribute + | StringSetAttribute + | NumberSetAttribute + | BinarySetAttribute + | MapAttribute + | ListAttribute + | NullAttribute + | BooleanAttribute +> { /** * Maps an attribute value coming from dynamodb to an javascript type */ - fromDb(attributeValue: AttributeValue, propertyMetadata?: PropertyMetadata): T + fromDb(attributeValue: R, propertyMetadata?: PropertyMetadata): T /** * Maps a js value to an attribute value so it can be stored in dynamodb, supported types are @@ -24,5 +48,5 @@ export interface MapperForType { * B(inary)S(et) * L(ist) */ - toDb(propertyValue: T, propertyMetadata?: PropertyMetadata): AttributeValue | null + toDb(propertyValue: T, propertyMetadata?: PropertyMetadata): R | null } diff --git a/src/mapper/for-type/boolean.mapper.spec.ts b/src/mapper/for-type/boolean.mapper.spec.ts index abca751a4..deaf3bd8d 100644 --- a/src/mapper/for-type/boolean.mapper.spec.ts +++ b/src/mapper/for-type/boolean.mapper.spec.ts @@ -44,13 +44,13 @@ describe('boolean mapper', () => { it('should throw (S cannot be mapped to boolean)', () => { expect(() => { - mapper.fromDb({ S: 'true' }) + mapper.fromDb({ S: 'true' }) }).toThrowError() }) it('should throw (N cannot be mapped to boolean)', () => { expect(() => { - mapper.fromDb({ N: '1' }) + mapper.fromDb({ N: '1' }) }).toThrowError() }) }) diff --git a/src/mapper/for-type/boolean.mapper.ts b/src/mapper/for-type/boolean.mapper.ts index df423c99f..cf9dcd74d 100644 --- a/src/mapper/for-type/boolean.mapper.ts +++ b/src/mapper/for-type/boolean.mapper.ts @@ -1,20 +1,18 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' +import { BooleanAttribute } from '../type/attribute.type' import { MapperForType } from './base.mapper' -export class BooleanMapper implements MapperForType { - fromDb(dbValue: any): boolean { +export class BooleanMapper implements MapperForType { + fromDb(dbValue: BooleanAttribute): boolean { if (dbValue.BOOL === undefined) { throw new Error('only attribute values with BOOL value can be mapped to a boolean') } - return dbValue.BOOL === true } - toDb(modelValue: boolean): AttributeValue { + toDb(modelValue: boolean): BooleanAttribute { if (!(modelValue === true || modelValue === false)) { throw new Error('only boolean values are mapped to a BOOl attribute') } - return { BOOL: modelValue } } } diff --git a/src/mapper/for-type/collection.mapper.ts b/src/mapper/for-type/collection.mapper.ts index 2f29b016f..7db64f9f6 100644 --- a/src/mapper/for-type/collection.mapper.ts +++ b/src/mapper/for-type/collection.mapper.ts @@ -1,46 +1,68 @@ -import { AttributeMap, AttributeValue } from 'aws-sdk/clients/dynamodb' -import { hasGenericType, PropertyMetadata } from '../../decorator/metadata/property-metadata.model' +import { hasGenericType, PropertyMetadata } from '../../decorator' +import { notNull } from '../../helper' import { Mapper } from '../mapper' +import { AttributeType } from '../type/attribute-type.type' +import { + BinarySetAttribute, + ListAttribute, + MapAttribute, + NullAttribute, + NumberSetAttribute, + StringSetAttribute, +} from '../type/attribute.type' import { Util } from '../util' import { MapperForType } from './base.mapper' -export class CollectionMapper implements MapperForType> { - fromDb(attributeValue: AttributeValue, propertyMetadata?: PropertyMetadata): any[] | Set { +type CollectionAttributeTypes = + | StringSetAttribute + | NumberSetAttribute + | BinarySetAttribute + | ListAttribute + | NullAttribute + +export class CollectionMapper implements MapperForType, CollectionAttributeTypes> { + fromDb( + attributeValue: CollectionAttributeTypes, + propertyMetadata?: PropertyMetadata + ): any[] | Set { const explicitType = propertyMetadata && propertyMetadata.typeInfo ? propertyMetadata.typeInfo.type : null - if (attributeValue.SS) { + + if ('SS' in attributeValue) { const arr: string[] = attributeValue.SS return explicitType && explicitType === Array ? arr : new Set(arr) } - if (attributeValue.NS) { + if ('NS' in attributeValue) { const arr: number[] = attributeValue.NS.map(item => parseFloat(item)) return explicitType && explicitType === Array ? arr : new Set(arr) } - if (attributeValue.BS) { + if ('BS' in attributeValue) { const arr: any[] = attributeValue.BS return explicitType && explicitType === Array ? arr : new Set(arr) } - if (attributeValue.L) { + if ('L' in attributeValue) { let arr: any[] if (hasGenericType(propertyMetadata)) { - arr = attributeValue.L.map(item => Mapper.fromDb(item.M, propertyMetadata!.typeInfo!.genericType)) + arr = attributeValue.L.map(item => + Mapper.fromDb((item).M, propertyMetadata!.typeInfo!.genericType) + ) } else { arr = attributeValue.L.map(value => Mapper.fromDbOne(value)) } - return explicitType && explicitType === Set ? new Set(arr) : arr } throw new Error('No Collection Data (SS | NS | BS | L) was found in attribute data') } - toDb(propertyValue: any[] | Set, propertyMetadata?: PropertyMetadata): AttributeValue | null { + toDb( + propertyValue: any[] | Set, + propertyMetadata?: PropertyMetadata + ): CollectionAttributeTypes | null { if (Array.isArray(propertyValue) || Util.isSet(propertyValue)) { - const attributeValue: AttributeValue = {} - - let collectionType + let collectionType: AttributeType if (propertyMetadata) { const explicitType = propertyMetadata && propertyMetadata.typeInfo ? propertyMetadata.typeInfo.type : null switch (explicitType) { @@ -99,30 +121,26 @@ export class CollectionMapper implements MapperForType> { } else { switch (collectionType) { case 'SS': - attributeValue.SS = propertyValue - break + return { SS: propertyValue } case 'NS': - attributeValue.NS = propertyValue.map(num => num.toString()) - break + return { NS: propertyValue.map(num => num.toString()) } case 'BS': - attributeValue.BS = propertyValue - break + return { BS: propertyValue } case 'L': if (hasGenericType(propertyMetadata)) { - attributeValue.L = (propertyValue).map(value => ({ - M: Mapper.toDb(value, propertyMetadata!.typeInfo!.genericType), - })) + return { + L: propertyValue.map(value => ({ + M: Mapper.toDb(value, propertyMetadata!.typeInfo!.genericType), + })), + } } else { - attributeValue.L = ( - (propertyValue).map(value => Mapper.toDbOne(value)).filter(value => value !== null) - ) + return { + L: propertyValue.map(value => Mapper.toDbOne(value)).filter(notNull), + } } - break default: throw new Error(`Collection type must be one of SS | NS | BS | L found type ${collectionType}`) } - - return attributeValue } } else { throw new Error(`given value is not an array ${propertyValue}`) diff --git a/src/mapper/for-type/date.mapper.spec.ts b/src/mapper/for-type/date.mapper.spec.ts index f8a6e0e81..8a0a8861e 100644 --- a/src/mapper/for-type/date.mapper.spec.ts +++ b/src/mapper/for-type/date.mapper.spec.ts @@ -33,7 +33,7 @@ describe('date mapper', () => { it('throws', () => { expect(() => { - dateMapper.fromDb({ S: 'noDate' }) + dateMapper.fromDb({ S: 'noDate' }) }).toThrowError() }) }) diff --git a/src/mapper/for-type/date.mapper.ts b/src/mapper/for-type/date.mapper.ts index 383d090c7..6ab6a72ca 100644 --- a/src/mapper/for-type/date.mapper.ts +++ b/src/mapper/for-type/date.mapper.ts @@ -1,8 +1,8 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' +import { NumberAttribute } from '../type/attribute.type' import { MapperForType } from './base.mapper' -export class DateMapper implements MapperForType { - fromDb(attributeValue: AttributeValue): Date { +export class DateMapper implements MapperForType { + fromDb(attributeValue: NumberAttribute): Date { if (attributeValue.N) { return new Date(parseInt(attributeValue.N, 10)) } else { @@ -10,7 +10,8 @@ export class DateMapper implements MapperForType { } } - toDb(modelValue: Date): AttributeValue { + toDb(modelValue: Date): NumberAttribute { + // noinspection SuspiciousInstanceOfGuard if (modelValue && modelValue instanceof Date) { return { N: `${modelValue.getTime()}` } } else { diff --git a/src/mapper/for-type/enum.mapper.ts b/src/mapper/for-type/enum.mapper.ts index 27666d8e3..d3fc71e84 100644 --- a/src/mapper/for-type/enum.mapper.ts +++ b/src/mapper/for-type/enum.mapper.ts @@ -1,14 +1,14 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' import { hasGenericType, PropertyMetadata } from '../../decorator/metadata/property-metadata.model' +import { NumberAttribute } from '../type/attribute.type' import { MapperForType } from './base.mapper' /** * Enums are mapped to numbers by default */ -export class EnumMapper implements MapperForType { +export class EnumMapper implements MapperForType { constructor() {} - toDb(value: E, propertyMetadata?: PropertyMetadata) { + toDb(value: E, propertyMetadata?: PropertyMetadata): NumberAttribute { if (Number.isInteger(value)) { if (hasGenericType(propertyMetadata) && (propertyMetadata!.typeInfo!.genericType)[value] === undefined) { throw new Error(`${value} is not a valid value for enum ${propertyMetadata!.typeInfo!.genericType}`) @@ -19,7 +19,7 @@ export class EnumMapper implements MapperForType { } } - fromDb(attributeValue: AttributeValue, propertyMetadata?: PropertyMetadata): E { + fromDb(attributeValue: NumberAttribute, propertyMetadata?: PropertyMetadata): E { if (!isNaN(parseInt(attributeValue.N!, 10))) { const enumValue = parseInt(attributeValue.N!, 10) if (propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.genericType) { diff --git a/src/mapper/for-type/moment.mapper.ts b/src/mapper/for-type/moment.mapper.ts index 21616dec0..0862df3a9 100644 --- a/src/mapper/for-type/moment.mapper.ts +++ b/src/mapper/for-type/moment.mapper.ts @@ -1,9 +1,9 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' import * as moment from 'moment' +import { StringAttribute } from '../type/attribute.type' import { MapperForType } from './base.mapper' -export class MomentMapper implements MapperForType { - fromDb(value: AttributeValue): moment.Moment { +export class MomentMapper implements MapperForType { + fromDb(value: StringAttribute): moment.Moment { const parsed: moment.Moment = moment(value.S, moment.ISO_8601) if (!parsed.isValid()) { throw new Error(`the value ${value} cannot be parsed into a valid moment date`) @@ -12,7 +12,7 @@ export class MomentMapper implements MapperForType { return parsed } - toDb(value: moment.Moment): AttributeValue { + toDb(value: moment.Moment): StringAttribute { if (moment.isMoment(value)) { if (value.isValid()) { // always store in utc, default format is ISO_8601 diff --git a/src/mapper/for-type/null.mapper.spec.ts b/src/mapper/for-type/null.mapper.spec.ts index a3a550e27..602726cbc 100644 --- a/src/mapper/for-type/null.mapper.spec.ts +++ b/src/mapper/for-type/null.mapper.spec.ts @@ -28,7 +28,7 @@ describe('null mapper', () => { it('should throw (no null value)', () => { expect(() => { - mapper.fromDb({ S: 'nullValue' }) + mapper.fromDb({ S: 'nullValue' }) }).toThrowError() }) }) diff --git a/src/mapper/for-type/null.mapper.ts b/src/mapper/for-type/null.mapper.ts index 9a47114e7..a87a621bd 100644 --- a/src/mapper/for-type/null.mapper.ts +++ b/src/mapper/for-type/null.mapper.ts @@ -1,10 +1,11 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' +import { NullAttribute } from '../type/attribute.type' + import { MapperForType } from './base.mapper' -export class NullMapper implements MapperForType { +export class NullMapper implements MapperForType { constructor() {} - fromDb(value: AttributeValue): null { + fromDb(value: NullAttribute): null { if (value.NULL) { return null } else { @@ -12,7 +13,7 @@ export class NullMapper implements MapperForType { } } - toDb(value: null): AttributeValue | null { + toDb(value: null): NullAttribute | null { if (value !== null) { throw new Error(`null mapper only supports null value, got ${value}`) } diff --git a/src/mapper/for-type/number.mapper.spec.ts b/src/mapper/for-type/number.mapper.spec.ts index 53f385047..8fe55e187 100644 --- a/src/mapper/for-type/number.mapper.spec.ts +++ b/src/mapper/for-type/number.mapper.spec.ts @@ -34,7 +34,7 @@ describe('number mapper', () => { it('should throw (no number value)', () => { expect(() => { - mapper.fromDb({ S: '56' }) + mapper.fromDb({ S: '56' }) }).toThrowError() }) }) diff --git a/src/mapper/for-type/number.mapper.ts b/src/mapper/for-type/number.mapper.ts index 7feab24b2..090408c64 100644 --- a/src/mapper/for-type/number.mapper.ts +++ b/src/mapper/for-type/number.mapper.ts @@ -1,9 +1,10 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' +import { NumberAttribute } from '../type/attribute.type' + import { isNumber } from 'lodash' import { MapperForType } from './base.mapper' -export class NumberMapper implements MapperForType { - fromDb(attributeValue: AttributeValue): number { +export class NumberMapper implements MapperForType { + fromDb(attributeValue: NumberAttribute): number { if (attributeValue.N) { const numberValue = Number.parseFloat(attributeValue.N) if (isNaN(numberValue)) { @@ -16,7 +17,7 @@ export class NumberMapper implements MapperForType { } } - toDb(modelValue: number): AttributeValue | null { + toDb(modelValue: number): NumberAttribute | null { if (!isNumber(modelValue)) { throw new Error('this mapper only support values of type number') } diff --git a/src/mapper/for-type/object.mapper.ts b/src/mapper/for-type/object.mapper.ts index 3fd05030b..2280a7bb7 100644 --- a/src/mapper/for-type/object.mapper.ts +++ b/src/mapper/for-type/object.mapper.ts @@ -1,20 +1,20 @@ -import { AttributeMap, AttributeValue, MapAttributeValue } from 'aws-sdk/clients/dynamodb' import { PropertyMetadata } from '../../decorator/metadata/property-metadata.model' import { Mapper } from '../mapper' +import { MapAttribute } from '../type/attribute.type' import { MapperForType } from './base.mapper' -export class ObjectMapper implements MapperForType { +export class ObjectMapper implements MapperForType { constructor() {} - fromDb(val: AttributeValue, propertyMetadata?: PropertyMetadata): any { + fromDb(val: MapAttribute, propertyMetadata?: PropertyMetadata): any { if (propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.isCustom) { - return Mapper.fromDb(val.M, propertyMetadata.typeInfo.type) + return Mapper.fromDb(val.M, propertyMetadata.typeInfo.type) } else { - return Mapper.fromDb(val.M) + return Mapper.fromDb(val.M) } } - toDb(modelValue: any, propertyMetadata?: PropertyMetadata): MapAttributeValue { + toDb(modelValue: any, propertyMetadata?: PropertyMetadata): MapAttribute { let value: any if (propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.isCustom) { value = Mapper.toDb(modelValue, propertyMetadata.typeInfo.type) diff --git a/src/mapper/for-type/string.mapper.spec.ts b/src/mapper/for-type/string.mapper.spec.ts index 351049b33..4b8f4d7c9 100644 --- a/src/mapper/for-type/string.mapper.spec.ts +++ b/src/mapper/for-type/string.mapper.spec.ts @@ -19,12 +19,12 @@ describe('string mapper', () => { }) it('should work (null)', () => { - const attributeValue = mapper.toDb(null) + const attributeValue = mapper.toDb(null) expect(attributeValue).toBe(null) }) it('should work (undefined)', () => { - const attributeValue = mapper.toDb(undefined) + const attributeValue = mapper.toDb(undefined) expect(attributeValue).toBe(null) }) }) diff --git a/src/mapper/for-type/string.mapper.ts b/src/mapper/for-type/string.mapper.ts index 02e3c6b9e..234d1f9e6 100644 --- a/src/mapper/for-type/string.mapper.ts +++ b/src/mapper/for-type/string.mapper.ts @@ -1,10 +1,10 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' +import { StringAttribute } from '../type/attribute.type' import { MapperForType } from './base.mapper' -export class StringMapper implements MapperForType { +export class StringMapper implements MapperForType { constructor() {} - fromDb(attributeValue: AttributeValue): string { + fromDb(attributeValue: StringAttribute): string { if (attributeValue.S) { return attributeValue.S } else { @@ -12,7 +12,7 @@ export class StringMapper implements MapperForType { } } - toDb(modelValue: string): AttributeValue | null { + toDb(modelValue: string): StringAttribute | null { // an empty string is not a valid value for string attribute if (modelValue === '' || modelValue === null || modelValue === undefined) { return null diff --git a/src/mapper/index.ts b/src/mapper/index.ts index 1715aa564..6636b69cf 100644 --- a/src/mapper/index.ts +++ b/src/mapper/index.ts @@ -1,6 +1,5 @@ -export * from './type/attribute-model.type' -export * from './type/attribute.type' -export * from './type/attribute-collection.type' +export * from './type/attribute-value-type.type' +export * from './type/attribute-type.type' export * from './mapper' export * from './type/enum.type' export * from './type/null.type' diff --git a/src/mapper/mapper.ts b/src/mapper/mapper.ts index df9424073..44ae463b9 100644 --- a/src/mapper/mapper.ts +++ b/src/mapper/mapper.ts @@ -1,4 +1,3 @@ -import { AttributeMap, AttributeValue } from 'aws-sdk/clients/dynamodb' import { Metadata } from '../decorator/metadata/metadata' import { MetadataHelper } from '../decorator/metadata/metadata-helper' import { PropertyMetadata } from '../decorator/metadata/property-metadata.model' @@ -13,7 +12,8 @@ import { NullMapper } from './for-type/null.mapper' import { NumberMapper } from './for-type/number.mapper' import { ObjectMapper } from './for-type/object.mapper' import { StringMapper } from './for-type/string.mapper' -import { AttributeModelType } from './type/attribute-model.type' +import { AttributeValueType } from './type/attribute-value-type.type' +import { Attribute, Attributes } from './type/attribute.type' import { Binary } from './type/binary.type' import { EnumType } from './type/enum.type' import { MomentType } from './type/moment.type' @@ -27,12 +27,12 @@ import { Util } from './util' * */ export class Mapper { - static mapperForType: Map> = new Map() + static mapperForType: Map> = new Map() // static logger = debug('Mapper'); - static toDb(item: T, modelConstructor?: ModelConstructor): AttributeMap { - const mapped: AttributeMap = {} + static toDb(item: T, modelConstructor?: ModelConstructor): Attributes { + const mapped: Attributes = {} if (modelConstructor) { const metadata: Metadata = MetadataHelper.get(modelConstructor) @@ -70,7 +70,7 @@ export class Mapper { ) } - let attributeValue: AttributeValue | undefined | null + let attributeValue: any | undefined | null // TODO concept maybe make this configurable how to map undefined & null values if (propertyValue === undefined || propertyValue === null) { @@ -80,7 +80,7 @@ export class Mapper { * 2) decide how to map the property depending on type or value */ - let propertyMetadata: PropertyMetadata | null | undefined + let propertyMetadata: PropertyMetadata | null | undefined if (modelConstructor) { propertyMetadata = MetadataHelper.forProperty(modelConstructor, propertyKey) } @@ -127,16 +127,16 @@ export class Mapper { return mapped } - static toDbOne(propertyValue: any, propertyMetadata?: PropertyMetadata): AttributeValue | null { - const explicitType: AttributeModelType | null = + static toDbOne(propertyValue: any, propertyMetadata?: PropertyMetadata): Attribute | null { + const explicitType: AttributeValueType | null = propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.isCustom ? propertyMetadata.typeInfo.type! : null - const type: AttributeModelType = explicitType || Util.typeOf(propertyValue) + const type: AttributeValueType = explicitType || Util.typeOf(propertyValue) const mapper = propertyMetadata && propertyMetadata.mapper ? new propertyMetadata.mapper() : Mapper.forType(type) - const attrValue: AttributeValue | null = explicitType + const attrValue: Attribute | null = explicitType ? mapper.toDb(propertyValue, propertyMetadata) : mapper.toDb(propertyValue) @@ -157,20 +157,20 @@ type ${type} cannot be used as partition key, value = ${JSON.stringify(propertyV return attrValue } - static fromDb(attributeMap: AttributeMap, modelClass?: ModelConstructor): T { + static fromDb(attributeMap: Attributes, modelClass?: ModelConstructor): T { const model: T = {} Object.getOwnPropertyNames(attributeMap).forEach(attributeName => { /* * 1) get the value of the property */ - const attributeValue: AttributeValue = attributeMap[attributeName] + const attributeValue = attributeMap[attributeName] /* * 2) decide how to map the property depending on type or value */ let modelValue: any - let propertyMetadata: PropertyMetadata | null | undefined + let propertyMetadata: PropertyMetadata | null | undefined if (modelClass) { propertyMetadata = MetadataHelper.forProperty(modelClass, attributeName) } @@ -215,12 +215,12 @@ type ${type} cannot be used as partition key, value = ${JSON.stringify(propertyV return model } - static fromDbOne(attributeValue: AttributeValue, propertyMetadata?: PropertyMetadata): T { - const explicitType: AttributeModelType | null = + static fromDbOne(attributeValue: Attribute, propertyMetadata?: PropertyMetadata): T { + const explicitType: AttributeValueType | null = propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.isCustom ? propertyMetadata.typeInfo.type! : null - const type: AttributeModelType = explicitType || Util.typeOfFromDb(attributeValue) + const type: AttributeValueType = explicitType || Util.typeOfFromDb(attributeValue) // Mapper.logger.log(`mapFromDbOne for type ${type}`) @@ -231,11 +231,11 @@ type ${type} cannot be used as partition key, value = ${JSON.stringify(propertyV } } - static forType(type: AttributeModelType): MapperForType { + static forType(type: AttributeValueType): MapperForType { // FIXME HIGH review this, we now use toString to compare because we had issues with ng client for moment when // using a GSI on creationDate (MomentType) was a different MomentType than for lastUpdatedDate if (!Mapper.mapperForType.has(type.toString())) { - let mapperForType: MapperForType + let mapperForType: MapperForType switch (type.toString()) { case String.toString(): mapperForType = new StringMapper() diff --git a/src/mapper/type/attribute-collection.type.ts b/src/mapper/type/attribute-collection.type.ts deleted file mode 100644 index a49e88838..000000000 --- a/src/mapper/type/attribute-collection.type.ts +++ /dev/null @@ -1 +0,0 @@ -export type AttributeCollectionType = 'SS' | 'NS' | 'BS' | 'L' diff --git a/src/mapper/type/attribute-type.type.ts b/src/mapper/type/attribute-type.type.ts new file mode 100644 index 000000000..018396401 --- /dev/null +++ b/src/mapper/type/attribute-type.type.ts @@ -0,0 +1,3 @@ +export type AttributeCollectionType = 'SS' | 'NS' | 'BS' | 'L' + +export type AttributeType = 'S' | 'N' | 'B' | 'M' | 'NULL' | 'BOOL' | AttributeCollectionType diff --git a/src/mapper/type/attribute-model.type.ts b/src/mapper/type/attribute-value-type.type.ts similarity index 90% rename from src/mapper/type/attribute-model.type.ts rename to src/mapper/type/attribute-value-type.type.ts index 5327a74ae..40195ccf7 100644 --- a/src/mapper/type/attribute-model.type.ts +++ b/src/mapper/type/attribute-value-type.type.ts @@ -3,7 +3,7 @@ import { MomentType } from './moment.type' import { NullType } from './null.type' import { UndefinedType } from './undefined.type' -export type AttributeModelType = +export type AttributeValueType = | string | number | boolean diff --git a/src/mapper/type/attribute.type.ts b/src/mapper/type/attribute.type.ts index 08590ceae..c0d3cecb8 100644 --- a/src/mapper/type/attribute.type.ts +++ b/src/mapper/type/attribute.type.ts @@ -1,3 +1,87 @@ -import { AttributeCollectionType } from './attribute-collection.type' +export type Attribute = + | StringAttribute + | NumberAttribute + | BinaryAttribute + | StringSetAttribute + | NumberSetAttribute + | BinarySetAttribute + | MapAttribute + | ListAttribute + | NullAttribute + | BooleanAttribute -export type AttributeType = 'S' | 'N' | 'B' | 'M' | 'NULL' | 'BOOL' | AttributeCollectionType +export interface Attributes { + [key: string]: Attribute +} + +/** + * An attribute of type String. For example: "S": "Hello" + */ +export interface StringAttribute { + S: string +} + +/** + * An attribute of type Number. For example: "N": "123.45" Numbers are sent across the network to DynamoDB as strings, to maximize compatibility across languages and libraries. However, DynamoDB treats them as number type attributes for mathematical operations. + */ +export interface NumberAttribute { + N: string +} + +/** + * An attribute of type Binary. For example: "B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" + */ +export interface BinaryAttribute { + B: Buffer | Uint8Array | {} | string +} + +/** + * An attribute of type String Set. For example: "SS": ["Giraffe", "Hippo" ,"Zebra"] + */ +export interface StringSetAttribute { + SS: string[] +} + +/** + * An attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", "3.14"] Numbers are sent across the network to DynamoDB as strings, to maximize compatibility across languages and libraries. However, DynamoDB treats them as number type attributes for mathematical operations. + */ +export interface NumberSetAttribute { + NS: string[] +} + +/** + * An attribute of type Binary Set. For example: "BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="] + */ +export interface BinarySetAttribute { + BS: BinaryAttribute[] +} + +/** + * An attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}} + */ + +export interface MapAttribute { + M: Attributes +} + +/** + * An attribute of type List. For example: "L": ["Cookies", "Coffee", 3.14159] + */ + +export interface ListAttribute { + L: Attribute[] +} + +/** + * An attribute of type Null. For example: "NULL": true + */ +export interface NullAttribute { + NULL: boolean +} + +/** + * An attribute of type Boolean. For example: "BOOL": true + */ +export interface BooleanAttribute { + BOOL: boolean +} diff --git a/src/mapper/util.ts b/src/mapper/util.ts index 440a932dd..d4c323de8 100644 --- a/src/mapper/util.ts +++ b/src/mapper/util.ts @@ -1,9 +1,8 @@ import { AttributeValue } from 'aws-sdk/clients/dynamodb' import { isNumber, isString } from 'lodash' import { isMoment } from 'moment' -import { AttributeCollectionType } from './type/attribute-collection.type' -import { AttributeModelType } from './type/attribute-model.type' -import { AttributeType } from './type/attribute.type' +import { AttributeCollectionType, AttributeType } from './type/attribute-type.type' +import { AttributeValueType } from './type/attribute-value-type.type' import { Binary } from './type/binary.type' import { MomentType } from './type/moment.type' import { NullType } from './type/null.type' @@ -154,7 +153,7 @@ export class Util { * @param data * @returns {AttributeModelTypeName} */ - static typeOf(data: any): AttributeModelType { + static typeOf(data: any): AttributeValueType { if (data === null) { return NullType } else { @@ -193,7 +192,7 @@ export class Util { * copied from https://github.com/aws/aws-sdk-js/blob/0c974a7ff6749a541594de584b43a040978d4b72/lib/dynamodb/types.js * should we work with string match */ - static typeOfFromDb(attributeValue?: AttributeValue): AttributeModelType { + static typeOfFromDb(attributeValue?: AttributeValue): AttributeValueType { if (attributeValue) { const dynamoType: AttributeType = Object.keys(attributeValue)[0] switch (dynamoType) { diff --git a/test/models/model-with-custom-mapper-for-sort-key.model.ts b/test/models/model-with-custom-mapper-for-sort-key.model.ts index 0a275d2ba..4c2f19adf 100644 --- a/test/models/model-with-custom-mapper-for-sort-key.model.ts +++ b/test/models/model-with-custom-mapper-for-sort-key.model.ts @@ -1,12 +1,12 @@ // tslint:disable:max-classes-per-file -import { DynamoDB } from 'aws-sdk' import * as moment from 'moment' import { PropertyMetadata, SortKey } from '../../src/decorator' import { PartitionKey } from '../../src/decorator/impl/key/partition-key.decorator' import { CustomMapper } from '../../src/decorator/impl/mapper/custom-mapper.decorator' import { Model } from '../../src/decorator/impl/model/model.decorator' import { MapperForType } from '../../src/mapper/for-type/base.mapper' +import { NumberAttribute } from '../../src/mapper/type/attribute.type' export class CustomId { private static MULTIPLIER = Math.pow(10, 5) @@ -30,12 +30,12 @@ export class CustomId { } } -export class CustomIdMapper implements MapperForType { - fromDb(attributeValue: DynamoDB.AttributeValue, propertyMetadata?: PropertyMetadata): CustomId { - return CustomId.parse(parseInt(attributeValue.N!, 10)) +export class CustomIdMapper implements MapperForType { + fromDb(attributeValue: NumberAttribute, propertyMetadata?: PropertyMetadata): CustomId { + return CustomId.parse(parseInt(attributeValue.N, 10)) } - toDb(propertyValue: CustomId, propertyMetadata?: PropertyMetadata): DynamoDB.AttributeValue { + toDb(propertyValue: CustomId, propertyMetadata?: PropertyMetadata): NumberAttribute { return { N: `${CustomId.unparse(propertyValue)}` } } } diff --git a/test/models/model-with-custom-mapper.model.ts b/test/models/model-with-custom-mapper.model.ts index f2ca25a2a..899716ea5 100644 --- a/test/models/model-with-custom-mapper.model.ts +++ b/test/models/model-with-custom-mapper.model.ts @@ -1,23 +1,23 @@ // tslint:disable:max-classes-per-file -import { AttributeValue } from 'aws-sdk/clients/dynamodb' import { PartitionKey } from '../../src/decorator/impl/key/partition-key.decorator' import { CustomMapper } from '../../src/decorator/impl/mapper/custom-mapper.decorator' import { Model } from '../../src/decorator/impl/model/model.decorator' import { PropertyMetadata } from '../../src/decorator/metadata/property-metadata.model' import { MapperForType } from '../../src/mapper/for-type/base.mapper' +import { StringAttribute } from '../../src/mapper/type/attribute.type' export class Id { counter: number year: number constructor(counter?: number, year?: number) { - this.counter = counter - this.year = year + this.counter = counter! + this.year = year! } } -export class IdMapper implements MapperForType { - fromDb(attributeValue: AttributeValue, propertyMetadata?: PropertyMetadata): Id { +export class IdMapper implements MapperForType { + fromDb(attributeValue: StringAttribute, propertyMetadata?: PropertyMetadata): Id { const id: Id = new Id() const idString = attributeValue.S @@ -27,12 +27,10 @@ export class IdMapper implements MapperForType { return id } - toDb(propertyValue: Id, propertyMetadata?: PropertyMetadata): AttributeValue { + toDb(propertyValue: Id, propertyMetadata?: PropertyMetadata): StringAttribute { // create leading zeroes so the counter matches the pattern /d{4} const leadingZeroes: string = new Array(4 + 1 - (propertyValue.counter + '').length).join('0') - return { - S: `${leadingZeroes}${propertyValue.counter}${propertyValue.year}`, - } + return { S: `${leadingZeroes}${propertyValue.counter}${propertyValue.year}` } } } diff --git a/test/models/real-world/form-id.mapper.ts b/test/models/real-world/form-id.mapper.ts index ed68f7488..2d9923757 100644 --- a/test/models/real-world/form-id.mapper.ts +++ b/test/models/real-world/form-id.mapper.ts @@ -1,31 +1,36 @@ // tslint:disable:max-classes-per-file -import { AttributeValue } from 'aws-sdk/clients/dynamodb' import { PropertyMetadata } from '../../../src/decorator' import { MapperForType } from '../../../src/mapper' +import { ListAttribute, StringAttribute } from '../../../src/mapper/type/attribute.type' import { FormId } from './form-id.model' -export class FormIdMapper implements MapperForType { - fromDb(attributeValue: AttributeValue, propertyMetadata?: PropertyMetadata): FormId { - return FormId.parse(attributeValue.S!) +export class FormIdMapper implements MapperForType { + fromDb(attributeValue: StringAttribute, propertyMetadata?: PropertyMetadata): FormId { + return FormId.parse(attributeValue.S) } - toDb(propertyValue: FormId, propertyMetadata?: PropertyMetadata): AttributeValue | null { + toDb(propertyValue: FormId, propertyMetadata?: PropertyMetadata): StringAttribute | null { return { S: FormId.toString(propertyValue) } } } -export class FormIdsMapper implements MapperForType { - fromDb(attributeValue: AttributeValue, propertyMetadata?: PropertyMetadata): FormId[] | FormId { - if (attributeValue.L) { - return attributeValue.L!.map(formIdDb => FormId.parse(formIdDb.S!)) - } else if (attributeValue.S) { - return FormId.parse(attributeValue.S!) +type AttributeStringOrListValue = StringAttribute | ListAttribute + +export class FormIdsMapper implements MapperForType { + fromDb(attributeValue: AttributeStringOrListValue, propertyMetadata?: PropertyMetadata): FormId[] | FormId { + if ('L' in attributeValue) { + return attributeValue.L.map(formIdDb => FormId.parse(formIdDb.S!)) + } else if ('S' in attributeValue) { + return FormId.parse(attributeValue.S) } else { - throw new Error('there is no mappind defined to read attributeValue ' + JSON.stringify(attributeValue)) + throw new Error('there is no mapping defined to read attributeValue ' + JSON.stringify(attributeValue)) } } - toDb(propertyValue: FormId[] | FormId, propertyMetadata?: PropertyMetadata): AttributeValue | null { + toDb( + propertyValue: FormId[] | FormId, + propertyMetadata?: PropertyMetadata + ): AttributeStringOrListValue | null { if (Array.isArray(propertyValue)) { return { L: propertyValue.map(a => ({ S: FormId.toString(a) })) } } else { diff --git a/test/models/real-world/number-enum.mapper.ts b/test/models/real-world/number-enum.mapper.ts index ab9978aca..eeed5b754 100644 --- a/test/models/real-world/number-enum.mapper.ts +++ b/test/models/real-world/number-enum.mapper.ts @@ -1,13 +1,13 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' import { PropertyMetadata } from '../../../src/decorator' import { MapperForType } from '../../../src/mapper' +import { NumberSetAttribute } from '../../../src/mapper/type/attribute.type' -export class NumberEnumMapper implements MapperForType { - fromDb(attributeValue: AttributeValue, propertyMetadata?: PropertyMetadata): any[] { - return attributeValue.NS!.map(numberEnumValue => parseInt(numberEnumValue, 10)) +export class NumberEnumMapper implements MapperForType { + fromDb(attributeValue: NumberSetAttribute, propertyMetadata?: PropertyMetadata): any[] { + return attributeValue.NS.map(numberEnumValue => parseInt(numberEnumValue, 10)) } - toDb(propertyValues: any[], propertyMetadata?: PropertyMetadata): AttributeValue | null { + toDb(propertyValues: any[], propertyMetadata?: PropertyMetadata): NumberSetAttribute { return { NS: propertyValues.map(propertyValue => propertyValue.toString()) } } } diff --git a/test/models/real-world/order-id.mapper.ts b/test/models/real-world/order-id.mapper.ts index 54540c701..593820a6e 100644 --- a/test/models/real-world/order-id.mapper.ts +++ b/test/models/real-world/order-id.mapper.ts @@ -1,14 +1,14 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' import { PropertyMetadata } from '../../../src/decorator' import { MapperForType } from '../../../src/mapper' +import { StringAttribute } from '../../../src/mapper/type/attribute.type' import { OrderId } from './order.model' -export class OrderIdMapper implements MapperForType { - fromDb(attributeValue: AttributeValue, propertyMetadata?: PropertyMetadata): OrderId { +export class OrderIdMapper implements MapperForType { + fromDb(attributeValue: StringAttribute, propertyMetadata?: PropertyMetadata): OrderId { return OrderId.fromDb(attributeValue) } - toDb(propertyValue: OrderId, propertyMetadata?: PropertyMetadata): AttributeValue | null { + toDb(propertyValue: OrderId, propertyMetadata?: PropertyMetadata): StringAttribute | null { return OrderId.toDb(propertyValue) } } diff --git a/test/models/real-world/order.model.ts b/test/models/real-world/order.model.ts index 6d599d408..b8bf0527f 100644 --- a/test/models/real-world/order.model.ts +++ b/test/models/real-world/order.model.ts @@ -1,11 +1,11 @@ // tslint:disable:max-classes-per-file -import { AttributeValue } from 'aws-sdk/clients/dynamodb' import * as moment from 'moment' import { GSIPartitionKey } from '../../../src/decorator/impl/index/gsi-partition-key.decorator' import { GSISortKey } from '../../../src/decorator/impl/index/gsi-sort-key.decorator' import { PartitionKey } from '../../../src/decorator/impl/key/partition-key.decorator' import { CustomMapper } from '../../../src/decorator/impl/mapper/custom-mapper.decorator' import { Model } from '../../../src/decorator/impl/model/model.decorator' +import { StringAttribute } from '../../../src/mapper/type/attribute.type' import { FormIdsMapper } from './form-id.mapper' import { FormId } from './form-id.model' import { NumberEnumMapper } from './number-enum.mapper' @@ -72,11 +72,11 @@ export class OrderId { return leadingZeroes + formId.counter + formId.year } - static toDb(modelValue: OrderId): AttributeValue { + static toDb(modelValue: OrderId): StringAttribute { return { S: modelValue.toString() } } - static fromDb(dbValue: AttributeValue): OrderId { + static fromDb(dbValue: StringAttribute): OrderId { return OrderId.parse(dbValue['S']) } From b5419e1f463e0e94bf5adf287773e4a833372133 Mon Sep 17 00:00:00 2001 From: Michael Wittwer Date: Fri, 26 Oct 2018 11:00:30 +0200 Subject: [PATCH 6/7] refactor(typing): get rid of AttributeMap & AttributeValue - Added generics for Attributes to have nicer support, maybe we should also use reflection in Attribute typing which would help for Types with complex values (Object, List) - remove all usages of AttributeMap & AttributeValue and also cast in test for nicer type checking --- src/dynamo/batchget/batch-get.request.ts | 10 +- .../condition-expression-builder.ts | 44 ++-- src/dynamo/expression/type/expression.type.ts | 4 +- .../expression/update-expression-builder.ts | 12 +- .../batch-get-single-table.request.ts | 8 +- src/dynamo/request/delete/delete.request.ts | 4 +- src/dynamo/request/get/get.request.spec.ts | 2 +- src/dynamo/request/get/get.request.ts | 4 +- src/dynamo/request/update/update.request.ts | 12 +- src/mapper/for-type/collection.mapper.spec.ts | 33 +-- src/mapper/for-type/enum.mapper.spec.ts | 9 +- src/mapper/mapper.ts | 6 +- src/mapper/type/attribute.type.ts | 5 +- src/mapper/util.ts | 6 +- test/data/organization-dynamodb.data.ts | 4 +- test/data/product-dynamodb.data.ts | 4 +- test/mapper.spec.ts | 221 +++++++++--------- 17 files changed, 195 insertions(+), 193 deletions(-) diff --git a/src/dynamo/batchget/batch-get.request.ts b/src/dynamo/batchget/batch-get.request.ts index cffd7d7bc..75fd34929 100644 --- a/src/dynamo/batchget/batch-get.request.ts +++ b/src/dynamo/batchget/batch-get.request.ts @@ -1,4 +1,4 @@ -import { AttributeMap, BatchGetItemInput } from 'aws-sdk/clients/dynamodb' +import { BatchGetItemInput } from 'aws-sdk/clients/dynamodb' import { isObject, isString } from 'lodash' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' @@ -45,11 +45,11 @@ export class BatchGetRequest { this.tables.set(tableName, modelClazz) const metadata = MetadataHelper.get(modelClazz) - const attributeMaps: AttributeMap[] = [] + const attributeMaps: Attributes[] = [] // loop over all the keys keys.forEach(key => { - const idOb: AttributeMap = {} + const idOb: Attributes = {} if (isString(key)) { // got a simple primary key @@ -101,8 +101,8 @@ export class BatchGetRequest { if (response.Responses && Object.keys(response.Responses).length) { Object.keys(response.Responses).forEach(tableName => { - const mapped = response.Responses![tableName].map(attributeMap => - Mapper.fromDb(attributeMap, this.tables.get(tableName)) + const mapped = response.Responses![tableName].map(attributes => + Mapper.fromDb(attributes, this.tables.get(tableName)) ) r.Responses![tableName] = mapped }) diff --git a/src/dynamo/expression/condition-expression-builder.ts b/src/dynamo/expression/condition-expression-builder.ts index 2c133de36..3f8b88662 100644 --- a/src/dynamo/expression/condition-expression-builder.ts +++ b/src/dynamo/expression/condition-expression-builder.ts @@ -1,8 +1,8 @@ -import { AttributeMap, AttributeValue } from 'aws-sdk/clients/dynamodb' import { curryRight, forEach, isPlainObject } from 'lodash' import { Metadata } from '../../decorator/metadata/metadata' import { PropertyMetadata } from '../../decorator/metadata/property-metadata.model' import { Mapper } from '../../mapper/mapper' +import { Attribute, Attributes } from '../../mapper/type/attribute.type' import { Util } from '../../mapper/util' import { resolveAttributeNames } from './functions/attribute-names.function' import { isFunctionOperator } from './functions/is-function-operator.function' @@ -151,17 +151,15 @@ export class ConditionExpressionBuilder { existingValueNames: string[] | undefined, propertyMetadata: PropertyMetadata | undefined ): Expression { - const attributeValues: AttributeMap = (values[0]) - .map(value => Mapper.toDbOne(value, propertyMetadata)) - .reduce( - (result, mappedValue: AttributeValue | null, index: number) => { - if (mappedValue !== null) { - result[`${valuePlaceholder}_${index}`] = mappedValue - } - return result - }, - {} - ) + const attributeValues: Attributes = (values[0]).map(value => Mapper.toDbOne(value, propertyMetadata)).reduce( + (result, mappedValue: Attribute | null, index: number) => { + if (mappedValue !== null) { + result[`${valuePlaceholder}_${index}`] = mappedValue + } + return result + }, + {} + ) const inStatement = (values[0]).map((value: any, index: number) => `${valuePlaceholder}_${index}`).join(', ') @@ -181,7 +179,7 @@ export class ConditionExpressionBuilder { existingValueNames: string[] | undefined, propertyMetadata: PropertyMetadata | undefined ): Expression { - const attributeValues: AttributeMap = {} + const attributes: Attributes = {} const mappedValue1 = Mapper.toDbOne(values[0], propertyMetadata) const mappedValue2 = Mapper.toDbOne(values[1], propertyMetadata) @@ -192,13 +190,13 @@ export class ConditionExpressionBuilder { const value2Placeholder = uniqAttributeValueName(attributePath, [valuePlaceholder].concat(existingValueNames || [])) const statement = `${namePlaceholder} BETWEEN ${valuePlaceholder} AND ${value2Placeholder}` - attributeValues[valuePlaceholder] = mappedValue1 - attributeValues[value2Placeholder] = mappedValue2 + attributes[valuePlaceholder] = mappedValue1 + attributes[value2Placeholder] = mappedValue2 return { statement, attributeNames, - attributeValues, + attributeValues: attributes, } } @@ -225,28 +223,28 @@ export class ConditionExpressionBuilder { statement = [namePlaceholder, operator, valuePlaceholder].join(' ') } - const attributeValues: AttributeMap = {} + const attributes: Attributes = {} if (hasValue) { - let value: AttributeValue | null + let attribute: Attribute | null switch (operator) { case 'contains': // TODO think about validation // ConditionExpressionBuilder.validateValueForContains(values[0], propertyMetadata) - value = Mapper.toDbOne(values[0], propertyMetadata) + attribute = Mapper.toDbOne(values[0], propertyMetadata) break default: - value = Mapper.toDbOne(values[0], propertyMetadata) + attribute = Mapper.toDbOne(values[0], propertyMetadata) } - if (value) { - attributeValues[valuePlaceholder] = value + if (attribute) { + attributes[valuePlaceholder] = attribute } } return { statement, attributeNames, - attributeValues, + attributeValues: attributes, } } diff --git a/src/dynamo/expression/type/expression.type.ts b/src/dynamo/expression/type/expression.type.ts index 102d65edc..698c89094 100644 --- a/src/dynamo/expression/type/expression.type.ts +++ b/src/dynamo/expression/type/expression.type.ts @@ -1,7 +1,7 @@ -import { AttributeMap } from 'aws-sdk/clients/dynamodb' +import { Attributes } from '../../../mapper/type/attribute.type' export interface Expression { attributeNames: { [key: string]: string } - attributeValues: AttributeMap + attributeValues: Attributes statement: string } diff --git a/src/dynamo/expression/update-expression-builder.ts b/src/dynamo/expression/update-expression-builder.ts index c2d560fbb..0e378a3f8 100644 --- a/src/dynamo/expression/update-expression-builder.ts +++ b/src/dynamo/expression/update-expression-builder.ts @@ -1,7 +1,7 @@ -import { AttributeMap, AttributeValue } from 'aws-sdk/clients/dynamodb' import { Metadata } from '../../decorator/metadata/metadata' import { PropertyMetadata } from '../../decorator/metadata/property-metadata.model' import { Mapper } from '../../mapper/mapper' +import { Attribute, Attributes } from '../../mapper/type/attribute.type' import { Util } from '../../mapper/util' import { ConditionExpressionBuilder } from './condition-expression-builder' import { resolveAttributeNames } from './functions/attribute-names.function' @@ -137,12 +137,12 @@ export class UpdateExpressionBuilder { const hasValue = !UpdateExpressionBuilder.isNoValueAction(operator.action) - const attributeValues: AttributeMap = {} + const attributes: Attributes = {} if (hasValue) { - const value: AttributeValue | null = Mapper.toDbOne(values[0], propertyMetadata) + const attribute: Attribute | null = Mapper.toDbOne(values[0], propertyMetadata) - if (value) { - attributeValues[valuePlaceholder] = value + if (attribute) { + attributes[valuePlaceholder] = attribute } } @@ -150,7 +150,7 @@ export class UpdateExpressionBuilder { type: operator.actionKeyword, statement, attributeNames, - attributeValues, + attributeValues: attributes, } } diff --git a/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts b/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts index e7ef597af..f0963a1b0 100644 --- a/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts +++ b/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts @@ -1,11 +1,11 @@ -import { AttributeMap, BatchGetItemInput } from 'aws-sdk/clients/dynamodb' +import { BatchGetItemInput } from 'aws-sdk/clients/dynamodb' import { isObject } from 'lodash' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { Metadata } from '../../../decorator/metadata/metadata' import { MetadataHelper } from '../../../decorator/metadata/metadata-helper' import { Mapper } from '../../../mapper/mapper' -import { Attributes } from '../../../mapper/type/attribute.type' +import { Attributes, Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' import { DynamoRx } from '../../dynamo-rx' import { BatchGetSingleTableResponse } from './batch-get-single-table.response' @@ -81,10 +81,10 @@ export class BatchGetSingleTableRequest { } private addKeyParams(keys: any[]) { - const attributeMaps: AttributeMap[] = [] + const attributeMaps: Attributes[] = [] keys.forEach(key => { - const idOb: AttributeMap = {} + const idOb: Attributes = {} if (isObject(key)) { // TODO add some more checks // got a composite primary key diff --git a/src/dynamo/request/delete/delete.request.ts b/src/dynamo/request/delete/delete.request.ts index f64426698..a419f3d4b 100644 --- a/src/dynamo/request/delete/delete.request.ts +++ b/src/dynamo/request/delete/delete.request.ts @@ -1,5 +1,4 @@ import { - AttributeMap, DeleteItemInput, DeleteItemOutput, ReturnConsumedCapacity, @@ -8,6 +7,7 @@ import { import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { Mapper } from '../../../mapper/mapper' +import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' import { DynamoRx } from '../../dynamo-rx' import { and } from '../../expression/logical-operator/and.function' @@ -33,7 +33,7 @@ export class DeleteRequest extends BaseRequest { throw new Error(`please provide the sort key for attribute ${this.metaData.getSortKey()}`) } - const keyAttributeMap: AttributeMap = {} + const keyAttributeMap: Attributes = {} // partition key const partitionKeyValue = Mapper.toDbOne(partitionKey, this.metaData.forProperty(this.metaData.getPartitionKey())) diff --git a/src/dynamo/request/get/get.request.spec.ts b/src/dynamo/request/get/get.request.spec.ts index ed6fd1f5d..7081c02a8 100644 --- a/src/dynamo/request/get/get.request.spec.ts +++ b/src/dynamo/request/get/get.request.spec.ts @@ -8,7 +8,7 @@ describe('get requst', () => { beforeEach(() => { request = new GetRequest( - null, + null, SimpleWithPartitionKeyModel, getTableName(SimpleWithPartitionKeyModel), 'partitionKeyValue' diff --git a/src/dynamo/request/get/get.request.ts b/src/dynamo/request/get/get.request.ts index 0b5b6b1af..ab828e59b 100644 --- a/src/dynamo/request/get/get.request.ts +++ b/src/dynamo/request/get/get.request.ts @@ -1,4 +1,4 @@ -import { AttributeMap, ReturnConsumedCapacity } from 'aws-sdk/clients/dynamodb' +import { ReturnConsumedCapacity } from 'aws-sdk/clients/dynamodb' import { values as objValues } from 'lodash' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' @@ -26,7 +26,7 @@ export class GetRequest extends BaseRequest { throw new Error(`please provide the sort key for attribute ${this.metaData.getSortKey()}`) } - const keyAttributeMap: AttributeMap = {} + const keyAttributeMap: Attributes = {} // partition key const partitionKeyValue = Mapper.toDbOne(partitionKey, this.metaData.forProperty(this.metaData.getPartitionKey())) diff --git a/src/dynamo/request/update/update.request.ts b/src/dynamo/request/update/update.request.ts index 0438a1eef..e44c44294 100644 --- a/src/dynamo/request/update/update.request.ts +++ b/src/dynamo/request/update/update.request.ts @@ -1,13 +1,9 @@ -import { - AttributeMap, - ReturnConsumedCapacity, - ReturnItemCollectionMetrics, - UpdateItemOutput, -} from 'aws-sdk/clients/dynamodb' +import { ReturnConsumedCapacity, ReturnItemCollectionMetrics, UpdateItemOutput } from 'aws-sdk/clients/dynamodb' import { forEach } from 'lodash' import { Observable } from 'rxjs' import { map } from 'rxjs/operators' import { Mapper } from '../../../mapper/mapper' +import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' import { DynamoRx } from '../../dynamo-rx' import { and } from '../../expression/logical-operator/and.function' @@ -39,7 +35,7 @@ export class UpdateRequest extends BaseRequest { throw new Error(`please provide the sort key for attribute ${this.metaData.getSortKey()}`) } - const keyAttributeMap: AttributeMap = {} + const keyAttributeMap: Attributes = {} // partition key const partitionKeyValue = Mapper.toDbOne(partitionKey, this.metaData.forProperty(this.metaData.getPartitionKey())) @@ -93,7 +89,7 @@ export class UpdateRequest extends BaseRequest { ) const actionStatements: string[] = [] - let attributeValues: AttributeMap = {} + let attributeValues: Attributes = {} let attributeNames: { [key: string]: string } = {} forEach(sortedByActionKeyWord, (value, key) => { diff --git a/src/mapper/for-type/collection.mapper.spec.ts b/src/mapper/for-type/collection.mapper.spec.ts index 285d0cc8a..dcdc8fc66 100644 --- a/src/mapper/for-type/collection.mapper.spec.ts +++ b/src/mapper/for-type/collection.mapper.spec.ts @@ -1,7 +1,8 @@ import { PropertyMetadata } from '../../decorator/metadata/property-metadata.model' +import { ListAttribute, NumberSetAttribute, StringSetAttribute } from '../type/attribute.type' import { CollectionMapper } from './collection.mapper' -fdescribe('collection mapper', () => { +describe('collection mapper', () => { let mapper: CollectionMapper beforeEach(() => { @@ -14,7 +15,7 @@ fdescribe('collection mapper', () => { * Arrays */ it('arr (homogeneous string)', () => { - const attributeValue = mapper.toDb(['value1', 'value2', 'value3']) + const attributeValue = mapper.toDb(['value1', 'value2', 'value3']) expect(Object.keys(attributeValue)[0]).toBe('SS') expect(Array.isArray(attributeValue.SS)).toBeTruthy() expect(attributeValue.SS.length).toBe(3) @@ -22,7 +23,7 @@ fdescribe('collection mapper', () => { }) it('arr (homogeneous number)', () => { - const attributeValue = mapper.toDb([5, 10]) + const attributeValue = mapper.toDb([5, 10]) expect(Object.keys(attributeValue)[0]).toBe('NS') expect(Array.isArray(attributeValue.NS)).toBeTruthy() expect(attributeValue.NS.length).toBe(2) @@ -30,7 +31,7 @@ fdescribe('collection mapper', () => { }) it('arr (homogeneous objects)', () => { - const attributeValue = mapper.toDb([{ name: 'name1' }, { name: 'name2' }]) + const attributeValue = mapper.toDb([{ name: 'name1' }, { name: 'name2' }]) expect(Object.keys(attributeValue)[0]).toBe('L') expect(Array.isArray(attributeValue.L)).toBeTruthy() expect(attributeValue.L.length).toBe(2) @@ -38,7 +39,7 @@ fdescribe('collection mapper', () => { }) it('arr (heterogeneous)', () => { - const attributeValue = mapper.toDb(['value1', 10]) + const attributeValue = mapper.toDb(['value1', 10]) expect(Object.keys(attributeValue)[0]).toBe('L') expect(Array.isArray(attributeValue.L)).toBeTruthy() expect(attributeValue.L.length).toBe(2) @@ -49,7 +50,7 @@ fdescribe('collection mapper', () => { * Set */ it('set (homogeneous string)', () => { - const attributeValue = mapper.toDb(new Set(['value1', 'value2', 'value3'])) + const attributeValue = mapper.toDb(new Set(['value1', 'value2', 'value3'])) expect(Object.keys(attributeValue)[0]).toBe('SS') expect(Array.isArray(attributeValue.SS)).toBeTruthy() expect(attributeValue.SS.length).toBe(3) @@ -57,7 +58,7 @@ fdescribe('collection mapper', () => { }) it('set (homogeneous number)', () => { - const attributeValue = mapper.toDb(new Set([5, 10])) + const attributeValue = mapper.toDb(new Set([5, 10])) expect(Object.keys(attributeValue)[0]).toBe('NS') expect(Array.isArray(attributeValue.NS)).toBeTruthy() expect(attributeValue.NS.length).toBe(2) @@ -65,7 +66,7 @@ fdescribe('collection mapper', () => { }) it('set (homogeneous objects)', () => { - const attributeValue = mapper.toDb(new Set([{ name: 'name1' }, { name: 'name2' }])) + const attributeValue = mapper.toDb(new Set([{ name: 'name1' }, { name: 'name2' }])) expect(Object.keys(attributeValue)[0]).toBe('L') expect(Array.isArray(attributeValue.L)).toBeTruthy() expect(attributeValue.L.length).toBe(2) @@ -73,7 +74,7 @@ fdescribe('collection mapper', () => { }) it('set (heterogeneous)', () => { - const attributeValue = mapper.toDb(new Set(['value1', 10])) + const attributeValue = mapper.toDb(new Set(['value1', 10])) expect(Object.keys(attributeValue)[0]).toBe('L') expect(Array.isArray(attributeValue.L)).toBeTruthy() expect(attributeValue.L.length).toBe(2) @@ -90,7 +91,7 @@ fdescribe('collection mapper', () => { } it('set (homogeneous, generic type is string)', () => { - const attributeValue = mapper.toDb(new Set(['value1', 'value2', 'value3']), { + const attributeValue = mapper.toDb(new Set(['value1', 'value2', 'value3']), { typeInfo: { type: Set, genericType: String }, }) expect(Object.keys(attributeValue)[0]).toBe('SS') @@ -100,7 +101,7 @@ fdescribe('collection mapper', () => { }) it('sorted arr (homogeneous string)', () => { - const attributeValue = mapper.toDb(['value1', 'value2', 'value3'], metadata) + const attributeValue = mapper.toDb(['value1', 'value2', 'value3'], metadata) expect(Object.keys(attributeValue)[0]).toBe('L') expect(Array.isArray(attributeValue.L)).toBeTruthy() expect(attributeValue.L.length).toBe(3) @@ -108,7 +109,7 @@ fdescribe('collection mapper', () => { }) it('sorted arr (homogeneous number)', () => { - const attributeValue = mapper.toDb([5, 10], metadata) + const attributeValue = mapper.toDb([5, 10], metadata) expect(Object.keys(attributeValue)[0]).toBe('L') expect(Array.isArray(attributeValue.L)).toBeTruthy() expect(attributeValue.L.length).toBe(2) @@ -116,7 +117,7 @@ fdescribe('collection mapper', () => { }) it('sorted set (homogeneous string)', () => { - const attributeValue = mapper.toDb(new Set(['value1', 'value2', 'value3']), metadata) + const attributeValue = mapper.toDb(new Set(['value1', 'value2', 'value3']), metadata) expect(Object.keys(attributeValue)[0]).toBe('L') expect(Array.isArray(attributeValue.L)).toBeTruthy() expect(attributeValue.L.length).toBe(3) @@ -124,7 +125,7 @@ fdescribe('collection mapper', () => { }) it('sorted set (homogeneous number)', () => { - const attributeValue = mapper.toDb([5, 10], metadata) + const attributeValue = mapper.toDb([5, 10], metadata) expect(Object.keys(attributeValue)[0]).toBe('L') expect(Array.isArray(attributeValue.L)).toBeTruthy() expect(attributeValue.L.length).toBe(2) @@ -139,14 +140,14 @@ fdescribe('collection mapper', () => { * S(et) */ it('arr (homogeneous string)', () => { - const stringSet: Set = mapper.fromDb({ SS: ['value1', 'value2', 'value3'] }) + const stringSet = >mapper.fromDb({ SS: ['value1', 'value2', 'value3'] }) expect(stringSet instanceof Set).toBeTruthy() expect(stringSet.size).toBe(3) expect(typeof Array.from(stringSet)[0]).toBe('string') }) it('arr (homogeneous number)', () => { - const numberSet: Set = mapper.fromDb({ NS: ['25', '10'] }) + const numberSet = >mapper.fromDb({ NS: ['25', '10'] }) expect(numberSet instanceof Set).toBeTruthy() expect(numberSet.size).toBe(2) expect(typeof Array.from(numberSet)[0]).toBe('number') diff --git a/src/mapper/for-type/enum.mapper.spec.ts b/src/mapper/for-type/enum.mapper.spec.ts index bd51525f2..2b29471da 100644 --- a/src/mapper/for-type/enum.mapper.spec.ts +++ b/src/mapper/for-type/enum.mapper.spec.ts @@ -1,4 +1,5 @@ import { PropertyMetadata } from '../../decorator/metadata/property-metadata.model' +import { NumberAttribute } from '../type/attribute.type' import { EnumMapper } from './enum.mapper' enum Tags { @@ -8,7 +9,7 @@ enum Tags { } describe('enum mapper', () => { - const propertyMetadata: PropertyMetadata = { + const propertyMetadata: PropertyMetadata = { typeInfo: { genericType: Tags, }, @@ -27,7 +28,7 @@ describe('enum mapper', () => { }) it('should work', () => { - const attributeValue = mapper.toDb(Tags.ONE, propertyMetadata) + const attributeValue = mapper.toDb(Tags.ONE, propertyMetadata) expect(attributeValue).toEqual({ N: '0' }) }) @@ -57,13 +58,13 @@ describe('enum mapper', () => { it('should throw', () => { expect(() => { - mapper.fromDb({ S: '2' }, propertyMetadata) + mapper.fromDb({ S: '2' }, propertyMetadata) }).toThrowError() }) it('should throw', () => { expect(() => { - mapper.fromDb({ S: '2' }) + mapper.fromDb({ S: '2' }) }).toThrowError() }) }) diff --git a/src/mapper/mapper.ts b/src/mapper/mapper.ts index 1b0d4ec49..4c4bf70d1 100644 --- a/src/mapper/mapper.ts +++ b/src/mapper/mapper.ts @@ -31,8 +31,8 @@ export class Mapper { // static logger = debug('Mapper'); - static toDb(item: T, modelConstructor?: ModelConstructor): Attributes { - const mapped: Attributes = {} + static toDb(item: T, modelConstructor?: ModelConstructor): Attributes { + const mapped = >{} if (modelConstructor) { const metadata: Metadata = MetadataHelper.get(modelConstructor) @@ -66,7 +66,7 @@ export class Mapper { * 2) decide how to map the property depending on type or value */ - let propertyMetadata: PropertyMetadata | null | undefined + let propertyMetadata: PropertyMetadata | null | undefined if (modelConstructor) { propertyMetadata = MetadataHelper.forProperty(modelConstructor, propertyKey) } diff --git a/src/mapper/type/attribute.type.ts b/src/mapper/type/attribute.type.ts index c0d3cecb8..ec92ba960 100644 --- a/src/mapper/type/attribute.type.ts +++ b/src/mapper/type/attribute.type.ts @@ -1,3 +1,4 @@ +// TODO investigate if we can make Attribute generic, would be useful for Map- / ListAttribute export type Attribute = | StringAttribute | NumberAttribute @@ -10,9 +11,7 @@ export type Attribute = | NullAttribute | BooleanAttribute -export interface Attributes { - [key: string]: Attribute -} +export type Attributes = { [key in keyof T & string]: Attribute } /** * An attribute of type String. For example: "S": "Hello" diff --git a/src/mapper/util.ts b/src/mapper/util.ts index d4c323de8..ced04ab4e 100644 --- a/src/mapper/util.ts +++ b/src/mapper/util.ts @@ -1,8 +1,8 @@ -import { AttributeValue } from 'aws-sdk/clients/dynamodb' import { isNumber, isString } from 'lodash' import { isMoment } from 'moment' import { AttributeCollectionType, AttributeType } from './type/attribute-type.type' import { AttributeValueType } from './type/attribute-value-type.type' +import { Attribute, StringAttribute } from './type/attribute.type' import { Binary } from './type/binary.type' import { MomentType } from './type/moment.type' import { NullType } from './type/null.type' @@ -192,12 +192,12 @@ export class Util { * copied from https://github.com/aws/aws-sdk-js/blob/0c974a7ff6749a541594de584b43a040978d4b72/lib/dynamodb/types.js * should we work with string match */ - static typeOfFromDb(attributeValue?: AttributeValue): AttributeValueType { + static typeOfFromDb(attributeValue?: Attribute): AttributeValueType { if (attributeValue) { const dynamoType: AttributeType = Object.keys(attributeValue)[0] switch (dynamoType) { case 'S': - if (Util.DATE_TIME_ISO8601.test(attributeValue.S!)) { + if (Util.DATE_TIME_ISO8601.test((attributeValue).S)) { return MomentType } else { return String diff --git a/test/data/organization-dynamodb.data.ts b/test/data/organization-dynamodb.data.ts index 33e4547c1..af16b01b9 100644 --- a/test/data/organization-dynamodb.data.ts +++ b/test/data/organization-dynamodb.data.ts @@ -1,12 +1,12 @@ -import { AttributeMap } from 'aws-sdk/clients/dynamodb' import * as moment from 'moment' +import { Attributes } from '../../src/mapper/type/attribute.type' export const organization1CreatedAt: moment.Moment = moment('2017-05-15', 'YYYY-MM-DD') export const organization1LastUpdated: moment.Moment = moment('2017-07-25', 'YYYY-MM-DD') export const organization1Employee1CreatedAt: moment.Moment = moment('2015-02-15', 'YYYY-MM-DD') export const organization1Employee2CreatedAt: moment.Moment = moment('2015-07-03', 'YYYY-MM-DD') -export const organizationFromDb: AttributeMap = { +export const organizationFromDb: Attributes = { id: { S: 'myId' }, createdAtDate: { S: organization1CreatedAt diff --git a/test/data/product-dynamodb.data.ts b/test/data/product-dynamodb.data.ts index e846b3723..7942ea0be 100644 --- a/test/data/product-dynamodb.data.ts +++ b/test/data/product-dynamodb.data.ts @@ -1,12 +1,12 @@ -import { AttributeMap } from 'aws-sdk/clients/dynamodb' import * as moment from 'moment' +import { Attributes } from '../../src/mapper/type/attribute.type' export const organization1CreatedAt: moment.Moment = moment('2017-05-15', 'YYYY-MM-DD') export const organization1LastUpdated: moment.Moment = moment('2017-07-25', 'YYYY-MM-DD') export const organization1Employee1CreatedAt: moment.Moment = moment('2015-02-15', 'YYYY-MM-DD') export const organization1Employee2CreatedAt: moment.Moment = moment('2015-07-03', 'YYYY-MM-DD') -export const productFromDb: AttributeMap = { +export const productFromDb: Attributes = { nestedValue: { M: { sortedSet: { diff --git a/test/mapper.spec.ts b/test/mapper.spec.ts index 0d01b651a..76ffa2173 100644 --- a/test/mapper.spec.ts +++ b/test/mapper.spec.ts @@ -1,14 +1,18 @@ -import { - AttributeMap, - AttributeValue, - AttributeValueList, - ListAttributeValue, - MapAttributeValue, - StringSetAttributeValue, -} from 'aws-sdk/clients/dynamodb' +import { AttributeMap, MapAttributeValue } from 'aws-sdk/clients/dynamodb' import * as moment from 'moment' import { PropertyMetadata } from '../src/decorator/metadata/property-metadata.model' import { Mapper } from '../src/mapper/mapper' +import { + Attribute, + Attributes, + BooleanAttribute, + ListAttribute, + MapAttribute, + NullAttribute, + NumberAttribute, + StringAttribute, + StringSetAttribute, +} from '../src/mapper/type/attribute.type' import { EnumType } from '../src/mapper/type/enum.type' import { MomentType } from '../src/mapper/type/moment.type' import { @@ -32,33 +36,33 @@ describe('Mapper', () => { describe('should map single values', () => { describe('to db', () => { it('string', () => { - const attrValue: AttributeValue = Mapper.toDbOne('foo')! + const attrValue = Mapper.toDbOne('foo')! expect(attrValue).toBeDefined() expect(attrValue!.S).toBeDefined() expect(attrValue!.S).toBe('foo') }) it('string (empty)', () => { - const attrValue: AttributeValue = Mapper.toDbOne('')! + const attrValue = Mapper.toDbOne('')! expect(attrValue).toBe(null) }) it('number', () => { - const attrValue: AttributeValue = Mapper.toDbOne(3)! + const attrValue = Mapper.toDbOne(3)! expect(attrValue).toBeDefined() expect(keyOf(attrValue!)).toBe('N') expect(attrValue!.N).toBe('3') }) it('boolean', () => { - const attrValue: AttributeValue = Mapper.toDbOne(false)! + const attrValue = Mapper.toDbOne(false)! expect(attrValue).toBeDefined() expect(keyOf(attrValue!)).toBe('BOOL') expect(attrValue!.BOOL).toBe(false) }) it('null', () => { - const attrValue: AttributeValue = Mapper.toDbOne(null)! + const attrValue = Mapper.toDbOne(null)! expect(attrValue).toBeDefined() expect(keyOf(attrValue!)).toBe('NULL') expect(attrValue!.NULL).toBe(true) @@ -66,7 +70,7 @@ describe('Mapper', () => { it('date (moment)', () => { const m = moment() - const attrValue: AttributeValue = Mapper.toDbOne(m)! + const attrValue = Mapper.toDbOne(m)! expect(attrValue).toBeDefined() expect(keyOf(attrValue!)).toBe('S') expect(attrValue!.S).toBe( @@ -78,14 +82,14 @@ describe('Mapper', () => { }) it('enum (no enum decorator)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(Type.FirstType)! + const attrValue = Mapper.toDbOne(Type.FirstType)! expect(attrValue).toBeDefined() expect(keyOf(attrValue!)).toBe('N') expect(attrValue!.N).toBe('0') }) it('enum (propertyMetadata -> no enum decorator)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(Type.FirstType, { + const attrValue: Attribute = Mapper.toDbOne(Type.FirstType, { typeInfo: { type: Object, isCustom: true }, })! expect(attrValue).toBeDefined() @@ -94,7 +98,7 @@ describe('Mapper', () => { }) it('enum (with decorator)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(Type.FirstType, { + const attrValue = Mapper.toDbOne(Type.FirstType, { typeInfo: { type: EnumType, isCustom: true }, })! expect(attrValue).toBeDefined() @@ -103,7 +107,7 @@ describe('Mapper', () => { }) it('array -> SS (homogen, no duplicates)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(['foo', 'bar'])! + const attrValue = Mapper.toDbOne(['foo', 'bar'])! expect(attrValue).toBeDefined() expect(keyOf(attrValue!)).toBe('SS') expect(attrValue!.SS![0]).toBe('foo') @@ -114,35 +118,35 @@ describe('Mapper', () => { const propertyMetadata = >>{ typeInfo: { type: Array, isCustom: true }, } - const attrValue: AttributeValue = Mapper.toDbOne(['foo', 'bar'], propertyMetadata)! + const attrValue = Mapper.toDbOne(['foo', 'bar'], propertyMetadata)! expect(attrValue).toBeDefined() expect(keyOf(attrValue!)).toBe('L') expect(keyOf(attrValue!.L![0])).toBe('S') - expect(attrValue!.L![0].S).toBe('foo') + expect((attrValue!.L![0]).S).toBe('foo') expect(keyOf(attrValue!.L![1])).toBe('S') - expect(attrValue!.L![1].S).toBe('bar') + expect((attrValue!.L![1]).S).toBe('bar') }) it('array -> L (heterogen, no duplicates)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(['foo', 56, true])! + const attrValue = Mapper.toDbOne(['foo', 56, true])! expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('L') expect(attrValue.L).toBeDefined() expect(attrValue.L!.length).toBe(3) - const foo: AttributeValue = attrValue.L![0] + const foo = attrValue.L![0] expect(foo).toBeDefined() expect(keyOf(foo)).toBe('S') expect(foo.S).toBe('foo') - const no: AttributeValue = attrValue.L![1] + const no = attrValue.L![1] expect(no).toBeDefined() expect(keyOf(no)).toBe('N') expect(no.N).toBe('56') - const bool: AttributeValue = attrValue.L![2] + const bool = attrValue.L![2] expect(bool).toBeDefined() expect(keyOf(bool)).toBe('BOOL') expect(bool.BOOL).toBe(true) @@ -150,25 +154,24 @@ describe('Mapper', () => { it('array -> L (homogen, complex type)', () => { const now = moment() - const attrValue: AttributeValue = Mapper.toDbOne([ - new Employee('max', 25, now, null), - new Employee('anna', 65, now, null), - ])! + const attrValue = ( + Mapper.toDbOne([new Employee('max', 25, now, null), new Employee('anna', 65, now, null)])! + ) expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('L') - const employee1 = attrValue.L![0] + const employee1 = attrValue.L![0] expect(employee1).toBeDefined() expect(keyOf(employee1)).toBe('M') expect(Object.keys(employee1.M!).length).toBe(3) expect(employee1.M!['name']).toBeDefined() expect(keyOf(employee1.M!['name'])).toBe('S') - expect(employee1.M!['name']['S']).toBe('max') + expect((employee1.M!['name']).S).toBe('max') expect(employee1.M!['age']).toBeDefined() expect(keyOf(employee1.M!['age'])).toBe('N') - expect(employee1.M!['age']['N']).toBe('25') + expect((employee1.M!['age']).N).toBe('25') expect(employee1.M!['createdAt']).toEqual({ S: now @@ -179,7 +182,7 @@ describe('Mapper', () => { }) it('set', () => { - const attrValue: AttributeValue = Mapper.toDbOne(new Set(['foo', 'bar', 25]))! + const attrValue = Mapper.toDbOne(new Set(['foo', 'bar', 25]))! expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('L') expect(attrValue.L![0]).toEqual({ S: 'foo' }) @@ -188,49 +191,51 @@ describe('Mapper', () => { }) it('set (empty)', () => { - const attrValue: AttributeValue = Mapper.toDbOne(new Set())! + const attrValue = Mapper.toDbOne(new Set())! expect(attrValue).toBe(null) }) it('set of employees', () => { const cd: moment.Moment = moment('2017-02-03', 'YYYY-MM-DD') const cd2: moment.Moment = moment('2017-02-28', 'YYYY-MM-DD') - const attrValue: AttributeValue = Mapper.toDbOne( - new Set([ - { name: 'foo', age: 56, createdAt: cd }, - { name: 'anna', age: 26, createdAt: cd2 }, - ]) - )! + const attrValue = ( + Mapper.toDbOne( + new Set([ + { name: 'foo', age: 56, createdAt: cd }, + { name: 'anna', age: 26, createdAt: cd2 }, + ]) + )! + ) expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('L') expect(attrValue.L!.length).toBe(2) - expect(attrValue.L![0].M).toBeDefined() - expect(attrValue.L![0].M!['name']).toBeDefined() - expect(keyOf(attrValue.L![0].M!['name'])).toBe('S') - expect(attrValue.L![0].M!['name'].S).toBe('foo') + expect((attrValue.L![0]).M).toBeDefined() + expect((attrValue.L![0]).M['name']).toBeDefined() + expect(keyOf((attrValue.L![0]).M['name'])).toBe('S') + expect(((attrValue.L![0]).M['name']).S).toBe('foo') }) it('object (Employee created using Object literal)', () => { const cr: moment.Moment = moment('2017-03-03', 'YYYY-MM-DD') - const attrValue: AttributeValue = Mapper.toDbOne({ name: 'foo', age: 56, createdAt: cr })! + const attrValue = Mapper.toDbOne({ name: 'foo', age: 56, createdAt: cr })! expect(attrValue).toBeDefined() expect(keyOf(attrValue)).toBe('M') // name expect(attrValue.M!['name']).toBeDefined() expect(keyOf(attrValue.M!['name'])).toBe('S') - expect(attrValue.M!['name'].S).toBe('foo') + expect((attrValue.M!['name']).S).toBe('foo') // age expect(attrValue.M!['age']).toBeDefined() expect(keyOf(attrValue.M!['age'])).toBe('N') - expect(attrValue.M!['age'].N).toBe('56') + expect((attrValue.M!['age']).N).toBe('56') // createdAt expect(attrValue.M!['createdAt']).toBeDefined() expect(keyOf(attrValue.M!['createdAt'])).toBe('S') - expect(attrValue.M!['createdAt'].S).toBe( + expect((attrValue.M!['createdAt']).S).toBe( cr .clone() .utc() @@ -240,24 +245,24 @@ describe('Mapper', () => { it('object (Employee created using constructor)', () => { const cr: moment.Moment = moment('2017-05-03', 'YYYY-MM-DD') - const attrValue: AttributeValue = Mapper.toDbOne(new Employee('foo', 56, cr, []))! + const attrValue = Mapper.toDbOne(new Employee('foo', 56, cr, []))! expect(attrValue).toBeDefined() expect(keyOf(attrValue!)).toBe('M') // name expect(attrValue!.M!['name']).toBeDefined() expect(keyOf(attrValue!.M!['name'])).toBe('S') - expect(attrValue!.M!['name'].S).toBe('foo') + expect((attrValue!.M!['name']).S).toBe('foo') // age expect(attrValue!.M!['age']).toBeDefined() expect(keyOf(attrValue!.M!['age'])).toBe('N') - expect(attrValue!.M!['age'].N).toBe('56') + expect((attrValue!.M!['age']).N).toBe('56') // createdAt expect(attrValue!.M!['createdAt']).toBeDefined() expect(keyOf(attrValue!.M!['createdAt'])).toBe('S') - expect(attrValue!.M!['createdAt'].S).toBe( + expect((attrValue!.M!['createdAt']).S).toBe( cr .clone() .utc() @@ -429,7 +434,7 @@ describe('Mapper', () => { describe('to db', () => { describe('model class created with new', () => { let organization: Organization - let organizationAttrMap: AttributeMap + let organizationAttrMap: Attributes let createdAt: moment.Moment let lastUpdated: moment.Moment let createdAtDateEmployee1: moment.Moment @@ -489,8 +494,8 @@ describe('Mapper', () => { it('createdAtDate', () => { expect(organizationAttrMap.createdAtDate).toBeDefined() - expect(organizationAttrMap.createdAtDate.S).toBeDefined() - expect(organizationAttrMap.createdAtDate.S).toBe( + expect((organizationAttrMap.createdAtDate).S).toBeDefined() + expect((organizationAttrMap.createdAtDate).S).toBe( createdAt .clone() .utc() @@ -500,8 +505,8 @@ describe('Mapper', () => { it('lastUpdated', () => { expect(organizationAttrMap.lastUpdated).toBeDefined() - expect(organizationAttrMap.lastUpdated.S).toBeDefined() - expect(organizationAttrMap.lastUpdated.S).toBe( + expect((organizationAttrMap.lastUpdated).S).toBeDefined() + expect((organizationAttrMap.lastUpdated).S).toBe( lastUpdated .clone() .utc() @@ -511,8 +516,8 @@ describe('Mapper', () => { it('active', () => { expect(organizationAttrMap.active).toBeDefined() - expect(organizationAttrMap.active.BOOL).toBeDefined() - expect(organizationAttrMap.active.BOOL).toBe(true) + expect((organizationAttrMap.active).BOOL).toBeDefined() + expect((organizationAttrMap.active).BOOL).toBe(true) }) it('count', () => { @@ -522,7 +527,7 @@ describe('Mapper', () => { it('domains', () => { expect(organizationAttrMap.domains).toBeDefined() - const domains: StringSetAttributeValue = organizationAttrMap.domains.SS! + const domains = (organizationAttrMap.domains).SS expect(domains).toBeDefined() expect(domains.length).toBe(3) @@ -534,39 +539,39 @@ describe('Mapper', () => { it('random details', () => { expect(organizationAttrMap.randomDetails).toBeDefined() - const randomDetails: ListAttributeValue = organizationAttrMap.randomDetails.L! + const randomDetails = (organizationAttrMap.randomDetails).L expect(randomDetails).toBeDefined() expect(randomDetails.length).toBe(3) expect(keyOf(randomDetails[0])).toBe('S') - expect(randomDetails[0].S).toBe('sample') + expect((randomDetails[0]).S).toBe('sample') expect(keyOf(randomDetails[1])).toBe('N') - expect(randomDetails[1].N).toBe('26') + expect((randomDetails[1]).N).toBe('26') expect(keyOf(randomDetails[2])).toBe('BOOL') - expect(randomDetails[2].BOOL).toBe(true) + expect((randomDetails[2]).BOOL).toBe(true) }) it('employees', () => { expect(organizationAttrMap.employees).toBeDefined() - const employeesL: AttributeValueList = organizationAttrMap.employees.L! + const employeesL = (organizationAttrMap.employees).L expect(employeesL).toBeDefined() expect(employeesL.length).toBe(2) expect(employeesL[0]).toBeDefined() - expect(employeesL[0].M).toBeDefined() + expect((employeesL[0]).M).toBeDefined() // test employee1 - const employee1: MapAttributeValue = employeesL[0].M! + const employee1 = (employeesL[0]).M expect(employee1['name']).toBeDefined() - expect(employee1['name'].S).toBeDefined() - expect(employee1['name'].S).toBe('max') + expect((employee1['name']).S).toBeDefined() + expect((employee1['name']).S).toBe('max') expect(employee1['age']).toBeDefined() - expect(employee1['age'].N).toBeDefined() - expect(employee1['age'].N).toBe('50') + expect((employee1['age']).N).toBeDefined() + expect((employee1['age']).N).toBe('50') expect(employee1['createdAt']).toBeDefined() - expect(employee1['createdAt'].S).toBeDefined() - expect(employee1['createdAt'].S).toBe( + expect((employee1['createdAt']).S).toBeDefined() + expect((employee1['createdAt']).S).toBe( createdAtDateEmployee1 .clone() .utc() @@ -574,7 +579,7 @@ describe('Mapper', () => { ) // test employee2 - const employee2: MapAttributeValue = employeesL[1].M! + const employee2: MapAttributeValue = (employeesL[1]).M expect(employee2['name']).toBeDefined() expect(employee2['name'].S).toBeDefined() expect(employee2['name'].S).toBe('anna') @@ -594,7 +599,7 @@ describe('Mapper', () => { it('cities', () => { expect(organizationAttrMap.cities).toBeDefined() - const citiesSS: StringSetAttributeValue = organizationAttrMap.cities.SS! + const citiesSS = (organizationAttrMap.cities).SS expect(citiesSS).toBeDefined() expect(citiesSS.length).toBe(2) expect(citiesSS[0]).toBe('zürich') @@ -604,17 +609,17 @@ describe('Mapper', () => { it('birthdays', () => { expect(organizationAttrMap.birthdays).toBeDefined() - const birthdays: ListAttributeValue = organizationAttrMap.birthdays.L! + const birthdays = (organizationAttrMap.birthdays).L expect(birthdays).toBeDefined() expect(birthdays.length).toBe(2) expect(keyOf(birthdays[0])).toBe('M') // birthday 1 - const birthday1: MapAttributeValue = birthdays[0]['M']! + const birthday1 = (birthdays[0]).M expect(birthday1['date']).toBeDefined() expect(keyOf(birthday1['date'])).toBe('S') - expect(birthday1['date']['S']).toBe( + expect((birthday1['date']).S).toBe( birthday1Date .clone() .utc() @@ -623,26 +628,26 @@ describe('Mapper', () => { expect(birthday1['presents']).toBeDefined() expect(keyOf(birthday1['presents'])).toBe('L') - expect(birthday1['presents']['L']!.length).toBe(2) - expect(keyOf(birthday1['presents']['L']![0])).toBe('M') + expect((birthday1['presents']).L.length).toBe(2) + expect(keyOf((birthday1['presents']).L[0])).toBe('M') - expect(keyOf(birthday1['presents']['L']![0])).toBe('M') + expect(keyOf((birthday1['presents']).L[0])).toBe('M') - const birthday1gift1 = birthday1['presents']['L']![0]['M']! + const birthday1gift1 = ((birthday1['presents']).L[0]).M expect(birthday1gift1['description']).toBeDefined() expect(keyOf(birthday1gift1['description'])).toBe('S') - expect(birthday1gift1['description']['S']).toBe('ticket to rome') + expect((birthday1gift1['description']).S).toBe('ticket to rome') - const birthday1gift2 = birthday1['presents']['L']![1]['M']! + const birthday1gift2 = ((birthday1['presents']).L[1]).M expect(birthday1gift2['description']).toBeDefined() expect(keyOf(birthday1gift2['description'])).toBe('S') - expect(birthday1gift2['description']['S']).toBe('camper van') + expect((birthday1gift2['description']).S).toBe('camper van') // birthday 2 - const birthday2: MapAttributeValue = birthdays[1]['M']! + const birthday2 = (birthdays[1]).M expect(birthday2['date']).toBeDefined() expect(keyOf(birthday2['date'])).toBe('S') - expect(birthday2['date']['S']).toBe( + expect((birthday2['date']).S).toBe( birthday2Date .clone() .utc() @@ -651,49 +656,51 @@ describe('Mapper', () => { expect(birthday2['presents']).toBeDefined() expect(keyOf(birthday2['presents'])).toBe('L') - expect(birthday2['presents']['L']!.length).toBe(2) - expect(keyOf(birthday2['presents']['L']![0])).toBe('M') + expect((birthday2['presents']).L.length).toBe(2) + expect(keyOf((birthday2['presents']).L[0])).toBe('M') - expect(keyOf(birthday2['presents']['L']![0])).toBe('M') + expect(keyOf((birthday2['presents']).L[0])).toBe('M') - const birthday2gift1 = birthday2['presents']['L']![0]['M']! + const birthday2gift1 = ((birthday2['presents']).L[0]).M expect(birthday2gift1['description']).toBeDefined() expect(keyOf(birthday2gift1['description'])).toBe('S') - expect(birthday2gift1['description']['S']).toBe('car') + expect((birthday2gift1['description']).S).toBe('car') - const birthday2gift2 = birthday2['presents']['L']![1]['M']! + const birthday2gift2 = ((birthday2['presents']).L[1]).M expect(birthday2gift2['description']).toBeDefined() expect(keyOf(birthday2gift2['description'])).toBe('S') - expect(birthday2gift2['description']['S']).toBe('gin') + expect((birthday2gift2['description']).S).toBe('gin') }) it('awards', () => { expect(organizationAttrMap.awards).toBeDefined() - const awards: ListAttributeValue = organizationAttrMap.awards.L! + const awards = (organizationAttrMap.awards).L expect(awards).toBeDefined() expect(awards.length).toBe(2) expect(keyOf(awards[0])).toBe('S') - expect(awards[0].S).toBe('good, better, shiftcode') + expect((awards[0]).S).toBe('good, better, shiftcode') - expect(keyOf(awards[0])).toBe('S') - expect(awards[1].S).toBe('jus kiddin') + expect(keyOf(awards[1])).toBe('S') + expect((awards[1]).S).toBe('jus kiddin') }) it('events', () => { expect(organizationAttrMap.events).toBeDefined() - const events: ListAttributeValue = organizationAttrMap.events.L! + const events = (organizationAttrMap.events).L expect(events).toBeDefined() expect(events.length).toBe(1) - expect(keyOf(events[0])).toBe('M') - expect(events[0]['M']!['name']).toBeDefined() - expect(keyOf(events[0]['M']!['name'])).toBe('S') - expect(events[0]['M']!['name']['S']).toBe('shift the web') + const a = events[0] + + expect(keyOf(a)).toBe('M') + expect(a.M['name']).toBeDefined() + expect(keyOf(a.M['name'])).toBe('S') + expect((a.M['name']).S).toBe('shift the web') - expect(events[0]['M']!['participantCount']).toBeDefined() - expect(keyOf(events[0]['M']!['participantCount'])).toBe('N') - expect(events[0]['M']!['participantCount']['N']).toBe('1520') + expect(a.M['participantCount']).toBeDefined() + expect(keyOf(a.M['participantCount'])).toBe('N') + expect((a.M['participantCount']).N).toBe('1520') }) it('transient', () => { @@ -906,7 +913,7 @@ describe('Mapper', () => { }) }) -function keyOf(attributeValue: AttributeValue): string | null { +function keyOf(attributeValue: Attribute): string | null { if (attributeValue && Object.keys(attributeValue).length) { return Object.keys(attributeValue)[0] } else { From cf9fed867107ff42308754aa8bbfa443ff9217b2 Mon Sep 17 00:00:00 2001 From: Michael Wittwer Date: Fri, 26 Oct 2018 11:13:15 +0200 Subject: [PATCH 7/7] fix(typing): resolve ts issue --- .../batchgetsingletable/batch-get-single-table.request.ts | 2 +- src/mapper/mapper.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts b/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts index f0963a1b0..34befa027 100644 --- a/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts +++ b/src/dynamo/request/batchgetsingletable/batch-get-single-table.request.ts @@ -5,7 +5,7 @@ import { map } from 'rxjs/operators' import { Metadata } from '../../../decorator/metadata/metadata' import { MetadataHelper } from '../../../decorator/metadata/metadata-helper' import { Mapper } from '../../../mapper/mapper' -import { Attributes, Attributes } from '../../../mapper/type/attribute.type' +import { Attributes } from '../../../mapper/type/attribute.type' import { ModelConstructor } from '../../../model/model-constructor' import { DynamoRx } from '../../dynamo-rx' import { BatchGetSingleTableResponse } from './batch-get-single-table.response' diff --git a/src/mapper/mapper.ts b/src/mapper/mapper.ts index 4c4bf70d1..1398fcdff 100644 --- a/src/mapper/mapper.ts +++ b/src/mapper/mapper.ts @@ -56,7 +56,7 @@ export class Mapper { */ const propertyValue = Mapper.getPropertyValue(item, propertyKey) - let attributeValue: any | undefined | null + let attributeValue: Attribute | undefined | null // TODO concept maybe make this configurable how to map undefined & null values if (propertyValue === undefined || propertyValue === null) { @@ -105,7 +105,7 @@ export class Mapper { } else if (attributeValue === null) { // empty values (string, set, list) will be ignored too } else { - mapped[propertyMetadata ? propertyMetadata.nameDb : propertyKey] = attributeValue + ;(mapped)[propertyMetadata ? propertyMetadata.nameDb : propertyKey] = attributeValue } } })