Skip to content

Commit 07c67c6

Browse files
feat(dataStore): add validation for table name
1 parent 702b66f commit 07c67c6

File tree

8 files changed

+121
-22
lines changed

8 files changed

+121
-22
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Dynamo-Easy
22
[![Travis](https://img.shields.io/travis/shiftcode/dynamo-easy.svg)](https://travis-ci.org/shiftcode/dynamo-easy)
3-
[![Coverage Status](https://img.shields.io/coveralls/jekyll/jekyll.svg)](https://coveralls.io/github/shiftcode/dynamo-easy)
3+
[![Coverage Status](https://img.shields.io/coveralls/jekyll/jekyll.svg)](https://coveralls.io/github/shiftcode/dynamo-easy?branch=master)
44
[![Dev Dependencies](https://img.shields.io/david/expressjs/express.svg)](https://david-dm.org/michaelwittwer/dynamo-easy?type=dev)
55
[![Greenkeeper badge](https://badges.greenkeeper.io/alexjoverm/typescript-library-starter.svg)](https://greenkeeper.io/)
66
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
@@ -11,7 +11,7 @@ to define some metadata for your models. You don't need to care about the mappin
1111

1212
Checkout the full technical api documentation [here](https://shiftcode.github.io/dynamo-easy/).
1313

14-
Built with :heart: by the crew from [shiftcode](https://www.shiftcode.ch).
14+
Built with :heart: by [shiftcode](https://www.shiftcode.ch).
1515

1616
# Get Started
1717
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { TableNameResolver } from '../../../dynamo/table-name-resolver.type'
2+
13
export interface ModelData {
24
tableName?: string
35
}

src/decorator/impl/model/model.decorator.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
11
import { kebabCase } from 'lodash'
22
import { ModelMetadata } from '../../metadata/model-metadata.model'
33
import { PropertyMetadata } from '../../metadata/property-metadata.model'
4-
// FIXME should be optional dependency
54
import { getMetadataType } from '../../util'
65
import { SecondaryIndex } from '../index/secondary-index'
76
import { KEY_PROPERTY } from '../property/property.decorator'
87
import { ModelData } from './model-data.model'
98

109
export const KEY_MODEL = 'sc-reflect:model'
1110

12-
/*
13-
* FIXME add validation for tableName
14-
* Table names and index names must be between 3 and 255 characters long, and can contain only the following characters:
15-
a-z
16-
A-Z
17-
0-9
18-
_ (underscore)
19-
- (dash)
20-
. (dot)
21-
*/
2211
export function Model(opts: ModelData = {}): ClassDecorator {
2312
// tslint:disable-next-line:ban-types
2413
return (constructor: Function) => {
@@ -27,7 +16,6 @@ export function Model(opts: ModelData = {}): ClassDecorator {
2716
const classType = getMetadataType(constructor)
2817
const type = constructor as any
2918

30-
// FIXME would better typing help with something
3119
// get all the properties with @Property() annotation
3220
const properties: Array<PropertyMetadata<any>> = Reflect.getOwnMetadata(KEY_PROPERTY, constructor)
3321

src/dynamo/dynamo-store.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Model } from '../decorator/impl/model/model.decorator'
2+
import { DynamoStore } from './dynamo-store'
3+
4+
// tslint:disable-next-line:max-classes-per-file
5+
@Model()
6+
class DynamoStoreModel {}
7+
8+
// tslint:disable-next-line:max-classes-per-file
9+
@Model({ tableName: 'myTableName' })
10+
class DynamoStoreModel2 {}
11+
12+
describe('dynamo store', () => {
13+
it('correct table name (default)', () => {
14+
const dynamoStore = new DynamoStore(DynamoStoreModel)
15+
16+
expect(dynamoStore.tableName).toBe('dynamo-store-models')
17+
})
18+
19+
it('correct table name ()', () => {
20+
const dynamoStore = new DynamoStore(DynamoStoreModel2)
21+
22+
expect(dynamoStore.tableName).toBe('myTableName')
23+
})
24+
25+
it('correct table name ()', () => {
26+
const dynamoStore = new DynamoStore(DynamoStoreModel2, tableName => `${tableName}-with-special-thing`)
27+
28+
expect(dynamoStore.tableName).toBe('myTableName-with-special-thing')
29+
})
30+
31+
it('throw error because table name is invalid', () => {
32+
expect(() => {
33+
// tslint:disable-next-line:no-unused-expression
34+
new DynamoStore(DynamoStoreModel2, tableName => `${tableName}$`)
35+
}).toThrowError()
36+
})
37+
})

src/dynamo/dynamo-store.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import { SessionValidityEnsurer } from './session-validity-ensurer.type'
1717
import { TableNameResolver } from './table-name-resolver.type'
1818

1919
export class DynamoStore<T> {
20+
/* http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-naming-rules */
21+
private static REGEX_TABLE_NAME = /^[a-zA-Z0-9_\-.]{3,255}$/
22+
2023
private readonly dynamoRx: DynamoRx
2124
private readonly mapper: Mapper
2225

@@ -28,7 +31,14 @@ export class DynamoStore<T> {
2831
sessionValidityEnsurer: SessionValidityEnsurer = DEFAULT_SESSION_VALIDITY_ENSURER
2932
) {
3033
this.dynamoRx = new DynamoRx(sessionValidityEnsurer)
31-
this.tableName = tableNameResolver(MetadataHelper.get(this.modelClazz).modelOptions.tableName)
34+
const tableName = tableNameResolver(MetadataHelper.get(this.modelClazz).modelOptions.tableName)
35+
if (!DynamoStore.REGEX_TABLE_NAME.test(tableName)) {
36+
throw new Error(
37+
'make sure the table name only contains these characters «a-z A-Z 0-9 - _ .» and is between 3 and 255 characters long'
38+
)
39+
}
40+
41+
this.tableName = tableName
3242
}
3343

3444
get dynamoDb(): DynamoDB {
@@ -111,12 +121,6 @@ export class DynamoStore<T> {
111121
})
112122
}
113123

114-
// private findBySingleKey(partitionKeyValue: any): Observable<T[]> {
115-
// return this.query()
116-
// .wherePartitionKey(partitionKeyValue)
117-
// .exec()
118-
// }
119-
120124
private createBaseParams(): { TableName: string } {
121125
const params: { TableName: string } = {
122126
TableName: this.tableName,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { PutItemInput } from 'aws-sdk/clients/dynamodb'
2+
import { getTableName } from '../../../../test/helper/get-table-name.function'
3+
import { SimpleWithIdModel } from '../../../../test/models/simple-with-id.model'
4+
import { PutRequest } from './put.request'
5+
6+
describe('put request', () => {
7+
describe('if exists condition', () => {
8+
it('simple', () => {
9+
const item: SimpleWithIdModel = { id: 'myId', age: 45 }
10+
const request = new PutRequest(null, SimpleWithIdModel, getTableName(SimpleWithIdModel), item)
11+
request.ifNotExists()
12+
13+
expect((<PutItemInput>request.params).ConditionExpression).toBe('attribute_not_exists (#id)')
14+
expect((<PutItemInput>request.params).ExpressionAttributeNames).toEqual({ '#id': 'id' })
15+
expect((<PutItemInput>request.params).ExpressionAttributeValues).toBeUndefined()
16+
})
17+
18+
it('predicate', () => {
19+
const item: SimpleWithIdModel = { id: 'myId', age: 45 }
20+
const request = new PutRequest(null, SimpleWithIdModel, getTableName(SimpleWithIdModel), item)
21+
request.ifNotExists(25 + 20 === 40)
22+
23+
expect((<PutItemInput>request.params).ConditionExpression).toBeUndefined()
24+
expect((<PutItemInput>request.params).ExpressionAttributeNames).toBeUndefined()
25+
expect((<PutItemInput>request.params).ExpressionAttributeValues).toBeUndefined()
26+
})
27+
})
28+
})
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { AttributeValue } from 'aws-sdk/clients/dynamodb'
2+
import { DateMapper } from './date.mapper'
3+
4+
describe('date mapper', () => {
5+
let dateMapper: DateMapper
6+
7+
beforeEach(() => {
8+
dateMapper = new DateMapper()
9+
})
10+
11+
describe('to db', () => {
12+
it('simple', () => {
13+
const now = new Date()
14+
const toDb: AttributeValue = dateMapper.toDb(now)
15+
16+
expect(toDb).toEqual({ N: `${now.getTime()}` })
17+
})
18+
19+
it('throws', () => {
20+
expect(() => {
21+
dateMapper.toDb(<any>'noDate')
22+
}).toThrowError()
23+
})
24+
})
25+
26+
describe('from db', () => {
27+
xit('simple', () => {
28+
const now = new Date()
29+
const fromDb = dateMapper.fromDb({ N: `${now.getTime()}` })
30+
31+
expect(fromDb).toEqual(now)
32+
})
33+
34+
it('throws', () => {
35+
expect(() => {
36+
dateMapper.fromDb({ S: <any>'noDate' })
37+
}).toThrowError()
38+
})
39+
})
40+
})

src/mapper/for-type/date.mapper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class DateMapper implements MapperForType<Date> {
1212

1313
toDb(modelValue: Date): AttributeValue {
1414
if (modelValue && modelValue instanceof Date) {
15-
return { N: modelValue.getTime().toString() }
15+
return { N: `${modelValue.getTime()}` }
1616
} else {
1717
throw new Error('the given model value must be an instance of Date')
1818
}

0 commit comments

Comments
 (0)