Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/add transact write update #654

Merged
merged 2 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 { batchWrite, BatchDeleteItemRequest, BatchPutItemRequest } from './batch'
export type { BatchWriteOptions, BatchWriteItemRequest } from './batch'
export { formatSavedItem } from './utils/formatSavedItem'
Expand Down
28 changes: 13 additions & 15 deletions src/v1/operations/transactions/deleteItem/operation.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'

import type { EntityV2 } from 'v1/entity'
import { DynamoDBToolboxError } from 'v1/errors'
import { KeyInput } from 'v1/operations/types'
import type { KeyInput } from 'v1/operations/types'

import { $entity, EntityOperation } from '../../class'
import type { DeleteItemTransactionOptions } from './options'
import { transactDeleteItemParams, TransactDeleteItemParams } from './transactDeleteItemParams'
import { WriteItemTransaction } from '../types'
import type { WriteItemTransaction } from '../types'

export const $key = Symbol('$key')
export type $key = typeof $key
Expand All @@ -21,12 +21,12 @@ export class DeleteItemTransaction<
>
extends EntityOperation<ENTITY>
implements WriteItemTransaction<ENTITY, 'Delete'> {
static operationName = 'transactDelete' as const
static operationName = 'transactDelete' as const;

private [$key]?: KeyInput<ENTITY>
public key: (keyInput: KeyInput<ENTITY>) => DeleteItemTransaction<ENTITY>
public [$options]: OPTIONS
public options: <NEXT_OPTIONS extends DeleteItemTransactionOptions<ENTITY>>(
[$key]?: KeyInput<ENTITY>
key: (keyInput: KeyInput<ENTITY>) => DeleteItemTransaction<ENTITY>;
[$options]: OPTIONS
options: <NEXT_OPTIONS extends DeleteItemTransactionOptions<ENTITY>>(
nextOptions: NEXT_OPTIONS
) => DeleteItemTransaction<ENTITY, NEXT_OPTIONS>

Expand All @@ -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'
14 changes: 7 additions & 7 deletions src/v1/operations/transactions/putItem/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { DynamoDBToolboxError } from 'v1/errors'

import { $entity, EntityOperation } from '../../class'
import type { PutItemInput } from '../../putItem/types'
import { WriteItemTransaction } from '../types'
import type { WriteItemTransaction } from '../types'
import { transactPutItemParams, TransactPutItemParams } from './transactPutItemParams'
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
import type { PutItemTransactionOptions } from './options'

export const $item = Symbol('$item')
Expand All @@ -21,12 +21,12 @@ export class PutItemTransaction<
>
extends EntityOperation<ENTITY>
implements WriteItemTransaction<ENTITY, 'Put'> {
static operationName = 'transactPut' as const
static operationName = 'transactPut' as const;

private [$item]?: PutItemInput<ENTITY>
public item: (nextItem: PutItemInput<ENTITY>) => PutItemTransaction<ENTITY>
public [$options]: OPTIONS
public options: <NEXT_OPTIONS extends PutItemTransactionOptions<ENTITY>>(
[$item]?: PutItemInput<ENTITY>
item: (nextItem: PutItemInput<ENTITY>) => PutItemTransaction<ENTITY>;
[$options]: OPTIONS
options: <NEXT_OPTIONS extends PutItemTransactionOptions<ENTITY>>(
nextOptions: NEXT_OPTIONS
) => PutItemTransaction<ENTITY, NEXT_OPTIONS>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export const parseEntityPutTransactionInput: EntityPutCommandInputParser = (enti
operationName: 'put'
})

const clonedInput = parser.next() // cloned
parser.next(clonedInput) // linked
parser.next() // cloned
parser.next() // linked

return parser
}
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 type { WriteItemTransaction } from '../types'
import { transactUpdateItemParams, TransactUpdateItemParams } from './transactUpdateItemParams'
import type { 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;

[$item]?: UpdateItemInput<ENTITY>
item: (nextItem: UpdateItemInput<ENTITY>) => UpdateItemTransaction<ENTITY>;
[$options]: OPTIONS
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.incompleteOperation', {
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 type { UpdateItemTransactionOptions } from '../options'
import type { TransactUpdateItemParams } from './transactUpdateItemParams'

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

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

const { condition, ...extraOptions } = updateItemTransactionOptions
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,28 @@
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() // linked
parser.next() // cloned
ThomasAribart marked this conversation as resolved.
Show resolved Hide resolved

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 } : {})
}
}