Skip to content

Commit

Permalink
Merge pull request #785 from jeremydaly/rework-transactions
Browse files Browse the repository at this point in the history
Rework transactions
  • Loading branch information
ThomasAribart committed Jul 4, 2024
2 parents 08e49c1 + a7866ec commit d763472
Show file tree
Hide file tree
Showing 66 changed files with 1,331 additions and 1,719 deletions.
17 changes: 4 additions & 13 deletions docs/docs/3-entities/3-actions/12-tansact-put/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,18 @@ sidebar_custom_props:

# PutItemTransaction

Build a `PutItem` transaction on an entity item, to be used within [TransactWriteItems operations](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html):
Builds a transaction to put an entity item, to be used within [TransactWriteItems operations](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html):

```ts
import { transactWriteItems } from 'dynamodb-toolbox/entity/actions/transactWrite'
import { execute } from 'dynamodb-toolbox/entity/actions/transactWrite'
import { PutItemTransaction } from 'dynamodb-toolbox/entity/actions/transactPut'

const transaction = PokemonEntity.build(PutItemTransaction)

const params = transaction.params()
await transactWriteItems([
transaction,
...otherTransactions
])
await execute([transaction, ...otherTransactions])
```

:::info

Check the [Transaction Documentation](../9-transactions/index.md) to learn how to use `PutItemTransactions`.

:::

## Request

### `.item(...)`
Expand All @@ -53,7 +44,7 @@ import type { PutItemInput } from 'dynamodb-toolbox/entity/actions/put'

const item: PutItemInput<typeof PokemonEntity> = {
pokemonId: 'pikachu1',
name: 'Pikachu'
name: 'Pikachu',
...
}

