Skip to content

Instantly share code, notes, and snippets.

@allenheltondev
Last active November 12, 2022 22:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save allenheltondev/ccf585a1470e6be200f3ef970187e636 to your computer and use it in GitHub Desktop.
Save allenheltondev/ccf585a1470e6be200f3ef970187e636 to your computer and use it in GitHub Desktop.
Generic PATCH Lambda Function With Ops
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;
};
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
@renatoargh
Copy link

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 (ConditionExpressions 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment