Skip to content

Commit 0c81b8e

Browse files
Merge pull request #163 from shiftcode/#158-collection-mapper
refactor(collection-mapper): make the mapping more consistent
2 parents e7ea877 + 1b0819f commit 0c81b8e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+897
-344
lines changed

src/decorator/decorators.spec.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
SimpleModel,
2020
} from '../../test/models'
2121
import { Form } from '../../test/models/real-world'
22-
import { GSIPartitionKey, GSISortKey, LSISortKey, PartitionKey, Property, SortedSet, SortKey, Transient } from './impl'
22+
import { GSIPartitionKey, GSISortKey, LSISortKey, PartitionKey, Property, SortKey, Transient } from './impl'
23+
import { CollectionProperty } from './impl/collection/collection-property.decorator'
2324
import { Model } from './impl/model/model.decorator'
2425
import { Metadata, metadataForClass, metadataForModel, ModelMetadata } from './index'
2526
import { metadataForProperty } from './metadata'
@@ -110,7 +111,6 @@ describe('Decorators should add correct metadata', () => {
110111
expect(prop!.key!.uuid).toBeFalsy()
111112
expect(prop!.transient).toBeFalsy()
112113
expect(prop!.typeInfo).toBeDefined()
113-
expect(prop!.typeInfo!.isCustom).toBeFalsy()
114114
expect(prop!.typeInfo!.type).toBe(String)
115115
})
116116

@@ -124,7 +124,6 @@ describe('Decorators should add correct metadata', () => {
124124
expect(prop!.key!.uuid).toBeFalsy()
125125
expect(prop!.transient).toBeFalsy()
126126
expect(prop!.typeInfo).toBeDefined()
127-
expect(prop!.typeInfo!.isCustom).toBeTruthy()
128127
})
129128

