Repository: typedorm/typedorm
Pull request #144 (2021-07-01 10:50:20)

Release 1.14.0
# Features 🚀
- brand new scan manager to perform all scan operations in an effective manner with full type safety [Docs](https://github.com/typedorm/typedorm/blob/beta/docs/api.md#scanmanager)
- added support for fine-grained attribute level custom transformation using class-transformer [Docs](https://github.com/typedorm/typedorm/blob/beta/docs/how-to-recipes.md#attribute-transformation)
- added support for updating primary key attributes (updates primary key attributes over the document client transaction API)
- added easy to use parallel scan support
- advanced update expression support, supports updating using all actions (SET, ADD, REMOVE, DELETE) [Docs](https://github.com/typedorm/typedorm/blob/beta/docs/how-to-recipes.md#advanced-updates)

# Bug fixes 🧑‍🔧
- correctly infer the return type of entityManager.find 

# Other
- fixed typos in some areas of documentation
- added more test coverage to complex APIs
- added more examples to docs

# Important Notes
- starting with 1.14.0, all entities `name` field must be unique, i.e - having multiple entities with `name` property set to `user` will now throw and error 


Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index c04ed0c..e50f5bd 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -26,7 +26,7 @@
       "type": "node",
       "request": "launch",
       "name": "Jest Debug Current file",
-      "runtimeVersion": "12.14.1",
+      "runtimeExecutable": "${env:HOME}/.nvm/versions/node/v12.14.1/bin/node",
       "runtimeArgs": [
         "--inspect-brk",
         "--nolazy",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0b887e5..9f58590 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -12,5 +12,6 @@
     "typedorm",
     "typeorm"
   ],
-  "editor.formatOnSave": true
+  "editor.formatOnSave": true,
+  "typescript.tsdk": "node_modules/typescript/lib"
 }
diff --git a/README.md b/README.md
index ec1451d..6cc8fd0 100644
--- a/README.md
+++ b/README.md
@@ -27,18 +27,23 @@ TypeDORM borrows decorator based syntax from TypeORM and provides fully type saf
 
 ## Features
 
-- Single table design first class support
+- Single-Table design pattern first class support
 - DataMapper development pattern
+- Attribute level per entity transformation, enabled via [class-transformer](https://github.com/typestack/class-transformer)
 - Full type safety
 - Declarative relational schema
-- Entity manager
-- Transaction manager
-- Batch manager
+- Entity manager - easy to use `findOne`, `find`, `count`, `exists`, `create`, `update`, and `delete` operations
+- Transaction manager - easy to use `write` and `read` operations
+- Batch manager - powerful `write` and `read` operations
+- Scan Manager - powerful `find`, `count`, `parallelScan` and `scan` operations
+- Safer parallel scan with configurable concurrency control
 - Multiple connections support
 - Code follows all possible best practices when modeling for dynamodb
-- Additional higher level features like, ability to declare non key attribute as unique, automatic retries
+- Supports specifying non key attribute as unique, built in pagination support for querying
+- Supports updating Primary key attributes
 - Auto Generated values for attributes
 - Auto update attribute values on UPDATE operations
+- Most type safe, powerful and flexible update expression generator engine
 - Dynamic & Static default values for attributes
 - Complex update, key condition and condition expression all made easy to work with
 - Powerful expression builder to auto generate expressions from input
diff --git a/docs/api.md b/docs/api.md
index 84aeb4c..01baf36 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -16,7 +16,7 @@ _Note: In an event of inconstancy between actual API and this document, API shou
     - [EntityManager.exists](#entitymanagerexists)
     - [EntityManager.update](#entitymanagerupdate)
     - [EntityManager.delete](#entitymanagerdelete)
-    - [EntityManager.query](#entitymanagerquery)
+    - [EntityManager.find](#entitymanagerfind)
     - [EntityManager.count](#entitymanagercount)
   - [BatchManager](#batchmanager)
     - [BatchManager.write](#batchmanagerwrite)
@@ -24,6 +24,11 @@ _Note: In an event of inconstancy between actual API and this document, API shou
   - [TransactionManager](#transactionmanager)
     - [TransactionManager.write](#transactionmanagerwrite)
     - [TransactionManager.read](#transactionmanagerread)
+  - [ScanManager](#scanmanager)
+    - [ScanManager.find](#scanmanagerfind)
+    - [ScanManager.count](#scanmanagercount)
+    - [ScanManager.parallelScan](#scanmanagerparallelscan)
+    - [ScanManager.scan](#scanmanagerscan)
 
 ## Connection
 
@@ -282,7 +287,15 @@ exists(
 
 ### EntityManager.update
 
-Update item with magically generated set operation
+Updates items and related items over document client's update API.
+
+Some additional features that TypeDORM provides on top of Document Client's update api features:
+
+- supports updating attributes that are referenced in primary key over transaction write API
+- supports updating attributes that are marked as unique, again using document client's transaction write API
+- handles updating all auto update attributes at the update time
+
+_Notes: Primary key attributes and non-primary key attributes can not be updated in same request._
 
 ```Typescript
 update(
@@ -362,7 +375,7 @@ delete(
 )
 ```
 
-### EntityManager.query
+### EntityManager.find
 
 Query items from db
 
@@ -626,3 +639,269 @@ read(
   }
 )
 ```
+
+## ScanManager
+
+Scan manager API. Works on top of Document Client's `scan` operation
+
+### ScanManager.find
+
+Finds entity from table by running a full table scan.
+
+```Typescript
+find(
+  // Entity class to find from the table
+  entityClass
+
+  // @optional
+  // Scan manager scan options
+  // @default none
+  scanOptions: {
+    // @optional
+    // Name of the index if running a scan against secondary indexes
+    // @default - scan will be run against main table
+    scanIndex
+
+    // @optional
+    // Total number of items to return
+    // @default no limit is applied
+    limit
+
+    // @optional
+    // Cursor to start scanning from
+    // @default - none
+    cursor
+
+    // @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
+    // see this https://www.alexdebrie.com/posts/dynamodb-filter-expressions/ for more details
+    where
+
+    // @optional
+    // Specify attributes to get, only selected attributes are fetched
+    // @default `ALL`
+    select
+
+    // @optional
+    // Total segments to divide this scan in
+    totalSegments
+
+    // @optional
+    // Item scan limit to apply per segment, this option is ignored when `totalSegments` is not provided
+    limitPerSegment
+
+    // @optional
+    // maximum concurrency to apply, this option is ignored when `totalSegments` is not provided
+    // @default PARALLEL_SCAN_CONCURRENCY_LIMIT
+    requestConcurrencyLimit
+
+  },
+
+  // @optional
+  // extra non-functional options
+  metadataOptions: {
+    // @optional
+    // Unique request id to use, throughout the request processing,
+    // @default a unique v4 uuid is set and used for all logs
+    requestId
+
+    // @optional
+    // Sets ReturnConsumedCapacity param to given value when making a request via document client
+    returnConsumedCapacity
+  }
+)
+```
+
+### ScanManager.count
+
+Counts entity from table by running a full table scan.
+
+```Typescript
+count(
+  // Entity class to count
+  entityClass
+
+  // @optional
+  // Scan manager scan options
+  // @default none
+  scanOptions: {
+    // @optional
+    // Name of the index if running a scan against secondary indexes
+    // @default - scan will be run against main table
+    scanIndex
+
+    // @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
+    // see this https://www.alexdebrie.com/posts/dynamodb-filter-expressions/ for more details
+    where
+  },
+
+  // @optional
+  // extra non-functional options
+  metadataOptions: {
+    // @optional
+    // Unique request id to use, throughout the request processing,
+    // @default a unique v4 uuid is set and used for all logs
+    requestId
+
+    // @optional
+    // Sets ReturnConsumedCapacity param to given value when making a request via document client
+    returnConsumedCapacity
+  }
+)
+```
+
+### ScanManager.parallelScan
+
+You should always use `.find` unless trying to scan all items from the table
+
+Returns all scanned items to the client that matches given filter, this is build on top of lower level `.scan` op but adds
+safer concurrency control - also used by `.find`.
+
+```Typescript
+parallelScan(
+  // @optional
+  // Scan manager scan options
+  // @default none
+  scanOptions: {
+    // @Optional
+    // Entity to scan
+    // @default - none
+    entity
+
+    // @optional
+    // Name of the index if running a scan against secondary indexes
+    // @default - scan will be run against main table
+    scanIndex
+
+    // @optional
+    // Total number of items to return
+    // @default no limit is applied
+    limit
+
+    // @optional
+    // Cursor to start scanning from
+    // @default - none
+    cursor
+
+    // @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
+    // see this https://www.alexdebrie.com/posts/dynamodb-filter-expressions/ for more details
+    where
+
+    // @optional
+    // Specify attributes to get, only selected attributes are fetched
+    // @default `ALL`
+    select
+
+    // @optional
+    // Current segment identified
+    segment
+
+    // @optional
+    // Total segments to divide this scan in
+    totalSegments
+
+    // @optional
+    // Item scan limit to apply per segment, this option is ignored when `totalSegments` is not provided
+    limitPerSegment
+
+    // @optional
+    // maximum concurrency to apply, this option is ignored when `totalSegments` is not provided
+    // @default PARALLEL_SCAN_CONCURRENCY_LIMIT
+    requestConcurrencyLimit
+  },
+
+  // @optional
+  // extra non-functional options
+  metadataOptions: {
+    // @optional
+    // Unique request id to use, throughout the request processing,
+    // @default a unique v4 uuid is set and used for all logs
+    requestId
+
+    // @optional
+    // Sets ReturnConsumedCapacity param to given value when making a request via document client
+    returnConsumedCapacity
+  }
+)
+```
+
+### ScanManager.scan
+
+Low level scan api - returns all scanned items to the client that matches given filter - also used by `.find`.
+You should always use `.find` unless trying to scan all items from the table.
+
+_Scan manager `.scan` does not enforce concurrency limit, consider using `.parallelScan` to perform safer parallel scan._
+
+```Typescript
+scan(
+  // @optional
+  // Scan manager scan options
+  // @default none
+  scanOptions: {
+    // @Optional
+    // Entity to scan
+    // @default - none
+    entity
+
+    // @optional
+    // Name of the index if running a scan against secondary indexes
+    // @default - scan will be run against main table
+    scanIndex
+
+    // @optional
+    // Total number of items to return
+    // @default no limit is applied
+    limit
+
+    // @optional
+    // Cursor to start scanning from
+    // @default - none
+    cursor
+
+    // @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
+    // see this https://www.alexdebrie.com/posts/dynamodb-filter-expressions/ for more details
+    where
+
+    // @optional
+    // Specify attributes to get, only selected attributes are fetched
+    // @default `ALL`
+    select
+
+    // @optional
+    // Current segment identified
+    segment
+
+    // @optional
+    // Total segments to divide this scan in
+    totalSegments
+
+    // @optional
+    // Item scan limit to apply per segment, this option is ignored when `totalSegments` is not provided
+    limitPerSegment
+  },
+
+  // @optional
+  // extra non-functional options
+  metadataOptions: {
+    // @optional
+    // Unique request id to use, throughout the request processing,
+    // @default a unique v4 uuid is set and used for all logs
+    requestId
+
+    // @optional
+    // Sets ReturnConsumedCapacity param to given value when making a request via document client
+    returnConsumedCapacity
+  }
+)
+```
diff --git a/docs/guide.md b/docs/guide.md
index 74a0227..f8680cd 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -2,24 +2,22 @@
 
 Working with relational data in dynamoDB can be painful, but it doesn't have to be. This guide will walk you through relational data modeling can be simplified using TypeDORM.
 
-## On this page
-
-### Setup
-
-- [Creating a table](#creating-a-table)
-- [Creating an entity](#creating-an-entity)
-- [Adding attributes to an entity](#adding-attributes-to-an-entity)
-- [Adding auto generated attributes](#adding-auto-generated-attributes)
-- [Creating a connection](#creating-a-connection)
-- [Get mangers for connection](#get-mangers-for-connection)
-
-### Operations
-
-- [Create a record](#create-a-record)
-- [Find a record](#find-a-record)
-- [Update a record](#update-a-record)
-- [Query 1-m relations](#query-1-m-relations)
-- [Delete a record](#delete-a-record)
+- [Step by step guide](#step-by-step-guide)
+  - [Creating a table](#creating-a-table)
+  - [Creating a model](#creating-a-model)
+  - [Creating an entity](#creating-an-entity)
+  - [Adding attributes to an entity](#adding-attributes-to-an-entity)
+  - [Adding auto generated attributes](#adding-auto-generated-attributes)
+  - [Creating a Connection](#creating-a-connection)
+  - [Get mangers for connection](#get-mangers-for-connection)
+    - [Get manager instance from current connection](#get-manager-instance-from-current-connection)
+    - [Get manager instance from current TypeDORM context](#get-manager-instance-from-current-typedorm-context)
+  - [Manager Operations](#manager-operations)
+    - [Create a record](#create-a-record)
+    - [Find a record](#find-a-record)
+    - [Update a record](#update-a-record)
+    - [Query 1-m relations](#query-1-m-relations)
+    - [Delete a record](#delete-a-record)
 
 ## Creating a table
 
@@ -261,7 +259,9 @@ const anotherTransactionManger = getTransactionManger('other-connection')
 // ...
 ```
 
-## Create a record
+## Manager Operations
+
+### Create a record
 
 This is all the minimum configuration we need, now let's create a user record.
 
@@ -289,7 +289,7 @@ const response = await getEntityManager().create(org);
 
 To understand how TypeDORM handles these entities under the hood see [this](./how-it-works.md#creating-a-record).
 
-## Find a record
+### Find a record
 
 Once item is created using TypeDORM, it can be retrieved/fetched using continent methods like `find`, `findOne`, `exists`.
 
@@ -312,7 +312,7 @@ const user = await getEntityManager().findOne(User, {id: 'some-auto-generated-uu
 }
 ```
 
-## Update a record
+### Update a record
 
 Items can be updated using simple update functions on entity manager and can be written like this
 
@@ -337,7 +337,7 @@ const user = await getEntityManager().update(User, {id: 'some-auto-generated-uui
 
 To get more insight on how how update works with TypeDORM, have a look at [this](./how-it-works.md#updating-a-record)
 
-## Query 1-m relations
+### Query 1-m relations
 
 Going ahead with earlier example of `User` entity, let's each of our user can have many orders, and our order entity looks like this
 
@@ -457,7 +457,7 @@ const recentPendingOrders = await getEntityManager().find(Order,
 
 To understand more on how the query input looks like when it is passed to DocumentClient, have a look at [this](./how-it-works.md#querying-items)
 
-## Delete a record
+### Delete a record
 
 Items can be updated using simple update functions on entity manager and can be written like this
 
diff --git a/docs/how-to-recipes.md b/docs/how-to-recipes.md
index 005f045..eadd012 100644
--- a/docs/how-to-recipes.md
+++ b/docs/how-to-recipes.md
@@ -6,9 +6,19 @@ This page will walk you through some of the unique recipes to enhance your devel
   - [Entity configuration](#entity-configuration)
     - [Define a simple entity](#define-a-simple-entity)
     - [Define an entity with index](#define-an-entity-with-index)
+  - [Attribute Transformation](#attribute-transformation)
+    - [Using TransformToDynamo](#using-transformtodynamo)
+    - [Using TransformFromDynamo](#using-transformfromdynamo)
   - [Define default values](#define-default-values)
     - [Static Default values](#static-default-values)
     - [Dynamic Default values](#dynamic-default-values)
+  - [Advanced updates](#advanced-updates)
+    - [Static vs Dynamic updates](#static-vs-dynamic-updates)
+    - [Using SET Action](#using-set-action)
+    - [Using ADD Action](#using-add-action)
+    - [Using REMOVE Action](#using-remove-action)
+    - [Using DELETE Action](#using-delete-action)
+    - [Using multiple actions in single update](#using-multiple-actions-in-single-update)
   - [Conditional update](#conditional-update)
     - [Specify condition when using entity manager](#specify-condition-when-using-entity-manager)
   - [Conditional create](#conditional-create)
@@ -78,6 +88,53 @@ class User {
 }
 ```
 
+## Attribute Transformation
+
+Attribute transformation in TypeDORM is enabled via third party package: class-transformer.
+Most of the decorators provided by class-transformer should work by default, if there are any not behaving
+the way they should be, please consider opening an issue.
+
+All entities are passed through to class-transformer two times form any given read operation:
+
+- First, when class instance is transformed to JSON to insert into DynamoDB.
+
+  - `TransformToDynamo`: used to overwrite attribute at insertion time (serialization)
+
+- Second, when receiving JSON object from DynamoDB and returning to client.
+
+  - `TransformFromDynamo`: used to overwrite attribute at retrieval time (de-serialization)
+
+### Using TransformToDynamo
+
+```Typescript
+@Entity({
+  ...entity options
+})
+export class Photo {
+  @Attribute()
+  @Type(() => Date)
+  @TransformToDynamo(({value}: {value: Moment}) => value.toString())
+  createdAt: Moment;
+}
+
+```
+
+### Using TransformFromDynamo
+
+```Typescript
+@Entity({
+  ...entity options
+})
+export class Photo {
+
+  @Attribute()
+  @Type(() => Date)
+  @TransformFromDynamo(({value}) => moment(value))
+  createdAt: Moment;
+}
+
+```
+
 ## Define default values
 
 `@Attribute` supports specifying default values by providing default values to add to entity at the creation time.
@@ -145,6 +202,163 @@ 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.
 
+## Advanced updates
+
+Starting with 1.14.0, TypeDORM now has a full support for generating update expression for all actions in the most type safe way possible. It even allows combining multiple actions in a single requests.
+
+### Static vs Dynamic updates
+
+There are some update actions whose results can not be calculated before the update has happened (they are considered dynamic attribute updates), such as `ADD`. For these actions, it is almost impossible for TypeDORM to statically infer the end result for attributes and there for they can not be used in conjunction with other attributes that references one or many of `Indexes`, `PrimaryKey` or `UniqueAttribute`.
+
+These are the scenario where attribute to be updated can not have dynamic values:
+
+- Updating unique attributes
+- Updating primary key attributes
+- Updating attributes that references one or more indexes
+
+You should also be aware that, custom transformation applied to attribute using `@TransformToDynamo` will not run for update actions where attribute had dynamic value attached to it.
+
+Please have a look at the table below to determine if the update you are trying to perform is considered static or dynamic.
+
+| Action | Operation        | description                                                             | type    |
+| ------ | ---------------- | ----------------------------------------------------------------------- | ------- |
+| SET    | =                |                                                                         | static  |
+| SET    | \$attribute[0] = | Trying to set value of nested attribute or on a specific item in a list | dynamic |
+| SET    | IF_NOT_EXISTS    |                                                                         | static  |
+| SET    | LIST_APPEND      |                                                                         | dynamic |
+| SET    | INCREMENT_BY     |                                                                         | dynamic |
+| SET    | DECREMENT_BY     |                                                                         | dynamic |
+| ADD    | \*               |                                                                         | dynamic |
+| DELETE | \*               |                                                                         | dynamic |
+| REMOVE | \*               |                                                                         | dynamic |
+
+### Using SET Action
+
+Set operations support Implicit and Explicit syntax. For example, writing `name: {SET: "new name"}` is identical to writing `name: "new name'`.
+
+Here is an example of how one would write a `SET` update action. By default all attributes are parsed to build a SET
+action.
+
+```Typescript
+const updatedResponse = await entityManager.update(
+  User,
+  {
+    id: '1'
+  },
+  {
+    name: 'new name' // this is be considered set update
+    email: {
+      SET: 'new name' // this is same as above
+    }
+  }
+)
+
+// this will generate following update expression with given values
+// UpdateExpression: 'SET #UE_name :UE_name, #UE_email :UE_email',
+```
+
+### Using ADD Action
+
+`ADD` action is only supported for number and list types. For more up-to-date information always look at the
+[official aws-sdk guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html).
+
+```Typescript
+const updatedResponse = await entityManager.update(
+  User,
+  {
+    id: '1'
+  },
+  {
+    age: {
+      ADD: 2
+    },
+    addresses: {
+      ADD: ["new address"]
+    }
+  }
+)
+
+// this will generate following update expression with given values
+// UpdateExpression: 'ADD #UE_age :UE_age, #UE_addresses :UE_addresses',
+```
+
+### Using REMOVE Action
+
+For more up-to-date information always look at the
+[official aws-sdk guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html).
+
+```Typescript
+const updatedResponse = await entityManager.update(
+  User,
+  {
+    id: '1'
+  },
+  {
+    age: {
+      REMOVE: true
+    },
+    addresses: {
+      REMOVE: {
+        $AT_INDEX: [2]
+      }
+    }
+  }
+)
+
+// this will generate following update expression with given values
+// UpdateExpression: 'REMOVE #UE_age, #UE_addresses[2]',
+```
+
+### Using DELETE Action
+
+`DELETE` action is only supported with Set data types. For more up-to-date information always look at the
+[official aws-sdk guide](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html).
+
+```Typescript
+const updatedResponse = await entityManager.update(
+  User,
+  {
+    id: '1'
+  },
+  {
+    color: {
+      DELETE: ["red", "blue"]
+    }
+  }
+)
+
+// this will generate following update expression with given values
+// UpdateExpression: 'DELETE #UE_color :UE_color',
+```
+
+### Using multiple actions in single update
+
+```Typescript
+  const update = expInputParser.parseToUpdate<User>({
+    id: '2',
+    name: {
+      IF_NOT_EXISTS: {
+        $PATH: 'id',
+        $VALUE: '123',
+      },
+    },
+    status: {
+      SET: {
+        IF_NOT_EXISTS: 'active',
+      },
+    },
+    age: {
+      ADD: 1,
+    },
+    addresses: {
+      DELETE: ['123'],
+    },
+  });
+
+// this will generate following update expression with given values
+// UpdateExpression: 'SET #UE_id = :UE_id, #UE_name = if_not_exists(#UE_id, :UE_name), #UE_status = if_not_exists(#UE_status, :UE_status) ADD #UE_age :UE_age DELETE #UE_addresses :UE_addresses',
+```
+
 ## Conditional update
 
 ### Specify condition when using entity manager
diff --git a/package-lock.json b/package-lock.json
index 98985ad..b5adb72 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2079,6 +2079,11 @@
       "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==",
       "dev": true
     },
+    "class-transformer": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz",
+      "integrity": "sha512-ETWD/H2TbWbKEi7m9N4Km5+cw1hNcqJSxlSYhsLsNjQzWWiZIYA1zafxpK9PwVfaZ6AqR5rrjPVUBGESm5tQUA=="
+    },
     "class-utils": {
       "version": "0.3.6",
       "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@@ -5750,6 +5755,11 @@
       "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==",
       "dev": true
     },
+    "moment": {
+      "version": "2.29.1",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
+      "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
+    },
     "ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
diff --git a/package.json b/package.json
index 7ec7c7a..832b312 100644
--- a/package.json
+++ b/package.json
@@ -22,9 +22,11 @@
   "dependencies": {
     "aws-sdk": "^2.799.0",
     "chalk": "^4.1.0",
+    "class-transformer": "^0.4.0",
     "debug": "^4.3.1",
     "fast-glob": "^3.2.4",
     "ksuid": "^2.0.0",
+    "moment": "^2.29.1",
     "p-limit": "^3.1.0",
     "reflect-metadata": "^0.1.13",
     "uuid": "^8.3.1"
diff --git a/packages/common/BUILD b/packages/common/BUILD
index 22e6538..b87b94d 100644
--- a/packages/common/BUILD
+++ b/packages/common/BUILD
@@ -10,7 +10,8 @@ setup_ts_build(
         "@npm//@types/uuid",
         "@npm//debug",
         "@npm//@types/debug",
-        "@npm//chalk"
+        "@npm//chalk",
+        "@npm//class-transformer"
     ],
     visibility = ["//packages:__subpackages__"]
 )
diff --git a/packages/common/package.json b/packages/common/package.json
index 64a767d..8442afc 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -14,7 +14,8 @@
   "dependencies": {
     "debug": "^4.3.1",
     "chalk": "^4.1.0",
-    "uuid": "^8.3.1"
+    "uuid": "^8.3.1",
+    "class-transformer": "^0.4.0"
   },
   "publishConfig": {
     "tag": "TAG-PLACEHOLDER",
diff --git a/packages/common/public-api.ts b/packages/common/public-api.ts
index 8923d6f..e89a9b9 100644
--- a/packages/common/public-api.ts
+++ b/packages/common/public-api.ts
@@ -2,6 +2,8 @@
 export * from './src/decorators/attribute.decorator';
 export * from './src/decorators/auto-generate-attribute.decorator';
 export * from './src/decorators/entity.decorator';
+export * from './src/decorators/transform-from-dynamo.decorator';
+export * from './src/decorators/transform-to-dynamo.decorator';
 
 // utils
 export * from './src/helpers/common-types';
@@ -16,11 +18,11 @@ export * from './src/helpers/require-at-least-one-type';
 export * from './src/helpers/require-at-least-two-type';
 export * from './src/helpers/require-only-one-type';
 export * from './src/helpers/scalar-type';
-export * from './src/helpers/update-attributes-type';
 export * from './src/helpers/replace-type';
 export * from './src/helpers/is-primary-key';
 export * from './src/helpers/nested-attributes-type';
 export * from './src/utils/get-entity-definition';
+export * from './src/helpers/invalid-type';
 
 // exceptions
 export * from './src/exceptions';
diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts
index e903387..baefe2b 100644
--- a/packages/common/src/constants.ts
+++ b/packages/common/src/constants.ts
@@ -34,6 +34,11 @@ export const BATCH_READ_ITEMS_LIMIT = 100;
  */
 export const BATCH_WRITE_CONCURRENCY_LIMIT = 5;
 
+/**
+ * Concurrency limit to apply, when running parallel scan requests
+ */
+export const PARALLEL_SCAN_CONCURRENCY_LIMIT = 10;
+
 /**
  * Max number of allowed attempts for batch write items
  */
diff --git a/packages/common/src/decorators/transform-from-dynamo.decorator.ts b/packages/common/src/decorators/transform-from-dynamo.decorator.ts
new file mode 100644
index 0000000..f95f496
--- /dev/null
+++ b/packages/common/src/decorators/transform-from-dynamo.decorator.ts
@@ -0,0 +1,17 @@
+import {Transform} from 'class-transformer';
+import {HeadType} from '../helpers/head-type';
+
+/**
+ * Simple @Transform Wrapper to only run when transforming items from DynamoDB
+ * @param transformFn Transform function to run, this is passed to class-transformer without any modifications
+ *
+ * @see https://github.com/typestack/class-transformer#basic-usage for usage examples
+ */
+export const TransformFromDynamo = (
+  transformFn: HeadType<Parameters<typeof Transform>>
+) => {
+  return Transform(transformFn, {
+    // run only for when transforming object literals to class type
+    toClassOnly: true,
+  });
+};
diff --git a/packages/common/src/decorators/transform-to-dynamo.decorator.ts b/packages/common/src/decorators/transform-to-dynamo.decorator.ts
new file mode 100644
index 0000000..0591154
--- /dev/null
+++ b/packages/common/src/decorators/transform-to-dynamo.decorator.ts
@@ -0,0 +1,17 @@
+import {Transform} from 'class-transformer';
+import {HeadType} from '../helpers/head-type';
+
+/**
+ * Simple @Transform wrapper to run when transforming items for to insert into DynamoDB
+ * @param transformFn Transform function to run, this is passed to class-transformer without any modifications
+ *
+ * @see https://github.com/typestack/class-transformer#basic-usage for usage examples
+ */
+export const TransformToDynamo = (
+  transformFn: HeadType<Parameters<typeof Transform>>
+) => {
+  return Transform(transformFn, {
+    // run only for when transforming class instance to object literal
+    toPlainOnly: true,
+  });
+};
diff --git a/packages/common/src/error/duplicate-entity-physical-name-error.ts b/packages/common/src/error/duplicate-entity-physical-name-error.ts
new file mode 100644
index 0000000..07de3b0
--- /dev/null
+++ b/packages/common/src/error/duplicate-entity-physical-name-error.ts
@@ -0,0 +1,9 @@
+export class DuplicateEntityPhysicalNameError extends Error {
+  name = 'DuplicateEntityPhysicalNameError';
+
+  constructor(physicalName: string) {
+    super();
+    this.message = `Entity physical name must be unique for all entities. 
+    Physical name "${physicalName}" already exists.`;
+  }
+}
diff --git a/packages/common/src/error/index.ts b/packages/common/src/error/index.ts
index 8b4c31c..de762d3 100644
--- a/packages/common/src/error/index.ts
+++ b/packages/common/src/error/index.ts
@@ -10,3 +10,13 @@ export * from './invalid-transaction-read-item-error';
 export * from './read-transaction-item-limit-exceeded-error';
 export * from './invalid-attribute-alias-schema-error';
 export * from './no-such-attribute-exists-error';
+export * from './no-such-index-found-error';
+export * from './invalid-filter-input-error';
+export * from './invalid-select-input-error';
+export * from './no-such-entity-exists-error';
+export * from './missing-required-entity-physical-name-error';
+export * from './duplicate-entity-physical-name-error';
+export * from './invalid-unique-attribute-update-error';
+export * from './invalid-primary-key-attributes-update-error';
+export * from './invalid-parallel-scan-limit-option-error';
+export * from './invalid-dynamic-update-attribute-value';
diff --git a/packages/common/src/error/invalid-dynamic-update-attribute-value.ts b/packages/common/src/error/invalid-dynamic-update-attribute-value.ts
new file mode 100644
index 0000000..217dc8b
--- /dev/null
+++ b/packages/common/src/error/invalid-dynamic-update-attribute-value.ts
@@ -0,0 +1,16 @@
+export class InvalidDynamicUpdateAttributeValueError extends Error {
+  name = 'InvalidDynamicUpdateAttributeValueError';
+
+  constructor(attributeName: string, attributeValue: any) {
+    super();
+    this.message = `Invalid dynamic value "${JSON.stringify(
+      attributeValue
+    )}" received for attribute "${attributeName}".
+    Any value that can not be statically recognized is considered a dynamic value, 
+    this could either be "SET" action with +,-,[<index>] operators or ADD, DELETE, REMOVE.
+
+    When attribute is referenced in one more "indexes", "primary key" updates or marked as "unique",
+    dynamic actions can not be performed.
+    `;
+  }
+}
diff --git a/packages/common/src/error/invalid-filter-input-error.ts b/packages/common/src/error/invalid-filter-input-error.ts
new file mode 100644
index 0000000..ca7a768
--- /dev/null
+++ b/packages/common/src/error/invalid-filter-input-error.ts
@@ -0,0 +1,10 @@
+export class InvalidFilterInputError extends Error {
+  name = 'InvalidFilterInputError';
+
+  constructor(input: any) {
+    super();
+    this.message = `Failed to build filter expression for input: ${JSON.stringify(
+      input
+    )}.`;
+  }
+}
diff --git a/packages/common/src/error/invalid-parallel-scan-limit-option-error.ts b/packages/common/src/error/invalid-parallel-scan-limit-option-error.ts
new file mode 100644
index 0000000..a040b6b
--- /dev/null
+++ b/packages/common/src/error/invalid-parallel-scan-limit-option-error.ts
@@ -0,0 +1,10 @@
+export class InvalidParallelScanLimitOptionError extends Error {
+  name = 'InvalidParallelScanLimitOptionError';
+  constructor(limit: number, limitPerSegment: number) {
+    super();
+    this.message = `Invalid scan option "limit", when using parallel scan, value for "limitPerSegment" cannot be greater than "limit". Consider using a scan operation instead.    
+    limit: ${limit}
+    limitPerSegment: ${limitPerSegment}
+    `;
+  }
+}
diff --git a/packages/common/src/error/invalid-primary-key-attributes-update-error.ts b/packages/common/src/error/invalid-primary-key-attributes-update-error.ts
new file mode 100644
index 0000000..97ce9cb
--- /dev/null
+++ b/packages/common/src/error/invalid-primary-key-attributes-update-error.ts
@@ -0,0 +1,17 @@
+export class InvalidPrimaryKeyAttributesUpdateError extends Error {
+  name = 'InvalidPrimaryKeyAttributesUpdateError';
+
+  constructor(
+    affectedPrimaryKeyAttributes: Record<string, string>,
+    nonKeyAttributes: string[]
+  ) {
+    super();
+    this.message = `Primary key attribute and non primary key attributes can not be
+    updated together. When updating attributes that references a primary key, it can not have
+    additional non key attributes.
+    \n Affected Primary key attributes: ${JSON.stringify(
+      affectedPrimaryKeyAttributes
+    )}
+    \n Tried to update non-key attributes: ${JSON.stringify(nonKeyAttributes)}`;
+  }
+}
diff --git a/packages/common/src/error/invalid-select-input-error.ts b/packages/common/src/error/invalid-select-input-error.ts
new file mode 100644
index 0000000..882a24a
--- /dev/null
+++ b/packages/common/src/error/invalid-select-input-error.ts
@@ -0,0 +1,10 @@
+export class InvalidSelectInputError extends Error {
+  name = 'InvalidSelectInputError';
+
+  constructor(input: any) {
+    super();
+    this.message = `Failed to build projection expression for input: ${JSON.stringify(
+      input
+    )}.`;
+  }
+}
diff --git a/packages/common/src/error/invalid-unique-attribute-update-error.ts b/packages/common/src/error/invalid-unique-attribute-update-error.ts
new file mode 100644
index 0000000..ed29a33
--- /dev/null
+++ b/packages/common/src/error/invalid-unique-attribute-update-error.ts
@@ -0,0 +1,12 @@
+export class InvalidUniqueAttributeUpdateError extends Error {
+  name = 'InvalidUniqueAttributeUpdateError';
+
+  constructor(primaryKey: Record<string, string>, attributes: string[]) {
+    super();
+    this.message = `Updates on a unique attributes is not supported when it is also referenced by 
+    primary key.
+    \n Affected primary key: ${JSON.stringify(primaryKey)}.
+    \n Unique Attributes tried to update: ${JSON.stringify(attributes)}.
+    \n If this is something you would like to have implemented, please file a new issue.`;
+  }
+}
diff --git a/packages/common/src/error/missing-required-entity-physical-name-error.ts b/packages/common/src/error/missing-required-entity-physical-name-error.ts
new file mode 100644
index 0000000..f375f69
--- /dev/null
+++ b/packages/common/src/error/missing-required-entity-physical-name-error.ts
@@ -0,0 +1,8 @@
+export class MissingRequiredEntityPhysicalNameError extends Error {
+  name = 'MissingRequiredEntityPhysicalNameError';
+
+  constructor(className: string) {
+    super();
+    this.message = `Missing required entity physical name for entity "${className}". Physical names must be non empty and unique.`;
+  }
+}
diff --git a/packages/common/src/error/no-such-entity-exists-error.ts b/packages/common/src/error/no-such-entity-exists-error.ts
new file mode 100644
index 0000000..ceacc3b
--- /dev/null
+++ b/packages/common/src/error/no-such-entity-exists-error.ts
@@ -0,0 +1,8 @@
+export class NoSuchEntityExistsError extends Error {
+  name = 'NoSuchEntityExistsError';
+  constructor(entityName: string) {
+    super();
+    this.message = `No such entity named "${entityName}" is known to TypeDORM. If such entity exists, 
+    please make sure it is registered to current connection.`;
+  }
+}
diff --git a/packages/common/src/error/no-such-index-found-error.ts b/packages/common/src/error/no-such-index-found-error.ts
new file mode 100644
index 0000000..b199813
--- /dev/null
+++ b/packages/common/src/error/no-such-index-found-error.ts
@@ -0,0 +1,9 @@
+export class NoSuchIndexFoundError extends Error {
+  name = 'NoSuchIndexFoundError';
+
+  constructor(tableName: string, indexName: string) {
+    super();
+    this.message = `No such index "${indexName}" was found on table "${tableName},"
+    Please check if you have provided correct index option and it was configured correctly.`;
+  }
+}
diff --git a/packages/common/src/helpers/head-type.ts b/packages/common/src/helpers/head-type.ts
new file mode 100644
index 0000000..1b19ce1
--- /dev/null
+++ b/packages/common/src/helpers/head-type.ts
@@ -0,0 +1,3 @@
+export type HeadType<T extends any[]> = T extends [any, ...any[]]
+  ? T[0]
+  : never;
diff --git a/packages/common/src/helpers/invalid-type.ts b/packages/common/src/helpers/invalid-type.ts
new file mode 100644
index 0000000..5794c4e
--- /dev/null
+++ b/packages/common/src/helpers/invalid-type.ts
@@ -0,0 +1,7 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export interface InvalidType<ErrorMessageT extends any[] | any> {
+  /**
+   * There should never be a value of this type
+   */
+  readonly __compileError: never;
+}
diff --git a/packages/common/src/helpers/prop-type.ts b/packages/common/src/helpers/prop-type.ts
new file mode 100644
index 0000000..a18d94f
--- /dev/null
+++ b/packages/common/src/helpers/prop-type.ts
@@ -0,0 +1,1 @@
+export type PropType<Entity, Key extends keyof Entity> = Entity[Key];
diff --git a/packages/common/src/helpers/require-only-one-type.ts b/packages/common/src/helpers/require-only-one-type.ts
index 30c6984..c053066 100644
--- a/packages/common/src/helpers/require-only-one-type.ts
+++ b/packages/common/src/helpers/require-only-one-type.ts
@@ -1,9 +1,11 @@
 // tslint:disable-next-line: max-line-length
 // Ref: https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist/49725198#49725198
 
+import {InvalidType} from './invalid-type';
+
 type InternalPickOne<T, Keys extends keyof T> = {
   [K in Keys]: Required<Pick<T, K>> &
-    Partial<Record<Exclude<Keys, K>, undefined>>;
+    Partial<Record<Exclude<Keys, K>, InvalidType<T>>>;
 }[Keys];
 
 export type RequireOnlyOne<T, Keys extends keyof T = keyof T> = Omit<T, Keys> &
diff --git a/packages/common/src/helpers/update-attributes-type.ts b/packages/common/src/helpers/update-attributes-type.ts
deleted file mode 100644
index 5c15f53..0000000
--- a/packages/common/src/helpers/update-attributes-type.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import {NonKeyAttributes} from './non-key-attributes-type';
-export type UpdateAttributes<Entity, PrimaryKey> = NonKeyAttributes<
-  Entity,
-  PrimaryKey
-> & {
-  // this is required to support additional nested attributes that may exist on the entity
-  // i.e when trying to use nested attribute like this `user.name.firstName`.
-  [key: string]: any;
-};
diff --git a/packages/common/src/logger/debug-logger.ts b/packages/common/src/logger/debug-logger.ts
index 8f8642d..bedd845 100644
--- a/packages/common/src/logger/debug-logger.ts
+++ b/packages/common/src/logger/debug-logger.ts
@@ -15,12 +15,18 @@ export enum MANAGER_NAME {
   ENTITY_MANAGER = 'ENTITY MANAGER',
   TRANSACTION_MANAGER = 'TRANSACTION MANAGER',
   BATCH_MANAGER = 'BATCH MANAGER',
+  SCAN_MANAGER = 'SCAN_MANAGER',
 }
 export enum TRANSFORM_TRANSACTION_TYPE {
   TRANSACTION_WRITE = 'TRANSACTION_WRITE',
   TRANSACTION_READ = 'TRANSACTION_READ',
 }
 
+export enum TRANSFORM_SCAN_TYPE {
+  SCAN = 'SCAN',
+  PARALLEL_SCAN = 'PARALLEL_SCAN',
+}
+
 export enum TRANSFORM_BATCH_TYPE {
   BATCH_WRITE = 'BATCH_WRITE',
   BATCH_READ = 'BATCH_READ',
@@ -39,6 +45,8 @@ export class DebugLogger {
   private debugTransformTransactionLog = debug(
     'typedorm:transform:transaction:log'
   );
+  // scan transform logger
+  private debugTransformScanLog = debug('typedorm:transform:scan:log');
   // info logger
   private debugInfoLog = debug('typedorm:info:log');
   private debugWarnLog = debug('typedorm:warn:log');
@@ -167,6 +175,40 @@ export class DebugLogger {
     }
   }
 
+  logTransformScan({
+    requestId,
+    prefix,
+    operation,
+    body,
+    options,
+  }: {
+    requestId?: string;
+    operation: TRANSFORM_SCAN_TYPE;
+    prefix: string;
+    body?: any;
+    options?: any;
+  }) {
+    if (this.debugTransformScanLog.enabled) {
+      this.debugTransformScanLog(
+        `${chalk.bold.bgCyanBright(requestId)} ${chalk.green(
+          operation
+        )} ${chalk.magenta(prefix)}:`,
+        ...(body
+          ? [
+              chalk.blueBright('\nBody: '),
+              chalk.white(this.ensurePrintable(body)),
+            ]
+          : []),
+        ...(options
+          ? [
+              chalk.blueBright('\nOptions: '),
+              chalk.white(this.ensurePrintable(options)),
+            ]
+          : [])
+      );
+    }
+  }
+
   logStats({
     requestId,
     requestSegment,
diff --git a/packages/common/src/operator-types.ts b/packages/common/src/operator-types.ts
index 6b5d5c9..b740f5b 100644
--- a/packages/common/src/operator-types.ts
+++ b/packages/common/src/operator-types.ts
@@ -46,6 +46,12 @@ export namespace FilterType {
   export type LogicalOperator = ConditionType.LogicalOperator;
 }
 
+export namespace UpdateType {
+  export type ArithmeticOperator = 'INCREMENT_BY' | 'DECREMENT_BY';
+  export type SetUpdateOperator = 'IF_NOT_EXISTS' | 'LIST_APPEND';
+  export type Action = 'SET' | 'REMOVE' | 'ADD' | 'DELETE';
+}
+
 export type SimpleOperator =
   | KeyConditionType.SimpleOperator
   | ConditionType.SimpleOperator
diff --git a/packages/core/BUILD b/packages/core/BUILD
index f8fc8c8..e7ad213 100644
--- a/packages/core/BUILD
+++ b/packages/core/BUILD
@@ -11,7 +11,8 @@ setup_ts_build(
         "@npm//ksuid",
         "@npm//fast-glob",
         "@npm//@types/uuid",
-        "@npm//p-limit"
+        "@npm//p-limit",
+        "@npm//class-transformer"
     ],
     visibility = ["//packages:__subpackages__"]
 
diff --git a/packages/core/__mocks__/photo.ts b/packages/core/__mocks__/photo.ts
new file mode 100644
index 0000000..2389a7d
--- /dev/null
+++ b/packages/core/__mocks__/photo.ts
@@ -0,0 +1,88 @@
+import {
+  Attribute,
+  AutoGenerateAttribute,
+  AUTO_GENERATE_ATTRIBUTE_STRATEGY,
+  Entity,
+  INDEX_TYPE,
+  TransformFromDynamo,
+  TransformToDynamo,
+} from '@typedorm/common';
+import {table} from './table';
+
+// Moment is only being used here to display the usage of @transform utility
+// eslint-disable-next-line node/no-extraneous-import
+import moment from 'moment';
+// eslint-disable-next-line node/no-extraneous-import
+import {Moment} from 'moment';
+
+export enum CATEGORY {
+  PETS = 'PETS',
+  KIDS = 'KIDS',
+}
+
+export interface PhotoPrimaryKey {
+  category: CATEGORY;
+  id: number;
+}
+
+@Entity({
+  table,
+  name: 'photo',
+  primaryKey: {
+    partitionKey: 'PHOTO#{{category}}',
+    sortKey: 'PHOTO#{{id}}',
+  },
+  indexes: {
+    GSI1: {
+      partitionKey: 'PHOTO#{{id}}',
+      sortKey: 'PHOTO#{{category}}',
+      type: INDEX_TYPE.GSI,
+    },
+  },
+})
+export class Photo implements PhotoPrimaryKey {
+  @AutoGenerateAttribute({
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4,
+  })
+  id: number;
+
+  @Attribute()
+  @TransformToDynamo(({value}) => {
+    if (value === CATEGORY.KIDS) {
+      return 'kids-new';
+    } else {
+      return value;
+    }
+  })
+  category: CATEGORY;
+
+  @Attribute()
+  name: string;
+
+  @Attribute({
+    default: () => new Date().toISOString(),
+  })
+  @TransformToDynamo(({value}: {value: Date}) =>
+    moment(value, moment.ISO_8601).utc().format('yyyy-MM-DD')
+  )
+  @TransformFromDynamo(({value}) => moment(value, 'yyyy-MM-DD'))
+  createdAt: Moment;
+
+  constructor(category: CATEGORY, name: string) {
+    this.name = name;
+    this.category = category;
+  }
+
+  @AutoGenerateAttribute({
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE,
+    autoUpdate: true,
+  })
+  @TransformToDynamo(({value}) => {
+    return value.toString();
+  })
+  updatedAt: Date;
+
+  createdDate() {
+    return this.createdAt.format('MM-DD-YYYY');
+  }
+}
diff --git a/packages/core/__mocks__/user-auto-generate-attributes.ts b/packages/core/__mocks__/user-auto-generate-attributes.ts
index 7ad3f59..d9bc0e3 100644
--- a/packages/core/__mocks__/user-auto-generate-attributes.ts
+++ b/packages/core/__mocks__/user-auto-generate-attributes.ts
@@ -13,7 +13,7 @@ export interface UserAutoGenerateAttributesPrimaryKey {
 
 @Entity({
   table,
-  name: 'user',
+  name: 'user-auto-generate-attr',
   primaryKey: {
     partitionKey: 'USER#{{id}}',
     sortKey: 'USER#{{id}}',
diff --git a/packages/core/__mocks__/user-custom-constructor.ts b/packages/core/__mocks__/user-custom-constructor.ts
new file mode 100644
index 0000000..7b7d56f
--- /dev/null
+++ b/packages/core/__mocks__/user-custom-constructor.ts
@@ -0,0 +1,26 @@
+import {Attribute, Entity} from '@typedorm/common';
+import {table} from './table';
+
+export interface UserPrimaryKey {
+  id: string;
+}
+
+@Entity({
+  table,
+  name: 'user-custom-constructor',
+  primaryKey: {
+    partitionKey: 'USER#{{id}}',
+    sortKey: 'USER#{{id}}',
+  },
+})
+export class UserCustomConstructor implements UserPrimaryKey {
+  @Attribute()
+  id: string;
+
+  @Attribute()
+  name: string;
+
+  constructor({name}: Partial<UserCustomConstructor>) {
+    this.name = name!;
+  }
+}
diff --git a/packages/core/__mocks__/user-unique-email.ts b/packages/core/__mocks__/user-unique-email.ts
index d89f62e..06a345b 100644
--- a/packages/core/__mocks__/user-unique-email.ts
+++ b/packages/core/__mocks__/user-unique-email.ts
@@ -12,7 +12,7 @@ export interface UserUniqueEmailGSI1 {
 
 @Entity({
   table,
-  name: 'user',
+  name: 'user-unique-email',
   primaryKey: {
     partitionKey: 'USER#{{id}}',
     sortKey: 'USER#{{id}}',
diff --git a/packages/core/__mocks__/user.ts b/packages/core/__mocks__/user.ts
index 2f2dfd7..07144fd 100644
--- a/packages/core/__mocks__/user.ts
+++ b/packages/core/__mocks__/user.ts
@@ -38,4 +38,7 @@ export class User implements UserPrimaryKey, UserGSI1 {
 
   @Attribute()
   age: number;
+
+  @Attribute()
+  addresses: string[];
 }
diff --git a/packages/core/package.json b/packages/core/package.json
index 57c00dc..f02bb5d 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -17,7 +17,8 @@
     "uuid": "^8.3.1",
     "ksuid": "^2.0.0",
     "fast-glob": "^3.2.4",
-    "p-limit": "^3.1.0"
+    "p-limit": "^3.1.0",
+    "class-transformer": "^0.4.0"
   },
   "publishConfig": {
     "tag": "TAG-PLACEHOLDER",
diff --git a/packages/core/public-api.ts b/packages/core/public-api.ts
index 5fece64..de6c735 100644
--- a/packages/core/public-api.ts
+++ b/packages/core/public-api.ts
@@ -18,6 +18,9 @@ export * from './src/classes/manager/entity-manager';
 export * from './src/classes/manager/batch-manager';
 export * from './src/classes/manager/transaction-manager';
 
+// classes
+export {Connection} from './src/classes/connection/connection';
+
 // public method exports
 
 export function createConnection(options: ConnectionOptions) {
@@ -54,6 +57,10 @@ export function getBatchManager(connectionName?: string) {
   return connectionManger().get(connectionName).batchManager;
 }
 
+export function getScanManager(connectionName?: string) {
+  return connectionManger().get(connectionName).scanManager;
+}
+
 // private methods
 
 function connectionManger() {
diff --git a/packages/core/src/classes/batch/type-guards.ts b/packages/core/src/classes/batch/type-guards.ts
index 4f2edaf..730eb0e 100644
--- a/packages/core/src/classes/batch/type-guards.ts
+++ b/packages/core/src/classes/batch/type-guards.ts
@@ -4,14 +4,14 @@ import {WriteBatchCreate, WriteBatchDelete} from './write-batch';
 export function isBatchAddCreateItem<Entity>(
   item: any
 ): item is WriteBatchCreate<Entity> {
-  return !!(item as WriteBatchCreate<Entity>).create && !isEmptyObject(item);
+  return !isEmptyObject(item) && !!(item as WriteBatchCreate<Entity>).create;
 }
 
 export function isBatchAddDeleteItem<Entity, PrimaryKey>(
   item: any
 ): item is WriteBatchDelete<Entity, PrimaryKey> {
   return (
-    !!(item as WriteBatchDelete<Entity, PrimaryKey>).delete &&
-    !isEmptyObject(item)
+    !isEmptyObject(item) &&
+    !!(item as WriteBatchDelete<Entity, PrimaryKey>).delete
   );
 }
diff --git a/packages/core/src/classes/connection/connection-metadata-builder.ts b/packages/core/src/classes/connection/connection-metadata-builder.ts
index 54f7ae9..6bdcd30 100644
--- a/packages/core/src/classes/connection/connection-metadata-builder.ts
+++ b/packages/core/src/classes/connection/connection-metadata-builder.ts
@@ -1,9 +1,14 @@
 import fg from 'fast-glob';
 import path from 'path';
-import {EntityTarget} from '@typedorm/common';
+import {
+  DuplicateEntityPhysicalNameError,
+  EntityTarget,
+  MissingRequiredEntityPhysicalNameError,
+} from '@typedorm/common';
 import {Connection} from './connection';
 import {EntityMetadataBuilder} from './entity-metadata-builder';
 import {MetadataManager} from '@typedorm/common';
+import {EntityMetadata} from '../metadata/entity-metadata';
 
 export class ConnectionMetadataBuilder {
   constructor(private connection: Connection) {}
@@ -21,7 +26,12 @@ export class ConnectionMetadataBuilder {
       MetadataManager.metadataStorage.hasKnownEntity(entity)
     );
 
-    return new EntityMetadataBuilder(this.connection).build(entitiesToBuild);
+    const entityMetadatas = new EntityMetadataBuilder(this.connection).build(
+      entitiesToBuild
+    );
+    this.sanitizeDuplicates(entityMetadatas);
+
+    return entityMetadatas;
   }
 
   private loadEntitiesFromDirs(pathMatchPattern: string) {
@@ -48,4 +58,19 @@ export class ConnectionMetadataBuilder {
     }
     return allLoaded;
   }
+
+  private sanitizeDuplicates(metadata: EntityMetadata[]) {
+    const cache: string[] = [];
+    metadata.forEach(metadata => {
+      if (!metadata.name) {
+        throw new MissingRequiredEntityPhysicalNameError(metadata.target.name);
+      }
+
+      if (cache.includes(metadata.name)) {
+        throw new DuplicateEntityPhysicalNameError(metadata.name);
+      } else {
+        cache.push(metadata.name);
+      }
+    });
+  }
 }
diff --git a/packages/core/src/classes/connection/connection.ts b/packages/core/src/classes/connection/connection.ts
index 7e40937..08b23f9 100644
--- a/packages/core/src/classes/connection/connection.ts
+++ b/packages/core/src/classes/connection/connection.ts
@@ -5,11 +5,13 @@ import {
   Table,
   DebugLogger,
   getEntityDefinition,
+  NoSuchEntityExistsError,
 } from '@typedorm/common';
 import {DynamoDB} from 'aws-sdk';
 import {isUsedForPrimaryKey} from '../../helpers/is-used-for-primary-key';
 import {BatchManager} from '../manager/batch-manager';
 import {EntityManager} from '../manager/entity-manager';
+import {ScanManager} from '../manager/scan-manager';
 import {TransactionManager} from '../manager/transaction-manager';
 import {AttributeMetadata} from '../metadata/attribute-metadata';
 import {AutoGeneratedAttributeMetadata} from '../metadata/auto-generated-attribute-metadata';
@@ -26,6 +28,7 @@ export class Connection {
   readonly entityManager: EntityManager;
   readonly transactionManger: TransactionManager;
   readonly batchManager: BatchManager;
+  readonly scanManager: ScanManager;
   readonly defaultConfig: {queryItemsImplicitLimit: number};
   readonly documentClient: DynamoDB.DocumentClient;
   readonly logger: DebugLogger;
@@ -45,6 +48,7 @@ export class Connection {
     this.entityManager = new EntityManager(this);
     this.batchManager = new BatchManager(this);
     this.transactionManger = new TransactionManager(this);
+    this.scanManager = new ScanManager(this);
     this.defaultConfig = {
       queryItemsImplicitLimit:
         options.dynamoQueryItemsImplicitLimit ??
@@ -117,12 +121,6 @@ export class Connection {
   getUniqueAttributesForEntity<Entity>(entityClass: EntityTarget<Entity>) {
     const entityMetadata = this.getEntityByTarget(entityClass);
 
-    if (!entityMetadata) {
-      throw new Error(
-        'Could not get unique attributes for entity, every class to be used as entity must have @Entity decorator on it.'
-      );
-    }
-
     return this.getAttributesForEntity<Entity>(entityClass).filter(attr => {
       // only attributes that are not part of primary key should be included
       return (
@@ -136,6 +134,24 @@ export class Connection {
     >[];
   }
 
+  /**
+   * Returns a list of attribute names that are referenced in primary key
+   * @param entityClass Entity to get primary key attributes for
+   * @returns
+   */
+  getPrimaryKeyAttributeInterpolationsForEntity<Entity>(
+    entityClass: EntityTarget<Entity>
+  ) {
+    const entityMetadata = this.getEntityByTarget(entityClass);
+    return [
+      ...new Set(
+        Object.values(
+          entityMetadata.schema.primaryKey.metadata._interpolations ?? {}
+        ).flat()
+      ),
+    ];
+  }
+
   getEntityByTarget<Entity>(entityClass: EntityTarget<Entity>) {
     const metadata = this._entityMetadatas.get(entityClass.name);
     if (!metadata) {
@@ -150,9 +166,7 @@ export class Connection {
     const entitySpec = getEntityDefinition(name);
 
     if (!entitySpec) {
-      throw new Error(
-        `No such entity with physical name (__en) "${name}" was found, if this continues please file an issue on github`
-      );
+      throw new NoSuchEntityExistsError(name);
     }
     return this.getEntityByTarget(entitySpec.target);
   }
diff --git a/packages/core/src/classes/expression/base-expression-input.ts b/packages/core/src/classes/expression/base-expression-input.ts
index 340a6ef..0d36322 100644
--- a/packages/core/src/classes/expression/base-expression-input.ts
+++ b/packages/core/src/classes/expression/base-expression-input.ts
@@ -8,8 +8,8 @@ export enum MERGE_STRATEGY {
 
 export abstract class BaseExpressionInput {
   expression: string;
-  _names?: {[key: string]: any};
-  _values?: {[key: string]: any};
+  protected _names?: {[key: string]: any};
+  protected _values?: {[key: string]: any};
 
   constructor() {
     this.expression = '';
@@ -124,19 +124,19 @@ export abstract class BaseExpressionInput {
   ): this {
     const {expression, names, values} = condition;
 
+    // if merging condition does not have anything to merge return
+    if (!expression) {
+      return this;
+    }
+
     // if base condition does not have any expression replace
     if (!this.expression) {
-      this.expression = expression;
+      this.expression += expression;
       this.names = names;
       this.values = values;
       return this;
     }
 
-    // if merging condition does not have anything to merge return
-    if (!expression) {
-      return this;
-    }
-
     if (strategy === MERGE_STRATEGY.OR) {
       this.or().appendToExpression(`(${expression})`);
     } else {
@@ -226,7 +226,8 @@ export abstract class BaseExpressionInput {
     }
   }
 
-  /** Use merge instead
+  /**
+   * Use merge instead
    * @deprecated
    */
   or(): this {
@@ -346,7 +347,11 @@ export abstract class BaseExpressionInput {
     return this;
   }
 
-  addBaseOperator(operator: SimpleOperator, key: string, value: any): this {
+  protected addBaseOperator(
+    operator: SimpleOperator,
+    key: string,
+    value: any
+  ): this {
     const attrExpName = this.addExpressionName(key);
     const attrExpValue = this.addExpressionValue(key, value);
     this.appendToExpression(
@@ -355,7 +360,7 @@ export abstract class BaseExpressionInput {
     return this;
   }
 
-  private getSymbolForOperator(operator: SimpleOperator): string {
+  protected getSymbolForOperator(operator: SimpleOperator): string {
     const symbolMap = {
       EQ: '=',
       LE: '<=',
diff --git a/packages/core/src/classes/expression/expression-builder.ts b/packages/core/src/classes/expression/expression-builder.ts
index 3ee7997..b912c0c 100644
--- a/packages/core/src/classes/expression/expression-builder.ts
+++ b/packages/core/src/classes/expression/expression-builder.ts
@@ -1,11 +1,11 @@
 import {Table} from '@typedorm/common';
 import {isEmptyObject} from '../../helpers/is-empty-object';
-import {isObject} from '../../helpers/is-object';
 import {MERGE_STRATEGY} from './base-expression-input';
 import {Condition} from './condition';
 import {Filter} from './filter';
 import {KeyCondition} from './key-condition';
 import {Projection} from './projection';
+import {Update} from './update/update';
 
 export class ExpressionBuilder {
   andMergeConditionExpressions(
@@ -103,58 +103,16 @@ export class ExpressionBuilder {
     return this.removeEmptyFieldsAndReturn(expression);
   }
 
-  buildUpdateExpression(
-    item: {[key: string]: any},
-    options?: {
-      nestedKeySeparator: string;
-    }
-  ) {
-    if (!isObject(item)) {
-      throw new Error('Item to generate expression must be of type object');
+  buildUpdateExpression(update: Update) {
+    if (!update.expression) {
+      return {};
     }
 
-    if (isEmptyObject(item)) {
-      throw new Error(
-        'Item to generate expression must contain at least one attribute'
-      );
-    }
-    const nestedKeySeparator = options?.nestedKeySeparator || '.';
-
-    // build update set expression
-    const expression = Object.keys(item).reduce(
-      (acc, key, index) => {
-        // only append comma from second key
-        if (index !== 0) {
-          acc.UpdateExpression += ',';
-        }
-
-        let expAttrKey = '';
-
-        if (key.includes(nestedKeySeparator)) {
-          const pathParts = key.split(nestedKeySeparator);
-          expAttrKey = pathParts
-            .map((innerKey, innerIndex) => {
-              const innerKeyExp = `#attr${index}_inner${innerIndex}`;
-              acc.ExpressionAttributeNames[innerKeyExp] = innerKey;
-              return innerKeyExp;
-            })
-            .join('.');
-        } else {
-          expAttrKey = `#attr${index}`;
-          acc.ExpressionAttributeNames[expAttrKey] = key;
-        }
-
-        const expValKey = `:val${index}`;
-        acc.UpdateExpression += ` ${expAttrKey} = ${expValKey}`;
-        acc.ExpressionAttributeValues[expValKey] = item[key] ?? null;
-        return acc;
-      },
-      {
-        UpdateExpression: 'SET',
-        ExpressionAttributeNames: {} as {[key: string]: string},
-        ExpressionAttributeValues: {} as {[key: string]: any},
-      }
-    );
+    const expression = {
+      UpdateExpression: update.expression.trim(),
+      ExpressionAttributeNames: update.names,
+      ExpressionAttributeValues: update.values,
+    };
     return this.removeEmptyFieldsAndReturn(expression);
   }
 
diff --git a/packages/core/src/classes/expression/expression-input-parser.ts b/packages/core/src/classes/expression/expression-input-parser.ts
index bde6c68..2e38947 100644
--- a/packages/core/src/classes/expression/expression-input-parser.ts
+++ b/packages/core/src/classes/expression/expression-input-parser.ts
@@ -1,13 +1,12 @@
 import {
   ATTRIBUTE_TYPE,
   RangeOperator,
-  RequireOnlyOne,
   ScalarType,
   SimpleOperator,
+  UpdateType,
 } from '@typedorm/common';
 import {isEmptyObject} from '../../helpers/is-empty-object';
 import {KeyCondition} from './key-condition';
-import {KeyConditionType} from '@typedorm/common';
 import {Filter} from './filter';
 import {BaseExpressionInput, MERGE_STRATEGY} from './base-expression-input';
 import {isScalarType} from '../../helpers/is-scalar-type';
@@ -15,20 +14,16 @@ import {FilterOptions} from './filter-options-type';
 import {ConditionOptions} from './condition-options-type';
 import {Condition} from './condition';
 import {Projection} from './projection';
+import {KeyConditionOptions} from './key-condition-options-type';
+import {ProjectionKeys} from './projection-keys-options-type';
+import {isSetOperatorComplexValueType, UpdateBody} from './update-body-type';
+import {SetUpdate} from './update/set-update';
+import {AddUpdate} from './update/add-update';
+import {Update} from './update/update';
 
-export type KeyConditionOptions = RequireOnlyOne<
-  {
-    [key in KeyConditionType.SimpleOperator]: ScalarType;
-  } &
-    {
-      [key in KeyConditionType.FunctionOperator]: ScalarType;
-    } &
-    {
-      [key in KeyConditionType.RangeOperator]: [ScalarType, ScalarType];
-    }
->;
-
-export type ProjectionKeys<Entity> = (keyof Entity)[] | string[];
+import {DeleteUpdate} from './update/delete-update';
+import {RemoveUpdate} from './update/remove-update';
+import {isObject} from '../../helpers/is-object';
 
 /**
  * Parses expression input to expression instances
@@ -55,10 +50,68 @@ export class ExpressionInputParser {
     return projection;
   }
 
+  parseToUpdate<Entity, AdditionalProperties = {}>(
+    body: UpdateBody<Entity, AdditionalProperties>,
+    attrValueOverrideMap: Record<string, any> = {}
+  ) {
+    return this.parseToUpdateExpression(body, attrValueOverrideMap);
+  }
+
+  /**
+   * Parses complex update object to a value and type
+   */
+  parseAttributeToUpdateValue(
+    attr: string,
+    value: any
+  ): {value: any; type: 'static' | 'dynamic'} {
+    const nestedKeyAccessRegex = /[\d+]/g;
+    if (isObject(value) && !isEmptyObject(value)) {
+      const [operator, operatorValue] = Object.entries(value as any)[0];
+
+      const parsedUpdate = this.parseValueToUpdateExp(
+        attr,
+        value,
+        operator as any,
+        operatorValue
+      );
+
+      const parsedValue = Object.values(parsedUpdate.values ?? {})[0];
+
+      // if expression contains any dynamic operation such as value manipulation or nested attribute manipulation in a list
+      if (
+        !(parsedUpdate instanceof SetUpdate) ||
+        parsedUpdate.expression.includes(' + ') ||
+        parsedUpdate.expression.includes(' - ') ||
+        nestedKeyAccessRegex.test(parsedUpdate.expression)
+      ) {
+        return {
+          value: parsedValue,
+          type: 'dynamic',
+        };
+      }
+
+      // return static value
+      return {
+        type: 'static',
+        value: parsedValue,
+      };
+    } else {
+      // if tried to update nested value for key, it is considered dynamic, as we do not know full value of updating attribute
+      if (nestedKeyAccessRegex.test(attr)) {
+        return {
+          type: 'dynamic',
+          value,
+        };
+      }
+      // return value as a default value
+      return {type: 'static', value};
+    }
+  }
+
   /**
    * Generic Recursive input parser
    * Recursively parses nested object to build expression of type ExpClass
-   * @param input Complex options object to parse
+   * @param options Complex options object to parse
    * @param ExpClass Type of expression to build, can be of type Filter, Condition etc.
    *
    */
@@ -123,6 +176,174 @@ export class ExpressionInputParser {
     );
   }
 
+  /**
+   * Parses input to update expression
+   * @param body body to parse
+   */
+  private parseToUpdateExpression(
+    body: any,
+    attrValueOverrideMap: Record<string, any>
+  ) {
+    return (
+      Object.entries(body)
+        .map(([attr, value]) => {
+          if (isObject(value) && !isEmptyObject(value)) {
+            const [operator, operatorValue] = Object.entries(
+              value as any
+            )[0] as [
+              (
+                | UpdateType.ArithmeticOperator
+                | UpdateType.SetUpdateOperator
+                | UpdateType.Action
+              ),
+              any
+            ];
+
+            return this.parseValueToUpdateExp(
+              attr,
+              value,
+              operator,
+              operatorValue,
+              attrValueOverrideMap[attr] // get any override value if exists
+            );
+          } else {
+            // fallback to default `SET` action based update
+            return new SetUpdate().setTo(attr, value);
+          }
+        })
+        // merge all expressions with matching action
+        .reduce(
+          (acc, currExp) => {
+            acc
+              .find(instance => instance.constructor === currExp.constructor)!
+              .merge(currExp);
+            return acc;
+          },
+          [
+            new SetUpdate(),
+            new AddUpdate(),
+            new RemoveUpdate(),
+            new DeleteUpdate(),
+          ]
+        )
+        // merge all expressions of different actions
+        .reduce((acc, curr) => {
+          acc.merge(curr);
+          return acc;
+        }, new Update())
+    );
+  }
+
+  /**
+   * Parses single attribute into update expression instance
+   * @param attribute name/path of the attribute
+   * @param attributeValue value to update
+   */
+  private parseValueToUpdateExp(
+    attribute: string,
+    attributeValue: any, // attribute value to set
+    operator:
+      | UpdateType.ArithmeticOperator
+      | UpdateType.SetUpdateOperator
+      | UpdateType.Action,
+    operatorValue: any,
+    staticValueToOverride?: any // value to override for attribute, this is set in cases where there was a custom property transform was requested
+  ): Update {
+    switch (operator) {
+      case 'INCREMENT_BY':
+      case 'DECREMENT_BY': {
+        return new SetUpdate().setTo(attribute, operatorValue, operator);
+      }
+      case 'IF_NOT_EXISTS': {
+        if (isSetOperatorComplexValueType(operatorValue)) {
+          return new SetUpdate().setToIfNotExists(
+            attribute,
+            staticValueToOverride || operatorValue.$VALUE,
+            operatorValue.$PATH
+          );
+        } else {
+          return new SetUpdate().setToIfNotExists(attribute, operatorValue);
+        }
+      }
+      case 'LIST_APPEND': {
+        if (isSetOperatorComplexValueType(operatorValue)) {
+          return new SetUpdate().setOrAppendToList(
+            attribute,
+            operatorValue.$VALUE,
+            operatorValue.$PATH
+          );
+        } else {
+          return new SetUpdate().setOrAppendToList(attribute, operatorValue);
+        }
+      }
+      case 'SET': {
+        /**
+         * aliased set support, this allows access patterns like
+         * {id: { SET: '1'}}
+         * behaves similar to {id: '1'}
+         */
+        // handle explicit set exp
+        if (isObject(operatorValue) && !isEmptyObject(operatorValue)) {
+          const [nestedOperator, nestedOperatorValue] = Object.entries(
+            operatorValue
+          )[0] as [
+            UpdateType.ArithmeticOperator | UpdateType.SetUpdateOperator,
+            any
+          ];
+
+          return this.parseValueToUpdateExp(
+            attribute,
+            nestedOperatorValue,
+            nestedOperator,
+            nestedOperatorValue,
+            staticValueToOverride
+          );
+        } else {
+          // handle attribute with map type
+          return new SetUpdate().setTo(
+            attribute,
+            staticValueToOverride || operatorValue
+          );
+        }
+      }
+      case 'ADD': {
+        if (isEmptyObject(operatorValue)) {
+          throw new Error(
+            `Invalid value ${operatorValue} received for action "ADD", Only numbers and lists are supported.`
+          );
+        }
+        return new AddUpdate().addTo(attribute, operatorValue);
+      }
+      case 'DELETE': {
+        return new DeleteUpdate().delete(attribute, operatorValue);
+      }
+      case 'REMOVE': {
+        if (typeof operatorValue === 'boolean' && !!operatorValue) {
+          return new RemoveUpdate().remove(attribute);
+        } else if (
+          !isEmptyObject(operatorValue) &&
+          Array.isArray(operatorValue.$AT_INDEX)
+        ) {
+          return new RemoveUpdate().remove(attribute, {
+            atIndexes: operatorValue.$AT_INDEX,
+          });
+        } else {
+          throw new Error(
+            `Invalid value ${operatorValue} received for action "REMOVE". Value must be set to boolean
+            In addition, You may use special value type {$AT_INDEX: Array<number>} for attribute of type list..`
+          );
+        }
+      }
+      default: {
+        // handle attribute with map type
+        return new SetUpdate().setTo(
+          attribute,
+          staticValueToOverride || attributeValue
+        );
+      }
+    }
+  }
+
   /**
    * When this is run, it is assumed that attribute/value are validated to not have any nested objects,
    * therefor this function will not running in any recursion itself
diff --git a/packages/core/src/classes/expression/key-condition-options-type.ts b/packages/core/src/classes/expression/key-condition-options-type.ts
new file mode 100644
index 0000000..cf58f40
--- /dev/null
+++ b/packages/core/src/classes/expression/key-condition-options-type.ts
@@ -0,0 +1,13 @@
+import {KeyConditionType, RequireOnlyOne, ScalarType} from '@typedorm/common';
+
+export type KeyConditionOptions = RequireOnlyOne<
+  {
+    [key in KeyConditionType.SimpleOperator]: ScalarType;
+  } &
+    {
+      [key in KeyConditionType.FunctionOperator]: ScalarType;
+    } &
+    {
+      [key in KeyConditionType.RangeOperator]: [ScalarType, ScalarType];
+    }
+>;
diff --git a/packages/core/src/classes/expression/projection-keys-options-type.ts b/packages/core/src/classes/expression/projection-keys-options-type.ts
new file mode 100644
index 0000000..12cf03b
--- /dev/null
+++ b/packages/core/src/classes/expression/projection-keys-options-type.ts
@@ -0,0 +1,1 @@
+export type ProjectionKeys<Entity> = (keyof Entity)[] | string[];
diff --git a/packages/core/src/classes/expression/update-body-type.ts b/packages/core/src/classes/expression/update-body-type.ts
new file mode 100644
index 0000000..9e7d066
--- /dev/null
+++ b/packages/core/src/classes/expression/update-body-type.ts
@@ -0,0 +1,113 @@
+import {UpdateType, RequireOnlyOne, InvalidType} from '@typedorm/common';
+import {isEmptyObject} from '../../helpers/is-empty-object';
+
+/**
+ * Type Guards
+ */
+export const isSetOperatorComplexValueType = (
+  value: any
+): value is {$PATH: string; $VALUE: any} => {
+  return !!(!isEmptyObject(value) && value['$PATH']);
+};
+
+/**
+ * **********************************
+ * Set Action
+ */
+type AbstractSetValueType<
+  Entity,
+  enKey extends keyof Entity
+> = (Entity[enKey] extends number
+  ? {
+      [key in UpdateType.ArithmeticOperator]?: number;
+    }
+  : Entity[enKey] extends any[]
+  ? {
+      LIST_APPEND?: Entity[enKey] | {$VALUE: Entity[enKey]; $PATH: string};
+    }
+  : {}) & {
+  IF_NOT_EXISTS?: Entity[enKey] | {$VALUE: Entity[enKey]; $PATH: string};
+};
+
+type SetImplicitValueType<Entity, enKey extends keyof Entity> =
+  | RequireOnlyOne<
+      // set value type
+      AbstractSetValueType<Entity, enKey>
+    >
+  // simple attribute type value
+  | Entity[enKey];
+
+type SetExplicitValueType<Entity, enKey extends keyof Entity> = RequireOnlyOne<{
+  SET?:
+    | AbstractSetValueType<Entity, enKey>
+    // simple attribute type value
+    | Entity[enKey];
+}>;
+
+type SetValueType<Entity, enKey extends keyof Entity> =
+  | SetImplicitValueType<Entity, enKey>
+  // explicit set  type
+  // almost identical to implicit type but has more explicit syntax
+  | SetExplicitValueType<Entity, enKey>;
+// **************************************
+
+/**
+ * *************************************
+ * ADD Action
+ */
+type AddValueType<Entity, enKey extends keyof Entity> = RequireOnlyOne<{
+  ADD?: Entity[enKey] extends number | any[]
+    ? Entity[enKey]
+    : InvalidType<
+        [
+          'number | any[]',
+          "Update action 'ADD' can not be used for attribute",
+          enKey
+        ]
+      >;
+}>;
+// **************************************
+
+/**
+ * *************************************
+ * REMOVE Action
+ */
+type RemoveValueType<Entity, enKey extends keyof Entity> = RequireOnlyOne<{
+  REMOVE?: Entity[enKey] extends any[]
+    ? {$AT_INDEX: number[]} | boolean
+    : boolean;
+}>;
+// **************************************
+
+/**
+ * *************************************
+ * DELETE Action
+ */
+type DeleteValueType<Entity, enKey extends keyof Entity> = RequireOnlyOne<{
+  DELETE?: Entity[enKey] extends any[]
+    ? Entity[enKey]
+    : InvalidType<
+        [any[], "Update action 'DELETE' can not be used for attribute", enKey]
+      >;
+}>;
+// **************************************
+
+/**
+ * Update Body
+ */
+export type UpdateBody<Entity, AdditionalProperties> = {
+  // implicit set  type
+  [enKey in keyof Entity]?:
+    | SetValueType<Entity, enKey>
+    | AddValueType<Entity, enKey>
+    | RemoveValueType<Entity, enKey>
+    | DeleteValueType<Entity, enKey>;
+} &
+  {
+    // implicit set  type
+    [additionalKey in keyof AdditionalProperties]?:
+      | SetValueType<AdditionalProperties, additionalKey>
+      | AddValueType<AdditionalProperties, additionalKey>
+      | RemoveValueType<AdditionalProperties, additionalKey>
+      | DeleteValueType<AdditionalProperties, additionalKey>;
+  };
diff --git a/packages/core/src/classes/expression/update/add-update.ts b/packages/core/src/classes/expression/update/add-update.ts
new file mode 100644
index 0000000..5f70286
--- /dev/null
+++ b/packages/core/src/classes/expression/update/add-update.ts
@@ -0,0 +1,14 @@
+import {ScalarType, UpdateType} from '@typedorm/common';
+import {Update} from './update';
+
+export class AddUpdate extends Update {
+  protected prefix: UpdateType.Action = 'ADD';
+
+  addTo(key: string, value: number | ScalarType[]): this {
+    const attrExpName = this.addExpressionName(key);
+    const attrExpValue = this.addExpressionValue(key, value);
+
+    this.appendToExpression(`${attrExpName} ${attrExpValue}`);
+    return this;
+  }
+}
diff --git a/packages/core/src/classes/expression/update/delete-update.ts b/packages/core/src/classes/expression/update/delete-update.ts
new file mode 100644
index 0000000..d3a4e81
--- /dev/null
+++ b/packages/core/src/classes/expression/update/delete-update.ts
@@ -0,0 +1,17 @@
+import {ScalarType, UpdateType} from '@typedorm/common';
+import {Update} from './update';
+
+export class DeleteUpdate extends Update {
+  protected prefix: UpdateType.Action = 'DELETE';
+
+  /**
+   * Deletes elements from a set
+   */
+  delete(key: string, value: ScalarType[]) {
+    const attrExpName = this.addExpressionName(key);
+    const attrExpValue = this.addExpressionValue(key, value);
+
+    this.appendToExpression(`${attrExpName} ${attrExpValue}`);
+    return this;
+  }
+}
diff --git a/packages/core/src/classes/expression/update/remove-update.ts b/packages/core/src/classes/expression/update/remove-update.ts
new file mode 100644
index 0000000..34ce875
--- /dev/null
+++ b/packages/core/src/classes/expression/update/remove-update.ts
@@ -0,0 +1,36 @@
+import {UpdateType} from '@typedorm/common';
+import {Update} from './update';
+
+export class RemoveUpdate extends Update {
+  protected prefix: UpdateType.Action = 'REMOVE';
+
+  remove(
+    attrOrPath: string,
+    options?: {
+      atIndexes?: number[];
+    }
+  ) {
+    if (options && Array.isArray(options.atIndexes)) {
+      let fullExp = '';
+
+      // create a string out of all index numbers
+      const attrExp = this.addExpressionName(attrOrPath);
+      options.atIndexes.forEach((index: number) => {
+        if (typeof index !== 'number') {
+          throw new Error(
+            `Invalid value for $AT_INDEX: ${index}, Index value must be of type number.`
+          );
+        }
+        const segExp = `${attrExp}[${index}]`;
+        fullExp += fullExp ? `, ${segExp}` : segExp;
+      });
+
+      this.appendToExpression(fullExp);
+      return this;
+    } else {
+      const attrExpName = this.addExpressionName(attrOrPath);
+      this.appendToExpression(attrExpName);
+      return this;
+    }
+  }
+}
diff --git a/packages/core/src/classes/expression/update/set-update.ts b/packages/core/src/classes/expression/update/set-update.ts
new file mode 100644
index 0000000..8debd39
--- /dev/null
+++ b/packages/core/src/classes/expression/update/set-update.ts
@@ -0,0 +1,68 @@
+import {UpdateType, ScalarType} from '@typedorm/common';
+import {Update} from './update';
+
+export class SetUpdate extends Update {
+  protected prefix: UpdateType.Action = 'SET';
+
+  /**
+   * Support specifying additional arithmetic operations
+   */
+  setTo(
+    key: string,
+    value: any,
+    incrementBy?: UpdateType.ArithmeticOperator
+  ): this {
+    const arithmeticOperator = incrementBy
+      ? this.getSymbolForArithmeticOperator(incrementBy)
+      : '';
+
+    const attrExpName = this.addExpressionName(key);
+    const attrExpValue = this.addExpressionValue(key, value);
+    this.appendToExpression(
+      `${attrExpName} ` +
+        `${this.getSymbolForOperator('EQ')} ` +
+        // builds exp like #UE_age = #UE_age + :UE_age if incrementBy was provided
+        `${arithmeticOperator ? `${attrExpName} ${arithmeticOperator} ` : ''}` +
+        `${attrExpValue}`
+    );
+    return this;
+  }
+
+  setToIfNotExists(key: string, value: any, otherKeyAttribute?: string): this {
+    const attrExpName = this.addExpressionName(key);
+    const attrExpValue = this.addExpressionValue(key, value);
+
+    // if no other key is specified, use default value of updating key
+    const keyofValueToCheck = otherKeyAttribute
+      ? this.addExpressionName(otherKeyAttribute)
+      : attrExpName;
+
+    this.appendToExpression(
+      `${attrExpName} ` +
+        `${this.getSymbolForOperator('EQ')} ` +
+        `if_not_exists(${keyofValueToCheck}, ${attrExpValue})`
+    );
+    return this;
+  }
+
+  setOrAppendToList(
+    key: string,
+    value: ScalarType[],
+    otherKeyAttribute?: string
+  ): this {
+    const attrExpName = this.addExpressionName(key);
+    const attrExpValue = this.addExpressionValue(key, value);
+
+    // if no other key is specified, use default value of updating key
+    const keyofValueToAppend = otherKeyAttribute
+      ? this.addExpressionName(otherKeyAttribute)
+      : attrExpName;
+
+    this.appendToExpression(
+      `${attrExpName} ` +
+        `${this.getSymbolForOperator('EQ')} ` +
+        `list_append(${keyofValueToAppend}, ${attrExpValue})`
+    );
+    return this;
+  }
+}
diff --git a/packages/core/src/classes/expression/update/update.ts b/packages/core/src/classes/expression/update/update.ts
new file mode 100644
index 0000000..0f0c87d
--- /dev/null
+++ b/packages/core/src/classes/expression/update/update.ts
@@ -0,0 +1,83 @@
+import {UpdateType} from '@typedorm/common';
+import {BaseExpressionInput} from '../base-expression-input';
+
+export class Update extends BaseExpressionInput {
+  // empty prefix for base update type
+  protected prefix = '';
+
+  protected getExpNameKey(key: string): string {
+    return `#UE_${key}`;
+  }
+  protected getExpValueKey(key: string): string {
+    return `:UE_${key}`;
+  }
+
+  /**
+   * @override and
+   */
+  and(): this {
+    this.expression += ', ';
+    return this;
+  }
+
+  /**
+   * @override or
+   */
+  or(): this {
+    throw new Error('"or" operation is not supported with update expression.');
+  }
+
+  /**
+   * Support merging multiple update expressions with same keyword
+   * @override merge
+   */
+  merge(update: Update) {
+    const {expression, names, values, prefix} = update;
+
+    if (!prefix) {
+      throw new Error(
+        'Can not merge with Base `Update` type, merging expression must have a valid prefix.'
+      );
+    }
+
+    // if merging condition does not have anything to merge return
+    if (!expression) {
+      return this;
+    }
+
+    if (update.constructor === this.constructor) {
+      this.expression += this.expression ? ',' : '';
+      this.appendToExpression(expression);
+    } else {
+      this.appendToExpression(`${prefix} ${expression}`);
+    }
+
+    this.names = {...this.names, ...names};
+    this.values = {...this.values, ...values};
+
+    return this;
+  }
+
+  /**
+   * Support merging multiple update expressions with same or different keywords
+   * @override  mergeMany
+   */
+  mergeMany<T = this>(inputs: T[]): any {
+    const merged = inputs.reduce((acc, upd) => {
+      acc.merge((upd as unknown) as this);
+      return acc;
+    }, this);
+
+    return merged;
+  }
+
+  protected getSymbolForArithmeticOperator(
+    operator: UpdateType.ArithmeticOperator
+  ) {
+    const symbolMap = {
+      INCREMENT_BY: '+',
+      DECREMENT_BY: '-',
+    };
+    return symbolMap[operator];
+  }
+}
diff --git a/packages/core/src/classes/manager/entity-manager.ts b/packages/core/src/classes/manager/entity-manager.ts
index 08ac7de..e74f417 100644
--- a/packages/core/src/classes/manager/entity-manager.ts
+++ b/packages/core/src/classes/manager/entity-manager.ts
@@ -5,7 +5,6 @@ import {
   MANAGER_NAME,
   QUERY_ORDER,
   STATS_TYPE,
-  UpdateAttributes,
 } from '@typedorm/common';
 import {getDynamoQueryItemsLimit} from '../../helpers/get-dynamo-query-items-limit';
 import {isEmptyObject} from '../../helpers/is-empty-object';
@@ -18,12 +17,11 @@ 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';
-import {
-  KeyConditionOptions,
-  ProjectionKeys,
-} from '../expression/expression-input-parser';
 import {MetadataOptions} from '../transformer/base-transformer';
 import {getUniqueRequestId} from '../../helpers/get-unique-request-id';
+import {ProjectionKeys} from '../expression/projection-keys-options-type';
+import {KeyConditionOptions} from '../expression/key-condition-options-type';
+import {UpdateBody} from '../expression/update-body-type';
 
 export interface EntityManagerCreateOptions<Entity> {
   /**
@@ -377,10 +375,14 @@ export class EntityManager {
    * @param body Attributes to update
    * @param options update options
    */
-  async update<Entity, PrimaryKey = Partial<Entity>>(
+  async update<
+    Entity,
+    PrimaryKey = Partial<Entity>,
+    AdditionalProperties = Entity
+  >(
     entityClass: EntityTarget<Entity>,
     primaryKeyAttributes: PrimaryKey,
-    body: UpdateAttributes<Entity, PrimaryKey>,
+    body: UpdateBody<Entity, AdditionalProperties>,
     options?: EntityManagerUpdateOptions<Entity>,
     metadataOptions?: MetadataOptions
   ): Promise<Entity | undefined> {
@@ -519,7 +521,7 @@ export class EntityManager {
     queryOptions?: EntityManagerFindOptions<Entity, PartitionKey>,
     metadataOptions?: MetadataOptions
   ): Promise<{
-    items: DynamoDB.DocumentClient.ItemList;
+    items: Entity[];
     cursor?: DynamoDB.DocumentClient.Key | undefined;
   }> {
     const requestId = getUniqueRequestId(metadataOptions?.requestId);
diff --git a/packages/core/src/classes/manager/scan-manager.ts b/packages/core/src/classes/manager/scan-manager.ts
new file mode 100644
index 0000000..cb1b71e
--- /dev/null
+++ b/packages/core/src/classes/manager/scan-manager.ts
@@ -0,0 +1,588 @@
+import {
+  EntityTarget,
+  INTERNAL_ENTITY_ATTRIBUTE,
+  InvalidParallelScanLimitOptionError,
+  MANAGER_NAME,
+  PARALLEL_SCAN_CONCURRENCY_LIMIT,
+  STATS_TYPE,
+} from '@typedorm/common';
+import {DynamoDB} from 'aws-sdk';
+import pLimit from 'p-limit';
+import {getUniqueRequestId} from '../../helpers/get-unique-request-id';
+import {Connection} from '../connection/connection';
+import {FilterOptions} from '../expression/filter-options-type';
+import {ProjectionKeys} from '../expression/projection-keys-options-type';
+import {MetadataOptions} from '../transformer/base-transformer';
+import {DocumentClientScanTransformer} from '../transformer/document-client-scan-transformer';
+
+interface ScanManageBaseOptions<Entity, PartitionKey> {
+  /**
+   * Index to scan for items
+   * @default - main table
+   */
+  scanIndex?: string;
+
+  /**
+   * Max number of records to query
+   * @default - implicit dynamo db query limit is applied
+   */
+  limit?: number;
+
+  /**
+   * Cursor to traverse from
+   * @default none
+   */
+  cursor?: DynamoDB.DocumentClient.Key;
+
+  /**
+   * Specify filter to apply
+   * Avoid using this where possible, since filters in dynamodb applies after items
+   * are read
+   * @default none
+   */
+  where?: FilterOptions<Entity, PartitionKey>;
+
+  /**
+   * Specifies which attributes to fetch
+   * @default all attributes are fetched
+   */
+  select?: ProjectionKeys<Entity>;
+}
+
+export interface ScanManagerFindOptions<Entity>
+  extends ScanManageBaseOptions<
+    Entity,
+    {} // empty object since all attributes including ones used in primary key can be used with filter
+  > {
+  /**
+   * Total number of segments to divide this scan in.
+   *
+   * @default none - all items are scanned sequentially
+   */
+  totalSegments?: number;
+
+  /**
+   * Limit to apply per segment.
+   *
+   * when no `totalSegments` is provided, this option is ignored
+   * @default none - limit to apply per segment
+   */
+  limitPerSegment?: number;
+
+  /**
+   * Item cursor, used for paginating a scan.
+   *
+   * When the `totalSegments` option is provided, this option should be of type {[segmentNo]: [Key]}
+   * @default none
+   */
+  cursor?:
+    | Record<number, ScanManageBaseOptions<Entity, {}>['cursor']>
+    | ScanManageBaseOptions<Entity, {}>['cursor'];
+
+  /**
+   * Max number of requests to run in parallel
+   *
+   * When requesting parallel scan on x segments, request are executed in parallel using Promise.all
+   * While it is okay to run small number of requests in parallel, it is often a good idea to enforce a concurrency controller to stop node from eating up the all the memory
+   *
+   * This parameter does exactly that. i.e if requested to run scan with `20,000` segments, and `requestsConcurrencyLimit` is set to `100`
+   * TypeDORM will make sure that there are always only `100` requests are running in parallel at any time until all `20,000` segments have finished processing.
+   *
+   * @default PARALLEL_SCAN_CONCURRENCY_LIMIT
+   */
+  requestsConcurrencyLimit?: number;
+}
+
+export type ScanManagerCountOptions<Entity> = Pick<
+  ScanManageBaseOptions<
+    Entity,
+    {} // empty object since all attributes including ones used in primary key can be used with filter
+  >,
+  'scanIndex' | 'where'
+>;
+
+export interface ScanManagerParallelScanOptions
+  extends ScanManageBaseOptions<any, any> {
+  /**
+   * Total number of segments to divide this scan in
+   */
+  totalSegments: number;
+
+  /**
+   * Limit to apply per segment
+   *
+   * @default none - limit to apply per segment
+   */
+  limitPerSegment?: number;
+
+  /**
+   * Entity to run scan for.
+   *
+   * When one is provided, filter expression in auto updated to include a default filer condition for matching entity
+   *
+   * @default none
+   */
+  entity?: EntityTarget<any>;
+
+  /**
+   * Per segment cursor, where key is the segment number, and value is the cursor options for that segment
+   *
+   * @default none
+   */
+  cursor?: Record<number, ScanManagerScanOptions['cursor']>;
+
+  /**
+   * Max number of requests to run in parallel
+   *
+   * When requesting parallel scan on x segments, request are executed in parallel using Promise.all
+   * While it is okay to run small number of requests in parallel, it is often a good idea to enforce a concurrency controller to stop node from eating up the all the memory
+   *
+   * This parameter does exactly that. i.e if requested to run scan with `20,000` segments, and `requestsConcurrencyLimit` is set to `100`
+   * TypeDORM will make sure that there are always only `100` requests are running in parallel at any time until all `20,000` segments have finished processing.
+   *
+   * @default PARALLEL_SCAN_CONCURRENCY_LIMIT
+   */
+  requestsConcurrencyLimit?: number;
+}
+
+export interface ScanManagerScanOptions
+  extends ScanManageBaseOptions<any, any> {
+  /**
+   * Entity to scan
+   *
+   * When one is provided, filter expression in auto updated to include filer condition for this entity
+   *
+   * @default none
+   */
+  entity?: EntityTarget<any>;
+
+  /**
+   * Number of current segment
+   *
+   * @default none - scan is not segmented
+   */
+  segment?: number;
+
+  /**
+   * Total number of segments to divide this scan in
+   *
+   * @default none - all items are scanned sequentially
+   */
+  totalSegments?: number;
+
+  /**
+   * Limit to apply per segment
+   *
+   * @default none - limit to apply per segment
+   */
+  limitPerSegment?: number;
+}
+
+export class ScanManager {
+  private itemsFetchedSoFarTotalParallelCount: number;
+  private limit = pLimit(PARALLEL_SCAN_CONCURRENCY_LIMIT);
+  private _dcScanTransformer: DocumentClientScanTransformer;
+
+  constructor(private connection: Connection) {
+    this._dcScanTransformer = new DocumentClientScanTransformer(connection);
+    this.itemsFetchedSoFarTotalParallelCount = 0;
+  }
+
+  /**
+   * Finds all the matching entity over document client scan operation
+   * @param entityClass Entity to find
+   * @param findOptions find query options
+   * @param metadataOptions Other metadata options
+   */
+  async find<Entity>(
+    entityClass: EntityTarget<Entity>,
+    findOptions?: ScanManagerFindOptions<Entity>,
+    metadataOptions?: MetadataOptions
+  ) {
+    const requestId = getUniqueRequestId(metadataOptions?.requestId);
+
+    let response: {
+      items?: Entity[];
+      unknownItems?: DynamoDB.DocumentClient.AttributeMap[];
+      cursor?:
+        | DynamoDB.DocumentClient.Key
+        | Record<number, DynamoDB.DocumentClient.Key>;
+    };
+
+    if (findOptions?.totalSegments) {
+      (response = await this.parallelScan<Entity>({
+        ...findOptions,
+        entity: entityClass,
+      } as ScanManagerParallelScanOptions)),
+        {
+          requestId,
+          returnConsumedCapacity: metadataOptions?.returnConsumedCapacity,
+        };
+    } else {
+      response = await this.scan<Entity>(
+        {...findOptions, entity: entityClass} as ScanManagerScanOptions,
+        {
+          requestId,
+          returnConsumedCapacity: metadataOptions?.returnConsumedCapacity,
+        }
+      );
+    }
+
+    if (response.unknownItems?.length) {
+      // log warning for items that were skipped form the response
+      // These are items that had __en attribute on them but TypeDORM does no longer know about them
+      this.connection.logger.logWarn({
+        requestId,
+        scope: MANAGER_NAME.SCAN_MANAGER,
+        log: `"${response.unknownItems.length}" items were skipped from the response because TypDORM failed to resolve them.`,
+      });
+    }
+
+    return {
+      items: response.items,
+      cursor: response.cursor,
+    };
+  }
+
+  /**
+   * Returns total count of all matching items for current entity
+   * @param entityClass Entity to count
+   * @param scanOptions Extra scan options
+   * @param metadataOptions Other metadata options
+   */
+  async count<Entity>(
+    entityClass: EntityTarget<Entity>,
+    scanOptions?: ScanManagerCountOptions<Entity>,
+    metadataOptions?: MetadataOptions
+  ): Promise<number> {
+    const requestId = getUniqueRequestId(metadataOptions?.requestId);
+
+    const dynamoScanInput = this._dcScanTransformer.toDynamoScanItem(
+      {...scanOptions, entity: entityClass, onlyCount: true, select: undefined}, // select projection and count can not be used together
+      {
+        requestId,
+        returnConsumedCapacity: metadataOptions?.returnConsumedCapacity,
+      }
+    );
+
+    const count = await this._internalRecursiveCount({
+      scanInput: dynamoScanInput,
+      metadataOptions: {
+        requestId,
+        returnConsumedCapacity: metadataOptions?.returnConsumedCapacity,
+      },
+    });
+
+    return count;
+  }
+
+  /**
+   * Scans all items from dynamo table in parallel while also respecting the max provisioned concurrency
+   * @param scanOptions Options for parallel scan
+   * @param metadataOptions Additional metadata options
+   */
+  async parallelScan<Entity>(
+    scanOptions: ScanManagerParallelScanOptions,
+    metadataOptions?: MetadataOptions
+  ): Promise<{
+    items: Entity[] | undefined;
+    unknownItems: DynamoDB.DocumentClient.AttributeMap[] | undefined;
+    cursor: Record<number, DynamoDB.DocumentClient.Key | undefined>;
+  }> {
+    // start with 0
+    this.itemsFetchedSoFarTotalParallelCount = 0;
+
+    const concurrencyLimit =
+      PARALLEL_SCAN_CONCURRENCY_LIMIT || scanOptions.requestsConcurrencyLimit;
+    const requestId = getUniqueRequestId(metadataOptions?.requestId);
+
+    if (scanOptions.requestsConcurrencyLimit) {
+      this.limit = pLimit(scanOptions.requestsConcurrencyLimit);
+    }
+
+    const parallelScanOptions: ScanManagerScanOptions[] = [];
+
+    if (
+      scanOptions?.limit &&
+      scanOptions?.limitPerSegment &&
+      scanOptions?.limit < scanOptions?.limitPerSegment
+    ) {
+      throw new InvalidParallelScanLimitOptionError(
+        scanOptions?.limit,
+        scanOptions?.limitPerSegment
+      );
+    }
+
+    for (let index = 0; index < scanOptions.totalSegments; index++) {
+      // only the cursor for same segment can be applied
+      const cursorForSegment = scanOptions.cursor
+        ? scanOptions.cursor[index]
+        : undefined;
+
+      parallelScanOptions.push({
+        ...scanOptions,
+        cursor: cursorForSegment,
+        segment: index,
+      });
+    }
+
+    this.connection.logger.logInfo({
+      requestId,
+      scope: MANAGER_NAME.SCAN_MANAGER,
+      log: `Running scan in parallel with ${scanOptions.totalSegments} segments.`,
+    });
+
+    if (concurrencyLimit < scanOptions.totalSegments) {
+      this.connection.logger.logInfo({
+        requestId,
+        scope: MANAGER_NAME.SCAN_MANAGER,
+        log: `Current request concurrency limit ${concurrencyLimit} is lower than requested segments count ${scanOptions.totalSegments}
+        So requests will be run in a batch of ${concurrencyLimit} at a time until all segments ${scanOptions.totalSegments} have processed.`,
+      });
+    }
+
+    const allPromisesResponse = await Promise.all(
+      parallelScanOptions.map(options =>
+        this.toLimited(this.scan<Entity>(options, metadataOptions))
+      )
+    );
+
+    // merge all responses
+    const response = allPromisesResponse.reduce(
+      (
+        acc: {
+          items: Entity[];
+          unknownItems: DynamoDB.DocumentClient.AttributeMap[];
+          cursor: Record<number, DynamoDB.DocumentClient.Key>;
+        },
+        current,
+        index
+      ) => {
+        if (current.items?.length) {
+          if (!acc.items) {
+            acc.items = [];
+          }
+          acc.items = [...acc.items, ...current.items];
+        }
+
+        if (current.unknownItems?.length) {
+          if (!acc.unknownItems) {
+            acc.unknownItems = [];
+          }
+          acc.unknownItems = [...acc.unknownItems, ...current.unknownItems];
+        }
+
+        if (current.cursor) {
+          if (!acc.cursor) {
+            acc.cursor = {};
+          }
+          acc.cursor = {
+            ...acc.cursor,
+            [index]: current.cursor,
+          };
+        }
+        return acc;
+      },
+      {} as {
+        items: Entity[];
+        unknownItems: DynamoDB.DocumentClient.AttributeMap[];
+        cursor: Record<number, DynamoDB.DocumentClient.Key>;
+      }
+    );
+
+    return response;
+  }
+
+  /**
+   * Low level scan operation.
+   *
+   * Perhaps you are looking for higher level ScanManager.find or ScanManager.parallelScan operation
+   * @param scanOptions scan options to run scan with
+   * @param metadataOptions any other metadata options
+   */
+  async scan<Entity>(
+    scanOptions?: ScanManagerScanOptions,
+    metadataOptions?: MetadataOptions
+  ): Promise<{
+    items: Entity[] | undefined;
+    unknownItems: DynamoDB.DocumentClient.AttributeMap[] | undefined;
+    cursor: DynamoDB.DocumentClient.Key | undefined;
+  }> {
+    const requestId = getUniqueRequestId(metadataOptions?.requestId);
+
+    const dynamoScanInput = this._dcScanTransformer.toDynamoScanItem(
+      {
+        ...scanOptions,
+        // if requested segmented scan, then apply segment limit or default to limit operator
+        limit: scanOptions?.totalSegments
+          ? scanOptions?.limitPerSegment
+          : scanOptions?.limit,
+      },
+      {
+        requestId,
+        returnConsumedCapacity: metadataOptions?.returnConsumedCapacity,
+      }
+    );
+
+    const response = await this._internalRecursiveScan({
+      scanInput: dynamoScanInput,
+      limit: scanOptions?.limit,
+      cursor: scanOptions?.cursor,
+      metadataOptions: {
+        requestId,
+        returnConsumedCapacity: metadataOptions?.returnConsumedCapacity,
+      },
+    });
+
+    const entities = this._dcScanTransformer.fromDynamoScanResponseItemList<
+      Entity
+    >(response.items);
+
+    if (scanOptions?.entity && entities.unknownItems) {
+      this.connection.logger.logWarn({
+        requestId,
+        scope: MANAGER_NAME.SCAN_MANAGER,
+        log: `
+        There were some items that looked like ${scanOptions?.entity.name} but TypeDORM was unable to convert it back to entity type,
+        This can happen when there are items in the table with "${INTERNAL_ENTITY_ATTRIBUTE.ENTITY_NAME} but was not created by TypeDORM.
+        You should remove them or update it to something different."`,
+      });
+    }
+
+    return {
+      items: entities.items?.length ? entities.items : undefined,
+      unknownItems: entities.unknownItems?.length
+        ? entities.unknownItems
+        : undefined,
+      cursor: response.cursor,
+    };
+  }
+
+  /**
+   * Recursively scans table with given options
+   */
+  private async _internalRecursiveScan({
+    scanInput,
+    limit,
+    cursor,
+    itemsFetched = [],
+    metadataOptions,
+  }: {
+    scanInput: DynamoDB.DocumentClient.ScanInput;
+    limit?: number;
+    cursor?: DynamoDB.DocumentClient.Key;
+    itemsFetched?: DynamoDB.DocumentClient.ItemList;
+    metadataOptions?: MetadataOptions;
+  }): Promise<{
+    items: DynamoDB.DocumentClient.ItemList;
+    cursor?: DynamoDB.DocumentClient.Key;
+  }> {
+    // return if the count is already met
+    if (limit && this.itemsFetchedSoFarTotalParallelCount >= limit) {
+      return {
+        items: itemsFetched,
+        cursor,
+      };
+    }
+
+    const {
+      LastEvaluatedKey,
+      Items = [],
+      ConsumedCapacity,
+    } = await this.connection.documentClient
+      .scan({...scanInput, ExclusiveStartKey: cursor})
+      .promise();
+
+    // stats
+    if (ConsumedCapacity) {
+      this.connection.logger.logStats({
+        requestId: metadataOptions?.requestId,
+        scope: MANAGER_NAME.SCAN_MANAGER,
+        statsType: STATS_TYPE.CONSUMED_CAPACITY,
+        consumedCapacityData: ConsumedCapacity,
+      });
+    }
+
+    // recheck if requested items limit is already met, may be other worker
+    // if so drop the result of current request and return
+    if (limit && this.itemsFetchedSoFarTotalParallelCount >= limit) {
+      return {
+        items: itemsFetched,
+        cursor,
+      };
+    }
+
+    itemsFetched = [...itemsFetched, ...Items];
+    this.itemsFetchedSoFarTotalParallelCount += Items.length;
+
+    if (LastEvaluatedKey) {
+      return this._internalRecursiveScan({
+        scanInput,
+        limit,
+        cursor: LastEvaluatedKey,
+        itemsFetched,
+        metadataOptions,
+      });
+    }
+
+    return {
+      items: itemsFetched,
+      cursor: LastEvaluatedKey,
+    };
+  }
+
+  /**
+   * Recursively counts items form table with given options
+   */
+  private async _internalRecursiveCount({
+    scanInput,
+    cursor,
+    currentCount = 0,
+    metadataOptions,
+  }: {
+    scanInput: DynamoDB.DocumentClient.ScanInput;
+    cursor?: DynamoDB.DocumentClient.Key;
+    currentCount?: number;
+    metadataOptions?: MetadataOptions;
+  }): Promise<number> {
+    const {
+      Count,
+      LastEvaluatedKey,
+      ConsumedCapacity,
+    } = await this.connection.documentClient
+      .scan({...scanInput, ExclusiveStartKey: cursor})
+      .promise();
+
+    // stats
+    if (ConsumedCapacity) {
+      this.connection.logger.logStats({
+        requestId: metadataOptions?.requestId,
+        scope: MANAGER_NAME.SCAN_MANAGER,
+        statsType: STATS_TYPE.CONSUMED_CAPACITY,
+        consumedCapacityData: ConsumedCapacity,
+      });
+    }
+
+    currentCount += Count || 0;
+
+    if (LastEvaluatedKey) {
+      return this._internalRecursiveCount({
+        scanInput,
+        cursor: LastEvaluatedKey,
+        currentCount,
+        metadataOptions,
+      });
+    }
+
+    return currentCount;
+  }
+
+  /**
+   * Simple wrapper to limit number of concurrent calls
+   * @param promise wraps promise in a limited factory
+   * @returns
+   */
+  private toLimited<T>(promise: Promise<T>) {
+    return this.limit(() => promise);
+  }
+}
diff --git a/packages/core/src/classes/manager/transaction-manager.ts b/packages/core/src/classes/manager/transaction-manager.ts
index be4b067..570cb95 100644
--- a/packages/core/src/classes/manager/transaction-manager.ts
+++ b/packages/core/src/classes/manager/transaction-manager.ts
@@ -51,7 +51,7 @@ export class TransactionManager {
 
     const lazyTransactionItems = (
       await Promise.all(
-        lazyTransactionWriteItemListLoader.map(async item => {
+        lazyTransactionWriteItemListLoader.map(async (item: any) => {
           // if updating/removing unique attribute in transaction, get previous value of attributes
           const existingItem = await this.connection.entityManager.findOne(
             item.entityClass,
diff --git a/packages/core/src/classes/transaction/read-transaction.ts b/packages/core/src/classes/transaction/read-transaction.ts
index 9762f03..7b8ab32 100644
--- a/packages/core/src/classes/transaction/read-transaction.ts
+++ b/packages/core/src/classes/transaction/read-transaction.ts
@@ -1,5 +1,5 @@
 import {EntityTarget} from '@typedorm/common';
-import {ProjectionKeys} from '../expression/expression-input-parser';
+import {ProjectionKeys} from '../expression/projection-keys-options-type';
 import {Transaction} from './transaction';
 
 interface ReadTransactionGetItemOptions<Entity> {
diff --git a/packages/core/src/classes/transaction/type-guards.ts b/packages/core/src/classes/transaction/type-guards.ts
index d493200..8bf4924 100644
--- a/packages/core/src/classes/transaction/type-guards.ts
+++ b/packages/core/src/classes/transaction/type-guards.ts
@@ -20,9 +20,10 @@ export function isTransactionAddGetItem<Entity, PrimaryKey>(
 
 export function isTransactionAddUpdateItem<Entity, PrimaryKey>(
   item: any
-): item is WriteTransactionUpdate<Entity, PrimaryKey> {
+): item is WriteTransactionUpdate<Entity, PrimaryKey, {}> {
   return (
-    (item as WriteTransactionUpdate<Entity, PrimaryKey>).update !== undefined
+    (item as WriteTransactionUpdate<Entity, PrimaryKey, {}>).update !==
+    undefined
   );
 }
 
diff --git a/packages/core/src/classes/transaction/write-transaction.ts b/packages/core/src/classes/transaction/write-transaction.ts
index 6b6cb96..18286d6 100644
--- a/packages/core/src/classes/transaction/write-transaction.ts
+++ b/packages/core/src/classes/transaction/write-transaction.ts
@@ -1,6 +1,7 @@
-import {EntityTarget, UpdateAttributes} from '@typedorm/common';
+import {EntityTarget} from '@typedorm/common';
 import {Connection} from '../connection/connection';
 import {ConditionOptions} from '../expression/condition-options-type';
+import {UpdateBody} from '../expression/update-body-type';
 import {Transaction} from './transaction';
 
 interface WriteTransactionCreateOptions<Entity> {
@@ -29,11 +30,15 @@ interface WriteTransactionUpdateOptions<Entity> {
    */
   where?: ConditionOptions<Entity>;
 }
-export interface WriteTransactionUpdate<Entity, PrimaryKey> {
+export interface WriteTransactionUpdate<
+  Entity,
+  PrimaryKey = Partial<Entity>,
+  AdditionalProperties = Entity
+> {
   update: {
     item: EntityTarget<Entity>;
     primaryKey: PrimaryKey;
-    body: UpdateAttributes<Entity, PrimaryKey>;
+    body: UpdateBody<Entity, AdditionalProperties>;
     options?: WriteTransactionUpdateOptions<Entity>;
   };
 }
@@ -51,13 +56,13 @@ export interface WriteTransactionDelete<Entity, PrimaryKey> {
     options?: WriteTransactionDeleteOptions<Entity>;
   };
 }
-export type WriteTransactionItem<Entity, PrimaryKey> =
+export type WriteTransactionItem<Entity, PrimaryKey, AdditionalProperties> =
   | WriteTransactionCreate<Entity>
-  | WriteTransactionUpdate<Entity, PrimaryKey>
+  | WriteTransactionUpdate<Entity, PrimaryKey, AdditionalProperties>
   | WriteTransactionDelete<Entity, PrimaryKey>;
 
 export class WriteTransaction extends Transaction<
-  WriteTransactionItem<any, any>
+  WriteTransactionItem<any, any, any>
 > {
   constructor(
     /** only here for backwards compatibility
@@ -70,7 +75,7 @@ export class WriteTransaction extends Transaction<
      * only here for backwards compatibility
      * @deprecated use `.add` for appending bulk items
      */
-    transactionItems?: WriteTransactionItem<any, any>[]
+    transactionItems?: WriteTransactionItem<any, any, any>[]
   ) {
     super();
 
@@ -85,13 +90,13 @@ export class WriteTransaction extends Transaction<
   /**
    * @deprecated use operation specific method or `.add` instead
    */
-  chian<Entity, PrimaryKey>(
-    chainedItem: WriteTransactionItem<Entity, PrimaryKey>
+  chian<Entity, PrimaryKey, AdditionalProperties = {}>(
+    chainedItem: WriteTransactionItem<Entity, PrimaryKey, AdditionalProperties>
   ): this {
-    return this.add([chainedItem as WriteTransactionItem<any, any>]);
+    return this.add([chainedItem as WriteTransactionItem<any, any, any>]);
   }
 
-  add(transactionItems: WriteTransactionItem<any, any>[]): this {
+  add(transactionItems: WriteTransactionItem<any, any, any>[]): this {
     this.items.push(...transactionItems);
     return this;
   }
@@ -109,10 +114,14 @@ export class WriteTransaction extends Transaction<
     return this;
   }
 
-  addUpdateItem<Entity, PrimaryKey = Partial<Entity>>(
+  addUpdateItem<
+    Entity,
+    PrimaryKey = Partial<Entity>,
+    AdditionalProperties = Entity
+  >(
     item: EntityTarget<Entity>,
     primaryKey: PrimaryKey,
-    body: UpdateAttributes<Entity, PrimaryKey>,
+    body: UpdateBody<Entity, AdditionalProperties>,
     options?: WriteTransactionUpdateOptions<Entity>
   ): this {
     this.items.push({
diff --git a/packages/core/src/classes/transformer/base-transformer.ts b/packages/core/src/classes/transformer/base-transformer.ts
index 2b2a57d..f782960 100644
--- a/packages/core/src/classes/transformer/base-transformer.ts
+++ b/packages/core/src/classes/transformer/base-transformer.ts
@@ -7,8 +7,8 @@ import {
   EntityTarget,
   Table,
   SparseIndexParseError,
-  UpdateAttributes,
   CONSUMED_CAPACITY_TYPE,
+  InvalidDynamicUpdateAttributeValueError,
 } from '@typedorm/common';
 import {getConstructorForInstance} from '../../helpers/get-constructor-for-instance';
 import {isEmptyObject} from '../../helpers/is-empty-object';
@@ -19,6 +19,8 @@ import {IsAutoGeneratedAttributeMetadata} from '../metadata/auto-generated-attri
 import {DynamoEntitySchemaPrimaryKey} from '../metadata/entity-metadata';
 import {isDynamoEntityKeySchema} from '../../helpers/is-dynamo-entity-key-schema';
 import {isKeyOfTypeAliasSchema} from '../../helpers/is-key-of-type-alias-schema';
+import {classToPlain} from 'class-transformer';
+import {ExpressionInputParser} from '../expression/expression-input-parser';
 
 export interface MetadataOptions {
   requestId?: string;
@@ -26,7 +28,11 @@ export interface MetadataOptions {
 }
 
 export abstract class BaseTransformer {
-  constructor(protected connection: Connection) {}
+  protected _expressionInputParser: ExpressionInputParser;
+
+  constructor(protected connection: Connection) {
+    this._expressionInputParser = new ExpressionInputParser();
+  }
   /**
    * Returns table name decorated for given entity class
    * @param entityClass Entity Class
@@ -37,43 +43,61 @@ export abstract class BaseTransformer {
     return entityMetadata.table.name;
   }
 
+  applyClassTransformerFormations<Entity>(entity: Entity) {
+    const transformedPlainEntity = classToPlain<Entity>(entity, {
+      enableImplicitConversion: true,
+      excludePrefixes: ['__'], // exclude internal attributes
+    });
+
+    return transformedPlainEntity as Entity;
+  }
+
   /**
    * Transforms entity to dynamo db entity schema
    * @param entity Entity to transform to DynamoDB entity type
    */
   toDynamoEntity<Entity>(entity: Entity) {
-    const explicitAttributes = {...entity};
     const entityClass = getConstructorForInstance(entity);
+
     // retrieve metadata and parse it to schema
     const entityMetadata = this.connection.getEntityByTarget(entityClass);
 
     this.connection.getAttributesForEntity(entityClass).forEach(attr => {
-      //  auto populate generated values
-      if (IsAutoGeneratedAttributeMetadata(attr)) {
-        entity = Object.assign(entity, {[attr.name]: attr.value});
-      }
+      // if no explicit value was provided, look for default/autoGenerate values
+      if (!Object.keys(entity).includes(attr.name)) {
+        //  auto populate generated values
+        if (IsAutoGeneratedAttributeMetadata(attr)) {
+          entity = Object.assign(entity, {[attr.name]: attr.value});
+        }
 
-      const attributeDefaultValue = (attr as AttributeMetadata)?.default;
+        const attributeDefaultValue = (attr as AttributeMetadata)?.default;
 
-      // include attribute with default value
-      if (
-        attributeDefaultValue &&
-        typeof attributeDefaultValue === 'function'
-      ) {
-        const attrDefaultValue = attributeDefaultValue(entity);
-        entity = Object.assign(entity, {
-          [attr.name]: attrDefaultValue,
-        });
+        // include attribute with default value
+        if (
+          attributeDefaultValue &&
+          typeof attributeDefaultValue === 'function'
+        ) {
+          const attrDefaultValue = attributeDefaultValue(entity);
+          entity = Object.assign(entity, {
+            [attr.name]: attrDefaultValue,
+          });
+        }
       }
     });
 
+    // pass through entity to class transformer to have all the metadata applied
+    const parsedEntity = this.applyClassTransformerFormations(entity);
+
     const parsedPrimaryKey = this.recursiveParseEntity(
       entityMetadata.schema.primaryKey.attributes,
-      entity
+      parsedEntity
     );
 
     const indexesToParse = entityMetadata.schema.indexes ?? {};
-    const rawParsedIndexes = this.recursiveParseEntity(indexesToParse, entity);
+    const rawParsedIndexes = this.recursiveParseEntity(
+      indexesToParse,
+      parsedEntity
+    );
 
     const parsedIndexes = Object.keys(rawParsedIndexes).reduce(
       (acc, currIndexKey) => {
@@ -117,7 +141,63 @@ export abstract class BaseTransformer {
       ...parsedIndexes,
     };
 
-    return {...entity, ...formattedSchema, ...explicitAttributes};
+    return {...parsedEntity, ...formattedSchema};
+  }
+
+  getAffectedPrimaryKeyAttributes<Entity>(
+    entityClass: EntityTarget<Entity>,
+    attributes: Record<string, any>,
+    attributesTypeMetadata: Record<string, 'static' | 'dynamic'>
+  ) {
+    const {
+      schema: {primaryKey},
+    } = this.connection.getEntityByTarget(entityClass);
+
+    const interpolations = primaryKey.metadata._interpolations;
+
+    // if none of partition or sort key has any referenced attributes, return
+    if (!interpolations || isEmptyObject(interpolations)) {
+      return;
+    }
+
+    const affectedKeyAttributes = Object.entries(attributes).reduce(
+      (acc, [attrKey, attrValue]: [string, any]) => {
+        // bail early if current attribute type is not of type scalar
+        if (!isScalarType(attrValue)) {
+          return acc;
+        }
+
+        // resolve all interpolations
+        Object.entries(interpolations).forEach(
+          ([primaryKeyAttrName, primaryKeyAttrRefs]) => {
+            // if no attributes are referenced for current primary key attribute, return
+            if (!primaryKeyAttrRefs.includes(attrKey)) {
+              return;
+            }
+
+            // if parsed value was of type we can not auto resolve indexes
+            // this must be resolved by the dev
+            if (attributesTypeMetadata[attrKey] === 'dynamic') {
+              throw new InvalidDynamicUpdateAttributeValueError(
+                attrKey,
+                attrValue
+              );
+            }
+
+            const parsedKey = parseKey(
+              primaryKey.attributes[primaryKeyAttrName],
+              attributes
+            );
+            acc[primaryKeyAttrName] = parsedKey;
+          }
+        );
+
+        return acc;
+      },
+      {} as Record<string, string>
+    );
+
+    return affectedKeyAttributes;
   }
 
   /**
@@ -126,9 +206,10 @@ export abstract class BaseTransformer {
    * @param attributes Attributes to check affected indexes for
    * @param options
    */
-  getAffectedIndexesForAttributes<Entity, PrimaryKey>(
+  getAffectedIndexesForAttributes<Entity>(
     entityClass: EntityTarget<Entity>,
-    attributes: UpdateAttributes<Entity, PrimaryKey>,
+    attributes: Record<string, any>,
+    attributesTypeMetadata: Record<string, 'static' | 'dynamic'>,
     options?: {nestedKeySeparator: string}
   ) {
     const nestedKeySeparator = options?.nestedKeySeparator ?? '.';
@@ -136,10 +217,9 @@ export abstract class BaseTransformer {
       schema: {indexes},
     } = this.connection.getEntityByTarget(entityClass);
 
-    const affectedIndexes = Object.keys(attributes).reduce(
-      (acc, attrKey: string) => {
-        const currAttrValue = attributes[attrKey];
-        // if current value is not if scalar type skip checking index
+    const affectedIndexes = Object.entries(attributes).reduce(
+      (acc, [attrKey, currAttrValue]) => {
+        // if current value is not of scalar type skip checking index
         if (
           attrKey.includes(nestedKeySeparator) ||
           !isScalarType(currAttrValue)
@@ -167,6 +247,15 @@ export abstract class BaseTransformer {
               interpolationsForCurrIndex[interpolationKey];
 
             if (currentInterpolation.includes(attrKey)) {
+              // if parsed value was of type we can not auto resolve indexes
+              // this must be resolved by the dev
+              if (attributesTypeMetadata[attrKey] === 'dynamic') {
+                throw new InvalidDynamicUpdateAttributeValueError(
+                  attrKey,
+                  currAttrValue
+                );
+              }
+
               try {
                 const parsedIndex = parseKey(
                   currIndex.attributes[interpolationKey],
@@ -198,7 +287,7 @@ export abstract class BaseTransformer {
   getParsedPrimaryKey<Entity>(
     table: Table,
     primaryKey: DynamoEntitySchemaPrimaryKey,
-    attributes: {[key in keyof Entity]: any}
+    attributes: Partial<Entity>
   ) {
     return this.recursiveParseEntity(primaryKey.attributes, attributes);
   }
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 b267059..c62c5e5 100644
--- a/packages/core/src/classes/transformer/document-client-request-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-request-transformer.ts
@@ -5,10 +5,15 @@ import {
   Replace,
   RETURN_VALUES,
   Table,
-  UpdateAttributes,
   TRANSFORM_TYPE,
   IndexOptions,
   QUERY_SELECT_TYPE,
+  NoSuchIndexFoundError,
+  InvalidFilterInputError,
+  InvalidSelectInputError,
+  InvalidUniqueAttributeUpdateError,
+  InvalidPrimaryKeyAttributesUpdateError,
+  InvalidDynamicUpdateAttributeValueError,
 } from '@typedorm/common';
 import {DynamoDB} from 'aws-sdk';
 import {dropProp} from '../../helpers/drop-prop';
@@ -22,10 +27,9 @@ import {AttributeMetadata} from '../metadata/attribute-metadata';
 import {DynamoEntitySchemaPrimaryKey} from '../metadata/entity-metadata';
 import {BaseTransformer, MetadataOptions} from './base-transformer';
 import {LazyTransactionWriteItemListLoader} from './is-lazy-transaction-write-item-list-loader';
-import {
-  ExpressionInputParser,
-  KeyConditionOptions,
-} from '../expression/expression-input-parser';
+import {KeyConditionOptions} from '../expression/key-condition-options-type';
+import {UpdateBody} from '../expression/update-body-type';
+import {isObject} from '../../helpers/is-object';
 
 export interface ManagerToDynamoPutItemOptions {
   /**
@@ -86,12 +90,10 @@ export interface ManagerToDynamoGetItemOptions {
 
 export class DocumentClientRequestTransformer extends BaseTransformer {
   protected _expressionBuilder: ExpressionBuilder;
-  protected _expressionInputParser: ExpressionInputParser;
 
   constructor(connection: Connection) {
     super(connection);
     this._expressionBuilder = new ExpressionBuilder();
-    this._expressionInputParser = new ExpressionInputParser();
   }
 
   get expressionBuilder() {
@@ -353,10 +355,10 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     return transformBody;
   }
 
-  toDynamoUpdateItem<Entity, PrimaryKey>(
+  toDynamoUpdateItem<Entity, PrimaryKey, AdditionalProperties = Entity>(
     entityClass: EntityTarget<Entity>,
     primaryKeyAttributes: PrimaryKey,
-    body: UpdateAttributes<Entity, PrimaryKey>,
+    body: UpdateBody<Entity, AdditionalProperties>,
     options: ManagerToDynamoUpdateItemsOptions = {},
     metadataOptions?: MetadataOptions
   ):
@@ -404,44 +406,153 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       },
       {} as {[key: string]: any}
     );
+    const rawAttributesToUpdate = {
+      ...body,
+      ...formattedAutoUpdateAttributes,
+    };
 
-    const attributesToUpdate = {...body, ...formattedAutoUpdateAttributes};
-
-    // get all affected indexes for attributes
-    const affectedIndexes = this.getAffectedIndexesForAttributes<
-      Entity,
-      PrimaryKey
-    >(entityClass, attributesToUpdate, {
-      nestedKeySeparator,
-    });
+    /**
+     * 1.0 - analyze attributes' value type (static/dynamic)
+     *
+     * Here we parse all attributes to it's update value and determine
+     * if it's value can be statically inferred
+     */
+    const staticOrDynamicUpdateAttributesWithMetadata = Object.entries({
+      ...rawAttributesToUpdate,
+    }).reduce(
+      (acc, [attrName, attrValue]) => {
+        const valueWithType = this.expressionInputParser.parseAttributeToUpdateValue(
+          attrName,
+          attrValue
+        ) as {value: any; type: 'static' | 'dynamic'};
+
+        acc.transformed[attrName] = valueWithType.value;
+        acc.typeMetadata[attrName] = valueWithType.type;
+        return acc;
+      },
+      {transformed: {}, typeMetadata: {}} as {
+        transformed: Record<string, any>;
+        typeMetadata: Record<string, 'dynamic' | 'static'>;
+      }
+    );
 
-    const {
-      UpdateExpression,
-      ExpressionAttributeNames,
-      ExpressionAttributeValues,
-    } = this.expressionBuilder.buildUpdateExpression({
-      ...attributesToUpdate,
-      ...affectedIndexes,
-    });
+    /**
+     * 2.0 - apply custom class transformation on static attributes
+     *
+     * we manually need to replace the constructor of the attributes to update
+     * with the entity class, so that we can pass it through to class-transformer
+     * to have all transformer metadata applied.
+     */
+
+    const onlyStaticAttributes = Object.entries(
+      staticOrDynamicUpdateAttributesWithMetadata.transformed
+    ).reduce((acc, [attrKey, attrValue]) => {
+      if (
+        staticOrDynamicUpdateAttributesWithMetadata.typeMetadata[attrKey] ===
+        'static'
+      ) {
+        acc[attrKey] = attrValue;
+      }
+      return acc;
+    }, {} as any);
+    onlyStaticAttributes.constructor = entityClass;
+    const classTransformedStaticAttributes = this.applyClassTransformerFormations(
+      onlyStaticAttributes
+    ) as Entity;
+    staticOrDynamicUpdateAttributesWithMetadata.transformed = {
+      ...staticOrDynamicUpdateAttributesWithMetadata.transformed,
+      ...classTransformedStaticAttributes,
+    };
 
+    /**
+     * 3.0 - Get referenced unique attributes and validate that current update body can be safely applied
+     */
     const uniqueAttributesToUpdate = this.connection
       .getUniqueAttributesForEntity(entityClass)
-      .filter(attr => !!body[attr.name]);
+      .filter(attr => !!(body as any)[attr.name])
+      .map(attr => {
+        // TODO: support updating unique attributes with dynamic exp
+        // we can't allow updating unique attributes when they contain dynamic update value
+        if (
+          staticOrDynamicUpdateAttributesWithMetadata.typeMetadata[
+            attr.name
+          ] === 'dynamic'
+        ) {
+          throw new InvalidDynamicUpdateAttributeValueError(
+            attr.name,
+            staticOrDynamicUpdateAttributesWithMetadata.transformed[attr.name]
+          );
+        }
+        return attr;
+      });
+
+    /**
+     * 3.1 - Get referenced primary key attributes and validate that current update body can be safely applied
+     */
+    const affectedPrimaryKeyAttributes = this.getAffectedPrimaryKeyAttributes<
+      Entity
+    >(
+      entityClass,
+      staticOrDynamicUpdateAttributesWithMetadata.transformed,
+      staticOrDynamicUpdateAttributesWithMetadata.typeMetadata
+    );
 
-    const itemToUpdate: DynamoDB.DocumentClient.UpdateItemInput = {
+    // validate primary key attributes
+    if (!isEmptyObject(affectedPrimaryKeyAttributes)) {
+      const primaryKeyReferencedAttributes = this.connection.getPrimaryKeyAttributeInterpolationsForEntity(
+        entityClass
+      );
+      const nonKeyAttributesToUpdate = Object.keys(body).filter(
+        attr => !primaryKeyReferencedAttributes.includes(attr)
+      );
+
+      // updates are not allowed for attributes that unique and also references primary key.
+      if (uniqueAttributesToUpdate.length) {
+        throw new InvalidUniqueAttributeUpdateError(
+          affectedPrimaryKeyAttributes!,
+          uniqueAttributesToUpdate.map(attr => attr.name)
+        );
+      }
+
+      // primary key and non key attributes can not be updated together
+      if (nonKeyAttributesToUpdate.length) {
+        throw new InvalidPrimaryKeyAttributesUpdateError(
+          affectedPrimaryKeyAttributes!,
+          nonKeyAttributesToUpdate
+        );
+      }
+    }
+
+    /**
+     * 3.2 - Get referenced indexes' attributes and validate that current update body can be safely applied
+     */
+    const affectedIndexes = this.getAffectedIndexesForAttributes<Entity>(
+      entityClass,
+      staticOrDynamicUpdateAttributesWithMetadata.transformed,
+      staticOrDynamicUpdateAttributesWithMetadata.typeMetadata,
+      {
+        nestedKeySeparator,
+      }
+    );
+
+    /**
+     * 4.0 - Build update Item body with given condition and options
+     */
+    const itemToUpdate:
+      | DynamoDB.DocumentClient.UpdateItemInput
+      | DynamoDB.DocumentClient.PutItemInput = {
       TableName: tableName,
       Key: {
         ...parsedPrimaryKey,
       },
       ReturnConsumedCapacity: metadataOptions?.returnConsumedCapacity,
-      UpdateExpression,
       // request all new attributes
       ReturnValues: RETURN_VALUES.ALL_NEW,
-      ExpressionAttributeNames,
-      ExpressionAttributeValues,
     };
 
-    // if 'where' was provided, build condition expression
+    /**
+     * 4.1 - if 'where' was provided, build condition expression
+     */
     if (options.where && !isEmptyObject(options.where)) {
       const condition = this.expressionInputParser.parseToCondition(
         options.where
@@ -464,46 +575,111 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       // append condition expression if one was built
       itemToUpdate.ConditionExpression = ConditionExpression;
       itemToUpdate.ExpressionAttributeNames = {
-        ...itemToUpdate.ExpressionAttributeNames,
         ...ExpressionAttributeNames,
+        ...itemToUpdate.ExpressionAttributeNames,
       };
       itemToUpdate.ExpressionAttributeValues = {
-        ...itemToUpdate.ExpressionAttributeValues,
         ...ExpressionAttributeValues,
+        ...itemToUpdate.ExpressionAttributeValues,
       };
     }
 
-    // when item does not have any unique attributes to update, return putItemInput
-    if (!uniqueAttributesToUpdate.length) {
-      this.connection.logger.logTransform({
-        requestId: metadataOptions?.requestId,
-        operation: TRANSFORM_TYPE.UPDATE,
-        prefix: 'After',
-        entityName: metadata.name,
-        primaryKey: null,
-        body: itemToUpdate,
-      });
-      return itemToUpdate;
+    /**
+     * 5.0 - update contains primary key attributes so it must be lazily updated
+     * This requires deleting old item and writing new item to the table both in a transaction
+     */
+    if (
+      isObject(affectedPrimaryKeyAttributes) &&
+      !isEmptyObject(affectedPrimaryKeyAttributes)
+    ) {
+      const lazyLoadTransactionWriteItems = this.lazyToDynamoUpdatePrimaryKeyFactory(
+        metadata.table,
+        metadata.name,
+        metadata.schema.primaryKey,
+        {
+          Item: {
+            ...affectedPrimaryKeyAttributes,
+            ...affectedIndexes,
+            ...staticOrDynamicUpdateAttributesWithMetadata.transformed,
+          },
+          TableName: metadata.table.name,
+          ReturnConsumedCapacity: itemToUpdate.ReturnConsumedCapacity,
+          ReturnValues: itemToUpdate.ReturnValues,
+          ConditionExpression: itemToUpdate.ConditionExpression,
+          ExpressionAttributeNames: itemToUpdate.ExpressionAttributeNames,
+          ExpressionAttributeValues: itemToUpdate.ExpressionAttributeValues,
+        },
+        metadataOptions
+      );
+
+      return {
+        primaryKeyAttributes,
+        entityClass,
+        lazyLoadTransactionWriteItems,
+      };
     }
 
-    // if there are unique attributes, return a lazy loader, which will return write item list
-    const lazyLoadTransactionWriteItems = this.lazyToDynamoUpdateItemFactory<
-      Entity,
-      PrimaryKey
-    >(
-      metadata.table,
-      metadata.name,
-      uniqueAttributesToUpdate,
-      dropProp(itemToUpdate, 'ReturnValues'),
-      body,
-      metadataOptions
+    /**
+     * 5.0.1 - build update expression with user provided body and all other auto transformation
+     */
+    const update = this.expressionInputParser.parseToUpdate(
+      {
+        ...rawAttributesToUpdate,
+        ...affectedIndexes,
+      },
+      staticOrDynamicUpdateAttributesWithMetadata.transformed
     );
 
-    return {
-      primaryKeyAttributes,
-      entityClass,
-      lazyLoadTransactionWriteItems,
+    const {
+      UpdateExpression,
+      ExpressionAttributeNames,
+      ExpressionAttributeValues,
+    } = this.expressionBuilder.buildUpdateExpression(update);
+    itemToUpdate.UpdateExpression = UpdateExpression;
+    itemToUpdate.ExpressionAttributeNames = {
+      ...ExpressionAttributeNames,
+      ...itemToUpdate.ExpressionAttributeNames,
     };
+    itemToUpdate.ExpressionAttributeValues = {
+      ...ExpressionAttributeValues,
+      ...itemToUpdate.ExpressionAttributeValues,
+    };
+
+    /**
+     * 5.1 - Update contains unique attributes, build a lazy unique attributes loader and return
+     */
+    if (uniqueAttributesToUpdate.length) {
+      // if there are unique attributes, return a lazy loader, which will return write item list
+      const lazyLoadTransactionWriteItems = this.lazyToDynamoUpdateUniqueItemFactory<
+        Entity
+      >(
+        metadata.table,
+        metadata.name,
+        uniqueAttributesToUpdate,
+        dropProp(itemToUpdate, 'ReturnValues'),
+        staticOrDynamicUpdateAttributesWithMetadata.transformed,
+        metadataOptions
+      );
+
+      return {
+        primaryKeyAttributes,
+        entityClass,
+        lazyLoadTransactionWriteItems,
+      };
+    }
+
+    /**
+     * 5.2 - return simple update body
+     */
+    this.connection.logger.logTransform({
+      requestId: metadataOptions?.requestId,
+      operation: TRANSFORM_TYPE.UPDATE,
+      prefix: 'After',
+      entityName: metadata.name,
+      primaryKey: null,
+      body: itemToUpdate,
+    });
+    return itemToUpdate;
   }
 
   toDynamoDeleteItem<Entity, PrimaryKey>(
@@ -627,9 +803,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     if (queryIndexName) {
       const matchingIndex = table.getIndexByKey(queryIndexName);
       if (!matchingIndex) {
-        throw new Error(
-          `Requested to query items from index "${queryIndexName}", but no such index exists on table "${table.name}".`
-        );
+        throw new NoSuchIndexFoundError(table.name, queryIndexName);
       }
 
       const matchingIndexOnEntity =
@@ -696,7 +870,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
         operation: TRANSFORM_TYPE.QUERY,
         prefix: 'After',
         entityName: name,
-        primaryKey: null,
+        primaryKey: partitionKeyAttributes,
         body: transformedQueryItem,
       });
 
@@ -772,11 +946,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       const filter = this.expressionInputParser.parseToFilter(where);
 
       if (!filter) {
-        throw new Error(
-          `Failed to build filter expression for input: ${JSON.stringify(
-            where
-          )}`
-        );
+        throw new InvalidFilterInputError(where);
       }
 
       const {
@@ -801,6 +971,11 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
 
     // check if only the count was requested
     if (onlyCount) {
+      if (select?.length) {
+        throw new Error(
+          'Attributes projection and count can not be used together'
+        );
+      }
       // count and projection selection can not be used together
       queryInputParams.Select = QUERY_SELECT_TYPE.COUNT;
     }
@@ -810,11 +985,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       const projection = this.expressionInputParser.parseToProjection(select);
 
       if (!projection) {
-        throw new Error(
-          `Failed to build projection expression for input: ${JSON.stringify(
-            select
-          )}`
-        );
+        throw new InvalidSelectInputError(select);
       }
 
       const {
@@ -837,20 +1008,67 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       operation: TRANSFORM_TYPE.QUERY,
       prefix: 'After',
       entityName: name,
-      primaryKey: null,
+      primaryKey: partitionKeyAttributes,
       body: queryInputParams,
     });
 
     return queryInputParams;
   }
 
+  private lazyToDynamoUpdatePrimaryKeyFactory(
+    table: Table,
+    entityName: string,
+    primaryKeySchema: DynamoEntitySchemaPrimaryKey,
+    newItemBody: DynamoDB.DocumentClient.PutItemInput,
+    metadataOptions?: MetadataOptions
+  ) {
+    return (previousItemBody: any) => {
+      const updateTransactionItems: DynamoDB.DocumentClient.TransactWriteItemList = [
+        {
+          Put: {
+            ...newItemBody,
+            // import existing current item
+            Item: {...previousItemBody, ...newItemBody.Item},
+          },
+        },
+      ] as DynamoDB.DocumentClient.TransactWriteItem[];
+
+      // if there was a previous existing item, basically remove it as part of this transaction
+      if (previousItemBody && !isEmptyObject(previousItemBody)) {
+        updateTransactionItems.push({
+          Delete: {
+            TableName: table.name,
+            Key: {
+              ...this.getParsedPrimaryKey(
+                table,
+                primaryKeySchema,
+                previousItemBody
+              ),
+            },
+          },
+        });
+      }
+
+      this.connection.logger.logTransform({
+        requestId: metadataOptions?.requestId,
+        operation: TRANSFORM_TYPE.UPDATE,
+        prefix: 'After',
+        entityName,
+        primaryKey: null,
+        body: updateTransactionItems,
+      });
+
+      return updateTransactionItems;
+    };
+  }
+
   /**
    * Lazy build update item input
    * This is helpful in cases where we don't you have all the attributes to build item input, and the caller will need to
    * to perform some sort of async call in order to fetch attributes and proceed with build
    *
    */
-  private lazyToDynamoUpdateItemFactory<Entity, PrimaryKey>(
+  private lazyToDynamoUpdateUniqueItemFactory<Entity>(
     table: Table,
     entityName: string,
     uniqueAttributesToUpdate: Replace<
@@ -861,7 +1079,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       }
     >[],
     mainItem: DynamoDB.DocumentClient.UpdateItemInput,
-    newBody: UpdateAttributes<Entity, PrimaryKey>,
+    newBody: any,
     metadataOptions?: MetadataOptions
   ) {
     // returns transact write item list
@@ -879,7 +1097,11 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
               Put: {
                 TableName: table.name,
                 Item: {
-                  ...this.getParsedPrimaryKey(table, attr.unique, newBody),
+                  ...this.getParsedPrimaryKey(
+                    table,
+                    attr.unique,
+                    newBody as Partial<Entity>
+                  ),
                 },
                 ...uniqueRecordConditionExpression,
               },
diff --git a/packages/core/src/classes/transformer/document-client-scan-transformer.ts b/packages/core/src/classes/transformer/document-client-scan-transformer.ts
new file mode 100644
index 0000000..2de861d
--- /dev/null
+++ b/packages/core/src/classes/transformer/document-client-scan-transformer.ts
@@ -0,0 +1,255 @@
+import {
+  EntityTarget,
+  INTERNAL_ENTITY_ATTRIBUTE,
+  InvalidFilterInputError,
+  InvalidSelectInputError,
+  NoSuchEntityExistsError,
+  NoSuchIndexFoundError,
+  QUERY_SELECT_TYPE,
+  TRANSFORM_SCAN_TYPE,
+} from '@typedorm/common';
+import {DynamoDB} from 'aws-sdk';
+import {isEmptyObject} from '../../helpers/is-empty-object';
+import {Connection} from '../connection/connection';
+import {MERGE_STRATEGY} from '../expression/base-expression-input';
+import {Filter} from '../expression/filter';
+import {MetadataOptions} from './base-transformer';
+import {LowOrderTransformers} from './low-order-transformers';
+
+interface ScanTransformerToDynamoScanOptions {
+  entity?: EntityTarget<any>;
+  scanIndex?: string;
+  limit?: number;
+  cursor?: DynamoDB.DocumentClient.Key;
+  where?: any;
+  select?: any[];
+  onlyCount?: boolean;
+  segment?: number; // current segment
+  totalSegments?: number; // total segments that this parallel scan will be divided in
+}
+
+export class DocumentClientScanTransformer extends LowOrderTransformers {
+  constructor(connection: Connection) {
+    super(connection);
+  }
+
+  /**
+   * Transforms TypeDORM input into dynamo scan operation input
+   */
+  toDynamoScanItem(
+    scanOptions?: ScanTransformerToDynamoScanOptions,
+    metadataOptions?: MetadataOptions
+  ): DynamoDB.DocumentClient.ScanInput {
+    this.connection.logger.logTransformScan({
+      requestId: metadataOptions?.requestId,
+      operation: TRANSFORM_SCAN_TYPE.SCAN,
+      prefix: 'Before',
+      options: scanOptions,
+    });
+
+    const tableToScan = scanOptions?.entity
+      ? this.connection.getEntityByTarget(scanOptions?.entity)?.table
+      : this.connection.table;
+
+    let verifiedIndexToScan: string | undefined;
+    // validate if index requested to scan belongs to current resolved table
+    if (scanOptions?.scanIndex) {
+      const scanIndexOptions = tableToScan.getIndexByKey(
+        scanOptions?.scanIndex
+      );
+      if (!scanIndexOptions) {
+        throw new NoSuchIndexFoundError(
+          tableToScan.name,
+          scanOptions?.scanIndex
+        );
+      }
+    }
+
+    let transformedScanInput: DynamoDB.DocumentClient.ScanInput = {
+      TableName: tableToScan.name,
+      IndexName: verifiedIndexToScan,
+      ReturnConsumedCapacity: metadataOptions?.returnConsumedCapacity,
+    };
+
+    // transform additional options
+    if (scanOptions && !isEmptyObject(scanOptions)) {
+      const {cursor, limit, where, onlyCount, select, entity} = scanOptions;
+
+      transformedScanInput.Limit = limit;
+      transformedScanInput.ExclusiveStartKey = cursor;
+
+      // check if only the count was requested
+      if (onlyCount) {
+        if (select?.length) {
+          throw new Error(
+            'Attributes projection and count can not be used together'
+          );
+        }
+        // count and projection selection can not be used together
+        transformedScanInput.Select = QUERY_SELECT_TYPE.COUNT;
+      }
+
+      // entity filter
+      let entityFilter: Filter | undefined = undefined;
+      if (entity) {
+        const metadata = this.connection.getEntityByTarget(entity);
+
+        if (!metadata) {
+          throw new NoSuchEntityExistsError(entity.name);
+        }
+        // build current entity filter
+        entityFilter = this.expressionInputParser.parseToFilter({
+          [INTERNAL_ENTITY_ATTRIBUTE.ENTITY_NAME]: {
+            EQ: metadata.name,
+          },
+        } as any);
+      }
+
+      // build filter expression
+      let optionsFilter: Filter | undefined = undefined;
+      if (where && !isEmptyObject(where)) {
+        const inputFilter = this.expressionInputParser.parseToFilter(where);
+
+        if (!inputFilter) {
+          throw new InvalidFilterInputError(where);
+        }
+        optionsFilter = inputFilter;
+      }
+
+      // merge filters of fall back to none
+      let filter: Filter | null;
+      if (entityFilter && optionsFilter) {
+        filter = new Filter().mergeMany(
+          [entityFilter, optionsFilter],
+          MERGE_STRATEGY.AND
+        );
+      } else {
+        filter = entityFilter || optionsFilter || null; // if non of the condition skip it all together
+      }
+
+      // if at least one condition was truthy, parse it and include it in the input
+      if (filter) {
+        const {
+          FilterExpression,
+          ExpressionAttributeNames,
+          ExpressionAttributeValues,
+        } = this.expressionBuilder.buildFilterExpression(filter);
+
+        transformedScanInput = {
+          ...transformedScanInput,
+          FilterExpression,
+          ExpressionAttributeNames: {
+            ...transformedScanInput.ExpressionAttributeNames,
+            ...ExpressionAttributeNames,
+          },
+          ExpressionAttributeValues: {
+            ...transformedScanInput.ExpressionAttributeValues,
+            ...ExpressionAttributeValues,
+          },
+        };
+      }
+
+      // projection builder
+      if (select?.length) {
+        const projection = this.expressionInputParser.parseToProjection(select);
+
+        if (!projection) {
+          throw new InvalidSelectInputError(select);
+        }
+
+        const {
+          ProjectionExpression,
+          ExpressionAttributeNames,
+        } = this.expressionBuilder.buildProjectionExpression(projection);
+
+        transformedScanInput = {
+          ...transformedScanInput,
+          ProjectionExpression,
+          ExpressionAttributeNames: {
+            ...transformedScanInput.ExpressionAttributeNames,
+            ...ExpressionAttributeNames,
+          },
+        };
+      }
+    }
+
+    // validate value for segment and totalSegment before appending
+    if (
+      scanOptions?.totalSegments !== undefined &&
+      scanOptions?.totalSegments !== null
+    ) {
+      if (scanOptions?.totalSegments === 0) {
+        throw new Error(`Invalid scan option totalSegment: ${scanOptions?.totalSegments}.
+        totalSegments is optional, but when provided it's value must be greater than 0.`);
+      }
+      if (scanOptions?.segment === undefined || scanOptions?.segment === null) {
+        throw new Error(`Invalid scan option segment: ${scanOptions?.segment}.
+        When totalSegments value is defined, value for option 'segment' must also be defined.`);
+      }
+      if (scanOptions?.segment >= scanOptions?.totalSegments) {
+        throw new Error(`Invalid scan option segment: ${scanOptions?.segment}.
+        When totalSegments value is defined, value for option 'segment' must be one less than totalSegment size.`);
+      }
+
+      transformedScanInput.TotalSegments = scanOptions?.totalSegments;
+      transformedScanInput.Segment = scanOptions?.segment;
+    }
+
+    this.connection.logger.logTransformScan({
+      requestId: metadataOptions?.requestId,
+      prefix: 'After',
+      operation: TRANSFORM_SCAN_TYPE.SCAN,
+      body: transformedScanInput,
+    });
+
+    return transformedScanInput;
+  }
+
+  /**
+   * Transforms DynamoDB scan output into entities
+   */
+  fromDynamoScanResponseItemList<T>(
+    itemList: DynamoDB.DocumentClient.ItemList,
+    metadataOptions?: MetadataOptions
+  ): {
+    items: T[];
+    unknownItems: DynamoDB.DocumentClient.AttributeMap[];
+  } {
+    const initialResponse: {
+      items: T[];
+      unknownItems: DynamoDB.DocumentClient.AttributeMap[];
+    } = {
+      items: [],
+      unknownItems: [],
+    };
+
+    if (!itemList.length) {
+      return initialResponse;
+    }
+
+    return itemList.reduce((acc: typeof initialResponse, responseItem) => {
+      const entityPhysicalName =
+        responseItem[INTERNAL_ENTITY_ATTRIBUTE.ENTITY_NAME];
+
+      // early return if no entity metadata was found on item
+      if (!entityPhysicalName) {
+        acc.unknownItems.push(responseItem);
+        return acc;
+      }
+
+      const entityMetadata = this.connection.getEntityByPhysicalName(
+        entityPhysicalName
+      );
+
+      const reverseTransformedItem = this.fromDynamoEntity(
+        entityMetadata.target,
+        responseItem,
+        metadataOptions
+      );
+
+      acc.items.push(reverseTransformedItem);
+
+      return acc;
+    }, initialResponse);
+  }
+}
diff --git a/packages/core/src/classes/transformer/document-client-transaction-transformer.ts b/packages/core/src/classes/transformer/document-client-transaction-transformer.ts
index e75a955..d616989 100644
--- a/packages/core/src/classes/transformer/document-client-transaction-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-transaction-transformer.ts
@@ -124,7 +124,7 @@ export class DocumentClientTransactionTransformer extends LowOrderTransformers {
    * - lazyTransactionWriteItemListLoaderItems: items that are must be processed in transaction but also requires other requests to be made first (i.e delete of unique items)
    */
   private innerTransformTransactionWriteItems(
-    transactionItems: WriteTransactionItem<any, any>[],
+    transactionItems: WriteTransactionItem<any, any, any>[],
     metadataOptions?: MetadataOptions
   ) {
     return transactionItems.reduce(
diff --git a/packages/core/src/classes/transformer/entity-transformer.ts b/packages/core/src/classes/transformer/entity-transformer.ts
index 8db1c28..3b8fa7d 100644
--- a/packages/core/src/classes/transformer/entity-transformer.ts
+++ b/packages/core/src/classes/transformer/entity-transformer.ts
@@ -1,5 +1,6 @@
 import {DynamoEntity, EntityTarget, TRANSFORM_TYPE} from '@typedorm/common';
 import {DocumentClient} from 'aws-sdk/clients/dynamodb';
+import {plainToClassFromExist} from 'class-transformer';
 import {unParseKey} from '../../helpers/unparse-key';
 import {Connection} from '../connection/connection';
 import {BaseTransformer, MetadataOptions} from './base-transformer';
@@ -49,19 +50,29 @@ export class EntityTransformer extends BaseTransformer {
       })
       .flat();
 
-    const transformedEntity = Object.keys(dynamoEntity).reduce((acc, key) => {
-      // if any of the below conditions are true, skip adding given attribute from returning response
-      if (
-        entityPrimaryKeys.includes(key) ||
-        entityIndexes.includes(key) ||
-        entityInternalAttributeKeys.includes(key) ||
-        entityHiddenAttributeKeys.includes(key)
-      ) {
+    const plainEntityAttributes = Object.keys(dynamoEntity).reduce(
+      (acc, key) => {
+        // if any of the below conditions are true, skip adding given attribute from returning response
+        if (
+          entityPrimaryKeys.includes(key) ||
+          entityIndexes.includes(key) ||
+          entityInternalAttributeKeys.includes(key) ||
+          entityHiddenAttributeKeys.includes(key)
+        ) {
+          return acc;
+        }
+        (acc as any)[key] = (dynamoEntity as any)[key];
         return acc;
-      }
-      (acc as any)[key] = (dynamoEntity as any)[key];
-      return acc;
-    }, {} as Entity);
+      },
+      {} as Object
+    );
+
+    // get reflected constructor to avoid initialization issues with custom constructor
+    const reflectedConstructor = Reflect.construct(Object, [], entityClass);
+    const transformedEntity = plainToClassFromExist(
+      reflectedConstructor,
+      plainEntityAttributes
+    );
 
     this.connection.logger.logTransform({
       requestId: metadataOptions?.requestId,
diff --git a/packages/core/src/helpers/is-object.ts b/packages/core/src/helpers/is-object.ts
index 1f2819e..2624263 100644
--- a/packages/core/src/helpers/is-object.ts
+++ b/packages/core/src/helpers/is-object.ts
@@ -1,3 +1,3 @@
 export function isObject(item: any) {
-  return typeof item === 'object' && item !== null;
+  return typeof item === 'object' && item !== null && !Array.isArray(item);
 }


Test patch:
diff --git a/packages/core/src/classes/connection/__test__/attributes-metadata-builder.spec.ts b/packages/core/src/classes/connection/__test__/attributes-metadata-builder.spec.ts
index df5f1b4..5609c57 100644
--- a/packages/core/src/classes/connection/__test__/attributes-metadata-builder.spec.ts
+++ b/packages/core/src/classes/connection/__test__/attributes-metadata-builder.spec.ts
@@ -48,6 +48,12 @@ test('builds simple attribute metadata', () => {
       entityClass: User,
       table,
     },
+    {
+      entityClass: User,
+      type: 'Array',
+      name: 'addresses',
+      table,
+    },
   ]);
 });
 
@@ -90,6 +96,12 @@ test('builds attribute metadata for inherited entity', () => {
       entityClass: Admin,
       table,
     },
+    {
+      entityClass: Admin,
+      type: 'Array',
+      name: 'addresses',
+      table,
+    },
   ]);
 });
 
diff --git a/packages/core/src/classes/connection/__test__/connection-metadata-builder.spec.ts b/packages/core/src/classes/connection/__test__/connection-metadata-builder.spec.ts
index cdf8764..e2e36ed 100644
--- a/packages/core/src/classes/connection/__test__/connection-metadata-builder.spec.ts
+++ b/packages/core/src/classes/connection/__test__/connection-metadata-builder.spec.ts
@@ -4,6 +4,12 @@ import {User} from '../../../../__mocks__/user';
 import path from 'path';
 import {createTestConnection, resetTestConnection} from '@typedorm/testing';
 import {ConnectionMetadataBuilder} from '../connection-metadata-builder';
+import {
+  Attribute,
+  DuplicateEntityPhysicalNameError,
+  Entity,
+} from '@typedorm/common';
+import {table} from '@typedorm/core/__mocks__/table';
 
 let metadataBuilder: ConnectionMetadataBuilder;
 beforeEach(() => {
@@ -34,6 +40,25 @@ test('builds entity metadata with path match', () => {
   const entities = metadataBuilder.buildEntityMetadatas(
     path.resolve(__dirname, '../../../../__mocks__/**/*.ts')
   );
+  expect(entities.length).toEqual(10);
+});
+
+test('throws when trying to register duplicated entities', () => {
+  @Entity({
+    table,
+    name: 'user',
+    primaryKey: {
+      partitionKey: '{{id}}',
+      sortKey: '{{id}}',
+    },
+  })
+  class DuplicateUser {
+    @Attribute()
+    id: string;
+  }
+
+  const entitiesFactory = () =>
+    metadataBuilder.buildEntityMetadatas([DuplicateUser, User]);
 
-  expect(entities.length).toEqual(8);
+  expect(entitiesFactory).toThrow(DuplicateEntityPhysicalNameError);
 });
diff --git a/packages/core/src/classes/expression/__test__/add-update.spec.ts b/packages/core/src/classes/expression/__test__/add-update.spec.ts
new file mode 100644
index 0000000..cb7474c
--- /dev/null
+++ b/packages/core/src/classes/expression/__test__/add-update.spec.ts
@@ -0,0 +1,25 @@
+import {AddUpdate} from '../update/add-update';
+
+/**
+ * @group addTo
+ */
+test('creates update expression for add update', () => {
+  const update = new AddUpdate()
+    .addTo('age', 2)
+    .and()
+    .addTo('book.titles', ['new book']);
+
+  expect(update).toEqual({
+    _names: {
+      '#UE_age': 'age',
+      '#UE_book': 'book',
+      '#UE_book_titles': 'titles',
+    },
+    _values: {
+      ':UE_age': 2,
+      ':UE_book_titles': ['new book'],
+    },
+    expression: '#UE_age :UE_age, #UE_book.#UE_book_titles :UE_book_titles',
+    prefix: 'ADD',
+  });
+});
diff --git a/packages/core/src/classes/expression/__test__/delete-update.spec.ts b/packages/core/src/classes/expression/__test__/delete-update.spec.ts
new file mode 100644
index 0000000..33fe437
--- /dev/null
+++ b/packages/core/src/classes/expression/__test__/delete-update.spec.ts
@@ -0,0 +1,15 @@
+import {DeleteUpdate} from '../update/delete-update';
+
+test('creates a update expression for deleting attributes from list', () => {
+  const update = new DeleteUpdate().delete('items', ['book 1']);
+  expect(update).toEqual({
+    _names: {
+      '#UE_items': 'items',
+    },
+    _values: {
+      ':UE_items': ['book 1'],
+    },
+    expression: '#UE_items :UE_items',
+    prefix: 'DELETE',
+  });
+});
diff --git a/packages/core/src/classes/expression/__test__/expression-builder.spec.ts b/packages/core/src/classes/expression/__test__/expression-builder.spec.ts
index 83ce1a0..a0bc23d 100644
--- a/packages/core/src/classes/expression/__test__/expression-builder.spec.ts
+++ b/packages/core/src/classes/expression/__test__/expression-builder.spec.ts
@@ -2,6 +2,11 @@ import {Condition} from '../condition';
 import {ExpressionBuilder} from '../expression-builder';
 import {Filter} from '../filter';
 import {Projection} from '../projection';
+import {AddUpdate} from '../update/add-update';
+import {DeleteUpdate} from '../update/delete-update';
+import {RemoveUpdate} from '../update/remove-update';
+import {SetUpdate} from '../update/set-update';
+import {Update} from '../update/update';
 
 const expressionBuilder = new ExpressionBuilder();
 
@@ -25,107 +30,142 @@ test('builds condition expression', () => {
  * @group buildUpdateExpression
  */
 test('builds update expression', () => {
-  const item = {
-    name: 'new name',
-  };
+  const item = new Update().merge(new SetUpdate().setTo('name', 'new name'));
   const expression = expressionBuilder.buildUpdateExpression(item);
   expect(expression).toEqual({
-    UpdateExpression: 'SET #attr0 = :val0',
+    UpdateExpression: 'SET #UE_name = :UE_name',
     ExpressionAttributeNames: {
-      '#attr0': 'name',
+      '#UE_name': 'name',
     },
     ExpressionAttributeValues: {
-      ':val0': 'new name',
+      ':UE_name': 'new name',
     },
   });
 });
 
 test('builds update expression for nested object', () => {
-  const item = {
-    'user.name': 'new name',
-  };
+  const item = new Update().merge(
+    new SetUpdate().setTo('user.name', 'new name')
+  );
   const expression = expressionBuilder.buildUpdateExpression(item);
   expect(expression).toEqual({
-    UpdateExpression: 'SET #attr0_inner0.#attr0_inner1 = :val0',
+    UpdateExpression: 'SET #UE_user.#UE_user_name = :UE_user_name',
     ExpressionAttributeNames: {
-      '#attr0_inner0': 'user',
-      '#attr0_inner1': 'name',
+      '#UE_user': 'user',
+      '#UE_user_name': 'name',
     },
     ExpressionAttributeValues: {
-      ':val0': 'new name',
+      ':UE_user_name': 'new name',
     },
   });
 });
 
 test('builds update expression for complex nested object', () => {
-  const item = {
-    application: 'test',
-    'profile.name.last': 'new Last name',
-    data: [1, 2, 3],
-    'address[0]': 'new address portion',
-    'complex.nested.object[1]': 'new value',
-  };
+  const item = new Update().merge(
+    new SetUpdate()
+      .setTo('application', 'test')
+      .and()
+      .setTo('profile.name.last', 'new Last name')
+      .and()
+      .setTo('data', [1, 2, 3])
+      .and()
+      .setTo('address[0]', 'new address portion')
+      .and()
+      .setTo('complex.nested.object[1]', 'new value')
+  );
   const expression = expressionBuilder.buildUpdateExpression(item);
   expect(expression).toEqual({
     UpdateExpression:
-      'SET #attr0 = :val0, #attr1_inner0.#attr1_inner1.#attr1_inner2 = :val1, #attr2 = :val2, #attr3 = :val3, #attr4_inner0.#attr4_inner1.#attr4_inner2 = :val4',
+      'SET #UE_application = :UE_application, #UE_profile.#UE_profile_name.#UE_profile_name_last = :UE_profile_name_last, #UE_data = :UE_data, #UE_address[0] = :UE_address[0], #UE_complex.#UE_complex_nested.#UE_complex_nested_object[1] = :UE_complex_nested_object[1]',
     ExpressionAttributeNames: {
-      '#attr0': 'application',
-      '#attr1_inner0': 'profile',
-      '#attr1_inner1': 'name',
-      '#attr1_inner2': 'last',
-      '#attr2': 'data',
-      '#attr3': 'address[0]',
-      '#attr4_inner0': 'complex',
-      '#attr4_inner1': 'nested',
-      '#attr4_inner2': 'object[1]',
+      '#UE_address[0]': 'address[0]',
+      '#UE_application': 'application',
+      '#UE_complex': 'complex',
+      '#UE_complex_nested': 'nested',
+      '#UE_complex_nested_object[1]': 'object[1]',
+      '#UE_data': 'data',
+      '#UE_profile': 'profile',
+      '#UE_profile_name': 'name',
+      '#UE_profile_name_last': 'last',
     },
     ExpressionAttributeValues: {
-      ':val0': 'test',
-      ':val1': 'new Last name',
-      ':val2': [1, 2, 3],
-      ':val3': 'new address portion',
-      ':val4': 'new value',
+      ':UE_address[0]': 'new address portion',
+      ':UE_application': 'test',
+      ':UE_complex_nested_object[1]': 'new value',
+      ':UE_data': [1, 2, 3],
+      ':UE_profile_name_last': 'new Last name',
     },
   });
 });
 
+test('builds update expression for body with different actions', () => {
+  const item = new Update().mergeMany([
+    new SetUpdate()
+      .setTo('application', 'test')
+      .and()
+      .setTo('profile.name.last', 'new Last name'),
+    new AddUpdate().addTo('data', [1, 2, 3]),
+    new RemoveUpdate().remove('address', {
+      atIndexes: [0],
+    }),
+    new DeleteUpdate().delete('complex.nested.attr', ['new value']),
+  ]);
+  const expression = expressionBuilder.buildUpdateExpression(item);
+  expect(expression).toEqual({
+    ExpressionAttributeNames: {
+      '#UE_address': 'address',
+      '#UE_application': 'application',
+      '#UE_complex': 'complex',
+      '#UE_complex_nested': 'nested',
+      '#UE_complex_nested_attr': 'attr',
+      '#UE_data': 'data',
+      '#UE_profile': 'profile',
+      '#UE_profile_name': 'name',
+      '#UE_profile_name_last': 'last',
+    },
+    ExpressionAttributeValues: {
+      ':UE_application': 'test',
+      ':UE_complex_nested_attr': ['new value'],
+      ':UE_data': [1, 2, 3],
+      ':UE_profile_name_last': 'new Last name',
+    },
+    UpdateExpression:
+      'SET #UE_application = :UE_application, #UE_profile.#UE_profile_name.#UE_profile_name_last = :UE_profile_name_last ADD #UE_data :UE_data REMOVE #UE_address[0] DELETE #UE_complex.#UE_complex_nested.#UE_complex_nested_attr :UE_complex_nested_attr',
+  });
+});
+
 /**
- * Issue 41
+ * Issue 41 null values
  */
-test('allows updating attribute with undefined value', () => {
-  const item = {
-    'user.name': undefined,
-  };
+test('allows updating attribute with null value', () => {
+  const item = new Update().merge(new SetUpdate().setTo('user.name', null));
   const expression = expressionBuilder.buildUpdateExpression(item);
   expect(expression).toEqual({
-    UpdateExpression: 'SET #attr0_inner0.#attr0_inner1 = :val0',
+    UpdateExpression: 'SET #UE_user.#UE_user_name = :UE_user_name',
     ExpressionAttributeNames: {
-      '#attr0_inner0': 'user',
-      '#attr0_inner1': 'name',
+      '#UE_user': 'user',
+      '#UE_user_name': 'name',
     },
     ExpressionAttributeValues: {
-      ':val0': null,
+      ':UE_user_name': null,
     },
   });
 });
 
 /**
- * Issue 41
+ * Issue 41 falsy values
  */
 test('allows updating attribute with empty string', () => {
-  const item = {
-    'user.name': '',
-  };
+  const item = new Update().merge(new SetUpdate().setTo('user.name', ''));
   const expression = expressionBuilder.buildUpdateExpression(item);
   expect(expression).toEqual({
-    UpdateExpression: 'SET #attr0_inner0.#attr0_inner1 = :val0',
+    UpdateExpression: 'SET #UE_user.#UE_user_name = :UE_user_name',
     ExpressionAttributeNames: {
-      '#attr0_inner0': 'user',
-      '#attr0_inner1': 'name',
+      '#UE_user': 'user',
+      '#UE_user_name': 'name',
     },
     ExpressionAttributeValues: {
-      ':val0': '',
+      ':UE_user_name': '',
     },
   });
 });
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 78e9def..06a6ef1 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
@@ -5,6 +5,7 @@ import {ExpressionInputParser} from '../expression-input-parser';
 import {Filter} from '../filter';
 import {KeyCondition} from '../key-condition';
 import {Projection} from '../projection';
+import {Update} from '../update/update';
 
 let expInputParser: ExpressionInputParser;
 beforeEach(() => {
@@ -230,3 +231,360 @@ test('parses options to valid projection', () => {
     '#PE_id, #PE_name, #PE_status.#PE_status_active'
   );
 });
+
+/**
+ * @group parseToUpdate
+ *
+ */
+test('parses update body to update expression', () => {
+  const update = expInputParser.parseToUpdate<
+    User,
+    {'user.newAddresses': Array<string>}
+  >({
+    id: '2',
+    name: {
+      IF_NOT_EXISTS: {
+        $PATH: 'id',
+        $VALUE: '123',
+      },
+    },
+    status: {
+      IF_NOT_EXISTS: '1',
+    },
+    age: {
+      INCREMENT_BY: 2,
+    },
+    addresses: {
+      LIST_APPEND: ['1234'],
+    },
+    'user.newAddresses': {
+      LIST_APPEND: {
+        $PATH: 'addresses',
+        $VALUE: ['123'],
+      },
+    },
+  });
+
+  expect(update).toBeInstanceOf(Update);
+  expect(update).toEqual({
+    _names: {
+      '#UE_addresses': 'addresses',
+      '#UE_age': 'age',
+      '#UE_id': 'id',
+      '#UE_name': 'name',
+      '#UE_status': 'status',
+      '#UE_user': 'user',
+      '#UE_user_newAddresses': 'newAddresses',
+    },
+    _values: {
+      ':UE_addresses': ['1234'],
+      ':UE_user_newAddresses': ['123'],
+      ':UE_age': 2,
+      ':UE_id': '2',
+      ':UE_name': '123',
+      ':UE_status': '1',
+    },
+    expression:
+      'SET #UE_id = :UE_id, #UE_name = if_not_exists(#UE_id, :UE_name), #UE_status = if_not_exists(#UE_status, :UE_status), #UE_age = #UE_age + :UE_age, #UE_addresses = list_append(#UE_addresses, :UE_addresses), #UE_user.#UE_user_newAddresses = list_append(#UE_addresses, :UE_user_newAddresses)',
+    prefix: '',
+  });
+});
+
+test('parses update body to update expression with custom transform overrides ', () => {
+  const update = expInputParser.parseToUpdate<
+    User,
+    {'user.newAddresses': Array<string>}
+  >(
+    {
+      id: '2',
+      name: {
+        IF_NOT_EXISTS: {
+          $PATH: 'id',
+          $VALUE: '123',
+        },
+      },
+    },
+    {
+      // when custom transformation is applied
+      name: 'custom-transformed-name',
+    }
+  );
+
+  expect(update).toBeInstanceOf(Update);
+  expect(update).toEqual({
+    _names: {
+      '#UE_id': 'id',
+      '#UE_name': 'name',
+    },
+    _values: {
+      ':UE_id': '2',
+      ':UE_name': 'custom-transformed-name',
+    },
+    expression:
+      'SET #UE_id = :UE_id, #UE_name = if_not_exists(#UE_id, :UE_name)',
+    prefix: '',
+  });
+});
+
+test('parses dynamic body to update expression with custom transform overrides', () => {
+  const update = expInputParser.parseToUpdate<
+    User,
+    {'user.newAddresses': Array<string>}
+  >(
+    {
+      id: '2',
+      age: {
+        ADD: 2,
+      },
+    },
+    {
+      // even tho we provided custom transformed value here, it should not be included
+      age: 200,
+    }
+  );
+
+  expect(update).toBeInstanceOf(Update);
+  expect(update).toEqual({
+    _names: {
+      '#UE_age': 'age',
+      '#UE_id': 'id',
+    },
+    _values: {
+      ':UE_age': 2,
+      ':UE_id': '2',
+    },
+    expression: 'SET #UE_id = :UE_id ADD #UE_age :UE_age',
+    prefix: '',
+  });
+});
+
+test('parses explicit set update body', () => {
+  const update = expInputParser.parseToUpdate<User, UserPrimaryKey>({
+    id: {
+      SET: '2',
+    },
+    name: {
+      SET: {
+        IF_NOT_EXISTS: {
+          $PATH: 'age',
+          $VALUE: '2',
+        },
+      },
+    },
+    age: {
+      SET: {
+        INCREMENT_BY: 2,
+      },
+    },
+    addresses: {
+      SET: {
+        LIST_APPEND: ['1234'],
+      },
+    },
+  });
+  expect(update).toBeInstanceOf(Update);
+  expect(update.expression).toEqual(
+    'SET #UE_id = :UE_id, #UE_name = if_not_exists(#UE_age, :UE_name), #UE_age = #UE_age + :UE_age, #UE_addresses = list_append(#UE_addresses, :UE_addresses)'
+  );
+  expect(update).toEqual({
+    _names: {
+      '#UE_addresses': 'addresses',
+      '#UE_age': 'age',
+      '#UE_id': 'id',
+      '#UE_name': 'name',
+    },
+    _values: {
+      ':UE_addresses': ['1234'],
+      ':UE_age': 2,
+      ':UE_id': '2',
+      ':UE_name': '2',
+    },
+    expression:
+      'SET #UE_id = :UE_id, #UE_name = if_not_exists(#UE_age, :UE_name), #UE_age = #UE_age + :UE_age, #UE_addresses = list_append(#UE_addresses, :UE_addresses)',
+    prefix: '',
+  });
+});
+
+test('parses explicit "ADD" update body', () => {
+  const update = expInputParser.parseToUpdate<
+    User,
+    {newAddresses: Array<number>}
+  >({
+    age: {
+      ADD: 1,
+    },
+    addresses: {
+      ADD: ['123'],
+    },
+    newAddresses: {
+      ADD: [1234],
+    },
+  });
+  expect(update).toBeInstanceOf(Update);
+  expect(update).toEqual({
+    _names: {
+      '#UE_addresses': 'addresses',
+      '#UE_age': 'age',
+      '#UE_newAddresses': 'newAddresses',
+    },
+    _values: {
+      ':UE_addresses': ['123'],
+      ':UE_age': 1,
+      ':UE_newAddresses': [1234],
+    },
+    expression:
+      'ADD #UE_age :UE_age, #UE_addresses :UE_addresses, #UE_newAddresses :UE_newAddresses',
+    prefix: '',
+  });
+});
+
+test('parses explicit "REMOVE" update body', () => {
+  const update = expInputParser.parseToUpdate<
+    User,
+    {newAddresses: Array<Buffer>}
+  >({
+    age: {
+      REMOVE: true,
+    },
+    newAddresses: {
+      REMOVE: {
+        $AT_INDEX: [1, 3, 4],
+      },
+    },
+  });
+
+  expect(update).toBeInstanceOf(Update);
+  expect(update).toEqual({
+    _names: {
+      '#UE_age': 'age',
+      '#UE_newAddresses': 'newAddresses',
+    },
+    _values: {},
+    expression:
+      'REMOVE #UE_age, #UE_newAddresses[1], #UE_newAddresses[3], #UE_newAddresses[4]',
+    prefix: '',
+  });
+});
+
+test('parses explicit "DELETE" update body', () => {
+  const update = expInputParser.parseToUpdate<
+    User,
+    {newAddresses: Array<Buffer>}
+  >({
+    addresses: {
+      DELETE: ['123'],
+    },
+    newAddresses: {
+      DELETE: [Buffer.from('12')],
+    },
+  });
+
+  expect(update).toBeInstanceOf(Update);
+  expect(update).toEqual({
+    _names: {
+      '#UE_addresses': 'addresses',
+      '#UE_newAddresses': 'newAddresses',
+    },
+    _values: {
+      ':UE_addresses': ['123'],
+      ':UE_newAddresses': [Buffer.from('12')],
+    },
+    expression:
+      'DELETE #UE_addresses :UE_addresses, #UE_newAddresses :UE_newAddresses',
+    prefix: '',
+  });
+});
+
+test('parses update body with mixed actions', () => {
+  const update = expInputParser.parseToUpdate<User>({
+    id: '2',
+    name: {
+      IF_NOT_EXISTS: {
+        $PATH: 'id',
+        $VALUE: '123',
+      },
+    },
+    status: {
+      SET: {
+        IF_NOT_EXISTS: 'active',
+      },
+    },
+    age: {
+      ADD: 1,
+    },
+    addresses: {
+      DELETE: ['123'],
+    },
+  });
+
+  expect(update).toBeInstanceOf(Update);
+  expect(update).toEqual({
+    _names: {
+      '#UE_addresses': 'addresses',
+      '#UE_age': 'age',
+      '#UE_id': 'id',
+      '#UE_name': 'name',
+      '#UE_status': 'status',
+    },
+    _values: {
+      ':UE_addresses': ['123'],
+      ':UE_age': 1,
+      ':UE_id': '2',
+      ':UE_name': '123',
+      ':UE_status': 'active',
+    },
+    expression:
+      'SET #UE_id = :UE_id, #UE_name = if_not_exists(#UE_id, :UE_name), #UE_status = if_not_exists(#UE_status, :UE_status) ADD #UE_age :UE_age DELETE #UE_addresses :UE_addresses',
+    prefix: '',
+  });
+});
+
+/**
+ * @group parseToUpdateValue
+ */
+test('correctly parses ADD and returns update values', () => {
+  const value = expInputParser.parseAttributeToUpdateValue('age', {
+    ADD: 1,
+  });
+  expect(value).toEqual({type: 'dynamic', value: 1});
+});
+
+test('skips parsing and returns update values', () => {
+  const value = expInputParser.parseAttributeToUpdateValue('name', {
+    firstName: 'test',
+  });
+  expect(value).toEqual({
+    type: 'static',
+    value: {
+      firstName: 'test',
+    },
+  });
+});
+test('parses SET action with static value', () => {
+  const value = expInputParser.parseAttributeToUpdateValue('name', {
+    IF_NOT_EXISTS: 'new name',
+  });
+  expect(value).toEqual({
+    type: 'static',
+    value: 'new name',
+  });
+});
+test('parses SET action as dynamic value', () => {
+  const value = expInputParser.parseAttributeToUpdateValue('age', {
+    INCREMENT_BY: 2,
+  });
+  expect(value).toEqual({
+    type: 'dynamic',
+    value: 2,
+  });
+});
+test('parses SET action as dynamic value for nested list actions', () => {
+  const value = expInputParser.parseAttributeToUpdateValue(
+    'addresses[1]',
+    'new address'
+  );
+  expect(value).toEqual({
+    type: 'dynamic',
+    value: 'new address',
+  });
+});
diff --git a/packages/core/src/classes/expression/__test__/remove-update.spec.ts b/packages/core/src/classes/expression/__test__/remove-update.spec.ts
new file mode 100644
index 0000000..e4c32e7
--- /dev/null
+++ b/packages/core/src/classes/expression/__test__/remove-update.spec.ts
@@ -0,0 +1,13 @@
+import {RemoveUpdate} from '../update/remove-update';
+
+test('creates a update expression for removing attributes from item', () => {
+  const update = new RemoveUpdate().remove('age').and().remove('items[2]');
+  expect(update).toEqual({
+    _names: {
+      '#UE_age': 'age',
+      '#UE_items[2]': 'items[2]',
+    },
+    expression: '#UE_age, #UE_items[2]',
+    prefix: 'REMOVE',
+  });
+});
diff --git a/packages/core/src/classes/expression/__test__/set-update.spec.ts b/packages/core/src/classes/expression/__test__/set-update.spec.ts
new file mode 100644
index 0000000..dd7cea3
--- /dev/null
+++ b/packages/core/src/classes/expression/__test__/set-update.spec.ts
@@ -0,0 +1,101 @@
+import {SetUpdate} from '../update/set-update';
+
+/**
+ * @group setTo
+ */
+test('creates update expression', () => {
+  const updateSet = new SetUpdate()
+    .setTo('name', 'user')
+    .and()
+    .setTo('email', 'email@user.com');
+
+  expect(updateSet).toEqual({
+    _names: {
+      '#UE_email': 'email',
+      '#UE_name': 'name',
+    },
+    _values: {
+      ':UE_email': 'email@user.com',
+      ':UE_name': 'user',
+    },
+    expression: '#UE_name = :UE_name, #UE_email = :UE_email',
+    prefix: 'SET',
+  });
+});
+
+/**
+ * @group setToIfNotExists
+ */
+test('merges update expressions', () => {
+  const updateSet = new SetUpdate().setTo('name', 'user');
+  const updateSet2 = new SetUpdate().setToIfNotExists('age', 3);
+
+  expect(updateSet.merge(updateSet2)).toEqual({
+    _names: {
+      '#UE_age': 'age',
+      '#UE_name': 'name',
+    },
+    _values: {
+      ':UE_age': 3,
+      ':UE_name': 'user',
+    },
+    expression:
+      '#UE_name = :UE_name, #UE_age = if_not_exists(#UE_age, :UE_age)',
+    prefix: 'SET',
+  });
+});
+
+/**
+ * @group setOrAppendToList
+ */
+test('creates update expression with different actions', () => {
+  const updateSet = new SetUpdate()
+    .setOrAppendToList('name', ['user'])
+    .and()
+    .setTo('email', 'email@user.com');
+
+  expect(updateSet).toEqual({
+    _names: {
+      '#UE_email': 'email',
+      '#UE_name': 'name',
+    },
+    _values: {
+      ':UE_email': 'email@user.com',
+      ':UE_name': ['user'],
+    },
+    expression:
+      '#UE_name = list_append(#UE_name, :UE_name), #UE_email = :UE_email',
+    prefix: 'SET',
+  });
+});
+
+test('creates update expression with optional increment strategy', () => {
+  const updateSet = new SetUpdate().setTo('age', 2, 'INCREMENT_BY');
+
+  expect(updateSet).toEqual({
+    _names: {
+      '#UE_age': 'age',
+    },
+    _values: {
+      ':UE_age': 2,
+    },
+    expression: '#UE_age = #UE_age + :UE_age',
+    prefix: 'SET',
+  });
+});
+
+test('creates update expression that checks if other attribute exists', () => {
+  const updateSet = new SetUpdate().setToIfNotExists('age', 2, 'age_old');
+
+  expect(updateSet).toEqual({
+    _names: {
+      '#UE_age': 'age',
+      '#UE_age_old': 'age_old',
+    },
+    _values: {
+      ':UE_age': 2,
+    },
+    expression: '#UE_age = if_not_exists(#UE_age_old, :UE_age)',
+    prefix: 'SET',
+  });
+});
diff --git a/packages/core/src/classes/expression/__test__/update.spec.ts b/packages/core/src/classes/expression/__test__/update.spec.ts
new file mode 100644
index 0000000..6cba4b0
--- /dev/null
+++ b/packages/core/src/classes/expression/__test__/update.spec.ts
@@ -0,0 +1,20 @@
+import {AddUpdate} from '../update/add-update';
+import {SetUpdate} from '../update/set-update';
+
+test('merges multiple update expressions with diff action', () => {
+  const updateSet = new SetUpdate().setTo('name', 'user');
+  const updateAdd = new AddUpdate().addTo('age', 3);
+
+  expect(updateSet.merge(updateAdd)).toEqual({
+    _names: {
+      '#UE_name': 'name',
+      '#UE_age': 'age',
+    },
+    _values: {
+      ':UE_name': 'user',
+      ':UE_age': 3,
+    },
+    expression: '#UE_name = :UE_name ADD #UE_age :UE_age',
+    prefix: 'SET',
+  });
+});
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 42a4372..d3829fe 100644
--- a/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
+++ b/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
@@ -12,7 +12,14 @@ import {
   UserAutoGenerateAttributes,
 } from '../../../../__mocks__/user-auto-generate-attributes';
 import {Connection} from '../../connection/connection';
-import {CONSUMED_CAPACITY_TYPE} from '@typedorm/common';
+import {
+  CONSUMED_CAPACITY_TYPE,
+  InvalidDynamicUpdateAttributeValueError,
+} from '@typedorm/common';
+import {
+  UserAttrAlias,
+  UserAttrAliasPrimaryKey,
+} from '@typedorm/core/__mocks__/user-with-attribute-alias';
 
 let manager: EntityManager;
 let connection: Connection;
@@ -26,7 +33,12 @@ const dcMock = {
 };
 beforeEach(() => {
   connection = createTestConnection({
-    entities: [User, UserUniqueEmail, UserAutoGenerateAttributes],
+    entities: [
+      User,
+      UserUniqueEmail,
+      UserAutoGenerateAttributes,
+      UserAttrAlias,
+    ],
     documentClient: dcMock,
   });
 
@@ -183,7 +195,7 @@ test('creates entity and returns all attributes, including auto generated ones',
       PK: 'USER#1',
       SK: 'USER#1',
       id: '1',
-      __en: 'user',
+      __en: 'user-auto-generate-attr',
       updatedAt: 1606896235,
     },
     TableName: 'test-table',
@@ -234,6 +246,7 @@ test('finds one entity by given primary key', async () => {
     name: 'Me',
     status: 'active',
   });
+  expect(userEntity).toBeInstanceOf(User);
 });
 
 // issue: 110
@@ -403,16 +416,70 @@ test('updates item and return all new attributes', async () => {
 
   expect(dcMock.update).toHaveBeenCalledWith({
     ExpressionAttributeNames: {
-      '#attr0': 'name',
-      '#attr1': 'status',
-      '#attr2': 'GSI1SK',
-      '#attr3': 'GSI1PK',
+      '#UE_GSI1PK': 'GSI1PK',
+      '#UE_GSI1SK': 'GSI1SK',
+      '#UE_name': 'name',
+      '#UE_status': 'status',
+    },
+    ExpressionAttributeValues: {
+      ':UE_GSI1PK': 'USER#STATUS#active',
+      ':UE_GSI1SK': 'USER#user',
+      ':UE_name': 'user',
+      ':UE_status': 'active',
+    },
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    ReturnValues: 'ALL_NEW',
+    TableName: 'test-table',
+    UpdateExpression:
+      'SET #UE_name = :UE_name, #UE_status = :UE_status, #UE_GSI1SK = :UE_GSI1SK, #UE_GSI1PK = :UE_GSI1PK',
+  });
+  expect(updatedItem).toEqual({id: '1', name: 'user', status: 'active'});
+});
+
+test('updates item with multiple body actions', 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',
+      },
+    }),
+  });
+  const updatedItem = await manager.update<User, UserPrimaryKey>(
+    User,
+    {id: '1'},
+    {
+      name: 'user',
+      status: {
+        IF_NOT_EXISTS: {
+          $PATH: 'id',
+          $VALUE: 'active',
+        },
+      },
+    }
+  );
+
+  expect(dcMock.update).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#UE_GSI1SK': 'GSI1SK',
+      '#UE_GSI1PK': 'GSI1PK',
+      '#UE_id': 'id',
+      '#UE_name': 'name',
+      '#UE_status': 'status',
     },
     ExpressionAttributeValues: {
-      ':val0': 'user',
-      ':val1': 'active',
-      ':val2': 'USER#user',
-      ':val3': 'USER#STATUS#active',
+      ':UE_GSI1SK': 'USER#user',
+      ':UE_GSI1PK': 'USER#STATUS#active',
+      ':UE_name': 'user',
+      ':UE_status': 'active',
     },
     Key: {
       PK: 'USER#1',
@@ -421,11 +488,60 @@ test('updates item and return all new attributes', async () => {
     ReturnValues: 'ALL_NEW',
     TableName: 'test-table',
     UpdateExpression:
-      'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2, #attr3 = :val3',
+      'SET #UE_name = :UE_name, #UE_status = if_not_exists(#UE_id, :UE_status), #UE_GSI1SK = :UE_GSI1SK, #UE_GSI1PK = :UE_GSI1PK',
   });
   expect(updatedItem).toEqual({id: '1', name: 'user', status: 'active'});
 });
 
+test('updates item when trying to update attribute with dynamic value that is not referenced in any index', async () => {
+  dcMock.update.mockReturnValue({
+    promise: () => ({}),
+  });
+
+  await manager.update<User, UserPrimaryKey>(
+    User,
+    {id: '1'},
+    {
+      age: {
+        INCREMENT_BY: 3,
+      },
+    }
+  );
+
+  expect(dcMock.update).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#UE_age': 'age',
+    },
+    ExpressionAttributeValues: {
+      ':UE_age': 3,
+    },
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    ReturnValues: 'ALL_NEW',
+    TableName: 'test-table',
+    UpdateExpression: 'SET #UE_age = #UE_age + :UE_age',
+  });
+});
+test('fails to transform when trying to use dynamic update expression for attribute that is also referenced in a index', async () => {
+  // this should fail as update to age is not static and is also referenced by GSI1
+  const updatedItem = async () =>
+    manager.update<UserAttrAlias, UserAttrAliasPrimaryKey>(
+      UserAttrAlias,
+      {id: '1'},
+      {
+        age: {
+          INCREMENT_BY: 3,
+        },
+      }
+    );
+  await expect(updatedItem).rejects.toThrow(
+    InvalidDynamicUpdateAttributeValueError
+  );
+  expect(dcMock.update).not.toHaveBeenCalled();
+});
+
 test('updates item and attributes marked to be autoUpdated', async () => {
   jest.useFakeTimers('modern').setSystemTime(new Date('2020-01-01'));
 
@@ -457,12 +573,12 @@ test('updates item and attributes marked to be autoUpdated', async () => {
 
   expect(dcMock.update).toHaveBeenCalledWith({
     ExpressionAttributeNames: {
-      '#attr0': 'updatedAt',
-      '#attr1': 'GSI1PK',
+      '#UE_updatedAt': 'updatedAt',
+      '#UE_GSI1PK': 'GSI1PK',
     },
     ExpressionAttributeValues: {
-      ':val0': 1577836800,
-      ':val1': 'USER#UPDATED_AT#1577836800',
+      ':UE_updatedAt': 1577836800,
+      ':UE_GSI1PK': 'USER#UPDATED_AT#1577836800',
     },
     Key: {
       PK: 'USER#1',
@@ -470,7 +586,8 @@ test('updates item and attributes marked to be autoUpdated', async () => {
     },
     ReturnValues: 'ALL_NEW',
     TableName: 'test-table',
-    UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+    UpdateExpression:
+      'SET #UE_updatedAt = :UE_updatedAt, #UE_GSI1PK = :UE_GSI1PK',
   });
   expect(updatedItem).toEqual({id: '1', name: 'Me', status: 'active'});
 });
@@ -529,17 +646,17 @@ test('updates item with unique attributes and returns all updated attributes', a
       {
         Update: {
           ExpressionAttributeNames: {
-            '#attr0': 'email',
+            '#UE_email': 'email',
           },
           ExpressionAttributeValues: {
-            ':val0': 'new@examil.com',
+            ':UE_email': 'new@examil.com',
           },
           Key: {
             PK: 'USER#1',
             SK: 'USER#1',
           },
           TableName: 'test-table',
-          UpdateExpression: 'SET #attr0 = :val0',
+          UpdateExpression: 'SET #UE_email = :UE_email',
         },
       },
       {
@@ -608,17 +725,17 @@ test('updates item and return all new attributes with given condition', async ()
 
   expect(dcMock.update).toHaveBeenCalledWith({
     ExpressionAttributeNames: {
-      '#attr0': 'name',
-      '#attr1': 'status',
-      '#attr2': 'GSI1SK',
-      '#attr3': 'GSI1PK',
+      '#UE_GSI1PK': 'GSI1PK',
+      '#UE_GSI1SK': 'GSI1SK',
+      '#UE_name': 'name',
+      '#UE_status': 'status',
       '#CE_age': 'age',
     },
     ExpressionAttributeValues: {
-      ':val0': 'user',
-      ':val1': 'active',
-      ':val2': 'USER#user',
-      ':val3': 'USER#STATUS#active',
+      ':UE_GSI1PK': 'USER#STATUS#active',
+      ':UE_GSI1SK': 'USER#user',
+      ':UE_name': 'user',
+      ':UE_status': 'active',
       ':CE_age_end': 11,
       ':CE_age_start': 1,
     },
@@ -629,7 +746,7 @@ test('updates item and return all new attributes with given condition', async ()
     ReturnValues: 'ALL_NEW',
     TableName: 'test-table',
     UpdateExpression:
-      'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2, #attr3 = :val3',
+      'SET #UE_name = :UE_name, #UE_status = :UE_status, #UE_GSI1SK = :UE_GSI1SK, #UE_GSI1PK = :UE_GSI1PK',
     ConditionExpression: '#CE_age BETWEEN :CE_age_start AND :CE_age_end',
   });
   expect(updatedItem).toEqual({
@@ -680,17 +797,17 @@ test('updates item and create new unique item when no previous record was found'
       {
         Update: {
           ExpressionAttributeNames: {
-            '#attr0': 'email',
+            '#UE_email': 'email',
           },
           ExpressionAttributeValues: {
-            ':val0': 'new@examil.com',
+            ':UE_email': 'new@examil.com',
           },
           Key: {
             PK: 'USER#1',
             SK: 'USER#1',
           },
           TableName: 'test-table',
-          UpdateExpression: 'SET #attr0 = :val0',
+          UpdateExpression: 'SET #UE_email = :UE_email',
         },
       },
       {
@@ -960,6 +1077,10 @@ test('finds items matching given query params', async () => {
       },
     ],
   });
+
+  users.items.forEach(user => {
+    expect(user).toBeInstanceOf(User);
+  });
 });
 
 test('finds items matching given query params and options', async () => {
@@ -1038,6 +1159,7 @@ test('finds items matching given query params and options', async () => {
       },
     ],
   });
+  expect(users.items[0]).toBeInstanceOf(User);
 });
 
 test('finds items with alternate syntax', async () => {
diff --git a/packages/core/src/classes/manager/__test__/scan-manager.spec.ts b/packages/core/src/classes/manager/__test__/scan-manager.spec.ts
new file mode 100644
index 0000000..4e85f90
--- /dev/null
+++ b/packages/core/src/classes/manager/__test__/scan-manager.spec.ts
@@ -0,0 +1,588 @@
+import {InvalidParallelScanLimitOptionError} from '@typedorm/common';
+import {table} from '@typedorm/core/__mocks__/table';
+import {User} from '@typedorm/core/__mocks__/user';
+import {UserAutoGenerateAttributes} from '@typedorm/core/__mocks__/user-auto-generate-attributes';
+import {UserUniqueEmail} from '@typedorm/core/__mocks__/user-unique-email';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {Connection} from '../../connection/connection';
+import {ScanManager} from '../scan-manager';
+
+let manager: ScanManager;
+let connection: Connection;
+const dcMock = {
+  scan: jest.fn(),
+};
+
+beforeEach(() => {
+  connection = createTestConnection({
+    entities: [User, UserUniqueEmail, UserAutoGenerateAttributes],
+    table,
+    documentClient: dcMock,
+  });
+
+  manager = new ScanManager(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('scan manager works', () => {
+  expect(manager).toBeTruthy();
+});
+
+/**
+ * @group scan
+ */
+test('scans table with simple options', async () => {
+  dcMock.scan.mockReturnValue({
+    promise: () => ({
+      Items: [
+        {
+          id: '1',
+          PK: 'USER#1',
+          __en: 'user',
+        },
+        {
+          id: '2',
+          PK: 'USER#1',
+          __en: 'user',
+        },
+      ],
+    }),
+  });
+  const response = await manager.scan({
+    limit: 1000,
+    scanIndex: 'GSI1',
+  });
+
+  expect(dcMock.scan).toHaveBeenCalledTimes(1);
+  expect(dcMock.scan).toHaveBeenCalledWith({
+    Limit: 1000,
+    TableName: 'test-table',
+  });
+  expect(response).toEqual({
+    items: [
+      {
+        id: '1',
+      },
+      {
+        id: '2',
+      },
+    ],
+  });
+  expect(
+    response.items?.forEach(item => {
+      expect(item).toBeInstanceOf(User);
+    })
+  );
+});
+
+test('scans table with parallel scan options', async () => {
+  dcMock.scan.mockReturnValue({
+    promise: () => ({
+      Items: [
+        {
+          id: '1',
+          PK: 'USER#1',
+          __en: 'user',
+        },
+        {
+          id: '2',
+          PK: 'USER#1',
+          __en: 'user',
+        },
+      ],
+    }),
+  });
+  const response = await manager.scan({
+    limitPerSegment: 1000,
+    segment: 0,
+    totalSegments: 2,
+    limit: 2000,
+  });
+
+  expect(dcMock.scan).toHaveBeenCalledTimes(1);
+  expect(dcMock.scan).toHaveBeenCalledWith({
+    Limit: 1000,
+    TableName: 'test-table',
+    Segment: 0,
+    TotalSegments: 2,
+  });
+  expect(response).toEqual({
+    items: [
+      {
+        id: '1',
+      },
+      {
+        id: '2',
+      },
+    ],
+  });
+  expect(
+    response.items?.forEach(item => {
+      expect(item).toBeInstanceOf(User);
+    })
+  );
+});
+
+test('scans table with multiple requests', async () => {
+  dcMock.scan
+    .mockImplementationOnce(() => ({
+      promise: () => ({
+        Items: [
+          {
+            id: '1',
+            __en: 'user',
+          },
+        ],
+        LastEvaluatedKey: {
+          PK: '1',
+        },
+      }),
+    }))
+    .mockImplementationOnce(() => ({
+      promise: () => ({
+        Items: [
+          {
+            id: '2',
+            __en: 'user',
+          },
+        ],
+      }),
+    }));
+  const response = await manager.scan({
+    limit: 1000,
+  });
+
+  expect(dcMock.scan).toHaveBeenCalledTimes(2);
+  expect(dcMock.scan.mock.calls).toEqual([
+    [
+      {
+        Limit: 1000,
+        TableName: 'test-table',
+      },
+    ],
+    [
+      {
+        ExclusiveStartKey: {
+          PK: '1',
+        },
+        Limit: 1000,
+        TableName: 'test-table',
+      },
+    ],
+  ]);
+  expect(response).toEqual({
+    items: [
+      {
+        id: '1',
+      },
+      {
+        id: '2',
+      },
+    ],
+  });
+});
+
+test('scans table with given options and returns deserialized and unknown items', async () => {
+  dcMock.scan.mockReturnValue({
+    promise: () => ({
+      Items: [
+        {
+          id: '1',
+          PK: 'USER#1',
+          __en: 'user',
+        },
+        {
+          id: '2',
+          data: '0x0000',
+        },
+      ],
+    }),
+  });
+  const response = await manager.scan({
+    limit: 1000,
+    scanIndex: 'GSI1',
+  });
+
+  expect(dcMock.scan).toHaveBeenCalledTimes(1);
+  expect(dcMock.scan).toHaveBeenCalledWith({
+    Limit: 1000,
+    TableName: 'test-table',
+  });
+  expect(response).toEqual({
+    items: [
+      {
+        id: '1',
+      },
+    ],
+    unknownItems: [
+      {
+        id: '2',
+        data: '0x0000',
+      },
+    ],
+  });
+
+  response.items?.forEach(item => {
+    expect(item).toBeInstanceOf(User);
+  });
+  response.unknownItems?.forEach(item => {
+    expect(item).not.toBeInstanceOf(User);
+  });
+});
+
+/**
+ * @group parallelScan
+ */
+test('runs parallel scan requests for each segment', async () => {
+  const callIndex = {
+    0: 0,
+    1: 1,
+  };
+  dcMock.scan.mockImplementation(({Segment}: {Segment: 0 | 1}) => {
+    callIndex[Segment] = callIndex[Segment] + 1;
+    return {
+      promise: () => ({
+        Items: new Array(10).fill({
+          id: Segment,
+          __en: 'user',
+        }),
+        LastEvaluatedKey: {
+          PK: callIndex[Segment],
+        },
+      }),
+    };
+  });
+
+  const response = await manager.parallelScan({
+    totalSegments: 2,
+    limitPerSegment: 10,
+    limit: 20,
+  });
+
+  // Here, we are still making extra one request even tho we managed to get 20 items from the
+  // first requests on each segment, this is not 100% ideal but the only way possible to handle
+  // parallel requests, as we do not upfront know, what request will have how many items.
+  // However, we did manage to kill of further request from second segment
+  expect(dcMock.scan).toHaveBeenCalledTimes(3);
+  expect(response.cursor).toEqual({
+    '0': {
+      PK: 1,
+    },
+    '1': {
+      PK: 2,
+    },
+  });
+  expect(response.items?.length).toEqual(20);
+  expect(dcMock.scan.mock.calls).toEqual([
+    [
+      {
+        Limit: 10,
+        Segment: 0,
+        TableName: 'test-table',
+        TotalSegments: 2,
+      },
+    ],
+    [
+      {
+        Limit: 10,
+        Segment: 1,
+        TableName: 'test-table',
+        TotalSegments: 2,
+      },
+    ],
+    [
+      {
+        // request was rerun for that segment with pk
+        ExclusiveStartKey: {PK: 1},
+        Limit: 10,
+        Segment: 0,
+        TableName: 'test-table',
+        TotalSegments: 2,
+      },
+    ],
+  ]);
+});
+
+test('throws when trying to run parallel scan with invalid limit values', async () => {
+  const responseFac = () =>
+    manager.parallelScan({
+      totalSegments: 2,
+      limit: 100,
+      limitPerSegment: 200,
+    });
+
+  await expect(responseFac).rejects.toThrow(
+    InvalidParallelScanLimitOptionError
+  );
+});
+
+/**
+ * @group find
+ */
+test('finds items from table with over the scan api', async () => {
+  dcMock.scan.mockReturnValue({
+    promise: () => ({
+      Items: [
+        {
+          id: '1',
+          PK: 'USER#1',
+          __en: 'user',
+        },
+        {
+          id: '2',
+          PK: 'USER#1',
+          __en: 'user',
+        },
+      ],
+    }),
+  });
+  const response = await manager.find(User);
+
+  expect(dcMock.scan).toHaveBeenCalledTimes(1);
+  expect(dcMock.scan).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#FE___en': '__en',
+    },
+    ExpressionAttributeValues: {
+      ':FE___en': 'user',
+    },
+    FilterExpression: '#FE___en = :FE___en',
+    TableName: 'test-table',
+  });
+  expect(response).toEqual({
+    items: [
+      {
+        id: '1',
+      },
+      {
+        id: '2',
+      },
+    ],
+  });
+});
+
+test('finds items from table with over the scan api with given options', async () => {
+  dcMock.scan.mockReturnValue({
+    promise: () => ({
+      Items: [
+        {
+          id: '1',
+          PK: 'USER#1',
+          __en: 'user',
+        },
+        {
+          id: '2',
+          PK: 'USER#1',
+          __en: 'user',
+        },
+      ],
+    }),
+  });
+  const response = await manager.find(User, {
+    where: {
+      age: {
+        GE: 1,
+      },
+    },
+  });
+
+  expect(dcMock.scan).toHaveBeenCalledTimes(1);
+  expect(dcMock.scan).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#FE___en': '__en',
+      '#FE_age': 'age',
+    },
+    ExpressionAttributeValues: {
+      ':FE___en': 'user',
+      ':FE_age': 1,
+    },
+    FilterExpression: '(#FE___en = :FE___en) AND (#FE_age >= :FE_age)',
+    TableName: 'test-table',
+  });
+  expect(response).toEqual({
+    items: [
+      {
+        id: '1',
+      },
+      {
+        id: '2',
+      },
+    ],
+  });
+});
+
+test('finds items from table with using parallel scan', async () => {
+  dcMock.scan.mockImplementation(({Segment}) => ({
+    promise: () => ({
+      Items: [
+        {
+          id: Segment,
+          PK: `USER#${Segment}`,
+          __en: 'user',
+        },
+      ],
+    }),
+  }));
+  const response = await manager.find(User, {
+    totalSegments: 2,
+    where: {
+      id: {
+        GE: '1',
+      },
+    },
+  });
+
+  expect(dcMock.scan).toHaveBeenCalledTimes(2);
+  expect(dcMock.scan.mock.calls).toEqual([
+    [
+      {
+        ExpressionAttributeNames: {
+          '#FE___en': '__en',
+          '#FE_id': 'id',
+        },
+        ExpressionAttributeValues: {
+          ':FE___en': 'user',
+          ':FE_id': '1',
+        },
+        FilterExpression: '(#FE___en = :FE___en) AND (#FE_id >= :FE_id)',
+        Segment: 0,
+        TableName: 'test-table',
+        TotalSegments: 2,
+      },
+    ],
+    [
+      {
+        ExpressionAttributeNames: {
+          '#FE___en': '__en',
+          '#FE_id': 'id',
+        },
+        ExpressionAttributeValues: {
+          ':FE___en': 'user',
+          ':FE_id': '1',
+        },
+        FilterExpression: '(#FE___en = :FE___en) AND (#FE_id >= :FE_id)',
+        Segment: 1,
+        TableName: 'test-table',
+        TotalSegments: 2,
+      },
+    ],
+  ]);
+  expect(response).toEqual({
+    items: [{id: 0}, {id: 1}],
+  });
+});
+
+/**
+ * @group count
+ */
+test('counts all entity items in the table', async () => {
+  dcMock.scan.mockReturnValue({
+    promise: () => ({
+      Count: 7,
+    }),
+  });
+  const response = await manager.count(User, {
+    where: {
+      name: {
+        CONTAINS: 'LAL',
+      },
+    },
+  });
+
+  expect(dcMock.scan).toHaveBeenCalledTimes(1);
+  expect(dcMock.scan).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#FE___en': '__en',
+      '#FE_name': 'name',
+    },
+    ExpressionAttributeValues: {
+      ':FE___en': 'user',
+      ':FE_name': 'LAL',
+    },
+    FilterExpression:
+      '(#FE___en = :FE___en) AND (contains(#FE_name, :FE_name))',
+    Select: 'COUNT',
+    TableName: 'test-table',
+  });
+  expect(response).toEqual(7);
+});
+
+test('counts all entity items recursively with multiple requests', async () => {
+  dcMock.scan
+    .mockReturnValueOnce({
+      promise: () => ({
+        Count: 7,
+        LastEvaluatedKey: {
+          PK: 'USER#8',
+        },
+      }),
+    })
+    .mockReturnValueOnce({
+      promise: () => ({
+        Count: 100,
+        LastEvaluatedKey: {
+          PK: 'USER#100',
+        },
+      }),
+    })
+    .mockReturnValueOnce({
+      promise: () => ({
+        Count: 8,
+      }),
+    });
+
+  const response = await manager.count(User);
+
+  expect(dcMock.scan).toHaveBeenCalledTimes(3);
+  expect(dcMock.scan.mock.calls).toEqual([
+    [
+      {
+        ExpressionAttributeNames: {
+          '#FE___en': '__en',
+        },
+        ExpressionAttributeValues: {
+          ':FE___en': 'user',
+        },
+        FilterExpression: '#FE___en = :FE___en',
+        Select: 'COUNT',
+        TableName: 'test-table',
+      },
+    ],
+    [
+      {
+        ExclusiveStartKey: {
+          PK: 'USER#8',
+        },
+        ExpressionAttributeNames: {
+          '#FE___en': '__en',
+        },
+        ExpressionAttributeValues: {
+          ':FE___en': 'user',
+        },
+        FilterExpression: '#FE___en = :FE___en',
+        Select: 'COUNT',
+        TableName: 'test-table',
+      },
+    ],
+    [
+      {
+        ExclusiveStartKey: {
+          PK: 'USER#100',
+        },
+        ExpressionAttributeNames: {
+          '#FE___en': '__en',
+        },
+        ExpressionAttributeValues: {
+          ':FE___en': 'user',
+        },
+        FilterExpression: '#FE___en = :FE___en',
+        Select: 'COUNT',
+        TableName: 'test-table',
+      },
+    ],
+  ]);
+  expect(response).toEqual(115);
+});
diff --git a/packages/core/src/classes/manager/__test__/transaction-manager.spec.ts b/packages/core/src/classes/manager/__test__/transaction-manager.spec.ts
index 9638585..0ec06d6 100644
--- a/packages/core/src/classes/manager/__test__/transaction-manager.spec.ts
+++ b/packages/core/src/classes/manager/__test__/transaction-manager.spec.ts
@@ -112,19 +112,20 @@ test('performs write transactions for simple writes', async () => {
       {
         Update: {
           ExpressionAttributeNames: {
-            '#attr0': 'status',
-            '#attr1': 'GSI1PK',
+            '#UE_status': 'status',
+            '#UE_GSI1PK': 'GSI1PK',
           },
           ExpressionAttributeValues: {
-            ':val0': 'active',
-            ':val1': 'USER#STATUS#active',
+            ':UE_status': 'active',
+            ':UE_GSI1PK': 'USER#STATUS#active',
           },
           Key: {
             PK: 'USER#1',
             SK: 'USER#1',
           },
           TableName: 'test-table',
-          UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+          UpdateExpression:
+            'SET #UE_status = :UE_status, #UE_GSI1PK = :UE_GSI1PK',
         },
       },
       {
@@ -195,14 +196,14 @@ test('performs write transactions for entities with unique attributes ', async (
       {
         Update: {
           ExpressionAttributeNames: {
-            '#attr0': 'email',
-            '#attr1': 'status',
-            '#attr2': 'GSI1PK',
+            '#UE_email': 'email',
+            '#UE_status': 'status',
+            '#UE_GSI1PK': 'GSI1PK',
           },
           ExpressionAttributeValues: {
-            ':val0': 'new@example.com',
-            ':val1': 'active',
-            ':val2': 'USER#STATUS#active',
+            ':UE_email': 'new@example.com',
+            ':UE_status': 'active',
+            ':UE_GSI1PK': 'USER#STATUS#active',
           },
           Key: {
             PK: 'USER#1',
@@ -210,7 +211,7 @@ test('performs write transactions for entities with unique attributes ', async (
           },
           TableName: 'test-table',
           UpdateExpression:
-            'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2',
+            'SET #UE_email = :UE_email, #UE_status = :UE_status, #UE_GSI1PK = :UE_GSI1PK',
         },
       },
       {
@@ -281,14 +282,14 @@ test('performs write transactions for entities with non existing unique attribut
       {
         Update: {
           ExpressionAttributeNames: {
-            '#attr0': 'email',
-            '#attr1': 'status',
-            '#attr2': 'GSI1PK',
+            '#UE_email': 'email',
+            '#UE_status': 'status',
+            '#UE_GSI1PK': 'GSI1PK',
           },
           ExpressionAttributeValues: {
-            ':val0': 'new@example.com',
-            ':val1': 'active',
-            ':val2': 'USER#STATUS#active',
+            ':UE_email': 'new@example.com',
+            ':UE_status': 'active',
+            ':UE_GSI1PK': 'USER#STATUS#active',
           },
           Key: {
             PK: 'USER#1',
@@ -296,7 +297,7 @@ test('performs write transactions for entities with non existing unique attribut
           },
           TableName: 'test-table',
           UpdateExpression:
-            'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2',
+            'SET #UE_email = :UE_email, #UE_status = :UE_status, #UE_GSI1PK = :UE_GSI1PK',
         },
       },
       {
@@ -373,6 +374,68 @@ test('performs write transactions when removing entities with unique attributes
   });
 });
 
+test('performs write transactions when with mixed update actions ', async () => {
+  dcMock.transactWrite.mockReturnValue({
+    on: jest.fn(),
+    send: jest.fn().mockImplementation(cb => {
+      cb(null, {
+        ConsumedCapacity: [{}],
+        ItemCollectionMetrics: [{}],
+      });
+    }),
+  });
+
+  const transaction = new WriteTransaction(connection).addUpdateItem<
+    User,
+    UserPrimaryKey
+  >(
+    User,
+    {id: '1'},
+    {
+      addresses: {
+        REMOVE: {
+          $AT_INDEX: [1],
+        },
+      },
+      name: {
+        IF_NOT_EXISTS: {
+          $PATH: 'updated',
+          $VALUE: 'active',
+        },
+      },
+    }
+  );
+
+  await manager.write(transaction);
+
+  expect(dcMock.transactWrite).toHaveBeenCalledTimes(1);
+  expect(dcMock.transactWrite).toHaveBeenCalledWith({
+    TransactItems: [
+      {
+        Update: {
+          ExpressionAttributeNames: {
+            '#UE_addresses': 'addresses',
+            '#UE_name': 'name',
+            '#UE_updated': 'updated',
+            '#UE_GSI1SK': 'GSI1SK',
+          },
+          ExpressionAttributeValues: {
+            ':UE_GSI1SK': 'USER#active',
+            ':UE_name': 'active',
+          },
+          Key: {
+            PK: 'USER#1',
+            SK: 'USER#1',
+          },
+          TableName: 'test-table',
+          UpdateExpression:
+            'SET #UE_name = if_not_exists(#UE_updated, :UE_name), #UE_GSI1SK = :UE_GSI1SK REMOVE #UE_addresses[1]',
+        },
+      },
+    ],
+  });
+});
+
 /**
  * @group read
  */
diff --git a/packages/core/src/classes/transformer/__test__/dc-batch-transformer.spec.ts b/packages/core/src/classes/transformer/__test__/dc-batch-transformer.spec.ts
index 696ff6c..480b422 100644
--- a/packages/core/src/classes/transformer/__test__/dc-batch-transformer.spec.ts
+++ b/packages/core/src/classes/transformer/__test__/dc-batch-transformer.spec.ts
@@ -156,7 +156,7 @@ test('transforms into batch write items', () => {
                 GSI1SK: 'USER#user 2',
                 PK: 'USER#2',
                 SK: 'USER#2',
-                __en: 'user',
+                __en: 'user-unique-email',
                 email: 'user@test.com',
                 id: '2',
                 name: 'user 2',
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 2f88990..01cb449 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
@@ -1,4 +1,10 @@
-import {Attribute, Entity, QUERY_ORDER} from '@typedorm/common';
+import {
+  Attribute,
+  Entity,
+  InvalidPrimaryKeyAttributesUpdateError,
+  InvalidUniqueAttributeUpdateError,
+  QUERY_ORDER,
+} from '@typedorm/common';
 import {Customer} from '../../../../__mocks__/inherited-customer';
 import {table} from '../../../../__mocks__/table';
 import {User, UserGSI1} from '../../../../__mocks__/user';
@@ -9,11 +15,15 @@ import {
   UserUniqueEmail,
   UserUniqueEmailPrimaryKey,
 } from '../../../../__mocks__/user-unique-email';
+import {CATEGORY, Photo, PhotoPrimaryKey} from '@typedorm/core/__mocks__/photo';
+// eslint-disable-next-line node/no-extraneous-import
+import moment from 'moment';
+jest.useFakeTimers('modern').setSystemTime(1622530750000);
 
 let transformer: DocumentClientRequestTransformer;
 beforeEach(async () => {
   const connection = createTestConnection({
-    entities: [User, Customer, UserUniqueEmail],
+    entities: [User, Customer, UserUniqueEmail, Photo],
   });
   transformer = new DocumentClientRequestTransformer(connection);
 });
@@ -235,7 +245,7 @@ test('transforms put item request with unique attributes and condition options',
           id: '1',
           name: 'test user',
           status: 'active',
-          __en: 'user',
+          __en: 'user-unique-email',
           GSI1PK: 'USER#STATUS#active',
           GSI1SK: 'USER#test user',
         },
@@ -448,12 +458,12 @@ test('transforms update item request', () => {
   );
   expect(updatedItem).toEqual({
     ExpressionAttributeNames: {
-      '#attr0': 'name',
-      '#attr1': 'GSI1SK',
+      '#UE_name': 'name',
+      '#UE_GSI1SK': 'GSI1SK',
     },
     ExpressionAttributeValues: {
-      ':val0': 'new name',
-      ':val1': 'USER#new name',
+      ':UE_name': 'new name',
+      ':UE_GSI1SK': 'USER#new name',
     },
     Key: {
       PK: 'USER#1',
@@ -461,10 +471,63 @@ test('transforms update item request', () => {
     },
     ReturnValues: 'ALL_NEW',
     TableName: 'test-table',
-    UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+    UpdateExpression: 'SET #UE_name = :UE_name, #UE_GSI1SK = :UE_GSI1SK',
   });
 });
 
+test('transforms update item request respects custom transforms applied via class transformer', () => {
+  const updatedItem = transformer.toDynamoUpdateItem(
+    Photo,
+    {
+      id: 1,
+      category: CATEGORY.KIDS,
+    },
+    {
+      category: {
+        // even tho we try to update `category` attribute to `KIDS` final result will have `new-kids`
+        // due to custom transformation defined on the Photo entity
+        SET: {
+          IF_NOT_EXISTS: CATEGORY.KIDS,
+        },
+      },
+    }
+  );
+  const writeItemList = (updatedItem as any).lazyLoadTransactionWriteItems({
+    id: 1,
+    category: CATEGORY.PETS,
+    PK: 'PHOTO#PETS',
+    SK: 'PHOTO#1',
+    GSI1PK: 'PHOTO#1',
+    GSI1SK: 'PHOTO#PETS',
+  });
+  expect(writeItemList).toEqual([
+    {
+      Put: {
+        Item: {
+          GSI1SK: 'PHOTO#kids-new',
+          GSI1PK: 'PHOTO#1',
+          PK: 'PHOTO#kids-new',
+          SK: 'PHOTO#1',
+          category: 'kids-new',
+          id: 1,
+          updatedAt: '1622530750',
+        },
+        ReturnValues: 'ALL_NEW',
+        TableName: 'test-table',
+      },
+    },
+    {
+      Delete: {
+        Key: {
+          PK: 'PHOTO#PETS',
+          SK: 'PHOTO#1',
+        },
+        TableName: 'test-table',
+      },
+    },
+  ]);
+});
+
 test('transforms update item record with unique attributes', () => {
   const updatedItem = transformer.toDynamoUpdateItem<
     UserUniqueEmail,
@@ -492,18 +555,19 @@ test('transforms update item record with unique attributes', () => {
     {
       Update: {
         ExpressionAttributeNames: {
-          '#attr0': 'name',
-          '#attr1': 'email',
-          '#attr2': 'GSI1SK',
+          '#UE_name': 'name',
+          '#UE_email': 'email',
+          '#UE_GSI1SK': 'GSI1SK',
         },
         ExpressionAttributeValues: {
-          ':val0': 'new name',
-          ':val1': 'new@email.com',
-          ':val2': 'USER#new name',
+          ':UE_name': 'new name',
+          ':UE_email': 'new@email.com',
+          ':UE_GSI1SK': 'USER#new name',
         },
         Key: {PK: 'USER#1', SK: 'USER#1'},
         TableName: 'test-table',
-        UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2',
+        UpdateExpression:
+          'SET #UE_name = :UE_name, #UE_email = :UE_email, #UE_GSI1SK = :UE_GSI1SK',
       },
     },
     {
@@ -530,6 +594,70 @@ test('transforms update item record with unique attributes', () => {
   ]);
 });
 
+test('transforms update item request with unique attributes and complex update value', () => {
+  const updatedItem = transformer.toDynamoUpdateItem(
+    UserUniqueEmail,
+    {
+      id: '1',
+    },
+    {
+      email: {
+        SET: 'new@email.com',
+      },
+    }
+  );
+  const lazyWriteItemListLoader = (updatedItem as any)
+    .lazyLoadTransactionWriteItems;
+  expect(typeof lazyWriteItemListLoader).toEqual('function');
+
+  const writeItemList = lazyWriteItemListLoader({
+    email: 'old@email.com',
+  });
+
+  expect(writeItemList).toEqual([
+    {
+      Update: {
+        ExpressionAttributeNames: {
+          '#UE_email': 'email',
+        },
+        ExpressionAttributeValues: {
+          ':UE_email': 'new@email.com',
+        },
+        Key: {
+          PK: 'USER#1',
+          SK: 'USER#1',
+        },
+        TableName: 'test-table',
+        UpdateExpression: 'SET #UE_email = :UE_email',
+      },
+    },
+    {
+      Put: {
+        ConditionExpression:
+          '(attribute_not_exists(#CE_PK)) AND (attribute_not_exists(#CE_SK))',
+        ExpressionAttributeNames: {
+          '#CE_PK': 'PK',
+          '#CE_SK': 'SK',
+        },
+        Item: {
+          PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#new@email.com',
+          SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#new@email.com',
+        },
+        TableName: 'test-table',
+      },
+    },
+    {
+      Delete: {
+        Key: {
+          PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#old@email.com',
+          SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#old@email.com',
+        },
+        TableName: 'test-table',
+      },
+    },
+  ]);
+});
+
 test('transforms update item request with condition input', () => {
   const updatedItem = transformer.toDynamoUpdateItem<User, UserPrimaryKey>(
     User,
@@ -549,13 +677,13 @@ test('transforms update item request with condition input', () => {
   );
   expect(updatedItem).toEqual({
     ExpressionAttributeNames: {
-      '#attr0': 'name',
-      '#attr1': 'GSI1SK',
+      '#UE_name': 'name',
+      '#UE_GSI1SK': 'GSI1SK',
       '#CE_age': 'age',
     },
     ExpressionAttributeValues: {
-      ':val0': 'new name',
-      ':val1': 'USER#new name',
+      ':UE_name': 'new name',
+      ':UE_GSI1SK': 'USER#new name',
       ':CE_age_end': 10,
       ':CE_age_start': 3,
     },
@@ -565,7 +693,7 @@ test('transforms update item request with condition input', () => {
     },
     ReturnValues: 'ALL_NEW',
     TableName: 'test-table',
-    UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+    UpdateExpression: 'SET #UE_name = :UE_name, #UE_GSI1SK = :UE_GSI1SK',
     ConditionExpression: '#CE_age BETWEEN :CE_age_start AND :CE_age_end',
   });
 });
@@ -604,21 +732,22 @@ test('transforms update item record with unique attributes and condition options
     {
       Update: {
         ExpressionAttributeNames: {
-          '#attr0': 'name',
-          '#attr1': 'email',
-          '#attr2': 'GSI1SK',
+          '#UE_name': 'name',
+          '#UE_email': 'email',
+          '#UE_GSI1SK': 'GSI1SK',
           '#CE_user': 'user',
           '#CE_user_name': 'name',
         },
         ExpressionAttributeValues: {
-          ':val0': 'new name',
-          ':val1': 'new@email.com',
-          ':val2': 'USER#new name',
+          ':UE_name': 'new name',
+          ':UE_email': 'new@email.com',
+          ':UE_GSI1SK': 'USER#new name',
           ':CE_user_name': 'test user',
         },
         Key: {PK: 'USER#1', SK: 'USER#1'},
         TableName: 'test-table',
-        UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2',
+        UpdateExpression:
+          'SET #UE_name = :UE_name, #UE_email = :UE_email, #UE_GSI1SK = :UE_GSI1SK',
         ConditionExpression: '#CE_user.#CE_user_name <> :CE_user_name',
       },
     },
@@ -646,6 +775,150 @@ test('transforms update item record with unique attributes and condition options
   ]);
 });
 
+test('transforms update item with primary key changes', () => {
+  const updateItem = transformer.toDynamoUpdateItem<
+    UserUniqueEmail,
+    UserPrimaryKey
+  >(
+    UserUniqueEmail,
+    {
+      id: '1',
+    },
+    {
+      id: '1a',
+    }
+  );
+
+  const lazyWriteItemListLoader = (updateItem as any)
+    .lazyLoadTransactionWriteItems;
+  expect(typeof lazyWriteItemListLoader).toEqual('function');
+
+  // with following existing item
+  const writeItemList = lazyWriteItemListLoader({
+    id: '1',
+    PK: 'USER#1',
+    SK: 'USER#1',
+    name: 'new name',
+    email: 'user@email.com',
+    __en: 'user',
+  });
+
+  expect(writeItemList).toEqual([
+    {
+      Put: {
+        Item: {
+          email: 'user@email.com',
+          id: '1a',
+          name: 'new name',
+          PK: 'USER#1a',
+          SK: 'USER#1a',
+          __en: 'user',
+        },
+        ReturnValues: 'ALL_NEW',
+        TableName: 'test-table',
+      },
+    },
+    {
+      Delete: {
+        Key: {
+          PK: 'USER#1',
+          SK: 'USER#1',
+        },
+        TableName: 'test-table',
+      },
+    },
+  ]);
+});
+
+test('transforms update item with attributes that reference primary key and indexes', () => {
+  const updateItem = transformer.toDynamoUpdateItem<Photo, PhotoPrimaryKey>(
+    Photo,
+    {
+      category: CATEGORY.PETS,
+      id: 1,
+    },
+    {
+      id: 2,
+    }
+  );
+
+  const lazyWriteItemListLoader = (updateItem as any)
+    .lazyLoadTransactionWriteItems;
+  expect(typeof lazyWriteItemListLoader).toEqual('function');
+
+  // with following existing item
+  const writeItemList = lazyWriteItemListLoader({
+    id: 1,
+    PK: 'PHOTO#PETS',
+    SK: 'PHOTO#1',
+    category: 'PETS',
+    GSI1PK: 'PHOTO#1',
+    GSI1SK: 'PHOTO#PETS',
+    __en: 'photo',
+  });
+
+  expect(writeItemList).toEqual([
+    {
+      Put: {
+        Item: {
+          GSI1PK: 'PHOTO#2',
+          GSI1SK: 'PHOTO#PETS',
+          PK: 'PHOTO#PETS',
+          SK: 'PHOTO#2',
+          __en: 'photo',
+          category: 'PETS',
+          id: 2,
+          updatedAt: '1622530750',
+        },
+        ReturnValues: 'ALL_NEW',
+        TableName: 'test-table',
+      },
+    },
+    {
+      Delete: {
+        Key: {
+          PK: 'PHOTO#PETS',
+          SK: 'PHOTO#1',
+        },
+        TableName: 'test-table',
+      },
+    },
+  ]);
+});
+
+test('throws when trying to update primary key attribute and unique attribute in the same request', () => {
+  const updateItem = () =>
+    transformer.toDynamoUpdateItem<UserUniqueEmail, UserUniqueEmailPrimaryKey>(
+      UserUniqueEmail,
+      {
+        id: 'OLD_ID',
+      },
+      {
+        id: 'NEW_ID',
+        email: 'new@email.com',
+      }
+    );
+
+  expect(updateItem).toThrow(InvalidUniqueAttributeUpdateError);
+});
+
+test('throws when trying to update primary key attribute and non key attribute in the same request', () => {
+  const updateItem = () =>
+    transformer.toDynamoUpdateItem<Photo, PhotoPrimaryKey>(
+      Photo,
+      {
+        category: CATEGORY.PETS,
+        id: 1,
+      },
+      {
+        id: 2,
+        createdAt: moment(),
+      }
+    );
+
+  expect(updateItem).toThrow(InvalidPrimaryKeyAttributesUpdateError);
+});
+
 test('transforms update item request with complex condition input', () => {
   const updatedItem = transformer.toDynamoUpdateItem<User, UserPrimaryKey>(
     User,
@@ -670,14 +943,14 @@ test('transforms update item request with complex condition input', () => {
   );
   expect(updatedItem).toEqual({
     ExpressionAttributeNames: {
-      '#attr0': 'name',
-      '#attr1': 'GSI1SK',
+      '#UE_name': 'name',
+      '#UE_GSI1SK': 'GSI1SK',
       '#CE_age': 'age',
       '#CE_status': 'status',
     },
     ExpressionAttributeValues: {
-      ':val0': 'new name',
-      ':val1': 'USER#new name',
+      ':UE_name': 'new name',
+      ':UE_GSI1SK': 'USER#new name',
       ':CE_age': 3,
       ':CE_status_0': 'active',
       ':CE_status_1': 'standby',
@@ -688,7 +961,7 @@ test('transforms update item request with complex condition input', () => {
     },
     ReturnValues: 'ALL_NEW',
     TableName: 'test-table',
-    UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+    UpdateExpression: 'SET #UE_name = :UE_name, #UE_GSI1SK = :UE_GSI1SK',
     ConditionExpression:
       '(#CE_age >= :CE_age) AND (#CE_status IN (:CE_status_0, :CE_status_1))',
   });
diff --git a/packages/core/src/classes/transformer/__test__/dc-scan-transformer.spec.ts b/packages/core/src/classes/transformer/__test__/dc-scan-transformer.spec.ts
new file mode 100644
index 0000000..144a159
--- /dev/null
+++ b/packages/core/src/classes/transformer/__test__/dc-scan-transformer.spec.ts
@@ -0,0 +1,198 @@
+import {
+  CONSUMED_CAPACITY_TYPE,
+  NoSuchEntityExistsError,
+  NoSuchIndexFoundError,
+} from '@typedorm/common';
+import {Organisation} from '@typedorm/core/__mocks__/organisation';
+import {table} from '@typedorm/core/__mocks__/table';
+import {User} from '@typedorm/core/__mocks__/user';
+import {UserUniqueEmail} from '@typedorm/core/__mocks__/user-unique-email';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {Connection} from '../../connection/connection';
+import {DocumentClientScanTransformer} from '../document-client-scan-transformer';
+
+jest.mock('uuid', () => ({
+  v4: jest.fn().mockReturnValue('66a7b3d6-323a-49b0-a12d-c99afff5005a'),
+}));
+
+let connection: Connection;
+let dcScanTransformer: DocumentClientScanTransformer;
+beforeEach(() => {
+  connection = createTestConnection({
+    entities: [User, Organisation, UserUniqueEmail],
+    table,
+  });
+  dcScanTransformer = new DocumentClientScanTransformer(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('correctly extends low order transformers', () => {
+  expect(dcScanTransformer.connection).toEqual(connection);
+});
+
+/**
+ * @group toDynamoScanItem
+ */
+test('transforms simple scan input', () => {
+  const transformed = dcScanTransformer.toDynamoScanItem();
+  expect(transformed).toEqual({TableName: 'test-table'});
+});
+
+test('transforms scan input with options', () => {
+  const transformed = dcScanTransformer.toDynamoScanItem(
+    {
+      cursor: {PK: 123},
+    },
+    {
+      returnConsumedCapacity: CONSUMED_CAPACITY_TYPE.TOTAL,
+    }
+  );
+
+  expect(transformed).toEqual({
+    ExclusiveStartKey: {
+      PK: 123,
+    },
+    ReturnConsumedCapacity: 'TOTAL',
+    TableName: 'test-table',
+  });
+});
+
+test('throws when trying to reference invalid index', () => {
+  const transformedFactory = () =>
+    dcScanTransformer.toDynamoScanItem({
+      scanIndex: 'SOME_UNKNOWN_INDEX',
+    });
+
+  expect(transformedFactory).toThrow(NoSuchIndexFoundError);
+});
+
+test('transforms input with filter and projection', () => {
+  const transformed = dcScanTransformer.toDynamoScanItem({
+    where: {
+      id: {
+        EQ: '1',
+      },
+    },
+    select: ['id'],
+  });
+
+  expect(transformed).toEqual({
+    ExpressionAttributeNames: {
+      '#FE_id': 'id',
+      '#PE_id': 'id',
+    },
+    ExpressionAttributeValues: {
+      ':FE_id': '1',
+    },
+    FilterExpression: '#FE_id = :FE_id',
+    ProjectionExpression: '#PE_id',
+    TableName: 'test-table',
+  });
+});
+
+test('transforms input with count only', () => {
+  const transformed = dcScanTransformer.toDynamoScanItem({
+    onlyCount: true,
+  });
+  expect(transformed).toEqual({
+    Select: 'COUNT',
+    TableName: 'test-table',
+  });
+});
+
+test('transforms input with entity filter and option filter', () => {
+  const transformed = dcScanTransformer.toDynamoScanItem({
+    where: {
+      id: {
+        EQ: '1',
+      },
+    },
+    select: ['id'],
+    entity: User,
+  });
+
+  expect(transformed).toEqual({
+    ExpressionAttributeNames: {
+      '#FE_id': 'id',
+      '#PE_id': 'id',
+      '#FE___en': '__en',
+    },
+    ExpressionAttributeValues: {
+      ':FE___en': 'user',
+      ':FE_id': '1',
+    },
+    FilterExpression: '(#FE___en = :FE___en) AND (#FE_id = :FE_id)',
+    ProjectionExpression: '#PE_id',
+    TableName: 'test-table',
+  });
+});
+
+test('transforms simple parallel scan item', () => {
+  const transformed = dcScanTransformer.toDynamoScanItem({
+    totalSegments: 3,
+    segment: 0,
+  });
+
+  expect(transformed).toEqual({
+    Segment: 0,
+    TableName: 'test-table',
+    TotalSegments: 3,
+  });
+});
+
+test('throws when invalid values are configured for segments', () => {
+  const transformedFac = () =>
+    dcScanTransformer.toDynamoScanItem({
+      // total segment is 2 but current segment id is 3,
+      totalSegments: 2,
+      segment: 3,
+    });
+
+  expect(transformedFac).toThrow(
+    `Invalid scan option segment: 3.
+        When totalSegments value is defined, value for option 'segment' must be one less than totalSegment size.`
+  );
+});
+
+/**
+ * @group fromDynamoScanResponseItemList
+ */
+test('transforms simple dynamodb output items', () => {
+  const transformed = dcScanTransformer.fromDynamoScanResponseItemList([
+    {id: '1', __en: 'user', name: 'test-user'},
+    {id: 'ORG_1', __en: 'organisation', name: 'test organisation'},
+    {
+      email: 'some@entity.com',
+    },
+  ]);
+
+  expect(transformed).toEqual({
+    items: [
+      {
+        id: '1',
+        name: 'test-user',
+      },
+      {
+        id: 'ORG_1',
+        name: 'test organisation',
+      },
+    ],
+    unknownItems: [
+      {
+        email: 'some@entity.com',
+      },
+    ],
+  });
+});
+
+test('throws for unknown entity name', () => {
+  const transformedFactory = () =>
+    dcScanTransformer.fromDynamoScanResponseItemList([
+      {id: 1, __en: 'unusual entity'},
+    ]);
+
+  expect(transformedFactory).toThrow(NoSuchEntityExistsError);
+});
diff --git a/packages/core/src/classes/transformer/__test__/dc-transaction-transformer.spec.ts b/packages/core/src/classes/transformer/__test__/dc-transaction-transformer.spec.ts
index 75c357b..c176791 100644
--- a/packages/core/src/classes/transformer/__test__/dc-transaction-transformer.spec.ts
+++ b/packages/core/src/classes/transformer/__test__/dc-transaction-transformer.spec.ts
@@ -78,19 +78,19 @@ test('transforms simple transaction write items', () => {
       {
         Update: {
           ExpressionAttributeNames: {
-            '#attr0': 'name',
-            '#attr1': 'GSI1SK',
+            '#UE_name': 'name',
+            '#UE_GSI1SK': 'GSI1SK',
           },
           ExpressionAttributeValues: {
-            ':val0': 'new name',
-            ':val1': 'USER#new name',
+            ':UE_name': 'new name',
+            ':UE_GSI1SK': 'USER#new name',
           },
           Key: {
             PK: 'USER#1',
             SK: 'USER#1',
           },
           TableName: 'test-table',
-          UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+          UpdateExpression: 'SET #UE_name = :UE_name, #UE_GSI1SK = :UE_GSI1SK',
         },
       },
     ],
diff --git a/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts b/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
index 467de3a..dd52f0d 100644
--- a/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
+++ b/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
@@ -14,6 +14,16 @@ import {EntityTransformer} from '../entity-transformer';
 import {UserSparseIndexes} from '../../../../__mocks__/user-sparse-indexes';
 import {table} from '@typedorm/core/__mocks__/table';
 import {UserAttrAlias} from '@typedorm/core/__mocks__/user-with-attribute-alias';
+import {CATEGORY, Photo} from '@typedorm/core/__mocks__/photo';
+// Moment is only being used here to display the usage of @transform utility
+// eslint-disable-next-line node/no-extraneous-import
+import moment from 'moment';
+import {UserCustomConstructor} from '@typedorm/core/__mocks__/user-custom-constructor';
+
+jest.mock('uuid', () => ({
+  v4: () => 'c0ac5395-ba7c-41bf-bbc3-09a6087bcca2',
+}));
+jest.useFakeTimers('modern').setSystemTime(1622530750000);
 import {UserWithDefaultValues} from '@typedorm/core/__mocks__/user-default-value';
 
 let transformer: EntityTransformer;
@@ -25,6 +35,8 @@ beforeEach(() => {
       UserAutoGenerateAttributes,
       UserSparseIndexes,
       UserAttrAlias,
+      Photo,
+      UserCustomConstructor,
       UserWithDefaultValues,
     ],
   });
@@ -56,6 +68,26 @@ test('transforms dynamo entity to entity model', () => {
   });
 });
 
+/**
+ * Issue: #134
+ */
+test('transforms dynamo entity to entity model with custom constructor', () => {
+  const dynamoEntity = {
+    PK: 'USER#1',
+    SK: 'USER#1',
+    id: '1',
+    name: 'Me',
+  };
+  const transformed = transformer.fromDynamoEntity(
+    UserCustomConstructor,
+    dynamoEntity
+  );
+  expect(transformed).toEqual({
+    id: '1',
+    name: 'Me',
+  });
+});
+
 test('transforms dynamo entity with aliased attributes to entity model', () => {
   const dynamoEntity = {
     PK: 'USER#1',
@@ -163,6 +195,28 @@ test('excludes hidden props from returned response', () => {
   });
 });
 
+test('transforms photo dynamo item to entity model instance ', () => {
+  const dynamoEntity = {
+    PK: 'PHOTO#PETS',
+    SK: 'PHOTO#1',
+    id: 1,
+    category: 'PETS',
+    name: 'my cute pet billy',
+    GSI1PK: 'PHOTO#c0ac5395-ba7c-41bf-bbc3-09a6087bcca2',
+    GSI1SK: 'PHOTO#kids-new',
+    createdAt: '2020-03-21T03:26:34.781Z',
+  };
+  const transformed = transformer.fromDynamoEntity(Photo, dynamoEntity);
+  expect(transformed).toMatchObject({
+    category: 'PETS',
+    createdAt: expect.any(moment),
+    id: 1,
+    name: 'my cute pet billy',
+  });
+  expect(transformed).toBeInstanceOf(Photo);
+  expect(transformed.createdDate()).toEqual('03-21-2020');
+});
+
 /**
  * @group toDynamoEntity
  */
@@ -184,6 +238,24 @@ test('transforms simple model to dynamo entity', () => {
   });
 });
 
+test('transforms photo entity to valid dynamo item ', () => {
+  const photo = new Photo(CATEGORY.KIDS, 'my baby');
+
+  const response = transformer.toDynamoEntity(photo);
+
+  expect(response).toEqual({
+    PK: 'PHOTO#kids-new',
+    SK: 'PHOTO#c0ac5395-ba7c-41bf-bbc3-09a6087bcca2',
+    GSI1PK: 'PHOTO#c0ac5395-ba7c-41bf-bbc3-09a6087bcca2',
+    GSI1SK: 'PHOTO#kids-new',
+    category: 'kids-new',
+    id: 'c0ac5395-ba7c-41bf-bbc3-09a6087bcca2',
+    updatedAt: '1622530750',
+    createdAt: '2021-06-01',
+    name: 'my baby',
+  });
+});
+
 test('transforms entity with attribute alias to dynamo entity', () => {
   const user = new UserAttrAlias();
   user.id = '111';
@@ -366,22 +438,40 @@ test('transforms put item requests with attributes containing default values', (
 /**
  * @group getAffectedIndexesForAttributes
  */
-test('returns all affected indexes for simple attributes', () => {
-  const affectedIndexes = transformer.getAffectedIndexesForAttributes(User, {
-    name: 'new updated name',
-  });
+test('returns all affected indexes for static attributes', () => {
+  const affectedIndexes = transformer.getAffectedIndexesForAttributes(
+    User,
+    {
+      name: 'new updated name',
+    },
+    {name: 'static'}
+  );
   expect(affectedIndexes).toEqual({
     GSI1SK: 'USER#new updated name',
   });
 });
 
+test('returns all affected indexes for dynamic update body', () => {
+  const affectedIndexes = transformer.getAffectedIndexesForAttributes<User>(
+    User,
+    {
+      age: {
+        DECREMENT_BY: 2,
+      },
+    },
+    {age: 'dynamic'}
+  );
+  expect(affectedIndexes).toEqual({});
+});
+
 test('returns all affected indexes for alias attributes', () => {
   const affectedIndexes = transformer.getAffectedIndexesForAttributes(
     UserAttrAlias,
     {
       age: 10,
       status: 'inactive',
-    }
+    },
+    {age: 'static', status: 'static'}
   );
   expect(affectedIndexes).toEqual({
     LSI1SK: 10, // <-- aliased indexes are correctly persisting attribute types
@@ -396,6 +486,11 @@ test('returns all affected indexes for complex attributes', () => {
       name: 'Updated name',
       teamCount: 12,
       active: false,
+    },
+    {
+      name: 'static',
+      teamCount: 'static',
+      active: 'static',
     }
   );
   expect(affectedIndexes).toEqual({
diff --git a/packages/testing/index.spec.ts b/packages/testing/index.spec.ts
new file mode 100644
index 0000000..b642e6f
--- /dev/null
+++ b/packages/testing/index.spec.ts
@@ -0,0 +1,42 @@
+import {
+  BatchManager,
+  EntityManager,
+  getBatchManager,
+  getEntityManager,
+  getScanManager,
+  getTransactionManger,
+  TransactionManager,
+} from '@typedorm/core';
+import {ScanManager} from '@typedorm/core/src/classes/manager/scan-manager';
+import {createTestConnection, resetTestConnection} from '.';
+
+beforeEach(() => {
+  createTestConnection({
+    entities: [],
+    documentClient: {},
+  });
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('gets entity manager', () => {
+  const entityManager = getEntityManager();
+  expect(entityManager).toBeInstanceOf(EntityManager);
+});
+
+test('gets transaction manager', () => {
+  const transactionManager = getTransactionManger();
+  expect(transactionManager).toBeInstanceOf(TransactionManager);
+});
+
+test('gets batch manager', () => {
+  const batchManager = getBatchManager();
+  expect(batchManager).toBeInstanceOf(BatchManager);
+});
+
+test('gets scan manager', () => {
+  const scanManager = getScanManager();
+  expect(scanManager).toBeInstanceOf(ScanManager);
+});