Skip to content

Commit

Permalink
feat(transactions): add UpdateItemTransaction
Browse files Browse the repository at this point in the history
  • Loading branch information
guiyom-e committed Jan 12, 2024
1 parent d49fd72 commit e452ad1
Show file tree
Hide file tree
Showing 12 changed files with 2,121 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/v1/operations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export { ScanCommand } from './scan'
export type { ScanOptions, ScanResponse } from './scan'
export { QueryCommand } from './query'
export type { QueryOptions, QueryResponse } from './query'
export { PutItemTransaction, DeleteItemTransaction } from './transactions'
export { PutItemTransaction, DeleteItemTransaction, UpdateItemTransaction } from './transactions'
export { formatSavedItem } from './utils/formatSavedItem'
export { parseCondition } from './expression/condition/parse'
export { parseProjection } from './expression/projection/parse'
Expand Down
12 changes: 5 additions & 7 deletions src/v1/operations/transactions/deleteItem/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,11 @@ export class DeleteItemTransaction<
documentClient: DynamoDBDocumentClient
type: 'Delete'
params: TransactDeleteItemParams
} => {
return {
documentClient: this[$entity].table.documentClient,
type: 'Delete',
params: this.params()
}
}
} => ({
documentClient: this[$entity].table.documentClient,
type: 'Delete',
params: this.params()
})
}

