Skip to content

Commit bf48c92

Browse files
feat(batchGet): add support for batch get item request
there are two possible scenarios for batchGetItem, one asking for multiple items of the same table and one asking for multiple items in different tables. the first scenario can be achived using the dynamoStore.batchGet() operation the other one by creating a new batchGet request
1 parent 1d5c087 commit bf48c92

File tree

11 files changed

+350
-37
lines changed

11 files changed

+350
-37
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Organization } from '../../../../test/models/organization.model'
2+
import { BatchGetRequest } from './batch-get.request'
3+
4+
describe('batch get', () => {
5+
let request: BatchGetRequest
6+
7+
beforeEach(() => {
8+
request = new BatchGetRequest()
9+
})
10+
11+
it('base params', () => {
12+
const params = request.params
13+
expect(params).toEqual({ RequestItems: {} })
14+
})
15+
16+
it('key', () => {
17+
request.forModel(Organization, ['idValue'])
18+
const params = request.params
19+
expect(params.RequestItems).toBeDefined()
20+
expect(params.RequestItems['Organization']).toBeDefined()
21+
expect(params.RequestItems['Organization']).toEqual({ Keys: [{ id: { S: 'idValue' } }] })
22+
})
23+
})
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { AttributeMap, BatchGetItemInput } from 'aws-sdk/clients/dynamodb'
2+
import { isObject } from 'lodash'
3+
import { Observable } from 'rxjs/Observable'
4+
import { MetadataHelper } from '../../decorator/metadata/metadata-helper'
5+
import { Mapper } from '../../mapper/mapper'
6+
import { ModelConstructor } from '../../model/model-constructor'
7+
import { DEFAULT_SESSION_VALIDITY_ENSURER } from '../default-session-validity-ensurer.const'
8+
import { DEFAULT_TABLE_NAME_RESOLVER } from '../default-table-name-resolver.const'
9+
import { DynamoRx } from '../dynamo-rx'
10+
import { REGEX_TABLE_NAME } from '../request/regex'
11+
import { SessionValidityEnsurer } from '../session-validity-ensurer.type'
12+
import { TableNameResolver } from '../table-name-resolver.type'
13+
14+
interface TableConfig<T> {
15+
tableName: string
16+
modelClazz: ModelConstructor<T>
17+
keys: any[]
18+
}
19+
20+
// tslint:disable-next-line:interface-over-type-literal
21+
export type BatchGetItemResponse = { [tableName: string]: any[] }
22+
23+
export class BatchGetRequest {
24+
private readonly dynamoRx: DynamoRx
25+
26+
private tables: Map<string, TableConfig<any>> = new Map()
27+
readonly params: BatchGetItemInput
28+
29+
constructor(
30+
private tableNameResolver: TableNameResolver = DEFAULT_TABLE_NAME_RESOLVER,
31+
sessionValidityEnsurer: SessionValidityEnsurer = DEFAULT_SESSION_VALIDITY_ENSURER
32+
) {
33+
this.dynamoRx = new DynamoRx(sessionValidityEnsurer)
34+
this.params = <BatchGetItemInput>{
35+
RequestItems: {},
36+
}
37+
}
38+
39+
/**
40+
* @param {ModelConstructor<T>} modelClazz
41+
* @param {any[]} keys either a simple string for partition key or an object with partitionKey and sortKey
42+
* @returns {BatchGetSingleTableRequest}
43+
*/
44+
forModel<T>(modelClazz: ModelConstructor<T>, keys: any[]): BatchGetRequest {
45+
const tableName = this.getTableName(modelClazz, this.tableNameResolver)
46+
if (this.tables.has(tableName)) {
47+
throw new Error('table name already exists, please provide all the keys for the same table at once')
48+
}
49+
50+
const metadata = MetadataHelper.get(modelClazz)
51+
const attributeMaps: AttributeMap[] = []
52+
53+
// loop over all the keys
54+
keys.forEach(key => {
55+
const idOb: AttributeMap = {}
56+
57+
if (isObject(key)) {
58+
// TODO add some more checks
59+
// got a composite primary key
60+
61+
// partition key
62+
const mappedPartitionKey = Mapper.toDbOne(key.partitionKey)
63+
if (mappedPartitionKey === null) {
64+
throw Error('please provide an actual value for partition key')
65+
}
66+
idOb[metadata.getPartitionKey()] = mappedPartitionKey
67+
68+
// sort key
69+
const mappedSortKey = Mapper.toDbOne(key.sortKey)
70+
if (mappedSortKey === null) {
71+
throw Error('please provide an actual value for partition key')
72+
}
73+
74+
idOb[metadata.getSortKey()!] = mappedSortKey
75+
} else {
76+
// got a simple primary key
77+
const value = Mapper.toDbOne(key)
78+
if (value === null) {
79+
throw Error('please provide an actual value for partition key')
80+
}
81+
82+
idOb[metadata.getPartitionKey()] = value
83+
}
84+
85+
attributeMaps.push(idOb)
86+
})
87+
88+
this.params.RequestItems[tableName] = {
89+
Keys: attributeMaps,
90+
}
91+
return this
92+
}
93+
94+
execFullResponse() {}
95+
96+
// TODO fix any
97+
// TODO add support for indexes
98+
exec(): Observable<BatchGetItemResponse> {
99+
return this.dynamoRx.batchGetItems(this.params).map(response => {
100+
const r = <BatchGetItemResponse>{}
101+
if (response.Responses && Object.keys(response.Responses).length) {
102+
const responses: { [key: string]: AttributeMap } = {}
103+
Object.keys(response.Responses).forEach(tableName => {
104+
const mapped = response.Responses![tableName].map(attributeMap =>
105+
Mapper.fromDb(attributeMap, this.tables.get(tableName)!.modelClazz)
106+
)
107+
r[tableName] = mapped
108+
})
109+
110+
return r
111+
} else {
112+
return {}
113+
}
114+
})
115+
}
116+
117+
private getTableName(modelClazz: ModelConstructor<any>, tableNameResolver: TableNameResolver) {
118+
const tableName = tableNameResolver(MetadataHelper.get(modelClazz).modelOptions.tableName)
119+
if (!REGEX_TABLE_NAME.test(tableName)) {
120+
throw new Error(
121+
'make sure the table name only contains these characters «a-z A-Z 0-9 - _ .» and is between 3 and 255 characters long'
122+
)
123+
}
124+
125+
return tableName
126+
}
127+
}

