Skip to content

Commit

Permalink
Allow generation of custom values for hash and range key
Browse files Browse the repository at this point in the history
  • Loading branch information
robhughadams committed Jun 22, 2023
1 parent df81b3d commit 06d5953
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 31 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,20 @@ custom:
attributeType: S
action: DeleteItem
cors: true
- dynamodb:
path: /dynamodb/{id}
method: get
tableName: { Ref: 'YourTable' }
hashKey:
keyName: id # Name of the attribute in the DynamoDB table
attributeValue: \${input.params().path.id1}_\${input.params().querystring.id2} # Custom mapping for the attribute. Use '\${}' to escape the '$' character.
attributeType: S
rangeKey:
keyName: range # Name of the attribute in the DynamoDB table
attributeValue: \${input.params().path.range1}_\${input.params().querystring.range2} # Custom mapping for the attribute. Use '\${}' to escape the '$' character.
attributeType: S
action: GetItem
cors: true
resources:
Resources:
Expand Down
6 changes: 4 additions & 2 deletions lib/apiGateway/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,15 @@ const dynamodbDefaultKeyScheme = Joi.object()
.keys({
pathParam: Joi.string(),
queryStringParam: Joi.string(),
keyName: Joi.string(),
attributeValue: Joi.string(),
attributeType: Joi.string().required()
})
.xor('pathParam', 'queryStringParam')
.xor('pathParam', 'queryStringParam', 'keyName')
.error(
customErrorBuilder(
'object.xor',
'key must contain "pathParam" or "queryStringParam" and only one'
'key must contain "pathParam" or "queryStringParam" or "keyName" and only one'
)
)

Expand Down
83 changes: 75 additions & 8 deletions lib/apiGateway/validate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1829,7 +1829,7 @@ describe('#validateServiceProxies()', () => {
)
})