export type DeleteItemTransactionClass = typeof DeleteItemTransaction
1 change: 1 addition & 0 deletions src/v1/operations/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { PutItemTransaction } from './putItem'
export { DeleteItemTransaction } from './deleteItem'
export { UpdateItemTransaction } from './updateItem'
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
schema,
set,
string,
DynamoDBToolboxError
DynamoDBToolboxError,
UpdateItemTransaction,
$append,
$set,
$add
} from 'v1'
import { transactWriteItems, getTransactWriteCommandInput } from './transactWriteItems'
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
Expand Down Expand Up @@ -160,6 +164,13 @@ describe('generateTransactWriteCommandInput', () => {
test_binary_set: new Set([Buffer.from('a'), Buffer.from('b')])
}),
TestEntity.build(DeleteItemTransaction).key({ email: 'tata@example.com', sort: 'tata' }),
TestEntity.build(UpdateItemTransaction).item({
email: 'titi@example.com',
sort: 'titi',
count: $add(3),
test_map: $set({ str: 'B' }),
test_list: $append(['toutou'])
}),
TestEntity2.build(PutItemTransaction).item({
email: 'toto@example.com',
test_composite: 'hey',
Expand Down Expand Up @@ -212,6 +223,37 @@ describe('generateTransactWriteCommandInput', () => {
TableName: 'test-table'
}
},
{
Update: {
Key: {
pk: 'titi@example.com',
sk: 'titi'
},
UpdateExpression:
'SET #s_1 = list_append(#s_1, :s_1), #s_2 = :s_2, #s_3 = if_not_exists(#s_4, :s_3), #s_5 = if_not_exists(#s_6, :s_4), #s_7 = :s_5 ADD #a_1 :a_1',
ExpressionAttributeNames: {
'#a_1': 'test_number',
'#s_1': 'test_list',
'#s_2': 'test_map',
'#s_3': '_et',
'#s_4': '_et',
'#s_5': '_ct',
'#s_6': '_ct',
'#s_7': '_md'
},
ExpressionAttributeValues: {
':a_1': 3,
':s_1': ['toutou'],
':s_2': {
str: 'B'
},
':s_3': 'TestEntity',
':s_4': mockDate,
':s_5': mockDate
},
TableName: 'test-table'
}
},
{
Put: {
Item: {
Expand Down
1 change: 1 addition & 0 deletions src/v1/operations/transactions/updateItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { UpdateItemTransaction } from './operation'
63 changes: 63 additions & 0 deletions src/v1/operations/transactions/updateItem/operation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { EntityV2 } from 'v1/entity'

import { DynamoDBToolboxError } from 'v1/errors'

import { $entity, EntityOperation } from '../../class'
import type { UpdateItemInput } from '../../updateItem/types'
import { WriteItemTransaction } from '../types'
import { transactUpdateItemParams, TransactUpdateItemParams } from './transactUpdateItemParams'
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
import type { UpdateItemTransactionOptions } from './options'

export const $item = Symbol('$item')
export type $item = typeof $item

export const $options = Symbol('$options')
export type $options = typeof $options

export class UpdateItemTransaction<
ENTITY extends EntityV2 = EntityV2,
OPTIONS extends UpdateItemTransactionOptions<ENTITY> = UpdateItemTransactionOptions<ENTITY>
>
extends EntityOperation<ENTITY>
implements WriteItemTransaction<ENTITY, 'Update'> {
static operationName = 'transactUpdate' as const

private [$item]?: UpdateItemInput<ENTITY>
public item: (nextItem: UpdateItemInput<ENTITY>) => UpdateItemTransaction<ENTITY>
public [$options]: OPTIONS
public options: <NEXT_OPTIONS extends UpdateItemTransactionOptions<ENTITY>>(
nextOptions: NEXT_OPTIONS
) => UpdateItemTransaction<ENTITY, NEXT_OPTIONS>

constructor(entity: ENTITY, item?: UpdateItemInput<ENTITY>, options: OPTIONS = {} as OPTIONS) {
super(entity)
this[$item] = item
this[$options] = options

this.item = nextItem => new UpdateItemTransaction(this[$entity], nextItem, this[$options])
this.options = nextOptions => new UpdateItemTransaction(this[$entity], this[$item], nextOptions)
}

params = (): TransactUpdateItemParams => {
if (!this[$item]) {
throw new DynamoDBToolboxError('operations.incompleteCommand', {
message: 'UpdateItemTransaction incomplete: Missing "item" property'
})
}

return transactUpdateItemParams(this[$entity], this[$item], this[$options])
}

get = (): {
documentClient: DynamoDBDocumentClient
type: 'Update'
params: TransactUpdateItemParams
} => ({
documentClient: this[$entity].table.documentClient,
type: 'Update',
params: this.params()
})
}

export type UpdateItemTransactionClass = typeof UpdateItemTransaction
6 changes: 6 additions & 0 deletions src/v1/operations/transactions/updateItem/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { EntityV2 } from 'v1/entity'
import type { ConditionOptions } from 'v1/operations/types/condition'

export type UpdateItemTransactionOptions<
ENTITY extends EntityV2 = EntityV2
> = ConditionOptions<ENTITY>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { transactUpdateItemParams } from './transactUpdateItemParams'
export type { TransactUpdateItemParams } from './transactUpdateItemParams'
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { EntityV2 } from 'v1/entity'
import { parseCondition } from 'v1/operations/expression/condition/parse'

import { rejectExtraOptions } from 'v1/operations/utils/parseOptions/rejectExtraOptions'

import { UpdateItemTransactionOptions } from '../options'
import type { TransactUpdateItemParams } from './transactUpdateItemParams'

type TransactionOptions = Omit<TransactUpdateItemParams, 'TableName' | 'Key' | 'UpdateExpression'>

export const parseUpdateItemTransactionOptions = <ENTITY extends EntityV2>(
entity: ENTITY,
putItemTransactionOptions: UpdateItemTransactionOptions<ENTITY>
): TransactionOptions => {
const transactionOptions: TransactionOptions = {}

const { condition, ...extraOptions } = putItemTransactionOptions
rejectExtraOptions(extraOptions)

if (condition !== undefined) {
const {
ExpressionAttributeNames,
ExpressionAttributeValues,
ConditionExpression
} = parseCondition(entity, condition)

transactionOptions.ExpressionAttributeNames = ExpressionAttributeNames
transactionOptions.ExpressionAttributeValues = ExpressionAttributeValues
transactionOptions.ConditionExpression = ConditionExpression
}

return transactionOptions
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { EntityV2 } from 'v1/entity'
import type { Item, RequiredOption } from 'v1/schema'
import { parseSchemaClonedInput } from 'v1/validation/parseClonedInput'
import { UpdateItemInputExtension } from 'v1/operations/types'
import { parseUpdateExtension } from 'v1/operations/updateItem/updateItemParams/extension/parseExtension'

type EntityUpdateCommandInputParser = (
entity: EntityV2,
input: Item<UpdateItemInputExtension>
) => Generator<Item<UpdateItemInputExtension>, Item<UpdateItemInputExtension>>

const requiringOptions = new Set<RequiredOption>(['always'])

export const parseEntityUpdateTransactionInput: EntityUpdateCommandInputParser = (
entity,
input
) => {
const parser = parseSchemaClonedInput(entity.schema, input, {
operationName: 'update',
requiringOptions,
parseExtension: parseUpdateExtension
})

parser.next() // cloned

return parser
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { TransactWriteCommandInput } from '@aws-sdk/lib-dynamodb'
import isEmpty from 'lodash.isempty'
import omit from 'lodash.omit'

import type { EntityV2 } from 'v1/entity'
import { parsePrimaryKey } from 'v1/operations/utils/parsePrimaryKey'

import type { UpdateItemInput } from '../../../updateItem'
import { parseEntityUpdateTransactionInput } from './parseUpdateTransactionInput'
import type { UpdateItemTransactionOptions } from '../options'

import { parseUpdateItemTransactionOptions } from './parseUpdateItemOptions'
import { parseUpdate } from 'v1/operations/updateItem/updateExpression/parse'

export type TransactUpdateItemParams = NonNullable<
NonNullable<TransactWriteCommandInput['TransactItems']>[number]['Update']
>

export const transactUpdateItemParams = <
ENTITY extends EntityV2,
OPTIONS extends UpdateItemTransactionOptions<ENTITY>
>(
entity: ENTITY,
input: UpdateItemInput<ENTITY>,
updateItemTransactionOptions: OPTIONS = {} as OPTIONS
): TransactUpdateItemParams => {
const validInputParser = parseEntityUpdateTransactionInput(entity, input)
const validInput = validInputParser.next().value
const collapsedInput = validInputParser.next().value

const keyInput = entity.computeKey ? entity.computeKey(validInput) : collapsedInput
const primaryKey = parsePrimaryKey(entity, keyInput)

const {
ExpressionAttributeNames: updateExpressionAttributeNames,
ExpressionAttributeValues: updateExpressionAttributeValues,
...update
} = parseUpdate(entity, omit(collapsedInput, Object.keys(primaryKey)))

const {
ExpressionAttributeNames: optionsExpressionAttributeNames,
ExpressionAttributeValues: optionsExpressionAttributeValues,
...options
} = parseUpdateItemTransactionOptions(entity, updateItemTransactionOptions)

const ExpressionAttributeNames = {
...optionsExpressionAttributeNames,
...updateExpressionAttributeNames
}

const ExpressionAttributeValues = {
...optionsExpressionAttributeValues,
...updateExpressionAttributeValues
}

return {
TableName: entity.table.getName(),
Key: primaryKey,
UpdateExpression: update.UpdateExpression,
...options,
...(!isEmpty(ExpressionAttributeNames) ? { ExpressionAttributeNames } : {}),
...(!isEmpty(ExpressionAttributeValues) ? { ExpressionAttributeValues } : {})
}
}

0 comments on commit e452ad1

Please sign in to comment.