Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/dynamo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export * from './dynamo-store'
export * from './primary-key.type'
export * from './session-validity-ensurer.type'
export * from './table-name-resolver.type'
export * from './transactget'
export * from './transactwrite'
3 changes: 3 additions & 0 deletions src/dynamo/transactget/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './transact-get.request'
export * from './transact-get.request.type'
export * from './transact-get-full.response'
6 changes: 6 additions & 0 deletions src/dynamo/transactget/transact-get-full.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as DynamoDB from 'aws-sdk/clients/dynamodb'

export interface TransactGetFullResponse<X> {
Items: X
ConsumedCapacity?: DynamoDB.ConsumedCapacityMultiple
}
146 changes: 146 additions & 0 deletions src/dynamo/transactget/transact-get.request.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// tslint:disable:no-non-null-assertion
// tslint:disable:no-unnecessary-class
import * as DynamoDB from 'aws-sdk/clients/dynamodb'
import { of } from 'rxjs'
import { SimpleWithCompositePartitionKeyModel, SimpleWithPartitionKeyModel } from '../../../test/models'
import { Attributes } from '../../mapper'
import { getTableName } from '../get-table-name.function'
import { TransactGetRequest } from './transact-get.request'
import { TransactGetRequest2 } from './transact-get.request.type'

describe('TransactGetRequest', () => {
let req: TransactGetRequest

describe('constructor', () => {
beforeEach(() => (req = new TransactGetRequest()))

it('shoud init params', () => {
expect(req.params).toBeDefined()
expect(req.params.TransactItems).toBeDefined()
expect(req.params.TransactItems.length).toBe(0)
})
})

describe('returnConsumedCapacity', () => {
beforeEach(() => (req = new TransactGetRequest()))

it('should set the param', () => {
req.returnConsumedCapacity('INDEXES')

expect(req.params.ReturnConsumedCapacity).toBe('INDEXES')
})
})

describe('forModel', () => {
beforeEach(() => (req = new TransactGetRequest()))

it('should add a single item to params', () => {
req.forModel(SimpleWithPartitionKeyModel, { id: 'myId' })

expect(req.params.TransactItems.length).toBe(1)
expect(req.params.TransactItems[0]).toEqual({
Get: {
TableName: getTableName(SimpleWithPartitionKeyModel),
Key: { id: { S: 'myId' } },
},
})
})

it('should a multiple items to params', () => {
req.forModel(SimpleWithPartitionKeyModel, { id: 'myId' })
const creationDate = new Date()
req.forModel(SimpleWithCompositePartitionKeyModel, { id: 'myId', creationDate })

expect(req.params.TransactItems.length).toBe(2)
expect(req.params.TransactItems[0].Get.TableName).toBe(getTableName(SimpleWithPartitionKeyModel))

expect(req.params.TransactItems[1].Get.TableName).toBe(getTableName(SimpleWithCompositePartitionKeyModel))
expect(req.params.TransactItems[1].Get.Key).toEqual({
id: { S: 'myId' },
creationDate: { S: creationDate.toISOString() },
})
})

it('should throw when non-model class is added', () => {
class FooBar {}
expect(() => req.forModel(FooBar, {})).toThrow()
})

it('should throw when more than 10 items are requested', () => {
for (let i = 0; i < 10; i++) {
req.forModel(SimpleWithPartitionKeyModel, { id: 'myId' })
}
// the 11th time
expect(() => req.forModel(SimpleWithPartitionKeyModel, { id: 'myId' })).toThrow()
})
})

describe('execNoMap, execFullResponse, exec', () => {
let transactGetItemsSpy: jasmine.Spy
let req2: TransactGetRequest2<SimpleWithPartitionKeyModel, SimpleWithCompositePartitionKeyModel>
let creationDate: Date

beforeEach(() => {
const dbItem: Attributes<SimpleWithPartitionKeyModel> = {
id: { S: 'myId' },
age: { N: '20' },
}
creationDate = new Date()
const dbItem2: Attributes<SimpleWithCompositePartitionKeyModel> = {
id: { S: 'myId' },
creationDate: { S: creationDate.toISOString() },
age: { N: '22' },
}
const output: DynamoDB.TransactGetItemsOutput = {
ConsumedCapacity: [],
Responses: [{ Item: dbItem }, { Item: dbItem2 }],
}
transactGetItemsSpy = jasmine.createSpy().and.returnValues(of(output))
req2 = new TransactGetRequest()
.forModel(SimpleWithPartitionKeyModel, { id: 'myId' })
.forModel(SimpleWithCompositePartitionKeyModel, { id: 'myId', creationDate })
Object.assign(req2, { dynamoRx: { transactGetItems: transactGetItemsSpy } })
})

it('exec should return the mapped item', async () => {
const result = await req2.exec().toPromise()
expect(Array.isArray(result)).toBeTruthy()
expect(result.length).toBe(2)
expect(result[0]).toEqual({
id: 'myId',
age: 20,
})
expect(result[1]).toEqual({
id: 'myId',
age: 22,
creationDate,
})
})

it('execFullResponse should return the mapped items', async () => {
const result = await req2.execFullResponse().toPromise()
expect(result).toBeDefined()
expect(result.ConsumedCapacity).toEqual([])
expect(result.Items).toBeDefined()
expect(result.Items[0]).toEqual({
id: 'myId',
age: 20,
})
expect(result.Items[1]).toEqual({
id: 'myId',
age: 22,
creationDate,
})
})

it('execNoMap should return the original response', async () => {
const result = await req2.execNoMap().toPromise()
expect(result.ConsumedCapacity).toEqual([])
expect(result.Responses).toBeDefined()
expect(result.Responses![0]).toBeDefined()
expect(result.Responses![0].Item).toBeDefined()
expect(result.Responses![1]).toBeDefined()
expect(result.Responses![1].Item).toBeDefined()
})
})
})
89 changes: 89 additions & 0 deletions src/dynamo/transactget/transact-get.request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as DynamoDB from 'aws-sdk/clients/dynamodb'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { metadataForClass } from '../../decorator/metadata/metadata-helper'
import { Attributes, createToKeyFn, fromDb } from '../../mapper'
import { ModelConstructor } from '../../model'
import { DynamoRx } from '../dynamo-rx'
import { getTableName } from '../get-table-name.function'
import { TransactGetFullResponse } from './transact-get-full.response'
import { TransactGetRequest1 } from './transact-get.request.type'