Expand Down
6 changes: 0 additions & 6 deletions docs/docs/3-entities/3-actions/6-batch-get/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,3 @@ const key: KeyInput<typeof PokemonEntity> = {
const request =
PokemonEntity.build(BatchGetRequest).key(key)
```

:::info

Contrary to [`GetItemCommands`](../1-get-item/index.md), batch gets cannot be [conditioned](../17-parse-condition/index.md).

:::
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'
import type { O } from 'ts-toolbelt'

import { EntityParser } from '~/entity/actions/parse.js'
import type { KeyInput } from '~/entity/actions/parse.js'
import type { Condition } from '~/entity/actions/parseCondition.js'
import { $entity, EntityAction } from '~/entity/index.js'
import { $entity } from '~/entity/index.js'
import type { Entity } from '~/entity/index.js'
import { DynamoDBToolboxError } from '~/errors/index.js'

import type { WriteItemTransaction } from '../types.js'
import { conditionCheckParams } from './conditionCheckParams/index.js'
import type { ConditionCheckParams } from './conditionCheckParams/index.js'
import { WriteTransaction } from '../transactWrite/transaction.js'
import type {
TransactWriteItem,
WriteTransactionImplementation
} from '../transactWrite/transaction.js'
import { parseOptions } from './options.js'

export const $key = Symbol('$key')
export type $key = typeof $key
Expand All @@ -17,26 +21,29 @@ export const $condition = Symbol('$condition')
export type $condition = typeof $condition

export class ConditionCheck<ENTITY extends Entity = Entity>
extends EntityAction<ENTITY>
implements WriteItemTransaction<ENTITY, 'ConditionCheck'>
extends WriteTransaction<ENTITY>
implements WriteTransactionImplementation<ENTITY>
{
static actionName = 'conditionCheck' as const

private [$key]?: KeyInput<ENTITY>
public key: (keyInput: KeyInput<ENTITY>) => ConditionCheck<ENTITY>
private [$condition]?: Condition<ENTITY>
public condition: (keyInput: Condition<ENTITY>) => ConditionCheck<ENTITY>

constructor(entity: ENTITY, key?: KeyInput<ENTITY>, condition?: Condition<ENTITY>) {
super(entity)
this[$key] = key
this[$condition] = condition
}

key(nextKey: KeyInput<ENTITY>): ConditionCheck<ENTITY> {
return new ConditionCheck(this[$entity], nextKey, this[$condition])
}

this.key = nextKey => new ConditionCheck(this[$entity], nextKey, this[$condition])
this.condition = nexCondition => new ConditionCheck(this[$entity], this[$key], nexCondition)
condition(nextCondition: Condition<ENTITY>): ConditionCheck<ENTITY> {
return new ConditionCheck(this[$entity], this[$key], nextCondition)
}

params = (): ConditionCheckParams => {
params(): O.Required<TransactWriteItem, 'ConditionCheck'> {
if (!this[$key]) {
throw new DynamoDBToolboxError('actions.incompleteAction', {
message: 'ConditionCheck incomplete: Missing "key" property'
Expand All @@ -49,18 +56,17 @@ export class ConditionCheck<ENTITY extends Entity = Entity>
})
}

return conditionCheckParams(this[$entity], this[$key], this[$condition])
}
const { key } = this[$entity].build(EntityParser).parse(this[$key], { mode: 'key' })
const options = parseOptions(this[$entity], this[$condition])

get = (): {
documentClient: DynamoDBDocumentClient
type: 'ConditionCheck'
params: ConditionCheckParams
} => ({
documentClient: this[$entity].table.getDocumentClient(),
type: 'ConditionCheck',
params: this.params()
})
return {
ConditionCheck: {
TableName: this[$entity].table.getName(),
Key: key,
...options
}
}
}
}

export type ConditionCheckClass = typeof ConditionCheck
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,8 @@ const documentClient = DynamoDBDocumentClient.from(dynamoDbClient)

const TestTable = new Table({
name: 'test-table',
partitionKey: {
type: 'string',
name: 'pk'
},
sortKey: {
type: 'string',
name: 'sk'
},
partitionKey: { type: 'string', name: 'pk' },
sortKey: { type: 'string', name: 'sk' },
documentClient
})

Expand Down Expand Up @@ -62,7 +56,7 @@ describe('condition check transaction', () => {
TestEntity.build(ConditionCheck)
.key({ email: 'x', sort: 'y' })
.condition({ attr: 'email', gt: 'test' })
.params()
.params().ConditionCheck

expect(ExpressionAttributeNames).toEqual({ '#c_1': 'pk' })
expect(ExpressionAttributeValues).toEqual({ ':c_1': 'test' })
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@ import { EntityConditionParser } from '~/entity/actions/parseCondition.js'
import type { Condition } from '~/entity/actions/parseCondition.js'
import type { Entity } from '~/entity/index.js'

import type { ConditionCheckParams } from './conditionCheckParams.js'
import type { TransactWriteItem } from '../transactWrite/transaction.js'

type TransactionOptions = Omit<ConditionCheckParams, 'TableName' | 'Key'>

type ConditionCheckOptionsParser = <ENTITY extends Entity>(
type OptionsParser = <ENTITY extends Entity>(
entity: ENTITY,
condition: Condition<ENTITY>
) => TransactionOptions
) => Omit<NonNullable<TransactWriteItem['ConditionCheck']>, 'TableName' | 'Key'>

export const parseConditionCheckOptions: ConditionCheckOptionsParser = (entity, condition) => {
export const parseOptions: OptionsParser = (entity, condition) => {
const { ExpressionAttributeNames, ExpressionAttributeValues, ConditionExpression } = entity
.build(EntityConditionParser)
.parse(condition)
.toCommandOptions()

const transactionOptions: TransactionOptions = { ConditionExpression }
const transactionOptions: ReturnType<OptionsParser> = { ConditionExpression }

if (!isEmpty(ExpressionAttributeNames)) {
transactionOptions.ExpressionAttributeNames = ExpressionAttributeNames
Expand Down
71 changes: 71 additions & 0 deletions src/entity/actions/transactDelete/deleteTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { O } from 'ts-toolbelt'

import { EntityParser } from '~/entity/actions/parse.js'
import type { KeyInput } from '~/entity/actions/parse.js'
import { $entity } from '~/entity/index.js'
import type { Entity } from '~/entity/index.js'
import { DynamoDBToolboxError } from '~/errors/index.js'

import { WriteTransaction } from '../transactWrite/transaction.js'
import type {
TransactWriteItem,
WriteTransactionImplementation
} from '../transactWrite/transaction.js'
import type { DeleteTransactionOptions } from './options.js'
import { parseOptions } from './options.js'

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

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

export class DeleteTransaction<
ENTITY extends Entity = Entity,
OPTIONS extends DeleteTransactionOptions<ENTITY> = DeleteTransactionOptions<ENTITY>
>
extends WriteTransaction<ENTITY>
implements WriteTransactionImplementation<ENTITY>
{
static actionName = 'transactDelete' as const;

[$key]?: KeyInput<ENTITY>;
[$options]: OPTIONS

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

key(nextKey: KeyInput<ENTITY>): DeleteTransaction<ENTITY> {
return new DeleteTransaction(this[$entity], nextKey, this[$options])
}

options<NEXT_OPTIONS extends DeleteTransactionOptions<ENTITY>>(
nextOptions: NEXT_OPTIONS
): DeleteTransaction<ENTITY, NEXT_OPTIONS> {
return new DeleteTransaction(this[$entity], this[$key], nextOptions)
}

params(): O.Required<TransactWriteItem, 'Delete'> {
if (!this[$key]) {
throw new DynamoDBToolboxError('actions.incompleteAction', {
message: 'DeleteTransaction incomplete: Missing "key" property'
})
}

const { key } = this[$entity].build(EntityParser).parse(this[$key], { mode: 'key' })
const options = parseOptions(this[$entity], this[$options])

return {
Delete: {
TableName: this[$entity].table.getName(),
Key: key,
...options
}
}
}
}

export type DeleteTransactionClass = typeof DeleteTransaction
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
DeleteItemTransaction,
DynamoDBToolboxError,
Entity,
Table,
schema,
string
} from '~/index.js'
import { DeleteTransaction, DynamoDBToolboxError, Entity, Table, schema, string } from '~/index.js'

const TestTable = new Table({
name: 'test-table',
Expand Down Expand Up @@ -41,30 +34,30 @@ const TestEntity2 = new Entity({

describe('delete transaction', () => {
test('deletes the key from inputs', async () => {
const { TableName, Key } = TestEntity.build(DeleteItemTransaction)
const { TableName, Key } = TestEntity.build(DeleteTransaction)
.key({ email: 'test-pk', sort: 'test-sk' })
.params()
.params().Delete

expect(TableName).toBe('test-table')
expect(Key).toStrictEqual({ pk: 'test-pk', sk: 'test-sk' })
})

test('filters out extra data', async () => {
const { Key } = TestEntity.build(DeleteItemTransaction)
const { Key } = TestEntity.build(DeleteTransaction)
.key({
email: 'test-pk',
sort: 'test-sk',
// @ts-expect-error
test: 'test'
})
.params()
.params().Delete

expect(Key).not.toHaveProperty('test')
})

test('fails with undefined input', () => {
expect(() =>
TestEntity.build(DeleteItemTransaction)
TestEntity.build(DeleteTransaction)
.key(
// @ts-expect-error
{}
Expand All @@ -75,7 +68,7 @@ describe('delete transaction', () => {

test('fails when missing the sortKey', () => {
expect(() =>
TestEntity.build(DeleteItemTransaction)
TestEntity.build(DeleteTransaction)
.key(
// @ts-expect-error
{ pk: 'test-pk' }
Expand All @@ -86,7 +79,7 @@ describe('delete transaction', () => {

test('fails when missing partitionKey (no alias)', () => {
expect(() =>
TestEntity2.build(DeleteItemTransaction)
TestEntity2.build(DeleteTransaction)
.key(
// @ts-expect-error
{}
Expand All @@ -97,7 +90,7 @@ describe('delete transaction', () => {

test('fails when missing the sortKey (no alias)', () => {
expect(() =>
TestEntity2.build(DeleteItemTransaction)
TestEntity2.build(DeleteTransaction)
.key(
// @ts-expect-error
{ pk: 'test-pk' }
Expand All @@ -109,7 +102,7 @@ describe('delete transaction', () => {
// Options
test('fails on extra options', () => {
const invalidCall = () =>
TestEntity.build(DeleteItemTransaction)
TestEntity.build(DeleteTransaction)
.key({ email: 'x', sort: 'y' })
.options({
// @ts-expect-error
Expand All @@ -123,18 +116,18 @@ describe('delete transaction', () => {

test('sets condition', () => {
const { ExpressionAttributeNames, ExpressionAttributeValues, ConditionExpression } =
TestEntity.build(DeleteItemTransaction)
TestEntity.build(DeleteTransaction)
.key({ email: 'x', sort: 'y' })
.options({ condition: { attr: 'email', gt: 'test' } })
.params()
.params().Delete

expect(ExpressionAttributeNames).toEqual({ '#c_1': 'pk' })
expect(ExpressionAttributeValues).toEqual({ ':c_1': 'test' })
expect(ConditionExpression).toBe('#c_1 > :c_1')
})

test('missing key', () => {
const invalidCall = () => TestEntity.build(DeleteItemTransaction).params()
const invalidCall = () => TestEntity.build(DeleteTransaction).params()

expect(invalidCall).toThrow(DynamoDBToolboxError)
expect(invalidCall).toThrow(expect.objectContaining({ code: 'actions.incompleteAction' }))
Expand Down
2 changes: 2 additions & 0 deletions src/entity/actions/transactDelete/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DeleteTransaction } from './deleteTransaction.js'
export type { DeleteTransactionOptions } from './options.js'
Loading

0 comments on commit d763472

Please sign in to comment.