diff --git a/src/dynamo/index.ts b/src/dynamo/index.ts index 1dfec06a0..1bbdb87e7 100644 --- a/src/dynamo/index.ts +++ b/src/dynamo/index.ts @@ -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' diff --git a/src/dynamo/transactget/index.ts b/src/dynamo/transactget/index.ts new file mode 100644 index 000000000..429ab2ddf --- /dev/null +++ b/src/dynamo/transactget/index.ts @@ -0,0 +1,3 @@ +export * from './transact-get.request' +export * from './transact-get.request.type' +export * from './transact-get-full.response' diff --git a/src/dynamo/transactget/transact-get-full.response.ts b/src/dynamo/transactget/transact-get-full.response.ts new file mode 100644 index 000000000..658aa218b --- /dev/null +++ b/src/dynamo/transactget/transact-get-full.response.ts @@ -0,0 +1,6 @@ +import * as DynamoDB from 'aws-sdk/clients/dynamodb' + +export interface TransactGetFullResponse { + Items: X + ConsumedCapacity?: DynamoDB.ConsumedCapacityMultiple +} diff --git a/src/dynamo/transactget/transact-get.request.spec.ts b/src/dynamo/transactget/transact-get.request.spec.ts new file mode 100644 index 000000000..04b75ce37 --- /dev/null +++ b/src/dynamo/transactget/transact-get.request.spec.ts @@ -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 + let creationDate: Date + + beforeEach(() => { + const dbItem: Attributes = { + id: { S: 'myId' }, + age: { N: '20' }, + } + creationDate = new Date() + const dbItem2: Attributes = { + 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() + }) + }) +}) diff --git a/src/dynamo/transactget/transact-get.request.ts b/src/dynamo/transactget/transact-get.request.ts new file mode 100644 index 000000000..a29be0c39 --- /dev/null +++ b/src/dynamo/transactget/transact-get.request.ts @@ -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> = [] + + constructor() { + this.dynamoRx = new DynamoRx() + this.params = { + TransactItems: [], + } + } + + + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest1 { + + // 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 this + } + + returnConsumedCapacity(level: DynamoDB.ReturnConsumedCapacity): TransactGetRequest { + this.params.ReturnConsumedCapacity = level + return this + } + + execNoMap(): Observable { + return this.dynamoRx.transactGetItems(this.params) + } + + execFullResponse(): Observable> { + 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(item.Item, this.tables[ix])) + : [] + return { + ConsumedCapacity: response.ConsumedCapacity, + Items, + } + } +} + diff --git a/src/dynamo/transactget/transact-get.request.type.ts b/src/dynamo/transactget/transact-get.request.type.ts new file mode 100644 index 000000000..6f8f98f36 --- /dev/null +++ b/src/dynamo/transactget/transact-get.request.type.ts @@ -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 +} + +export interface TransactGetRequest1 extends TransactGetRequestBase { + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest2 + + execFullResponse(): Observable> + + exec(): Observable<[A]> +} + +export interface TransactGetRequest2 extends TransactGetRequestBase { + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest3 + + execFullResponse(): Observable> + + exec(): Observable<[A, B]> +} + +export interface TransactGetRequest3 extends TransactGetRequestBase { + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest4 + + execFullResponse(): Observable> + + exec(): Observable<[A, B, C]> +} + +export interface TransactGetRequest4 extends TransactGetRequestBase { + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest5 + + execFullResponse(): Observable> + + exec(): Observable<[A, B, C, D]> +} + +export interface TransactGetRequest5 extends TransactGetRequestBase { + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest6 + + execFullResponse(): Observable> + + exec(): Observable<[A, B, C, D, E]> +} + +export interface TransactGetRequest6 extends TransactGetRequestBase { + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest7 + + execFullResponse(): Observable> + + exec(): Observable<[A, B, C, D, E, F]> +} + +export interface TransactGetRequest7 extends TransactGetRequestBase { + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest8 + + execFullResponse(): Observable> + + exec(): Observable<[A, B, C, D, E, F, G]> +} + +export interface TransactGetRequest8 extends TransactGetRequestBase { + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest9 + + execFullResponse(): Observable> + + exec(): Observable<[A, B, C, D, E, F, G, H]> +} + +export interface TransactGetRequest9 extends TransactGetRequestBase { + forModel(modelClazz: ModelConstructor, key: Partial): TransactGetRequest10 + + execFullResponse(): Observable> + + exec(): Observable<[A, B, C, D, E, F, G, H, I]> +} + +export interface TransactGetRequest10 extends TransactGetRequestBase { + + execFullResponse(): Observable> + + exec(): Observable<[A, B, C, D, E, F, G, H, I, J]> +}