Skip to content

Commit 5e71f1c

Browse files
committed
refactor(collection-mapper): make the mapping more consistent
BREAKING CHANGE some mappings will no longer work due to missing information for a correct parsing without decorator, collections are consistently mapped as follows: - Array <--> [L]ist - Set<number|string|Binary> <--> [(N|S|B)S]et Set<ComplextType> --> is no longer mapped to [L]ist, only possible with decorator
1 parent e7ea877 commit 5e71f1c

39 files changed

+879
-291
lines changed

src/decorator/decorators.spec.ts

Lines changed: 3 additions & 2 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'
@@ -370,7 +371,7 @@ describe('Decorators should add correct metadata', () => {
370371

371372
@Model()
372373
class B extends A {
373-
@SortedSet()
374+
@CollectionProperty({ sorted: true })
374375
myOwnProp: string[]
375376
}
376377

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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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: {
30+
type,
31+
isCustom: true,
32+
},
33+
isSortedCollection: !!opts.sorted,
34+
}
35+
36+
const hasItemType = 'itemType' in opts && !!opts.itemType
37+
const hasItemMapper = 'itemMapper' in opts && !!opts.itemMapper
38+
39+
if (hasItemMapper && hasItemType) {
40+
throw new Error(`[${target.constructor.name}::${propertyKey}] provide either itemType or itemMapper, not both`)
41+
}
42+
43+
if (hasItemType) {
44+
meta.typeInfo.genericType = opts.itemType
45+
}
46+
47+
if (hasItemMapper) {
48+
const itemMapper = <MapperForType<any, any>>opts.itemMapper
49+
50+
const wrappedMapper: MapperForType<any, any> = type === Array
51+
? !!opts.sorted
52+
? wrapMapperForDynamoListJsArray(itemMapper)
53+
: wrapMapperForDynamoSetJsArray(itemMapper)
54+
: !!opts.sorted
55+
? wrapMapperForDynamoListJsSet(itemMapper)
56+
: wrapMapperForDynamoSetJsSet(itemMapper)
57+
58+
meta.mapper = () => wrappedMapper
59+
meta.mapperForSingleItem = () => itemMapper
60+
}
61+
62+
initOrUpdateProperty(meta, target, propertyKey)
63+
}
64+
}
65+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ 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) => {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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) => {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ 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) => {

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') {
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
}

src/decorator/metadata/property-metadata.model.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { MapperForType } from '../../mapper/for-type/base.mapper'
2-
31
// def good
42
import * as DynamoDB from 'aws-sdk/clients/dynamodb'
3+
import { MapperForType } from '../../mapper/for-type/base.mapper'
54
import { Attribute } from '../../mapper/type/attribute.type'
65
import { ModelConstructor } from '../../model/model-constructor'
76

@@ -40,6 +39,8 @@ export interface PropertyMetadata<T, R extends Attribute = Attribute> {
4039

4140
mapper?: () => MapperForType<any, R>
4241

42+
mapperForSingleItem?: () => MapperForType<any, any>
43+
4344
// maps the index name to the key type to describe for which GSI this property describes a key attribute
4445
keyForGSI?: Record<string, DynamoDB.KeyType>
4546

@@ -55,3 +56,16 @@ export function hasGenericType(
5556
): propertyMetadata is PropertyMetadata<any, any> & { typeInfo: { genericType: ModelConstructor<any> } } {
5657
return !!(propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.genericType)
5758
}
59+
60+
export function alterCollectionPropertyMetadataForSingleItem<T>(propertyMeta?: PropertyMetadata<T> | null): PropertyMetadata<T> | undefined {
61+
if (!propertyMeta) {
62+
return
63+
}
64+
if (propertyMeta.mapper && propertyMeta.mapperForSingleItem) {
65+
return { ...propertyMeta, mapper: propertyMeta.mapperForSingleItem }
66+
}
67+
if (propertyMeta.typeInfo && (propertyMeta.typeInfo.type === Set || propertyMeta.typeInfo.type === Array)) {
68+
return
69+
}
70+
return { ...propertyMeta }
71+
}

0 commit comments

Comments
 (0)