it('should throw error if the "hashKey" is object and missing "pathParam" or "queryStringParam" properties', () => {
it('should throw error if the "hashKey" is object and missing "pathParam", "queryStringParam" or "keyName" properties', () => {
serverlessApigatewayServiceProxy.serverless.service.custom = {
apiGatewayServiceProxies: [
{
Expand All @@ -1847,7 +1847,7 @@ describe('#validateServiceProxies()', () => {
}

expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
'child "dynamodb" fails because [child "hashKey" fails because ["hashKey" must contain at least one of [pathParam, queryStringParam]]]'
'child "dynamodb" fails because [child "hashKey" fails because ["hashKey" must contain at least one of [pathParam, queryStringParam, keyName]]]'
)
})

Expand Down Expand Up @@ -1915,7 +1915,7 @@ describe('#validateServiceProxies()', () => {
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
})

it('should not throw error if the "hashKey" is object and has both "pathParam" and "attributeType" properties', () => {
it('should not throw error if the "hashKey" is object and has both "keyName" and "attributeType" properties', () => {
serverlessApigatewayServiceProxy.serverless.service.custom = {
apiGatewayServiceProxies: [
{
Expand All @@ -1925,7 +1925,8 @@ describe('#validateServiceProxies()', () => {
method: 'post',
action: 'PutItem',
hashKey: {
pathParam: 'id',
keyName: 'id',
attributeValue: '${input.params().path.id1}_${input.params().path.id2}',
attributeType: 'S'
}
}
Expand Down Expand Up @@ -1992,11 +1993,31 @@ describe('#validateServiceProxies()', () => {
}

expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
'child "dynamodb" fails because [child "hashKey" fails because [key must contain "pathParam" or "queryStringParam" and only one]]'
'child "dynamodb" fails because [child "hashKey" fails because [key must contain "pathParam" or "queryStringParam" or "keyName" and only one]]'
)
})

it('should throw error if the "hashKey" is a pathParam and a keyName at the same time', () => {
serverlessApigatewayServiceProxy.serverless.service.custom = {
apiGatewayServiceProxies: [
{
dynamodb: {
tableName: 'yourStream',
path: 'dynamodb',
method: 'put',
action: 'PutItem',
hashKey: { pathParam: 'id', keyName: 'id', attributeType: 'S' }
}
}
]
}

expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
'child "dynamodb" fails because [child "hashKey" fails because [key must contain "pathParam" or "queryStringParam" or "keyName" and only one]]'
)
})

it('should throw error if the "rangeKey" is object and missing "pathParam" or "queryStringParam" properties', () => {
it('should throw error if the "rangeKey" is object and missing "pathParam", "queryStringParam" or "keyName" properties', () => {
serverlessApigatewayServiceProxy.serverless.service.custom = {
apiGatewayServiceProxies: [
{
Expand All @@ -2018,7 +2039,7 @@ describe('#validateServiceProxies()', () => {
}

expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
'child "dynamodb" fails because [child "rangeKey" fails because ["rangeKey" must contain at least one of [pathParam, queryStringParam]]]'
'child "dynamodb" fails because [child "rangeKey" fails because ["rangeKey" must contain at least one of [pathParam, queryStringParam, keyName]]]'
)
})

Expand Down Expand Up @@ -2098,6 +2119,31 @@ describe('#validateServiceProxies()', () => {
expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
})

it('should not throw error if the "rangeKey" is object and has both "keyName" and "attributeType" properties', () => {
serverlessApigatewayServiceProxy.serverless.service.custom = {
apiGatewayServiceProxies: [
{
dynamodb: {
tableName: 'yourTable',
path: 'dynamodb',
method: 'post',
action: 'PutItem',
hashKey: {
keyName: 'id',
attributeType: 'S'
},
hashKey: {
keyName: 'sort',
attributeType: 'S'
}
}
}
]
}

expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.not.throw()
})

it('should throw error if the "rangeKey" is not a string or an object', () => {
serverlessApigatewayServiceProxy.serverless.service.custom = {
apiGatewayServiceProxies: [
Expand Down Expand Up @@ -2164,7 +2210,28 @@ describe('#validateServiceProxies()', () => {
}

expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
'child "dynamodb" fails because [child "rangeKey" fails because [key must contain "pathParam" or "queryStringParam" and only one]]'
'child "dynamodb" fails because [child "rangeKey" fails because [key must contain "pathParam" or "queryStringParam" or "keyName" and only one]]'
)
})

it('should throw error if the "rangeKey" is a pathParam and a keyName at the same time', () => {
serverlessApigatewayServiceProxy.serverless.service.custom = {
apiGatewayServiceProxies: [
{
dynamodb: {
tableName: 'yourStream',
path: 'dynamodb',
method: 'put',
action: 'PutItem',
hashKey: { pathParam: 'id', attributeType: 'S' },
rangeKey: { pathParam: 'id', keyName: 'id', attributeType: 'S' }
}
}
]
}

expect(() => serverlessApigatewayServiceProxy.validateServiceProxies()).to.throw(
'child "dynamodb" fails because [child "rangeKey" fails because [key must contain "pathParam" or "queryStringParam" or "keyName" and only one]]'
)
})

Expand Down
45 changes: 24 additions & 21 deletions lib/package/dynamodb/compileMethodsToDynamodb.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,39 +155,42 @@ module.exports = {
},

getDynamodbObjectHashkeyParameter(http) {
if (http.hashKey.pathParam) {
return {
key: http.hashKey.pathParam,
attributeType: http.hashKey.attributeType,
attributeValue: `$input.params().path.${http.hashKey.pathParam}`
}
}
return this.getDynamodbObjectKeyParameter(http.hashKey)
},

if (http.hashKey.queryStringParam) {
getDynamodbObjectRangekeyParameter(http) {
return this.getDynamodbObjectKeyParameter(http.rangeKey)
},

getDynamodbObjectKeyParameter(key) {
if (key.pathParam) {
return {
key: http.hashKey.queryStringParam,
attributeType: http.hashKey.attributeType,
attributeValue: `$input.params().querystring.${http.hashKey.queryStringParam}`
key: key.pathParam,
attributeType: key.attributeType,
attributeValue: `$input.params().path.${key.pathParam}`
}
}
},

getDynamodbObjectRangekeyParameter(http) {
if (http.rangeKey.pathParam) {
if (key.queryStringParam) {
return {
key: http.rangeKey.pathParam,
attributeType: http.rangeKey.attributeType,
attributeValue: `$input.params().path.${http.rangeKey.pathParam}`
key: key.queryStringParam,
attributeType: key.attributeType,
attributeValue: `$input.params().querystring.${key.queryStringParam}`
}
}

if (http.rangeKey.queryStringParam) {
if (key.keyName) {
if (!key.attributeValue) {
throw new Error('If keyName is provided, attributeValue must be provided as well')
}
return {
key: http.rangeKey.queryStringParam,
attributeType: http.rangeKey.attributeType,
attributeValue: `$input.params().querystring.${http.rangeKey.queryStringParam}`
key: key.keyName,
attributeType: key.attributeType,
attributeValue: key.attributeValue
}
}

throw new Error('No valid key type found')
},

getDynamodbResponseTemplates(http, statusType) {
Expand Down
83 changes: 83 additions & 0 deletions lib/package/dynamodb/compileMethodsToDynamodb.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,43 @@ describe('#compileMethodsToDynamodb()', () => {
)
})

it('should create corresponding resources when hashkey is given with custom options', () => {
const intRequestTemplate = {
'Fn::Sub': [
'{"TableName": "${TableName}","Key":{"${HashKey}": {"${HashAttributeType}": "${HashAttributeValue}"}}}',
{
TableName: {
Ref: 'MyTable'
},
HashKey: 'id',
HashAttributeType: 'S',
HashAttributeValue: '${input.params().path.id1}_${input.params().querystring.id2}'
}
]
}
const intResponseTemplate =
'#set($item = $input.path(\'$.Item\')){#foreach($key in $item.keySet())#set ($value = $item.get($key))#foreach( $type in $value.keySet())"$key":"$value.get($type)"#if($foreach.hasNext()),#end#end#if($foreach.hasNext()),#end#end}'
testGetItem(
{
hashKey: {
keyName: 'id',
attributeValue: '${input.params().path.id1}_${input.params().querystring.id2}',
attributeType: 'S'
},
path: '/dynamodb',
action: 'GetItem'
},
{
'application/json': intRequestTemplate,
'application/x-www-form-urlencoded': intRequestTemplate
},
{
'application/json': intResponseTemplate,
'application/x-www-form-urlencoded': intResponseTemplate
}
)
})

it('should create corresponding resources when rangekey is given with a path parameter', () => {
const intRequestTemplate = {
'Fn::Sub': [
Expand Down Expand Up @@ -568,6 +605,52 @@ describe('#compileMethodsToDynamodb()', () => {
}
)
})

it('should create corresponding resources when rangekey is given with custom options', () => {
const intRequestTemplate = {
'Fn::Sub': [
'{"TableName": "${TableName}","Key":{"${HashKey}": {"${HashAttributeType}": "${HashAttributeValue}"},"${RangeKey}": {"${RangeAttributeType}": "${RangeAttributeValue}"}}}',
{
TableName: {
Ref: 'MyTable'
},
HashKey: 'id',
HashAttributeType: 'S',
HashAttributeValue: '${input.params().path.id1}_${input.params().querystring.id2}',
RangeKey: 'range',
RangeAttributeType: 'S',
RangeAttributeValue:
'${input.params().path.range1}_${input.params().querystring.range2}'
}
]
}
const intResponseTemplate =
'#set($item = $input.path(\'$.Item\')){#foreach($key in $item.keySet())#set ($value = $item.get($key))#foreach( $type in $value.keySet())"$key":"$value.get($type)"#if($foreach.hasNext()),#end#end#if($foreach.hasNext()),#end#end}'
testGetItem(
{
hashKey: {
keyName: 'id',
attributeValue: '${input.params().path.id1}_${input.params().querystring.id2}',
attributeType: 'S'
},
rangeKey: {
keyName: 'range',
attributeValue: '${input.params().path.range1}_${input.params().querystring.range2}',
attributeType: 'S'
},
path: '/dynamodb/{id}',
action: 'GetItem'
},
{
'application/json': intRequestTemplate,
'application/x-www-form-urlencoded': intRequestTemplate
},
{
'application/json': intResponseTemplate,
'application/x-www-form-urlencoded': intResponseTemplate
}
)
})
})

describe('#delete method', () => {
Expand Down

0 comments on commit 06d5953

Please sign in to comment.