130129
it('active', () => {
@@ -135,7 +134,6 @@ describe('Decorators should add correct metadata', () => {
135134
expect(prop.key).toBeUndefined()
136135
expect(prop.transient).toBeFalsy()
137136
expect(prop.typeInfo).toBeDefined()
138-
expect(prop.typeInfo.isCustom).toBeFalsy()
139137
expect(prop.typeInfo.type).toBe(Boolean)
140138
})
141139

@@ -147,7 +145,6 @@ describe('Decorators should add correct metadata', () => {
147145
expect(prop.key).toBeUndefined()
148146
expect(prop.transient).toBeFalsy()
149147
expect(prop.typeInfo).toBeDefined()
150-
expect(prop.typeInfo.isCustom).toBeTruthy()
151148
expect(prop.typeInfo.type).toBe(Set)
152149
})
153150

@@ -160,7 +157,6 @@ describe('Decorators should add correct metadata', () => {
160157
expect(prop.transient).toBeFalsy()
161158
expect(prop.isSortedCollection).toBeTruthy()
162159
expect(prop.typeInfo).toBeDefined()
163-
expect(prop.typeInfo.isCustom).toBeTruthy()
164160
expect(prop.typeInfo.type).toBe(Set)
165161
})
166162

@@ -174,7 +170,6 @@ describe('Decorators should add correct metadata', () => {
174170
expect(prop.isSortedCollection).toBeTruthy()
175171

176172
expect(prop.typeInfo).toBeDefined()
177-
expect(prop.typeInfo.isCustom).toBeTruthy()
178173
expect(prop.typeInfo.type).toBe(Set)
179174

180175
expect(prop.typeInfo.genericType).toBeDefined()
@@ -189,7 +184,6 @@ describe('Decorators should add correct metadata', () => {
189184
expect(prop.key).toBeUndefined()
190185
expect(prop.transient).toBeFalsy()
191186
expect(prop.typeInfo).toBeDefined()
192-
expect(prop.typeInfo.isCustom).toBeTruthy()
193187
expect(prop.typeInfo.type).toBe(Map)
194188
})
195189

@@ -201,7 +195,6 @@ describe('Decorators should add correct metadata', () => {
201195
expect(prop.key).toBeUndefined()
202196
expect(prop.transient).toBeTruthy()
203197
expect(prop.typeInfo).toBeDefined()
204-
expect(prop.typeInfo.isCustom).toBeFalsy()
205198
expect(prop.typeInfo.type).toBe(String)
206199
})
207200

@@ -218,7 +211,6 @@ describe('Decorators should add correct metadata', () => {
218211
expect(prop.key).toBeUndefined()
219212
expect(prop.transient).toBeFalsy()
220213
expect(prop.typeInfo).toBeDefined()
221-
expect(prop.typeInfo.isCustom).toBeTruthy()
222214
expect(prop.typeInfo.type).toBe(NestedObject)
223215
})
224216
})
@@ -327,11 +319,11 @@ describe('Decorators should add correct metadata', () => {
327319
it('should add enum type to property', () => {
328320
const enumPropertyMetadata = metadata.forProperty('type')!
329321
expect(enumPropertyMetadata.typeInfo).toBeDefined()
330-
expect(enumPropertyMetadata.typeInfo).toEqual({ type: Number, isCustom: false })
322+
expect(enumPropertyMetadata.typeInfo).toEqual({ type: Number })
331323

332324
const strEnumPropertyMetadata = metadata.forProperty('strType')!
333325
expect(strEnumPropertyMetadata.typeInfo).toBeDefined()
334-
expect(strEnumPropertyMetadata.typeInfo).toEqual({ type: String, isCustom: false })
326+
expect(strEnumPropertyMetadata.typeInfo).toEqual({ type: String })
335327
})
336328
})
337329

@@ -370,7 +362,7 @@ describe('Decorators should add correct metadata', () => {
370362

371363
@Model()
372364
class B extends A {
373-
@SortedSet()
365+
@CollectionProperty({ sorted: true })
374366
myOwnProp: string[]
375367
}
376368

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { BinaryAttribute, MapperForType, NumberAttribute, StringAttribute } from '../../../mapper'
2+
import { ModelConstructor } from '../../../model'
3+
4+
export interface CollectionPropertyDataBase<R, T extends StringAttribute | NumberAttribute | BinaryAttribute> {
5+
/**
6+
* the name of property how it is named in dynamoDB
7+
*/
8+
name?: string
9+
10+
/**
11+
* if the collection should preserve the order. if so it will be stored as (L)ist
12+
*/
13+
sorted?: boolean
14+
15+
/**
16+
* provide an itemMapper if you want your complex items being mapped to String|Number|Binary attribute
17+
* (e.g. because you can't store it in a (S)et otherwise)
18+
* only provide either itemMapper or itemType --> not both
19+
* itemMapper is basically intended to be used with [S]et. though it also works with [L]ist
20+
*/
21+
itemMapper?: MapperForType<R, T>
22+
23+
/**
24+
* provide an itemType (class with @Model decorator) for complex types (eg. Set<ComplexType>)
25+
* collections with complex types will be stored as (L)ist
26+
* only provide either itemType or itemMapper --> not both
27+
*/
28+
itemType?: ModelConstructor<any>
29+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { BinaryAttribute, MapperForType, NumberAttribute, StringAttribute } from '../../../mapper'
2+
import {
3+
wrapMapperForDynamoListJsArray,
4+
wrapMapperForDynamoListJsSet,
5+
wrapMapperForDynamoSetJsArray,
6+
wrapMapperForDynamoSetJsSet,
7+
} from '../../../mapper/wrap-mapper-for-collection.function'
8+
import { ModelConstructor } from '../../../model'
9+
import { PropertyMetadata, TypeInfo } from '../../metadata'
10+
import { getMetadataType } from '../../util'
11+
import { initOrUpdateProperty } from '../property/init-or-update-property.function'
12+
import { CollectionPropertyDataBase } from './collection-property-data.model'
13+
14+
type DecoratorFn = (target: object, propertyKey: string | symbol) => void
15+
16+
export function CollectionProperty<R, T extends StringAttribute | NumberAttribute | BinaryAttribute>(opts: CollectionPropertyDataBase<R, T> = {}): DecoratorFn {
17+
return (target: object, propertyKey: string | symbol) => {
18+
if (typeof propertyKey === 'string') {
19+
20+
const type: ModelConstructor<any> = getMetadataType(target, propertyKey)
21+
22+
if (type !== Set && type !== Array) {
23+
throw new Error(`[${target.constructor.name}::${propertyKey}] The CollectionProperty decorator is meant for properties of type Set or Array`)
24+
}
25+
26+
const meta: Partial<PropertyMetadata<any>> & { typeInfo: TypeInfo } = {
27+
name: propertyKey,
28+
nameDb: opts && opts.name || propertyKey,
29+
typeInfo: {type},
30+
isSortedCollection: !!opts.sorted,
31+
}
32+
33+
const hasItemType = 'itemType' in opts && !!opts.itemType
34+
const hasItemMapper = 'itemMapper' in opts && !!opts.itemMapper
35+
36+
if (hasItemMapper && hasItemType) {
37+
throw new Error(`[${target.constructor.name}::${propertyKey}] provide either itemType or itemMapper, not both`)
38+
}
39+
40+
if (hasItemType) {
41+
meta.typeInfo.genericType = opts.itemType
42+
}
43+
44+
if (hasItemMapper) {
45+
const itemMapper = <MapperForType<any, any>>opts.itemMapper
46+
47+
const wrappedMapper: MapperForType<any, any> = type === Array
48+
? !!opts.sorted
49+
? wrapMapperForDynamoListJsArray(itemMapper)
50+
: wrapMapperForDynamoSetJsArray(itemMapper)
51+
: !!opts.sorted
52+
? wrapMapperForDynamoListJsSet(itemMapper)
53+
: wrapMapperForDynamoSetJsSet(itemMapper)
54+
55+
meta.mapper = () => wrappedMapper
56+
meta.mapperForSingleItem = () => itemMapper
57+
}
58+
59+
initOrUpdateProperty(meta, target, propertyKey)
60+
}
61+
}
62+
}

src/decorator/impl/collection/sorted-set.decorator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { initOrUpdateProperty } from '../property/init-or-update-property.functi
66
* Only the L(ist) dynamo datatype preservers the order of inserted items, so we have to make sure the L(ist) type is used
77
* when persisting. The modelConstructor is required if the collection items
88
* have some property decorators, so we can retrieve this information using the model class.
9+
* @deprecated instead use @CollectionProperty({ sorted: true, itemType?: ModelConstructor })
910
*/
1011
export function SortedSet(modelConstructor?: ModelConstructor<any>): PropertyDecorator {
1112
return (target: any, propertyKey: string | symbol) => {
1213
if (typeof propertyKey === 'string') {
1314
const typeInfo: TypeInfo = {
14-
type: Set,
15-
isCustom: true,
15+
type: Set
1616
}
1717

1818
if (modelConstructor) {

src/decorator/impl/collection/typed-array.decorator.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@ import { initOrUpdateProperty } from '../property/init-or-update-property.functi
55
/**
66
* Makes sure the property will be mapped to a L(ist) type. The modelConstructor is required if the collection items
77
* have some property decorators, so we can retrieve this information using the model class.
8+
* @deprecated instead use @CollectionProperty({ itemType?: ModelConstructor })
89
*/
910
export function TypedArray<T>(modelConstructor?: ModelConstructor<T>): PropertyDecorator {
1011
return (target: object, propertyKey: string | symbol) => {
1112
if (typeof propertyKey === 'string') {
12-
const typeInfo: TypeInfo = {
13-
type: Array,
14-
isCustom: true,
15-
}
13+
const typeInfo: TypeInfo = {type: Array}
1614

1715
if (modelConstructor) {
1816
typeInfo.genericType = modelConstructor

src/decorator/impl/collection/typed-set.decorator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { initOrUpdateProperty } from '../property/init-or-update-property.functi
55
/**
66
* Makes sure the property will be marshalled to a S(et) type. The modelConstructor is required if the collection items
77
* have some property decorators, so we can retrieve this information using the model class.
8+
* @deprecated instead use @CollectionProperty({ itemType?: modelConstructor })
89
*/
910
export function TypedSet(modelConstructor?: ModelConstructor<any>): PropertyDecorator {
1011
return (target: any, propertyKey: string | symbol) => {
1112
if (typeof propertyKey === 'string') {
1213
const typeInfo: TypeInfo = {
13-
type: Set,
14-
isCustom: true,
14+
type: Set
1515
}
1616

1717
if (modelConstructor) {

src/decorator/impl/custom-mapper/custom-mapper.decorator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { MapperForType } from '../../../mapper'
22
import { initOrUpdateProperty } from '../property/init-or-update-property.function'
33

4+
/**
5+
*
6+
* @param customMapper
7+
* @deprecated use @Property({mapper: YourCustomMapper}) instead
8+
*/
49
export function CustomMapper(customMapper: MapperForType<any, any>): PropertyDecorator {
510
return (target: any, propertyKey: string | symbol) => {
611
if (typeof propertyKey === 'string') {

src/decorator/impl/property/init-or-update-property.function.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { AttributeValueType } from '../../../mapper/type/attribute-value-type.type'
21
import { Attribute } from '../../../mapper/type/attribute.type'
32
import { ModelConstructor } from '../../../model/model-constructor'
43
import { PropertyMetadata, TypeInfo } from '../../metadata/property-metadata.model'
@@ -35,12 +34,7 @@ function createNewProperty(
3534
propertyKey: string,
3635
): PropertyMetadata<any> {
3736
const propertyType: ModelConstructor<any> = getMetadataType(target, propertyKey)
38-
const isCustom = isCustomType(propertyType)
39-
40-
const typeInfo: TypeInfo = {
41-
type: propertyType,
42-
isCustom,
43-
}
37+
const typeInfo: TypeInfo = { type: propertyType}
4438

4539
propertyOptions = {
4640
name: propertyKey,
@@ -51,11 +45,3 @@ function createNewProperty(
5145

5246
return <PropertyMetadata<any>>propertyOptions
5347
}
54-
55-
/**
56-
* TODO LOW:BINARY make sure to implement the context dependant details of Binary (Buffer vs. Uint8Array)
57-
* @returns {boolean} true if the type cannot be mapped by dynamo document client
58-
*/
59-
function isCustomType(type: AttributeValueType): boolean {
60-
return <any>type !== String && <any>type !== Number && <any>type !== Boolean && <any>type !== Uint8Array
61-
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import { MapperForType } from '../../../mapper'
2+
13
export interface PropertyData {
24
// the name of property how it is named in dynamoDB
35
name: string
6+
mapper: MapperForType<any, any>
47
}

src/decorator/impl/property/property.decorator.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ export function Property(opts: Partial<PropertyData> = {}): PropertyDecorator {
99
name: propertyKey,
1010
nameDb: opts.name || propertyKey,
1111
}
12+
13+
if ('mapper' in opts && !!opts.mapper) {
14+
const m = opts.mapper
15+
propertyOptions.mapper = () => m
16+
}
17+
1218
initOrUpdateProperty(propertyOptions, target, propertyKey)
1319
}
1420
}

0 commit comments

Comments
 (0)