const MAX_REQUEST_ITEM_COUNT = 10

export class TransactGetRequest {
readonly params: DynamoDB.TransactGetItemsInput
private readonly dynamoRx: DynamoRx
private readonly tables: Array<ModelConstructor<any>> = []

constructor() {
this.dynamoRx = new DynamoRx()
this.params = {
TransactItems: [],
}
}


forModel<T>(modelClazz: ModelConstructor<T>, key: Partial<T>): TransactGetRequest1<T> {

// check if modelClazz is really an @Model() decorated class
const metadata = metadataForClass(modelClazz)
if (!metadata.modelOptions) {
throw new Error('given ModelConstructor has no @Model decorator')
}

this.tables.push(modelClazz)

// check if table was already used in this request
const tableName = getTableName(metadata)


// check if keys to add do not exceed max count
if (this.params.TransactItems.length + 1 > MAX_REQUEST_ITEM_COUNT) {
throw new Error(`you can request at max ${MAX_REQUEST_ITEM_COUNT} items per request`)
}

this.params.TransactItems.push({
Get: {
TableName: tableName,
Key: createToKeyFn(modelClazz)(key),
},
},
)
return <any>this
}

returnConsumedCapacity(level: DynamoDB.ReturnConsumedCapacity): TransactGetRequest {
this.params.ReturnConsumedCapacity = level
return this
}

execNoMap(): Observable<DynamoDB.TransactGetItemsOutput> {
return this.dynamoRx.transactGetItems(this.params)
}

execFullResponse(): Observable<TransactGetFullResponse<[]>> {
return this.dynamoRx.transactGetItems(this.params).pipe(
map(this.mapResponse),
)
}

exec(): Observable<[]> {
return this.dynamoRx.transactGetItems(this.params).pipe(
map(this.mapResponse),
map(r => r.Items),
)
}


private mapResponse = (response: DynamoDB.TransactGetItemsOutput): TransactGetFullResponse<[]> => {
const Items: any = response.Responses && Object.keys(response.Responses).length
? response.Responses.map((item, ix) => fromDb(<Attributes>item.Item, this.tables[ix]))
: []
return {
ConsumedCapacity: response.ConsumedCapacity,
Items,
}
}
}