src/dynamo/batchget/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './batch-get.request'

src/dynamo/dynamo-store.ts

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,31 @@ import { DEFAULT_SESSION_VALIDITY_ENSURER } from './default-session-validity-ens
77
import { DEFAULT_TABLE_NAME_RESOLVER } from './default-table-name-resolver.const'
88
import { DynamoApiOperations } from './dynamo-api-operations.type'
99
import { DynamoRx } from './dynamo-rx'
10+
import { BatchGetSingleTableRequest } from './request/batchgetsingletable/batch-get-single-table.request'
1011
import { DeleteRequest } from './request/delete/delete.request'
1112
import { GetRequest } from './request/get/get.request'
1213
import { PutRequest } from './request/put/put.request'
1314
import { QueryRequest } from './request/query/query.request'
15+
import { REGEX_TABLE_NAME } from './request/regex'
1416
import { ScanRequest } from './request/scan/scan.request'
1517
import { UpdateRequest } from './request/update/update.request'
1618
import { SessionValidityEnsurer } from './session-validity-ensurer.type'
1719
import { TableNameResolver } from './table-name-resolver.type'
1820

1921
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-
2322
private readonly dynamoRx: DynamoRx
2423
private readonly mapper: Mapper
2524

2625
readonly tableName: string
2726

