Skip to content

Commit 2f27208

Browse files
first base implementation of decorator and mapper
1 parent af33d23 commit 2f27208

Some content is hidden

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

54 files changed

+2816
-160
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ end_of_line = lf
66
charset = utf-8
77
trim_trailing_whitespace = true
88
insert_final_newline = true
9-
max_line_length = 100
9+
max_line_length = 170
1010
indent_size = 2
1111

1212
[*.md]

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,75 @@
99

1010
A starter project that makes creating a TypeScript library extremely easy.
1111

12+
## Object Mapper
13+
14+
#### Decorators
15+
Decorators are used to add some metadata to our model classes required by the mapper for some special cases.
16+
17+
This is an experimental feature and requires to set the
18+
19+
- "experimentalDecorators": true
20+
- "emitDecoratorMetadata": true
21+
22+
compiler options.
23+
Additionally we rely on the reflect-metadata (https://www.npmjs.com/package/reflect-metadata) package for reflection api.
24+
25+
To get started with decorators just add a @Model() Decorator to any ts class. By default this enables the custom mapping functionality
26+
and will get you started to work with Dynamo DB.
27+
28+
We make heavy usage of compile time informations about our models and the property types.
29+
ES6 types like Set, Map will be mapped to Object when calling for the type via Reflect.get, so we need some extra info.
30+
31+
Along the way we will probably need some extra features. Here is a list of these:
32+
33+
**Custom TableName**
34+
@Model({tableName: tableName})
35+
36+
37+
#### Types
38+
The type defines how a value will be mapped. Types can be defined using decorators (for complex types) or we use one of the following methods:
39+
fromDB -> use default for DynamoDB type (see type table)
40+
toDB -> use property value to resolve the type
41+
42+
design:type
43+
String, Number, Boolean, Undefined, Object
44+
45+
unsupported
46+
Set, Map, Date, moment.Moment
47+
48+
49+
#### Dynamo DB
50+
51+
To map an js object into the attribute map required by dynamodb requests, we implement our very oppinionated custom mapper.
52+
We use the DynamoDB Document Mapper to map all «default» types to dynamodb attribute values.
53+
54+
There are some custom requirements for these cases:
55+
56+
- MomentJs Dates
57+
- Use ES6 Map, Set types
58+
59+
Mapper Strategy:
60+
61+
-> To DB
62+
1) check if we have some property metadata
63+
64+
YES NO
65+
66+
isCustomType document client can map (check with typeof propertyValue for additional security)
67+
68+
YES NO
69+
70+
custom mapping document client can map
71+
-> From DB
72+
73+
74+
## Open Tasks
75+
Null Values?
76+
How does DynamoDb treat empty lists, sets or emtpy strings?
77+
78+
## Node Project Template
79+
80+
1281
### Usage
1382