88 changes: 88 additions & 0 deletions src/dynamo/transactget/transact-get.request.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as DynamoDB from 'aws-sdk/clients/dynamodb'
import { Observable } from 'rxjs'
import { ModelConstructor } from '../../model'
import { TransactGetFullResponse } from './transact-get-full.response'

export interface TransactGetRequestBase {
readonly params: DynamoDB.TransactGetItemsInput
execNoMap(): Observable<DynamoDB.TransactGetItemsOutput>
}

export interface TransactGetRequest1<A> extends TransactGetRequestBase {
forModel<B>(modelClazz: ModelConstructor<B>, key: Partial<B>): TransactGetRequest2<A, B>

execFullResponse(): Observable<TransactGetFullResponse<[A]>>

exec(): Observable<[A]>
}

export interface TransactGetRequest2<A, B> extends TransactGetRequestBase {
forModel<C>(modelClazz: ModelConstructor<C>, key: Partial<C>): TransactGetRequest3<A, B, C>

execFullResponse(): Observable<TransactGetFullResponse<[A, B]>>

exec(): Observable<[A, B]>
}

export interface TransactGetRequest3<A, B, C> extends TransactGetRequestBase {
forModel<D>(modelClazz: ModelConstructor<D>, key: Partial<D>): TransactGetRequest4<A, B, C, D>

execFullResponse(): Observable<TransactGetFullResponse<[A, B, C]>>

exec(): Observable<[A, B, C]>
}

export interface TransactGetRequest4<A, B, C, D> extends TransactGetRequestBase {
forModel<E>(modelClazz: ModelConstructor<E>, key: Partial<E>): TransactGetRequest5<A, B, C, D, E>

execFullResponse(): Observable<TransactGetFullResponse<[A, B, C, D]>>

exec(): Observable<[A, B, C, D]>
}

export interface TransactGetRequest5<A, B, C, D, E> extends TransactGetRequestBase {
forModel<F>(modelClazz: ModelConstructor<F>, key: Partial<F>): TransactGetRequest6<A, B, C, D, E, F>

execFullResponse(): Observable<TransactGetFullResponse<[A, B, C, D, E]>>

exec(): Observable<[A, B, C, D, E]>
}

export interface TransactGetRequest6<A, B, C, D, E, F> extends TransactGetRequestBase {
forModel<G>(modelClazz: ModelConstructor<G>, key: Partial<G>): TransactGetRequest7<A, B, C, D, E, F, G>

execFullResponse(): Observable<TransactGetFullResponse<[A, B, C, D, E, F]>>

exec(): Observable<[A, B, C, D, E, F]>
}

export interface TransactGetRequest7<A, B, C, D, E, F, G> extends TransactGetRequestBase {
forModel<H>(modelClazz: ModelConstructor<H>, key: Partial<H>): TransactGetRequest8<A, B, C, D, E, F, G, H>

execFullResponse(): Observable<TransactGetFullResponse<[A, B, C, D, E, F, G]>>

exec(): Observable<[A, B, C, D, E, F, G]>
}

export interface TransactGetRequest8<A, B, C, D, E, F, G, H> extends TransactGetRequestBase {
forModel<I>(modelClazz: ModelConstructor<I>, key: Partial<I>): TransactGetRequest9<A, B, C, D, E, F, G, H, I>

execFullResponse(): Observable<TransactGetFullResponse<[A, B, C, D, E, F, G, H]>>

exec(): Observable<[A, B, C, D, E, F, G, H]>
}

export interface TransactGetRequest9<A, B, C, D, E, F, G, H, I> extends TransactGetRequestBase {
forModel<J>(modelClazz: ModelConstructor<J>, key: Partial<J>): TransactGetRequest10<A, B, C, D, E, F, G, H, I, J>

execFullResponse(): Observable<TransactGetFullResponse<[A, B, C, D, E, F, G, H, I]>>

exec(): Observable<[A, B, C, D, E, F, G, H, I]>
}

export interface TransactGetRequest10<A, B, C, D, E, F, G, H, I, J> extends TransactGetRequestBase {

execFullResponse(): Observable<TransactGetFullResponse<[A, B, C, D, E, F, G, H, I, J]>>

exec(): Observable<[A, B, C, D, E, F, G, H, I, J]>
}