2827
constructor(
2928
private modelClazz: ModelConstructor<T>,
30-
tableNameResolver: TableNameResolver = DEFAULT_TABLE_NAME_RESOLVER,
29+
private tableNameResolver: TableNameResolver = DEFAULT_TABLE_NAME_RESOLVER,
3130
sessionValidityEnsurer: SessionValidityEnsurer = DEFAULT_SESSION_VALIDITY_ENSURER
3231
) {
3332
this.dynamoRx = new DynamoRx(sessionValidityEnsurer)
3433
const tableName = tableNameResolver(MetadataHelper.get(this.modelClazz).modelOptions.tableName)
35-
if (!DynamoStore.REGEX_TABLE_NAME.test(tableName)) {
34+
if (!REGEX_TABLE_NAME.test(tableName)) {
3635
throw new Error(
3736
'make sure the table name only contains these characters «a-z A-Z 0-9 - _ .» and is between 3 and 255 characters long'
3837
)
@@ -69,42 +68,18 @@ export class DynamoStore<T> {
6968
return new QueryRequest(this.dynamoRx, this.modelClazz, this.tableName)
7069
}
7170

71+
/**
72+
* This is a special implementation of BatchGetItem request, because it only supports one table, if you wish to retrieve items from multiple tables
73+
* get an instance of BatchGetItem request and call it there.
74+
*/
75+
batchGetItem(keys: any[]): BatchGetSingleTableRequest<T> {
76+
return new BatchGetSingleTableRequest(this.dynamoRx, this.modelClazz, this.tableName, keys)
77+
}
78+
7279
makeRequest<Z>(operation: DynamoApiOperations, params?: { [key: string]: any }): Observable<Z> {
7380
return this.dynamoRx.makeRequest(operation, params)
7481
}
7582

76-
// TODO implement BatchGetItem request (think about support for secondary indexes)
77-
// batchGetItems(keys: any[]): Observable<T[]> {
78-
// const requestItems: { [nameDb: string]: { Keys: DynamoDB.AttributeMap[] } } = {}
79-
// const attributeMaps: DynamoDB.AttributeMap[] = []
80-
// keys.forEach(id => {
81-
// const idOb: DynamoDB.AttributeMap = {}
82-
// const value = Mapper.toDbOne(id)
83-
// if (value === null) {
84-
// throw Error('please provide an actual value for partition key')
85-
// }
86-
//
87-
// idOb[MetadataHelper.get(this.modelClazz).getPartitionKey()] = value
88-
// attributeMaps.push(idOb)
89-
// })
90-
//
91-
// requestItems[this.tableName] = {
92-
// Keys: attributeMaps,
93-
// }
94-
//
95-
// const params: DynamoDB.BatchGetItemInput = {
96-
// RequestItems: requestItems,
97-
// }
98-
//
99-
// return this.dynamoRx.batchGetItems(params).map(response => {
100-
// if (response.Responses && Object.keys(response.Responses).length) {
101-
// return response.Responses[this.tableName].map(attributeMap => Mapper.fromDb(attributeMap, this.modelClazz))
102-
// } else {
103-
// return []
104-
// }
105-
// })
106-
// }
107-
10883
private createBaseParams(): { TableName: string } {
10984
const params: { TableName: string } = {
11085
TableName: this.tableName,

src/dynamo/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from './request'
44
export * from './dynamo-rx'
55
export * from './dynamo-store'
66
export * from './session-validity-ensurer.type'
7+
export * from './batchget'

src/dynamo/request/base.request.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
AttributeMap,
3+
BatchGetItemInput,
34
DeleteItemInput,
45
GetItemInput,
56
Key,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { BatchGetPartitionValueList } from 'aws-sdk/clients/glue'
2+
import * as moment from 'moment'
3+
import { getTableName } from '../../../../test/helper/get-table-name.function'
4+
import { Organization } from '../../../../test/models/organization.model'
5+
import { BatchGetSingleTableRequest } from './batch-get-single-table.request'
6+
7+
describe('batch get', () => {
8+
describe('correct params', () => {
9+
it('simple primary key', () => {
10+
const request = new BatchGetSingleTableRequest<any>(null, Organization, getTableName(Organization), [
11+
'myId',
12+
'myId2',
13+
])
14+
expect(request.params.RequestItems).toBeDefined()
15+
expect(request.params.RequestItems).toEqual({
16+
Organization: { Keys: [{ id: { S: 'myId' } }, { id: { S: 'myId2' } }] },
17+
})
18+
})
19+
20+
it('composite primary key', () => {
21+
const now = moment()
22+
const keys = [{ partitionKey: 'myId', sortKey: now }]
23+
const request = new BatchGetSingleTableRequest<any>(null, Organization, getTableName(Organization), keys)
24+
25+
expect(request.params.RequestItems).toBeDefined()
26+
expect(request.params.RequestItems).toEqual({
27+
Organization: {
28+
Keys: [
29+
{
30+
id: { S: 'myId' },
31+
createdAtDate: {
32+
S: now
33+
.clone()
34+
.utc()
35+
.format(),
36+
},
37+
},
38+
],
39+
},
40+
})
41+
})
42+
})
43+
})

0 commit comments

Comments
 (0)