1483
```bash

attribute-map.type.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { AttributeValue } from 'aws-sdk/clients/dynamodb';
2+
3+
export type AttributeMap<T> = {[key in keyof T]: AttributeValue};

package.json

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
}
5050
},
5151
"jest": {
52+
"testEnvironment": "node",
5253
"transform": {
5354
".(ts|tsx)": "<rootDir>/node_modules/ts-jest/preprocessor.js"
5455
},
@@ -72,37 +73,44 @@
7273
}
7374
},
7475
"devDependencies": {
75-
"@types/jest": "^20.0.0",
76-
"@types/node": "^8.0.0",
76+
"@types/debug": "^0.0.30",
77+
"@types/jest": "^20.0.6",
78+
"@types/lodash": "^4.14.71",
79+
"@types/node": "^8.0.19",
7780
"colors": "^1.1.2",
7881
"commitizen": "^2.9.6",
7982
"coveralls": "^2.13.1",
80-
"cross-env": "^5.0.1",
83+
"cross-env": "^5.0.4",
8184
"cz-conventional-changelog": "^2.0.0",
8285
"husky": "^0.14.0",
8386
"jest": "^20.0.4",
84-
"lint-staged": "^4.0.0",
87+
"lint-staged": "^4.0.3",
8588
"lodash.camelcase": "^4.3.0",
8689
"prettier": "^1.4.4",
8790
"prompt": "^1.0.0",
8891
"replace-in-file": "^2.5.0",
8992
"rimraf": "^2.6.1",
90-
"rollup": "^0.43.1",
91-
"rollup-plugin-commonjs": "^8.0.2",
93+
"rollup": "^0.45.2",
94+
"rollup-plugin-commonjs": "^8.1.0",
9295
"rollup-plugin-node-resolve": "^3.0.0",
9396
"rollup-plugin-sourcemaps": "^0.4.2",
9497
"semantic-release": "^6.3.6",
95-
"ts-jest": "^20.0.6",
98+
"ts-jest": "^20.0.10",
9699
"ts-node": "^3.0.6",
97100
"tsc-watch": "^1.0.5",
98-
"tslint": "^5.4.3",
99-
"tslint-config-prettier": "^1.1.0",
101+
"tslint": "^5.6.0",
102+
"tslint-config-prettier": "^1.3.0",
100103
"tslint-config-standard": "^6.0.0",
101-
"typedoc": "^0.7.1",
104+
"typedoc": "^0.8.0",
102105
"typescript": "^2.3.4",
103-
"validate-commit-msg": "^2.12.2"
106+
"validate-commit-msg": "^2.14.0"
104107
},
105108
"dependencies": {
106-
"aws-sdk": "^2.91.0"
109+
"aws-sdk": "^2.91.0",
110+
"core-js": "^2.5.0",
111+
"debug": "^2.6.8",
112+
"lodash": "^4.17.4",
113+
"moment": "^2.18.1",
114+
"reflect-metadata": "^0.1.10"
107115
}
108116
}

src/decorators/decorators.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import "reflect-metadata"
2+
3+
// these reflection keys are built in using the reflect-metadata library
4+
export const KEY_TYPE = "design:type"
5+
export const KEY_PARAMETER = "design:paramtypes"
6+
export const KEY_RETURN_TYPE = "design:returntype"
7+
8+
export const getMetadataType = makeMetadataGetter(KEY_TYPE)
9+
10+
export function makeMetadataGetter(
11+
metadataKey: string
12+
): (target: any, targetKey?: string) => any {
13+
return function(target: any, targetKey?: string) {
14+
return Reflect.getMetadata(metadataKey, target, targetKey)
15+
}
16+
}
17+
18+
/**
19+
* Property index configuration
20+
*/
21+
// export interface IModelAttributeIndex {
22+
// name: string
23+
// unique?: boolean
24+
// isSecondaryKey?: boolean
25+
// sortKey?: string
26+
// }
27+
28+
// export function Property(opts: AttributeOptions): PropertyDecorator {
29+
// return (target: Object, propertyKey: string | symbol) => {
30+
//
31+
// };
32+
// }

src/decorators/metadata.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ModelClass } from "../model/model"
2+
import { ModelMetadata } from "./model-metadata.model"
3+
import { KEY_MODEL } from "./model.decorator"
4+
import { PropertyMetadata } from "./property-metadata.model"
5+
6+
export class Metadata<T> {
7+
readonly modelOptions: ModelMetadata
8+
9+
constructor(modelClass: ModelClass<T>) {
10+
this.modelOptions = Reflect.getMetadata(KEY_MODEL, modelClass)
11+
}
12+
13+
forProperty(propertyKey: string): PropertyMetadata | undefined {
14+
let options: PropertyMetadata | undefined
15+
16+
if (this.modelOptions.properties) {
17+
options = this.modelOptions.properties.find(
18+
property => property.key === propertyKey
19+
)
20+
}
21+
22+
return options
23+
}
24+
}
25+
26+
export class MetadataHelper {
27+
static get<T>(modelClass: ModelClass<T>): Metadata<T> {
28+
return new Metadata(modelClass)
29+
}
30+
31+
static forModel<T>(modelClass: ModelClass<T>): ModelMetadata {
32+
return Reflect.getMetadata(KEY_MODEL, modelClass)
33+
}
34+
35+
static forProperty<T>(
36+
modelClass: ModelClass<T>,
37+
propertyKey: keyof T
38+
): PropertyMetadata {
39+
let modelOptions = Reflect.getMetadata(KEY_MODEL, modelClass)
40+
41+
let options: PropertyMetadata | undefined
42+
if (modelOptions.properties) {
43+
options = modelOptions.properties.find(
44+
property => property.key === propertyKey
45+
)
46+
}
47+
48+
return options
49+
}
50+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { PropertyMetadata } from "./property-metadata.model"
2+
3+
export interface ModelData {
4+
tableName?: string
5+
}
6+
7+
/**
8+
* Options provided to model
9+
* decorator annotation
10+
*/
11+
export interface ModelMetadata {
12+
clazzName?: string
13+
clazz?: any
14+
tableName?: string
15+
properties?: PropertyMetadata[]
16+
transientProperties?: string[]
17+
}

src/decorators/model.decorator.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { kebabCase } from "lodash"
2+
import { getMetadataType } from "./decorators"
3+
import { ModelData, ModelMetadata } from "./model-metadata.model"
4+
import { PropertyMetadata } from "./property-metadata.model"
5+
import { KEY_PROPERTY } from "./property.decorator"
6+
// FIXME should be optional dependency
7+
import moment from "moment"
8+
9+
export const KEY_MODEL = "sc-reflect:model"
10+
11+
export function Model(opts: ModelData = {}): ClassDecorator {
12+
return function(constructor: Function) {
13+
// Make sure everything is valid
14+
const classType = getMetadataType(constructor)
15+
const type = constructor as any
16+
17+
// get all the properties with @Property() annotation
18+
const properties: PropertyMetadata[] = Reflect.getOwnMetadata(
19+
KEY_PROPERTY,
20+
constructor
21+
)
22+
23+
const transientProperties: string[] =
24+
properties && properties.length
25+
? properties
26+
.filter(property => property.transient === true)
27+
.map(property => property.key)
28+
: []
29+
30+
const finalOpts = Object.assign<
31+
Partial<ModelMetadata>,
32+
Partial<ModelMetadata>,
33+
Partial<ModelMetadata>
34+
>(
35+
{},
36+
{
37+
clazz: constructor,
38+
clazzName: type.name,
39+
tableName: kebabCase(type.name),
40+
properties,
41+
transientProperties
42+
},
43+
opts
44+
)
45+
46+
console.log(`Decorating: ${finalOpts.clazzName}`, finalOpts)
47+
Reflect.defineMetadata(KEY_MODEL, finalOpts, constructor)
48+
}
49+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { PropertyMetadata } from "./property-metadata.model"
2+
import { initOrUpdateProperty, KEY_PROPERTY } from "./property.decorator"
3+
4+
// FIXME check for type of partition key only some scalars are allowed
5+
export function PartitionKey(): PropertyDecorator {
6+
return function(target: Object, propertyKey: string) {
7+
initOrUpdateProperty({ partitionKey: true }, target, propertyKey)
8+
}
9+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface PropertyData {
2+
// the name of property how it is named in dynamoDb
3+
name: string
4+
}

0 commit comments

Comments
 (0)