Last active
November 12, 2022 22:30
-
-
Save allenheltondev/ccf585a1470e6be200f3ef970187e636 to your computer and use it in GitHub Desktop.
Generic PATCH Lambda Function With Ops
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { marshall } = require('@aws-sdk/util-dynamodb'); | |
const { DynamoDBClient, UpdateItemCommand } = require('@aws-sdk/client-dynamodb'); | |
const ddb = new DynamoDBClient(); | |
exports.handler = async (event) => { | |
try { | |
// Example input | |
// [ | |
// { "op": "add", "path": "/comment", "value": "This is a nasty gopher" }, | |
// { "op": "replace", "path": "/location/longitude", "value": 24.554 }, | |
// { "op": "remove", "path": "/color" } | |
// ] | |
const input = JSON.parse(event.body); | |
const id = event.pathParameters.id; | |
const addUpdateProperties = input.filter(field => ['add', 'replace'].includes(field.op)); | |
const removeProperties = input.filter(field => field.op == 'remove'); | |
const params = { | |
TableName: process.env.TABLE_NAME, | |
Key: marshall({ | |
pk: id, | |
sk: 'sortkey' | |
}), | |
ConditionExpression: 'attribute_exists(#pk)', | |
UpdateExpression: '', | |
ExpressionAttributeNames: { | |
'#pk': 'pk' | |
}, | |
ExpressionAttributeValues: {} | |
}; | |
if (addUpdateProperties.length) { | |
params.UpdateExpression = buildUpdateExpression(addUpdateProperties, params, 'SET'); | |
} | |
if (removeProperties.length) { | |
const removeExpression = buildUpdateExpression(removeProperties, params, 'REMOVE'); | |
params.UpdateExpression = `${params.UpdateExpression} ${removeExpression}` | |
} | |
params.ExpressionAttributeValues = marshall(params.ExpressionAttributeValues); | |
await ddb.send(new UpdateItemCommand(params)); | |
return { statusCode: 204 }; | |
} catch (err) { | |
console.error(err); | |
return { | |
statusCode: 500, | |
body: JSON.stringify({ message: 'Something went wrong.' }) | |
}; | |
} | |
}; | |
exports.buildUpdateExpression = (ops, params, ddbOp) => { | |
let expression = ddbOp; | |
for (const prop of ops) { | |
const path = prop.path.split('/').splice(1); | |
let expressionPath = ''; | |
for (const pathPiece of path) { | |
expressionPath = `${expressionPath}#${pathPiece}.`; | |
if (!params.ExpressionAttributeNames[`#${pathPiece}`]) { | |
params.ExpressionAttributeNames[`#${pathPiece}`] = pathPiece; | |
} | |
} | |
expressionPath = expressionPath.slice(0, -1); | |
if (prop.value) { | |
params.ExpressionAttributeValues[`:${path[path.length - 1]}`] = prop.value; | |
expression = `${expression} ${expressionPath} = :${path[path.length - 1]},`; | |
} else { | |
expression = `${expression} ${expressionPath},`; | |
} | |
} | |
expression = expression.slice(0, -1); | |
return expression; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
openapi: 3.0.0 | |
info: | |
title: Gopher Holes Unlimited API! | |
version: 1.0.0 | |
x-amazon-apigateway-request-validators: | |
Validate All: | |
validateRequestParameters: true | |
validateRequestBody: true | |
x-amazon-apigateway-gateway-responses: | |
BAD_REQUEST_BODY: | |
statusCode: 400 | |
responseTemplates: | |
application/json: '{ "message": "$context.error.validationErrorString" }' | |
INVALID_API_KEY: | |
statusCode: 401 | |
responseTemplates: | |
application/json: '{ "message": "Unauthorized" }' | |
security: | |
- api_key: [] | |
paths: | |
patch: | |
summary: Update a subset of details of a specific gopher | |
description: If updates are necessary to the gopher, provide only the details that have changed | |
tags: | |
- Gophers | |
requestBody: | |
required: true | |
content: | |
application/json: | |
schema: | |
$ref: '#/components/schemas/PatchGopher' | |
responses: | |
204: | |
$ref: '#/components/responses/NoContent' | |
400: | |
$ref: '#/components/responses/BadRequest' | |
x-amazon-apigateway-request-validator: Validate All | |
x-amazon-apigateway-integration: | |
uri: | |
Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PatchGopherFunction.Arn}/invocations | |
httpMethod: POST | |
type: aws_proxy | |
components: | |
schemas: | |
PatchGopher: | |
type: array | |
minItems: 1 | |
items: | |
type: object | |
additionalProperties: false | |
required: | |
- op | |
- path | |
properties: | |
op: | |
type: string | |
enum: | |
- add | |
- replace | |
- remove | |
path: | |
type: string | |
enum: | |
- /comment | |
- /location/longitude | |
- /location/latitude | |
- /color | |
- /type | |
value: | |
type: string | |
minLength: 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for the code, it's fantastic!
I have recently implemented a very similar code just to realize that in my case, sometimes, I have to evaluate a business rule before patching the resource in the database. Because an Update command has limited logic capabilities (
ConditionExpression
s are not feature-complete enough to allow me to evaluate business rules at the DB) I found this approach of dynamically generating the Update expression not to be very helpful in most of my cases.What I do instead is load the entire resource from the DB, apply the patch operations and then check the business rules at the application level. This obviously generates concurrency problems so instead of blindly updating the resource I do a conditional check against an incremental version field on the resource.