From 06d59534b35360102a4bd759fabe6676f636af81 Mon Sep 17 00:00:00 2001 From: Robert Adams Date: Thu, 22 Jun 2023 17:15:16 +0100 Subject: [PATCH] Allow generation of custom values for hash and range key --- README.md | 14 ++++ lib/apiGateway/schema.js | 6 +- lib/apiGateway/validate.test.js | 83 +++++++++++++++++-- .../dynamodb/compileMethodsToDynamodb.js | 45 +++++----- .../dynamodb/compileMethodsToDynamodb.test.js | 83 +++++++++++++++++++ 5 files changed, 200 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index bc09fc9..dcd46d9 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/lib/apiGateway/schema.js b/lib/apiGateway/schema.js index a682797..e39e4ea 100644 --- a/lib/apiGateway/schema.js +++ b/lib/apiGateway/schema.js @@ -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' ) ) diff --git a/lib/apiGateway/validate.test.js b/lib/apiGateway/validate.test.js index 77869c9..428dbfd 100644 --- a/lib/apiGateway/validate.test.js +++ b/lib/apiGateway/validate.test.js @@ -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: [ { @@ -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]]]' ) }) @@ -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: [ { @@ -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' } } @@ -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: [ { @@ -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]]]' ) }) @@ -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: [ @@ -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]]' ) }) diff --git a/lib/package/dynamodb/compileMethodsToDynamodb.js b/lib/package/dynamodb/compileMethodsToDynamodb.js index ef1b690..c965148 100644 --- a/lib/package/dynamodb/compileMethodsToDynamodb.js +++ b/lib/package/dynamodb/compileMethodsToDynamodb.js @@ -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) { diff --git a/lib/package/dynamodb/compileMethodsToDynamodb.test.js b/lib/package/dynamodb/compileMethodsToDynamodb.test.js index c363d5a..eb879ed 100644 --- a/lib/package/dynamodb/compileMethodsToDynamodb.test.js +++ b/lib/package/dynamodb/compileMethodsToDynamodb.test.js @@ -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': [ @@ -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', () => {