Repository: typedorm/typedorm (ts)

Issue:
Add support for specifying ConditionExpression when performing `update`
Hi there,

I said it before but I will say it again: great library! Thank you very much.

I currently have a case where I need to pass in a `ConditionExpression` into an update call. As far as I can tell this is not yet possible, but should be easy to expose since you are already using `ConditionExpression` internally as documented [here](https://github.com/typedorm/typedorm/blob/main/docs/how-it-works.md).

Any thoughts on this?

Pull request description:
Feature/89 condition exp support to update
# Changes

- [x] add support for condition-based updates using the entity manager
- [x] add support for condition-based updates using the transaction manager
- [x] add test coverage
- [x] update docs

closes #89 

Patch:
diff --git a/docs/api.md b/docs/api.md
index c93a310..c0d1618 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -242,6 +242,11 @@ exists(
     // body can include 'user$name' set to new value)
     // @default it '.'
     nestedKeySeparator
+
+    // @Optional
+    // condition based updates
+    // when present, it must evaluate to true in order for operation to succeed.
+    where
   }
 
 )
@@ -301,7 +306,7 @@ find(
     // @default ASC
     orderBy
 
-    // optional
+    // @optional
     // Filter returned items
     // Any conditions listed here will apply after items have been read from dynamodb and
     // therefore this should be avoided wherever possible, but can be helpful in some cases
diff --git a/docs/how-to-recipes.md b/docs/how-to-recipes.md
index bf19594..0c8f108 100644
--- a/docs/how-to-recipes.md
+++ b/docs/how-to-recipes.md
@@ -6,6 +6,8 @@ This page will walk you through some of the unique recipes to enhance your devel
   - [Define default values](#define-default-values)
     - [Static Default values](#static-default-values)
     - [Dynamic Default values](#dynamic-default-values)
+  - [Condition based updates](#condition-based-updates)
+    - [Specify condition when using entity manager](#specify-condition-when-using-entity-manager)
   - [Apply filter to query](#apply-filter-to-query)
     - [Apply filter to query when using entity manager](#apply-filter-to-query-when-using-entity-manager)
   - [Write items in batches](#write-items-in-batches)
@@ -82,11 +84,39 @@ user.lastName = 'Zuk'
 
 | **A word of advice**: There is also a `@AutoGeneratedAttribute` which comes with some most used strategies and should be used over implementing own specification.
 
+## Condition based updates
+
+### Specify condition when using entity manager
+
+To Perform a conditional update, specify a condition in a `where` option. The given condition must evaluate to true in order for operation to succeed.
+
+```Typescript
+const updatedResponse = await entityManager.update(
+  User,
+  {
+    id: '1'
+  },
+  {
+    name: 'new name'
+  },
+  {
+    where: {
+      age: {
+        BETWEEN: [2,5]
+      }
+    }
+  }
+)
+
+// this will generate following condition expression with given values
+// ConditionExpression: '#CE_age BETWEEN :CE_age_start AND :CE_age_end',
+```
+
 ## Apply filter to query
 
 ### Apply filter to query when using entity manager
 
-TypeDORM generates can help you with building fluent filter expressions and all with full type safety.
+TypeDORM can help you with building fluent filter expressions and all with full type safety.
 
 i.e when writing query using entity manager for entity 'User', it can intelligently provide you with all the supported filter options so
 no more typos.
diff --git a/packages/core/src/classes/expression/condition-options-type.ts b/packages/core/src/classes/expression/condition-options-type.ts
new file mode 100644
index 0000000..8bab84e
--- /dev/null
+++ b/packages/core/src/classes/expression/condition-options-type.ts
@@ -0,0 +1,93 @@
+import {
+  NonKeyAttributes,
+  RequireOnlyOne,
+  ConditionType,
+  ScalarType,
+  ATTRIBUTE_TYPE,
+  RequireAtLeastOne,
+} from '@typedorm/common';
+
+type AttributeConditionOptions<PrimaryKey, Entity> =
+  | NonKeyAttributes<
+      PrimaryKey,
+      Entity,
+      RequireOnlyOne<
+        // if condition is one of the below, value must be of scalar type
+        {
+          [key in
+            | ConditionType.SimpleOperator
+            | Extract<
+                ConditionType.FunctionOperator,
+                'CONTAINS' | 'BEGINS_WITH'
+              >]: ScalarType;
+        } &
+          // if between operator, value must be an array of two items
+          {
+            [key in Extract<ConditionType.RangeOperator, 'BETWEEN'>]: [
+              ScalarType,
+              ScalarType
+            ];
+          } &
+          // for 'IN' operator value must be a list of scalar type
+          {
+            [key in Extract<ConditionType.RangeOperator, 'IN'>]: ScalarType[];
+          } &
+          // for 'ATTRIBUTE_TYPE' value must be one of the given enum values
+          {
+            [key in Extract<
+              ConditionType.FunctionOperator,
+              'ATTRIBUTE_TYPE'
+            >]: ATTRIBUTE_TYPE;
+          } &
+          // for 'SIZE' operator value must be a map of comparator and as scalar value
+          {
+            [key in Extract<
+              ConditionType.FunctionOperator,
+              'SIZE'
+            >]: RequireOnlyOne<
+              {
+                [key in ConditionType.SimpleOperator]: ScalarType;
+              }
+            >;
+          }
+      >
+    >
+  // for 'ATTRIBUTE_EXISTS' and 'ATTRIBUTE_NOT_EXISTS', key can be higher level attribute or a path to nested attribute
+  | NonKeyAttributes<
+      PrimaryKey,
+      Entity,
+      Extract<
+        ConditionType.FunctionOperator,
+        'ATTRIBUTE_EXISTS' | 'ATTRIBUTE_NOT_EXISTS'
+      >
+    >;
+
+type RecursiveConditionOptions<PrimaryKey, Entity> = {
+  // for `AND` and `OR` logical operators require at least one of defined options or other self
+  [key in Extract<
+    ConditionType.LogicalOperator,
+    'OR' | 'AND'
+  >]: RequireAtLeastOne<
+    AttributeConditionOptions<PrimaryKey, Entity> &
+      // manually infer recursive type
+      RecursiveConditionOptions<PrimaryKey, Entity> extends infer R
+      ? R
+      : never
+  >;
+} &
+  // for `NOT` logical operators require one from defined options or other self
+  {
+    [key in Extract<ConditionType.LogicalOperator, 'NOT'>]: RequireOnlyOne<
+      AttributeConditionOptions<PrimaryKey, Entity> &
+        // manually infer recursive type
+        RecursiveConditionOptions<PrimaryKey, Entity> extends infer R
+        ? R
+        : never
+    >;
+  } &
+  // require attribute filter
+  AttributeConditionOptions<PrimaryKey, Entity>;
+
+export type ConditionOptions<PrimaryKey, Entity> = RequireOnlyOne<
+  RecursiveConditionOptions<PrimaryKey, Entity>
+>;
diff --git a/packages/core/src/classes/expression/expression-builder.ts b/packages/core/src/classes/expression/expression-builder.ts
index 0eb53a5..3adf605 100644
--- a/packages/core/src/classes/expression/expression-builder.ts
+++ b/packages/core/src/classes/expression/expression-builder.ts
@@ -24,9 +24,15 @@ export class ExpressionBuilder {
     return this.buildConditionExpression(uniqueRecordCondition);
   }
 
-  buildConditionExpression(condition: Condition) {
+  buildConditionExpression(
+    condition: Condition
+  ): {
+    ConditionExpression?: string;
+    ExpressionAttributeNames?: Record<string, any>;
+    ExpressionAttributeValues?: Record<string, any>;
+  } {
     if (!condition.expression) {
-      return {ConditionExpression: ''};
+      return {};
     }
     const expression = {
       ConditionExpression: condition.expression.trim(),
diff --git a/packages/core/src/classes/expression/expression-input-parser.ts b/packages/core/src/classes/expression/expression-input-parser.ts
index 38a6fd9..49deddc 100644
--- a/packages/core/src/classes/expression/expression-input-parser.ts
+++ b/packages/core/src/classes/expression/expression-input-parser.ts
@@ -12,6 +12,8 @@ import {Filter} from './filter';
 import {BaseExpressionInput, MERGE_STRATEGY} from './base-expression-input';
 import {isScalarType} from '../../helpers/is-scalar-type';
 import {FilterOptions} from './filter-options-type';
+import {ConditionOptions} from './condition-options-type';
+import {Condition} from './condition';
 
 export type KeyConditionOptions = RequireOnlyOne<
   {
@@ -39,6 +41,12 @@ export class ExpressionInputParser {
     return this.recursiveParseToBaseExpression(options, Filter).pop();
   }
 
+  parseToCondition<PrimaryKey, Entity>(
+    options: ConditionOptions<PrimaryKey, Entity>
+  ) {
+    return this.recursiveParseToBaseExpression(options, Condition).pop();
+  }
+
   /**
    * Generic Recursive input parser
    * Recursively parses nested object to build expression of type ExpClass
diff --git a/packages/core/src/classes/expression/filter-options-type.ts b/packages/core/src/classes/expression/filter-options-type.ts
index 7b83c81..fd648cf 100644
--- a/packages/core/src/classes/expression/filter-options-type.ts
+++ b/packages/core/src/classes/expression/filter-options-type.ts
@@ -62,14 +62,20 @@ type RecursiveFilterOptions<PrimaryKey, Entity> = {
   // for `AND` and `OR` logical operators require at least one of defined options or other self
   [key in Extract<FilterType.LogicalOperator, 'OR' | 'AND'>]: RequireAtLeastOne<
     AttributeFilterOptions<PrimaryKey, Entity> &
-      RecursiveFilterOptions<PrimaryKey, Entity>
+      // manually infer recursive type
+      RecursiveFilterOptions<PrimaryKey, Entity> extends infer R
+      ? R
+      : never
   >;
 } &
   // for `NOT` logical operators require one from defined options or other self
   {
     [key in Extract<FilterType.LogicalOperator, 'NOT'>]: RequireOnlyOne<
       AttributeFilterOptions<PrimaryKey, Entity> &
-        RecursiveFilterOptions<PrimaryKey, Entity>
+        // manually infer recursive type
+        RecursiveFilterOptions<PrimaryKey, Entity> extends infer R
+        ? R
+        : never
     >;
   } &
   // require attribute filter
@@ -78,3 +84,5 @@ type RecursiveFilterOptions<PrimaryKey, Entity> = {
 export type FilterOptions<PrimaryKey, Entity> = RequireOnlyOne<
   RecursiveFilterOptions<PrimaryKey, Entity>
 >;
+
+// export type FilterOptions<PrimaryKey, Entity> = any;
diff --git a/packages/core/src/classes/manager/entity-manager.ts b/packages/core/src/classes/manager/entity-manager.ts
index 7c43ab3..2c928d1 100644
--- a/packages/core/src/classes/manager/entity-manager.ts
+++ b/packages/core/src/classes/manager/entity-manager.ts
@@ -19,12 +19,18 @@ import {isUsedForPrimaryKey} from '../../helpers/is-used-for-primary-key';
 import {isWriteTransactionItemList} from '../transaction/type-guards';
 import {isLazyTransactionWriteItemListLoader} from '../transformer/is-lazy-transaction-write-item-list-loader';
 import {FilterOptions} from '../expression/filter-options-type';
+import {ConditionOptions} from '../expression/condition-options-type';
 
-export interface EntityManagerUpdateOptions {
+export interface EntityManagerUpdateOptions<PrimaryKey, Entity> {
   /**
    * @default '.'
    */
   nestedKeySeparator?: string;
+
+  /**
+   * Specify condition to apply
+   */
+  where?: ConditionOptions<PrimaryKey, Entity>;
 }
 
 export interface EntityManagerQueryOptions<PrimaryKey, Entity>
@@ -217,7 +223,7 @@ export class EntityManager {
     entityClass: EntityTarget<Entity>,
     primaryKeyAttributes: PrimaryKeyAttributes<PrimaryKey, any>,
     body: UpdateAttributes<PrimaryKey, Entity>,
-    options?: EntityManagerUpdateOptions
+    options?: EntityManagerUpdateOptions<PrimaryKey, Entity>
   ): Promise<Entity> {
     const dynamoUpdateItem = this._dcReqTransformer.toDynamoUpdateItem<
       PrimaryKey,
diff --git a/packages/core/src/classes/transaction/transaction.ts b/packages/core/src/classes/transaction/transaction.ts
index 5f48059..91626c2 100644
--- a/packages/core/src/classes/transaction/transaction.ts
+++ b/packages/core/src/classes/transaction/transaction.ts
@@ -18,7 +18,7 @@ export interface WriteTransactionUpdate<PrimaryKey, Entity> {
     item: EntityTarget<Entity>;
     primaryKey: PrimaryKeyAttributes<PrimaryKey, any>;
     body: UpdateAttributes<PrimaryKey, Entity>;
-    options?: EntityManagerUpdateOptions;
+    options?: EntityManagerUpdateOptions<PrimaryKey, Entity>;
   };
 }
 export interface WriteTransactionDelete<PrimaryKey, Entity> {
diff --git a/packages/core/src/classes/transaction/write-transaction.ts b/packages/core/src/classes/transaction/write-transaction.ts
index a7f5e8c..4e2cc7b 100644
--- a/packages/core/src/classes/transaction/write-transaction.ts
+++ b/packages/core/src/classes/transaction/write-transaction.ts
@@ -42,7 +42,7 @@ export class WriteTransaction extends Transaction {
     if (isCreateTransaction(chainedItem)) {
       this.items = this.chainCreateTransaction(chainedItem);
       // update
-    } else if (isUpdateTransaction(chainedItem)) {
+    } else if (isUpdateTransaction<PrimaryKey, Entity>(chainedItem)) {
       const {item, body, primaryKey, options} = chainedItem.update;
 
       const itemToUpdate = this._dcReqTransformer.toDynamoUpdateItem<
@@ -57,7 +57,7 @@ export class WriteTransaction extends Transaction {
         this.items.push(itemToUpdate);
       }
       // remove
-    } else if (isRemoveTransaction(chainedItem)) {
+    } else if (isRemoveTransaction<PrimaryKey, Entity>(chainedItem)) {
       const {item, primaryKey} = chainedItem.delete;
 
       const itemToRemove = this._dcReqTransformer.toDynamoDeleteItem<
diff --git a/packages/core/src/classes/transformer/document-client-request-transformer.ts b/packages/core/src/classes/transformer/document-client-request-transformer.ts
index d6e8812..e59d22f 100644
--- a/packages/core/src/classes/transformer/document-client-request-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-request-transformer.ts
@@ -18,7 +18,6 @@ import {parseKey} from '../../helpers/parse-key';
 import {KeyCondition} from '../expression/key-condition';
 import {Connection} from '../connection/connection';
 import {ExpressionBuilder} from '../expression/expression-builder';
-import {EntityManagerUpdateOptions} from '../manager/entity-manager';
 import {AttributeMetadata} from '../metadata/attribute-metadata';
 import {DynamoEntitySchemaPrimaryKey} from '../metadata/entity-metadata';
 import {BaseTransformer} from './base-transformer';
@@ -54,6 +53,16 @@ export interface TransformerToDynamoQueryItemsOptions {
   where?: any;
 }
 
+export interface TransformerToDynamoUpdateItemsOptions {
+  /**
+   * key separator
+   * @default '.''
+   */
+  nestedKeySeparator?: string;
+
+  where?: any;
+}
+
 export interface ManagerToDynamoPutItemOptions {
   /**
    * @default false
@@ -247,7 +256,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     entityClass: EntityTarget<Entity>,
     primaryKeyAttributes: PrimaryKeyAttributes<PrimaryKey, any>,
     body: UpdateAttributes<PrimaryKey, Entity>,
-    options: EntityManagerUpdateOptions = {}
+    options: TransformerToDynamoUpdateItemsOptions = {}
   ):
     | DynamoDB.DocumentClient.UpdateItemInput
     | LazyTransactionWriteItemListLoader {
@@ -328,6 +337,38 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       ExpressionAttributeValues,
     };
 
+    // if 'where' was provided, build condition expression
+    if (options.where && !isEmptyObject(options.where)) {
+      const condition = this._expressionInputParser.parseToCondition(
+        options.where
+      );
+
+      if (!condition) {
+        throw new Error(
+          `Failed to build condition expression for input: ${JSON.stringify(
+            options.where
+          )}`
+        );
+      }
+
+      const {
+        ConditionExpression,
+        ExpressionAttributeNames,
+        ExpressionAttributeValues,
+      } = this._expressionBuilder.buildConditionExpression(condition);
+
+      // append condition expression if one was built
+      itemToUpdate.ConditionExpression = ConditionExpression;
+      itemToUpdate.ExpressionAttributeNames = {
+        ...itemToUpdate.ExpressionAttributeNames,
+        ...ExpressionAttributeNames,
+      };
+      itemToUpdate.ExpressionAttributeValues = {
+        ...itemToUpdate.ExpressionAttributeValues,
+        ...ExpressionAttributeValues,
+      };
+    }
+
     // when item does not have any unique attributes to update, return putItemInput
     if (!uniqueAttributesToUpdate.length) {
       this.connection.logger.logTransform(
@@ -575,9 +616,11 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       const filter = this._expressionInputParser.parseToFilter(where);
 
       if (!filter) {
-        throw `Failed to build filter expression for input: ${JSON.stringify(
-          where
-        )}`;
+        throw new Error(
+          `Failed to build filter expression for input: ${JSON.stringify(
+            where
+          )}`
+        );
       }
 
       const {


Test patch:
diff --git a/packages/core/src/classes/expression/__test__/expression-input-parser.spec.ts b/packages/core/src/classes/expression/__test__/expression-input-parser.spec.ts
index 445d1c2..a0e97b6 100644
--- a/packages/core/src/classes/expression/__test__/expression-input-parser.spec.ts
+++ b/packages/core/src/classes/expression/__test__/expression-input-parser.spec.ts
@@ -1,5 +1,6 @@
 import {ATTRIBUTE_TYPE} from '@typedorm/common';
 import {User, UserPrimaryKey} from '@typedorm/core/__mocks__/user';
+import {Condition} from '../condition';
 import {ExpressionInputParser} from '../expression-input-parser';
 import {Filter} from '../filter';
 import {KeyCondition} from '../key-condition';
@@ -181,3 +182,36 @@ test('parses filter with complex nested logical operators', () => {
     ':FE_status': 'SS',
   });
 });
+
+test('parses deep nested condition', () => {
+  const parsedCondition = expInputParser.parseToCondition<UserPrimaryKey, User>(
+    {
+      NOT: {
+        NOT: {
+          OR: {
+            AND: {
+              age: 'ATTRIBUTE_EXISTS',
+              name: 'ATTRIBUTE_NOT_EXISTS',
+            },
+            status: {
+              LE: '1',
+            },
+          },
+        },
+      },
+    }
+  );
+
+  expect(parsedCondition).toBeInstanceOf(Condition);
+  expect(parsedCondition?.expression).toEqual(
+    'NOT (NOT (((attribute_exists(#CE_age)) AND (attribute_not_exists(#CE_name))) OR (#CE_status <= :CE_status)))'
+  );
+  expect(parsedCondition?.names).toEqual({
+    '#CE_age': 'age',
+    '#CE_name': 'name',
+    '#CE_status': 'status',
+  });
+  expect(parsedCondition?.values).toEqual({
+    ':CE_status': '1',
+  });
+});
diff --git a/packages/core/src/classes/manager/__test__/entity-manager.spec.ts b/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
index 4caf8f1..eec4ea5 100644
--- a/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
+++ b/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
@@ -423,6 +423,71 @@ test('updates item with unique attributes and returns all updated attributes', a
   });
 });
 
+test('updates item and return all new attributes with given condition', async () => {
+  dcMock.update.mockReturnValue({
+    promise: () => ({
+      Attributes: {
+        PK: 'USER#1',
+        SK: 'USER#1',
+        GSI1PK: 'USER#STATUS#active',
+        GSI1SK: 'USER#Me',
+        id: '1',
+        name: 'user',
+        status: 'active',
+        age: 4,
+      },
+    }),
+  });
+  const updatedItem = await manager.update<UserPrimaryKey, User>(
+    User,
+    {id: '1'},
+    {
+      name: 'user',
+      status: 'active',
+    },
+    {
+      where: {
+        age: {
+          BETWEEN: [1, 11],
+        },
+      },
+    }
+  );
+
+  expect(dcMock.update).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#attr0': 'name',
+      '#attr1': 'status',
+      '#attr2': 'GSI1SK',
+      '#attr3': 'GSI1PK',
+      '#CE_age': 'age',
+    },
+    ExpressionAttributeValues: {
+      ':val0': 'user',
+      ':val1': 'active',
+      ':val2': 'USER#user',
+      ':val3': 'USER#STATUS#active',
+      ':CE_age_end': 11,
+      ':CE_age_start': 1,
+    },
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    ReturnValues: 'ALL_NEW',
+    TableName: 'test-table',
+    UpdateExpression:
+      'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2, #attr3 = :val3',
+    ConditionExpression: '#CE_age BETWEEN :CE_age_start AND :CE_age_end',
+  });
+  expect(updatedItem).toEqual({
+    id: '1',
+    name: 'user',
+    status: 'active',
+    age: 4,
+  });
+});
+
 test('does not update an item when failed to get item by key', async () => {
   manager.findOne = jest.fn();
 
diff --git a/packages/core/src/classes/transaction/__test__/write-transaction.spec.ts b/packages/core/src/classes/transaction/__test__/write-transaction.spec.ts
index 1131d03..7929377 100644
--- a/packages/core/src/classes/transaction/__test__/write-transaction.spec.ts
+++ b/packages/core/src/classes/transaction/__test__/write-transaction.spec.ts
@@ -34,6 +34,52 @@ test('creates transaction item with existing items', () => {
   ]);
 });
 
+test('creates transaction update item with given condition', () => {
+  const transaction = new WriteTransaction(connection);
+  transaction.chian({
+    update: {
+      item: User,
+      primaryKey: {
+        id: '111',
+      },
+      body: {
+        name: 'updated name',
+      },
+      options: {
+        where: {
+          age: {
+            LE: 1,
+          },
+        },
+      },
+    },
+  });
+
+  expect(transaction.items).toEqual([
+    {
+      Update: {
+        ConditionExpression: '#CE_age <= :CE_age',
+        ExpressionAttributeNames: {
+          '#CE_age': 'age',
+          '#attr0': 'name',
+          '#attr1': 'GSI1SK',
+        },
+        ExpressionAttributeValues: {
+          ':CE_age': 1,
+          ':val0': 'updated name',
+          ':val1': 'USER#updated name',
+        },
+        Key: {
+          PK: 'USER#111',
+          SK: 'USER#111',
+        },
+        TableName: 'test-table',
+        UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+      },
+    },
+  ]);
+});
+
 /**
  * @group chain
  */
diff --git a/packages/core/src/classes/transformer/__test__/dc-request-transformer.spec.ts b/packages/core/src/classes/transformer/__test__/dc-request-transformer.spec.ts
index 7e776d8..8aba9d0 100644
--- a/packages/core/src/classes/transformer/__test__/dc-request-transformer.spec.ts
+++ b/packages/core/src/classes/transformer/__test__/dc-request-transformer.spec.ts
@@ -418,6 +418,94 @@ test('transforms update item record with unique attributes', () => {
   ]);
 });
 
+test('transforms update item request with condition input', () => {
+  const updatedItem = transformer.toDynamoUpdateItem<UserPrimaryKey, User>(
+    User,
+    {
+      id: '1',
+    },
+    {
+      name: 'new name',
+    },
+    {
+      where: {
+        age: {
+          BETWEEN: [3, 10],
+        },
+      },
+    }
+  );
+  expect(updatedItem).toEqual({
+    ExpressionAttributeNames: {
+      '#attr0': 'name',
+      '#attr1': 'GSI1SK',
+      '#CE_age': 'age',
+    },
+    ExpressionAttributeValues: {
+      ':val0': 'new name',
+      ':val1': 'USER#new name',
+      ':CE_age_end': 10,
+      ':CE_age_start': 3,
+    },
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    ReturnValues: 'ALL_NEW',
+    TableName: 'test-table',
+    UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+    ConditionExpression: '#CE_age BETWEEN :CE_age_start AND :CE_age_end',
+  });
+});
+
+test('transforms update item request with complex condition input', () => {
+  const updatedItem = transformer.toDynamoUpdateItem<UserPrimaryKey, User>(
+    User,
+    {
+      id: '1',
+    },
+    {
+      name: 'new name',
+    },
+    {
+      where: {
+        AND: {
+          age: {
+            GE: 3,
+          },
+          status: {
+            IN: ['active', 'standby'],
+          },
+        },
+      },
+    }
+  );
+  expect(updatedItem).toEqual({
+    ExpressionAttributeNames: {
+      '#attr0': 'name',
+      '#attr1': 'GSI1SK',
+      '#CE_age': 'age',
+      '#CE_status': 'status',
+    },
+    ExpressionAttributeValues: {
+      ':val0': 'new name',
+      ':val1': 'USER#new name',
+      ':CE_age': 3,
+      ':CE_status_0': 'active',
+      ':CE_status_1': 'standby',
+    },
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    ReturnValues: 'ALL_NEW',
+    TableName: 'test-table',
+    UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+    ConditionExpression:
+      '(#CE_age >= :CE_age) AND (#CE_status IN (:CE_status_0, :CE_status_1))',
+  });
+});
+
 /**
  * @group toDynamoDeleteItem
  */