June 21, 2022
2 min read
My app has a table with users, and I usually query just a handful of fields instead of the whole user object. That's why we have a second parameter called attributes
. It is an array of user keys.
export async function getUserById<T extends (keyof User)[]>(
id: string,
attributes?: T
): Promise<Pick<User, T[number]> | undefined> {
const { Item } = await documentClient
.get(mergeParams(getUserItemParams(id), projectionExpression(attributes)))
.promise()
return Item as Pick<User, T[number]>
}
To simplify interactions with DynamoDB, I have a handful of helpers.
projectionExpression
converts an array to an object we can pass to the document client.
export const projectionExpression = (attributes?: string[]) => {
if (!attributes) {
return {}
}
const ProjectionExpression = attributes
.map((attr) => `${attr.includes(".") ? "" : "#"}${attr}, `)
.reduce((acc, str) => acc + str)
.slice(0, -2)
const attributesToExpression = attributes.filter(
(attr) => !attr.includes(".")
)
const ExpressionAttributeNames = attributesToExpression.reduce<{
[key: string]: string
}>((acc, attr) => {
acc["#" + attr] = attr
return acc
}, {})
return attributesToExpression.length
? { ProjectionExpression, ExpressionAttributeNames }
: { ProjectionExpression }
}
I have a function like getUserItemParams
for every table. It returns an object we need to get, update or delete an item.
export const getUserItemParams = (id: string) => ({
TableName: tableName.users,
Key: { id },
})
Sometimes those helpers can return an object with a shared field ExpressionAttributeNames
. To resolve such clashes, I use the mergeParams
function.
export const mergeParams = (...params: any[]) =>
params.reduce(
(acc, param) =>
Object.entries(param).reduce((acc, [key, value]) => {
acc[key] = Array.isArray(value)
? [...acc[key], ...value]
: typeof value === "object"
? { ...acc[key], ...value }
: value
return acc
}, acc),
{}
)