Repository: typedorm/typedorm
Pull request #52 (2021-01-14 14:18:08)

Release 1.5.0


Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index c04ed0c..19a6efa 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -43,7 +43,8 @@
       "internalConsoleOptions": "neverOpen",
       "port": 9229,
       "env": {
-        "NODE_ENV": "test"
+        "NODE_ENV": "test",
+        "DEBUG": "typedorm:*"
       }
     }
   ]
diff --git a/README.md b/README.md
index 4aba00b..235095c 100644
--- a/README.md
+++ b/README.md
@@ -30,9 +30,9 @@ Please submit an issue for any bugs or suggestions [here](https://github.com/typ
 - Declarative relational schema
 - Entity manager
 - Transaction manager
-- Multiple connections support (Partial)
+- Multiple connections support
 - Code follows all possible best practices when modeling for dynamodb
-- Additional higher level features like, ability to declare non key attribute a unique
+- Additional higher level features like, ability to declare non key attribute as unique(beta)
 - Auto Generated values fro attributes
 - Auto Update values on update
 - Complex update, key condition and condition expression all made easy to work with (Partial)
diff --git a/docs/api.md b/docs/api.md
index 496ebc7..59ed3f5 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -2,7 +2,7 @@
 
 This section contains information on all the public apis that are available within TypeDORM, this is likely to change to a live generated documentation, but until that is done, this can be used as a starting point.
 
-_Note: In an event of inconstancy between actual API and this document, API should looked as the source of truth, and also file an issue in this repository or submit a PR._
+_Note: In an event of inconstancy between actual API and this document, API should looked as the source of truth, and also if possible, file an issue in this repository or submit a PR._
 
 ## On this page
 
@@ -12,6 +12,7 @@ _Note: In an event of inconstancy between actual API and this document, API shou
 - [Entity](#entity)
 - [Attribute](#attribute)
 - [AutoGeneratedAttribute](#autogeneratedattribute)
+- [EntityManager](#entitymanager)
 
 ## Table
 
@@ -19,17 +20,17 @@ Declares table to be used to store entity, can be per entity based on single tab
 
 ```Typescript
 Table({
-  // name of the table, must match exactly to what is provisioned on aws
+  // Name of the table, must match exactly to what is provisioned on aws
   name
 
-  // partition key of table
+  // Partition key of table
   partitionKey // identifier of partition key
 
-  // sort key of table
+  // Sort key of table
   // @optional
   sortKey
 
-  // indexes if there are any
+  // Indexes if there are any
   indexes
 })
 ```
@@ -42,23 +43,23 @@ Declare an entity to be registered in TypeDORM domain
 
 ```Typescript
 @Entity({
-  // name of the entity that will be saved as __en attribute on each record
+  // Name of the entity that will be saved as __en attribute on each record
   name,
 
   // table where the entity be saved
   // @optional
-  // required when no global table exits in connection
+  // Required when no global table exits in connection
   table
 
   // primary key of entity, must adhere to table primary key
   primaryKey: {
     partitionKey
     // @optional
-    // required if table uses composite primary key
+    // Required if table uses composite primary key
     sortKey
   }
 
-  // additional indexes to add to entity
+  // Additional indexes to add to entity
   // @optional
   indexes
 })
@@ -72,13 +73,13 @@ Declare Attribute on an entity
 @Attribute({
 
   // @optional
-  // can either be of type boolean or with primary key
-  // when primary key is set to unique attribute, it can only reference it self
+  // Can either be of type boolean or with primary key
+  // When primary key is set to unique attribute, it can only reference it self
   // i.e `USER#{{email}}` on unique attribute named `email` is considered valid but not on `id`
   unique
 
   // @optional
-  // required when attribute type is enum and referenced in key schema
+  // Required when attribute type is enum and referenced in key schema
   isEnum
 })
 ```
@@ -90,13 +91,142 @@ Declare Auto generated attribute on an entity
 ```Typescript
 @AutoGenerateAttribute({
 
-  // strategy to use when auto generating attribute
-  // valid values are of enum `AUTO_GENERATE_ATTRIBUTE_STRATEGY`
+  // Strategy to use when auto generating attribute
+  // Valid values are of enum `AUTO_GENERATE_ATTRIBUTE_STRATEGY`
   strategy
 
   // @optional
-  // when true, attribute will be auto updated on write request with specified strategy
-  // useful for defining attributes like `updatedAt`
+  // When true, attribute will be auto updated on write request with specified strategy
+  // Useful for defining attributes like `updatedAt`
   autoUpdate
 })
 ```
+
+## EntityManager
+
+To Manage entity with ease.
+
+### EntityManager.create
+
+Create entity with given params.
+
+```Typescript
+create(
+  // Entity to put into db
+  // Item will be created to table configured on @Entity or Connection
+  entity
+)
+```
+
+### EntityManager.findOne
+
+Finds single item matching given primary key or returns undefined
+
+```Typescript
+findOne(
+  // Entity class to resolve schema against
+  entityClass
+
+  // All attributes referenced in primary key
+  primaryKeyAttributes
+)
+```
+
+### EntityManager.exists
+
+Checks existence of item
+
+```Typescript
+exists(
+  // Entity class to resolve schema against
+  entityClass
+
+  // Primary key attributes or unique attributes referenced in schemas
+  attributes
+)
+```
+
+### EntityManager.update
+
+Update item with magically generated set operation
+
+```Typescript
+exists(
+  // Entity class to resolve schema against
+  entityClass
+
+  // Primary key attributes referenced in schemas
+  primaryKeyAttributes
+
+  // Attributes to update, if doesn't already exist,it will be created
+  body
+
+  // @Optional;
+  // Additional options
+  options: {
+    // @optional
+    // Nested key separator to use, (i.e when updating user:{name},
+    // body can include 'user$name' set to new value)
+    // @default it '.'
+    nestedKeySeparator
+  }
+
+)
+```
+
+### EntityManager.delete
+
+Delete item by key
+
+```Typescript
+delete(
+  // Entity class to resolve schema against
+  entityClass
+
+  // Primary key attributes or unique attributes referenced in schemas
+  primaryKeyAttributes
+)
+```
+
+### EntityManager.query
+
+Query items from db
+
+```Typescript
+find(
+  // Entity class to resolve schema against
+  entityClass
+
+  // all attributes referenced in partition key
+  partitionKeyAttributes: {
+    // @optional
+    // Name of the index if querying against GSI/LSI
+    // @default - query will be run against main table
+    queryIndex
+  }
+
+  // @optional
+  // Entity manager query options
+  // @default none - if non specified, query will be made using only partition key
+  queryOptions: {
+    // @optional
+    // Cursor to start querying from
+    // @default - none
+    cursor
+
+    // Key condition to use when querying items
+    // i.e this could be `{BEGINS_WITH: 'ORDER#'}`
+    keyCondition
+
+    // @optional
+    // Total number of items to return
+    // @default max limit configured in connection
+    limit
+
+    // @optional
+    // Order in which to perform query
+    // @default ASC
+    orderBy
+  }
+)
+```
diff --git a/docs/debugging.md b/docs/debugging.md
new file mode 100644
index 0000000..07850c3
--- /dev/null
+++ b/docs/debugging.md
@@ -0,0 +1,62 @@
+# Debugging with TypeDORM
+
+TypeDORM implements debug based logger which can be enabled by setting `DEBUG` environment variable to `typedorm:*`.
+
+## Enabling Transform Logs
+
+All Transform logs can be enabled by setting `DEBUG` environment variable to `typedorm:transform:log`. All the logs uses syntax highlighting (using [chalk](https://github.com/chalk/chalk))
+
+For example, executing `EntityManager.create` operation with transformed log enabled, this will be printed to console.
+
+```Typescript
+  typedorm:transform:log PUT user Before:
+Body:
+{
+  "id": "1",
+  "name": "Test User",
+  "status": "active"
+}
+ +0ms
+  typedorm:transform:log PUT user After:
+Body:
+{
+  "Item": {
+    "__en": "user",
+    "id": "1",
+    "name": "Test User",
+    "status": "active",
+    "PK": "USER#1",
+    "SK": "USER#1",
+    "GSI1PK": "USER#STATUS#active",
+    "GSI1SK": "USER#Test User"
+  },
+  "TableName": "test-table",
+  "ConditionExpression": "attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)",
+  "ExpressionAttributeNames": {
+    "#CE_PK": "PK",
+    "#CE_SK": "SK"
+  }
+}
+ +0ms
+  typedorm:transform:log RESPONSE user Before:
+Body:
+{
+  "__en": "user",
+  "id": "1",
+  "name": "Test User",
+  "status": "active",
+  "PK": "USER#1",
+  "SK": "USER#1",
+  "GSI1PK": "USER#STATUS#active",
+  "GSI1SK": "USER#Test User"
+}
+ +0ms
+  typedorm:transform:log RESPONSE user After:
+Body:
+{
+  "id": "1",
+  "name": "Test User",
+  "status": "active"
+}
+ +0ms
+```
diff --git a/docs/index.md b/docs/index.md
index dcdc511..9219a73 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -6,4 +6,5 @@ Document index that covers, list of pages available
 - [Step by step guide](./guide.md)
 - [Entity inheritance](./entity-inheritance.md)
 - [Working with multiple connections](./multiple-connection.md)
+- [Debugging](./debugging.md)
 - [API](./api.md)
diff --git a/package-lock.json b/package-lock.json
index bc02e85..6ac88d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1170,6 +1170,12 @@
         "@babel/types": "^7.3.0"
       }
     },
+    "@types/debug": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
+      "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
+      "dev": true
+    },
     "@types/graceful-fs": {
       "version": "4.1.4",
       "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz",
@@ -1504,7 +1510,6 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
       "requires": {
         "color-convert": "^2.0.1"
       }
@@ -1987,7 +1992,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
       "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
-      "dev": true,
       "requires": {
         "ansi-styles": "^4.1.0",
         "supports-color": "^7.1.0"
@@ -2141,7 +2145,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
       "requires": {
         "color-name": "~1.1.4"
       }
@@ -2149,8 +2152,7 @@
     "color-name": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
     },
     "colors": {
       "version": "1.0.3",
@@ -2512,10 +2514,9 @@
       "dev": true
     },
     "debug": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
-      "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
-      "dev": true,
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+      "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
       "requires": {
         "ms": "2.1.2"
       }
@@ -4029,8 +4030,7 @@
     "has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
     },
     "has-value": {
       "version": "1.0.0",
@@ -5641,8 +5641,7 @@
     "ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "mute-stream": {
       "version": "0.0.8",
@@ -11026,7 +11025,6 @@
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
       "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
       "requires": {
         "has-flag": "^4.0.0"
       }
diff --git a/package.json b/package.json
index 224ef2d..e3a8e8f 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,8 @@
   },
   "dependencies": {
     "aws-sdk": "^2.799.0",
+    "chalk": "^4.1.0",
+    "debug": "^4.3.1",
     "fast-glob": "^3.2.4",
     "ksuid": "^2.0.0",
     "reflect-metadata": "^0.1.13",
@@ -33,6 +35,7 @@
     "@bazel/ibazel": "^0.14.0",
     "@bazel/typescript": "^2.3.0",
     "@commitlint/config-conventional": "^11.0.0",
+    "@types/debug": "^4.1.5",
     "@types/node": "^14.11.2",
     "@types/uuid": "^8.3.0",
     "babel": "^6.23.0",
diff --git a/packages/common/BUILD b/packages/common/BUILD
index 7855a62..5ddf36a 100644
--- a/packages/common/BUILD
+++ b/packages/common/BUILD
@@ -5,7 +5,10 @@ setup_ts_build(
     name = "library",
     module_name = "@typedorm/common",
     deps = [
-        "@npm//reflect-metadata"
+        "@npm//reflect-metadata",
+        "@npm//debug",
+        "@npm//@types/debug",
+        "@npm//chalk"
     ],
     visibility = ["//packages:__subpackages__"]
 )
diff --git a/packages/common/package.json b/packages/common/package.json
index 8ea6ff9..e5f41f2 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -12,6 +12,10 @@
     "@typedorm/core": "0.0.0-PLACEHOLDER",
     "reflect-metadata": "^0.1.13"
   },
+  "dependencies": {
+    "debug": "^4.3.1",
+    "chalk": "^4.1.0"
+  },
   "publishConfig": {
     "tag": "TAG-PLACEHOLDER",
     "access": "public"
diff --git a/packages/common/private-api.ts b/packages/common/private-api.ts
index 94478b0..d0430a3 100644
--- a/packages/common/private-api.ts
+++ b/packages/common/private-api.ts
@@ -1,2 +1,3 @@
 export * from './src/metadata/metadata-manager';
 export * from './src/metadata/metadata-storage';
+export * from './src/logger/debug-logger';
diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts
index f8a4de7..51f4712 100644
--- a/packages/common/src/constants.ts
+++ b/packages/common/src/constants.ts
@@ -1,5 +1,5 @@
 /**
- * Current transaction write items limit
+ * Current transaction write items limit set by dynamodb itself
  */
 export const TRANSACTION_WRITE_ITEMS_LIMIT = 25;
 
diff --git a/packages/common/src/enums.ts b/packages/common/src/enums.ts
index 113d863..44483e2 100644
--- a/packages/common/src/enums.ts
+++ b/packages/common/src/enums.ts
@@ -3,6 +3,7 @@ export enum RETURN_VALUES {
   ALL_NEW = 'ALL_NEW',
   UPDATED_OLD = 'UPDATED_OLD',
   UPDATED_NEW = 'UPDATED_NEW',
+  NONE = 'NONE',
 }
 
 export enum INDEX_TYPE {
diff --git a/packages/common/src/logger/debug-logger.ts b/packages/common/src/logger/debug-logger.ts
new file mode 100644
index 0000000..2817cc4
--- /dev/null
+++ b/packages/common/src/logger/debug-logger.ts
@@ -0,0 +1,58 @@
+import debug from 'debug';
+import chalk from 'chalk';
+
+export enum TRANSFORM_TYPE {
+  GET = 'GET',
+  PUT = 'PUT',
+  UPDATE = 'UPDATE',
+  DELETE = 'DELETE',
+  QUERY = 'QUERY',
+  RESPONSE = 'RESPONSE',
+}
+
+export class DebugLogger {
+  // log
+  private debugTransformLog = debug('typedorm:transform:log');
+  constructor() {}
+
+  logTransform(
+    operation: TRANSFORM_TYPE,
+    prefix: string,
+    entityName: string,
+    primaryKey: any,
+    body?: any,
+    options?: any
+  ) {
+    if (this.debugTransformLog.enabled) {
+      this.debugTransformLog(
+        `${chalk.green(operation)} ${chalk.blue(entityName)} ${chalk.magenta(
+          prefix
+        )}: `,
+        ...(primaryKey
+          ? [
+              chalk.blueBright('\nPrimary key: '),
+              chalk.white(this.ensurePrintable(primaryKey)),
+            ]
+          : []),
+        ...(body
+          ? [
+              chalk.blueBright('\nBody: '),
+              chalk.white(this.ensurePrintable(body)),
+            ]
+          : []),
+        ...(options
+          ? [
+              chalk.blueBright('\nOptions: '),
+              chalk.white(this.ensurePrintable(options)),
+            ]
+          : [])
+      );
+    }
+  }
+
+  private ensurePrintable(log: any) {
+    if (typeof log === 'object' && log !== null) {
+      return `\n${JSON.stringify(log, null, 2)}\n`;
+    } else return log;
+  }
+}
diff --git a/packages/common/src/metadata/metadata-storage.ts b/packages/common/src/metadata/metadata-storage.ts
index 26fdc7f..73a297f 100644
--- a/packages/common/src/metadata/metadata-storage.ts
+++ b/packages/common/src/metadata/metadata-storage.ts
@@ -2,6 +2,10 @@ import {AUTO_GENERATE_ATTRIBUTE_STRATEGY, EntityTarget} from '@typedorm/common';
 import {IndexOptions, Table} from '../table';
 import {AttributeOptionsUniqueType} from '../decorators/attribute.decorator';
 
+export const IsAutoGenerateAttributeRawMetadataOptions = (
+  attr: any
+): attr is AutoGenerateAttributeRawMetadataOptions => !!attr.strategy;
+
 export type PrimaryKey = SimplePrimaryKey | CompositePrimaryKey;
 export type SimplePrimaryKey = {
   partitionKey: string;
@@ -24,22 +28,25 @@ export interface EntityRawMetadataOptions {
   table?: Table;
 }
 
-export interface AttributeRawMetadataOptions {
+interface BaseAttributeRawMetadataOptions {
   name: string;
   type: any;
+}
+export interface AttributeRawMetadataOptions
+  extends BaseAttributeRawMetadataOptions {
   unique?: AttributeOptionsUniqueType;
 }
 
 export interface AutoGenerateAttributeRawMetadataOptions
-  extends AttributeRawMetadataOptions {
+  extends BaseAttributeRawMetadataOptions {
   strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY;
   autoUpdate?: boolean;
 }
 
 export class MetadataStorage {
-  private _entities: Map<string, EntityRawMetadataOptions>;
+  private _entities: Map<EntityTarget<any>, EntityRawMetadataOptions>;
   private _attributes: Map<
-    string,
+    EntityTarget<any>,
     Map<
       string,
       AttributeRawMetadataOptions | AutoGenerateAttributeRawMetadataOptions
@@ -51,12 +58,12 @@ export class MetadataStorage {
     this._attributes = new Map();
   }
 
-  hasKnownEntityByName(name: string) {
-    return this._entities.has(name);
+  hasKnownEntity<Entity>(entityClass: EntityTarget<Entity>) {
+    return this._entities.has(entityClass);
   }
 
   getRawAttributesForEntity<Entity>(entityClass: EntityTarget<Entity>) {
-    const attributes = this._attributes.get(entityClass.name)?.values();
+    const attributes = this._attributes.get(entityClass)?.values();
 
     if (!attributes) {
       throw new Error(`No entity with name "${entityClass.name}" could be resolved, 
@@ -66,7 +73,7 @@ export class MetadataStorage {
   }
 
   getRawEntityByTarget<Entity>(entityClass: EntityTarget<Entity>) {
-    const entity = this._entities.get(entityClass.name);
+    const entity = this._entities.get(entityClass);
 
     if (!entity) {
       throw new Error(`No entity with name "${entityClass.name}" could be resolved, 
@@ -79,18 +86,18 @@ export class MetadataStorage {
     entityClass: EntityTarget<Entity>,
     attribute: AttributeRawMetadataOptions
   ) {
-    let attributesForEntity = this._attributes.get(entityClass.name);
+    let attributesForEntity = this._attributes.get(entityClass);
 
     if (!attributesForEntity) {
       attributesForEntity = new Map();
     }
 
     attributesForEntity.set(attribute.name, attribute);
-    this._attributes.set(entityClass.name, attributesForEntity);
+    this._attributes.set(entityClass, attributesForEntity);
   }
 
   addRawEntity(entity: EntityRawMetadataOptions) {
-    this._entities.set(entity.target.name, entity);
+    this._entities.set(entity.target, entity);
   }
 
   get entities() {
diff --git a/packages/core/src/classes/condition/base-condition.ts b/packages/core/src/classes/condition/base-condition.ts
index f8aec69..bd72052 100644
--- a/packages/core/src/classes/condition/base-condition.ts
+++ b/packages/core/src/classes/condition/base-condition.ts
@@ -123,12 +123,12 @@ export abstract class BaseCondition {
     return this;
   }
 
-  and(): BaseCondition {
+  and(): this {
     this.appendToExpression('AND');
     return this;
   }
 
-  or(): BaseCondition {
+  or(): this {
     this.appendToExpression('OR');
     return this;
   }
diff --git a/packages/core/src/classes/connection/attribute-metadata-builder.ts b/packages/core/src/classes/connection/attribute-metadata-builder.ts
index 96eed6b..8321082 100644
--- a/packages/core/src/classes/connection/attribute-metadata-builder.ts
+++ b/packages/core/src/classes/connection/attribute-metadata-builder.ts
@@ -1,31 +1,82 @@
-import {MetadataManager, Table} from '@typedorm/common';
-import {AttributeMetadata} from '../metadata/attribute-metadata';
 import {
-  AutoGeneratedAttributeMetadata,
-  IsAutoGeneratedAttributeMetadata,
-} from '../metadata/auto-generated-attribute-metadata';
+  AutoGenerateAttributeRawMetadataOptions,
+  EntityRawMetadataOptions,
+  IsAutoGenerateAttributeRawMetadataOptions,
+  MetadataManager,
+  Table,
+} from '@typedorm/common';
+import {isUsedForPrimaryKey} from '../../helpers/is-used-for-primary-key';
+import {AttributeMetadata} from '../metadata/attribute-metadata';
+import {AutoGeneratedAttributeMetadata} from '../metadata/auto-generated-attribute-metadata';
 
 export class AttributesMetadataBuilder {
   constructor() {}
 
-  build(table: Table, entityClass: Function, decoratedClass?: Function) {
+  build(
+    table: Table,
+    attributeTargetEntityClass: Function,
+    decoratedEntityClass: Function
+  ) {
     const getRawAttributesForEntity = MetadataManager.metadataStorage.getRawAttributesForEntity(
-      entityClass
+      attributeTargetEntityClass
     );
 
     return getRawAttributesForEntity.map(attr => {
-      if (IsAutoGeneratedAttributeMetadata(attr)) {
-        return new AutoGeneratedAttributeMetadata({
-          ...attr,
-        });
+      if (IsAutoGenerateAttributeRawMetadataOptions(attr)) {
+        const entityMetadata = this.tryGetEntityMetadata(
+          attributeTargetEntityClass,
+          decoratedEntityClass
+        );
+
+        return this.validateAndBuildAutoGeneratedAttributeMetadata(
+          attr,
+          entityMetadata
+        );
       }
 
       return new AttributeMetadata({
         table,
         // when working with entity with multiple inheritance, use class with @Entity() if available
-        entityClass: decoratedClass ?? entityClass,
+        entityClass: decoratedEntityClass ?? attributeTargetEntityClass,
         ...attr,
       });
     });
   }
+
+  private validateAndBuildAutoGeneratedAttributeMetadata(
+    attr: AutoGenerateAttributeRawMetadataOptions,
+    entityMetadata: EntityRawMetadataOptions
+  ) {
+    if (attr.autoUpdate) {
+      if (isUsedForPrimaryKey(entityMetadata.primaryKey, attr.name)) {
+        throw new Error(
+          `Failed to build metadata for "${attr.name}", attributes referenced in primary key cannot be auto updated.`
+        );
+      }
+    }
+    return new AutoGeneratedAttributeMetadata({
+      ...attr,
+    });
+  }
+
+  private tryGetEntityMetadata(
+    attributeTargetEntityClass: Function,
+    decoratedEntityClass: Function
+  ) {
+    let entityMetadata: EntityRawMetadataOptions;
+
+    try {
+      // check if attribute target entity has metadata
+      entityMetadata = MetadataManager.metadataStorage.getRawEntityByTarget(
+        attributeTargetEntityClass
+      );
+    } catch (err) {
+      // fall back to using derived class metadata
+      entityMetadata = MetadataManager.metadataStorage.getRawEntityByTarget(
+        decoratedEntityClass
+      );
+    }
+
+    return entityMetadata;
+  }
 }
diff --git a/packages/core/src/classes/connection/connection-metadata-builder.ts b/packages/core/src/classes/connection/connection-metadata-builder.ts
index 6812ad4..54f7ae9 100644
--- a/packages/core/src/classes/connection/connection-metadata-builder.ts
+++ b/packages/core/src/classes/connection/connection-metadata-builder.ts
@@ -18,7 +18,7 @@ export class ConnectionMetadataBuilder {
 
     // filter all entities that are not marked with `@Entity` decorator
     const entitiesToBuild = possibleEntitiesToBuild.filter(entity =>
-      MetadataManager.metadataStorage.hasKnownEntityByName(entity.name)
+      MetadataManager.metadataStorage.hasKnownEntity(entity)
     );
 
     return new EntityMetadataBuilder(this.connection).build(entitiesToBuild);
diff --git a/packages/core/src/classes/connection/connection.ts b/packages/core/src/classes/connection/connection.ts
index 3f6f3d6..ec28630 100644
--- a/packages/core/src/classes/connection/connection.ts
+++ b/packages/core/src/classes/connection/connection.ts
@@ -1,16 +1,19 @@
 import {
   DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT,
   EntityTarget,
+  Replace,
   Table,
+  DebugLogger,
 } from '@typedorm/common';
 import {DynamoDB} from 'aws-sdk';
+import {isUsedForPrimaryKey} from '../../helpers/is-used-for-primary-key';
 import {EntityManager} from '../manager/entity-manager';
 import {TransactionManager} from '../manager/transaction-manager';
 import {AttributeMetadata} from '../metadata/attribute-metadata';
 import {AutoGeneratedAttributeMetadata} from '../metadata/auto-generated-attribute-metadata';
 import {
-  DynamoEntitySchemaPrimaryKey,
   EntityMetadata,
+  DynamoEntitySchemaPrimaryKey,
 } from '../metadata/entity-metadata';
 import {ConnectionMetadataBuilder} from './connection-metadata-builder';
 import {ConnectionOptions} from './connection-options';
@@ -22,6 +25,7 @@ export class Connection {
   readonly transactionManger: TransactionManager;
   readonly defaultConfig: {queryItemsImplicitLimit: number};
   readonly documentClient: DynamoDB.DocumentClient;
+  readonly logger: DebugLogger;
 
   private _entityMetadatas: Map<string, EntityMetadata>;
   private isConnected: boolean;
@@ -52,6 +56,7 @@ export class Connection {
      * This makes sure that we only ever build entity metadatas once per connection
      */
     this.isConnected = false;
+    this.logger = new DebugLogger();
   }
 
   connect() {
@@ -118,9 +123,13 @@ export class Connection {
       // only attributes that are not part of primary key should be included
       return (
         (attr as AttributeMetadata)?.unique &&
-        !this.isUsedForPrimaryKey(entityMetadata.schema.primaryKey, attr.name)
+        !isUsedForPrimaryKey(entityMetadata.schema.primaryKey, attr.name)
       );
-    }) as AttributeMetadata[];
+    }) as Replace<
+      AttributeMetadata,
+      'unique',
+      {unique: DynamoEntitySchemaPrimaryKey}
+    >[];
   }
 
   getEntityByTarget<Entity>(entityClass: EntityTarget<Entity>) {
diff --git a/packages/core/src/classes/connection/entity-metadata-builder.ts b/packages/core/src/classes/connection/entity-metadata-builder.ts
index d59d7d7..3044394 100644
--- a/packages/core/src/classes/connection/entity-metadata-builder.ts
+++ b/packages/core/src/classes/connection/entity-metadata-builder.ts
@@ -13,14 +13,16 @@ export class EntityMetadataBuilder {
     this.attributesMetadataBuilder = new AttributesMetadataBuilder();
   }
   build(entityClasses: Function[]): EntityMetadata[] {
-    return entityClasses.map(entityClass => {
+    return entityClasses.map(decoratedEntityClass => {
       const {
         target,
         table,
         name,
         primaryKey,
         indexes,
-      } = MetadataManager.metadataStorage.getRawEntityByTarget(entityClass);
+      } = MetadataManager.metadataStorage.getRawEntityByTarget(
+        decoratedEntityClass
+      );
 
       if (table) {
         // if no entity level table is defined fallback to global connection table
@@ -29,15 +31,17 @@ export class EntityMetadataBuilder {
         this.table = this.connection.table;
       }
 
-      const inheritedClasses = this.recursiveGetInheritanceTree(entityClass);
+      const inheritedClasses = this.recursiveGetInheritanceTree(
+        decoratedEntityClass
+      );
 
       // metadata are sorted by [very base class] -> [very derived class]
       const inheritedEntitiesAttributesMetadata = inheritedClasses
-        .map(inheritedClass =>
+        .map(derivedClass =>
           this.attributesMetadataBuilder.build(
             this.table,
-            inheritedClass,
-            entityClass
+            derivedClass,
+            decoratedEntityClass
           )
         )
         .reverse();
diff --git a/packages/core/src/classes/expression-builder.ts b/packages/core/src/classes/expression-builder.ts
index 5867ff4..f0fd827 100644
--- a/packages/core/src/classes/expression-builder.ts
+++ b/packages/core/src/classes/expression-builder.ts
@@ -1,9 +1,25 @@
+import {Table} from '@typedorm/common';
 import {isEmptyObject} from '../helpers/is-empty-object';
 import {isObject} from '../helpers/is-object';
 import {Condition} from './condition/condition';
 import {KeyCondition} from './condition/key-condition';
 
 export class ExpressionBuilder {
+  /**
+   * Higher level function to build unique record condition expression
+   * @param table table to build unique record expression for
+   */
+  buildUniqueRecordConditionExpression(table: Table) {
+    const uniqueRecordCondition = table.usesCompositeKey()
+      ? new Condition()
+          .attributeNotExist(table.partitionKey)
+          .and()
+          .attributeNotExist(table.sortKey)
+      : new Condition().attributeNotExist(table.partitionKey);
+
+    return this.buildConditionExpression(uniqueRecordCondition);
+  }
+
   buildConditionExpression(condition: Condition) {
     if (!condition.expression) {
       return {ConditionExpression: ''};
diff --git a/packages/core/src/classes/manager/entity-manager.ts b/packages/core/src/classes/manager/entity-manager.ts
index b7c1fbd..5408178 100644
--- a/packages/core/src/classes/manager/entity-manager.ts
+++ b/packages/core/src/classes/manager/entity-manager.ts
@@ -3,7 +3,6 @@ import {
   EntityAttributes,
   EntityTarget,
   PrimaryKeyAttributes,
-  RETURN_VALUES,
   UpdateAttributes,
 } from '@typedorm/common';
 import {getDynamoQueryItemsLimit} from '../../helpers/get-dynamo-query-items-limit';
@@ -16,17 +15,15 @@ import {
 } from '../transformer/document-client-request-transformer';
 import {EntityTransformer} from '../transformer/entity-transformer';
 import {getConstructorForInstance} from '../../helpers/get-constructor-for-instance';
+import {isUsedForPrimaryKey} from '../../helpers/is-used-for-primary-key';
+import {isWriteTransactionItemList} from '../transaction/type-guards';
+import {isLazyTransactionWriteItemListLoader} from '../transformer/is-lazy-transaction-write-item-list-loder';
 
 export interface EntityManagerUpdateOptions {
   /**
    * @default '.'
    */
   nestedKeySeparator?: string;
-
-  /**
-   * @default ALL_NEW
-   */
-  returnValues?: RETURN_VALUES;
 }
 
 export interface EntityManagerQueryOptions
@@ -51,8 +48,10 @@ export class EntityManager {
     const dynamoPutItemInput = this._dcReqTransformer.toDynamoPutItem(entity);
     const entityClass = getConstructorForInstance(entity);
 
-    if (!Array.isArray(dynamoPutItemInput)) {
+    if (!isWriteTransactionItemList(dynamoPutItemInput)) {
       await this.connection.documentClient.put(dynamoPutItemInput).promise();
+
+      // by default dynamodb does not return attributes on create operation, so return one
       const itemToReturn = this._entityTransformer.fromDynamoEntity<Entity>(
         entityClass,
         dynamoPutItemInput.Item
@@ -61,18 +60,19 @@ export class EntityManager {
       return itemToReturn;
     }
 
-    // when put item is set to array, one or more attributes are marked as unique
-    // to maintain all records consistency, all items must be put into db as a single transaction
+    // dynamoPutItemInput is a transact item list, meaning that it contains one or more unique attributes, which also
+    // needs to be created along with original item
+
     const transaction = new WriteTransaction(
       this.connection,
-      dynamoPutItemInput.map(putItem => ({Put: putItem}))
+      dynamoPutItemInput
     );
     await this.connection.transactionManger.write(transaction);
 
     const itemToReturn = this._entityTransformer.fromDynamoEntity<Entity>(
       entityClass,
-      // if create operation contains multiple items, first one will the original requested item
-      dynamoPutItemInput[0].Item
+      // if create operation contains multiple items, first one will the original item
+      dynamoPutItemInput[0]?.Put?.Item ?? {}
     );
 
     return itemToReturn;
@@ -131,12 +131,7 @@ export class EntityManager {
       attributes
     ).reduce(
       (acc, [attrKey, value]) => {
-        if (
-          this.connection.isUsedForPrimaryKey(
-            metadata.schema.primaryKey,
-            attrKey
-          )
-        ) {
+        if (isUsedForPrimaryKey(metadata.schema.primaryKey, attrKey)) {
           acc.primaryKeyAttributes[attrKey] = value;
         } else if (uniqueAttributeNames.includes(attrKey)) {
           acc.uniqueAttributes[attrKey] = value;
@@ -206,47 +201,103 @@ export class EntityManager {
   /**
    *
    * @param entityClass Entity class to update
-   * @param primaryKey Primary key
+   * @param primaryKeyAttributes Primary key
    * @param body Attributes to update
    * @param options update options
    */
   async update<PrimaryKey, Entity>(
     entityClass: EntityTarget<Entity>,
-    primaryKey: PrimaryKeyAttributes<PrimaryKey, any>,
+    primaryKeyAttributes: PrimaryKeyAttributes<PrimaryKey, any>,
     body: UpdateAttributes<PrimaryKey, Entity>,
     options?: EntityManagerUpdateOptions
   ): Promise<Entity> {
     const dynamoUpdateItem = this._dcReqTransformer.toDynamoUpdateItem<
       PrimaryKey,
       Entity
-    >(entityClass, primaryKey, body, options);
+    >(entityClass, primaryKeyAttributes, body, options);
 
-    const response = await this.connection.documentClient
-      .update(dynamoUpdateItem)
-      .promise();
+    if (!isLazyTransactionWriteItemListLoader(dynamoUpdateItem)) {
+      const response = await this.connection.documentClient
+        .update(dynamoUpdateItem)
+        .promise();
+
+      return this._entityTransformer.fromDynamoEntity<Entity>(
+        entityClass,
+        // return all new attributes
+        response.Attributes ?? {}
+      );
+    }
 
-    return this._entityTransformer.fromDynamoEntity<Entity>(
+    // first get existing item, so that we can safely clear previous unique attributes
+    const existingItem = await this.findOne<PrimaryKey, Entity>(
       entityClass,
-      response.Attributes ?? {}
+      primaryKeyAttributes
+    );
+
+    if (!existingItem) {
+      throw new Error(
+        `Failed to update entity, could not find entity with primary key "${JSON.stringify(
+          primaryKeyAttributes
+        )}"`
+      );
+    }
+
+    const updateItemList = dynamoUpdateItem.lazyLoadTransactionWriteItems(
+      existingItem
     );
+
+    const transaction = new WriteTransaction(this.connection, updateItemList);
+    // since write transaction does not return any, we will need to get the latest one from dynamo
+    await this.connection.transactionManger.write(transaction);
+    const updatedItem = (await this.findOne<PrimaryKey, Entity>(
+      entityClass,
+      primaryKeyAttributes
+    )) as Entity;
+
+    return updatedItem;
   }
 
   /**
    * Deletes an entity by primary key
    * @param entityClass Entity Class to delete
-   * @param primaryKey Entity Primary key
+   * @param primaryKeyAttributes Entity Primary key
    */
-  async delete<PrimaryKey, Entity>(
+  async delete<PrimaryKeyAttributes, Entity>(
     entityClass: EntityTarget<Entity>,
-    primaryKey: PrimaryKey
+    primaryKeyAttributes: PrimaryKeyAttributes
   ) {
     const dynamoDeleteItem = this._dcReqTransformer.toDynamoDeleteItem<
-      PrimaryKey,
+      PrimaryKeyAttributes,
       Entity
-    >(entityClass, primaryKey);
+    >(entityClass, primaryKeyAttributes);
 
-    await this.connection.documentClient.delete(dynamoDeleteItem).promise();
+    if (!isLazyTransactionWriteItemListLoader(dynamoDeleteItem)) {
+      await this.connection.documentClient.delete(dynamoDeleteItem).promise();
+      return {
+        success: true,
+      };
+    }
 
+    // first get existing item, so that we can safely clear previous unique attributes
+    const existingItem = await this.findOne<PrimaryKeyAttributes, Entity>(
+      entityClass,
+      primaryKeyAttributes
+    );
+
+    if (!existingItem) {
+      throw new Error(
+        `Failed to update entity, could not find entity with primary key "${JSON.stringify(
+          primaryKeyAttributes
+        )}"`
+      );
+    }
+
+    const deleteItemList = dynamoDeleteItem.lazyLoadTransactionWriteItems(
+      existingItem
+    );
+    const transaction = new WriteTransaction(this.connection, deleteItemList);
+    // delete main item and all it's unique attributes as part of single transaction
+    await this.connection.transactionManger.write(transaction);
     return {
       success: true,
     };
diff --git a/packages/core/src/classes/manager/transaction-manager.ts b/packages/core/src/classes/manager/transaction-manager.ts
index 94485a8..0fb5497 100644
--- a/packages/core/src/classes/manager/transaction-manager.ts
+++ b/packages/core/src/classes/manager/transaction-manager.ts
@@ -2,6 +2,7 @@ import {DynamoDB} from 'aws-sdk';
 import {TRANSACTION_WRITE_ITEMS_LIMIT} from '@typedorm/common';
 import {WriteTransaction} from '../transaction/write-transaction';
 import {Connection} from '../connection/connection';
+import {isLazyTransactionWriteItemListLoader} from '../transformer/is-lazy-transaction-write-item-list-loder';
 
 /**
  * Performs transactions using document client's writeTransaction
@@ -10,14 +11,42 @@ export class TransactionManager {
   constructor(private connection: Connection) {}
 
   async write(transaction: WriteTransaction) {
-    if (transaction.items.length > TRANSACTION_WRITE_ITEMS_LIMIT) {
+    const transactionItems = (
+      await Promise.all(
+        transaction.items.map(async item => {
+          if (!isLazyTransactionWriteItemListLoader(item)) {
+            return item;
+          }
+
+          // if updating/removing unique attribute in transaction, get previous value of attributes
+          const existingItem = await this.connection.entityManager.findOne(
+            item.entityClass,
+            item.primaryKeyAttributes
+          );
+
+          if (!existingItem) {
+            throw new Error(
+              `Failed to process entity "${
+                item.entityClass.name
+              }", could not find entity with primary key "${JSON.stringify(
+                item.primaryKeyAttributes
+              )}"`
+            );
+          }
+
+          return item.lazyLoadTransactionWriteItems(existingItem);
+        })
+      )
+    ).flat();
+
+    if (transactionItems.length > TRANSACTION_WRITE_ITEMS_LIMIT) {
       throw new Error(
         `Transaction write limit exceeded, current transaction write limit is "${TRANSACTION_WRITE_ITEMS_LIMIT}"`
       );
     }
 
     const transactionInput: DynamoDB.DocumentClient.TransactWriteItemsInput = {
-      TransactItems: transaction.items,
+      TransactItems: transactionItems,
     };
 
     // FIXME: current promise implementation of document client transact write does not provide a way
@@ -55,8 +84,9 @@ export class TransactionManager {
           );
           return reject(transactionError);
         }
+
         return resolve(response);
       });
-    });
+    }) as Promise<DynamoDB.DocumentClient.TransactWriteItemsOutput>;
   }
 }
diff --git a/packages/core/src/classes/metadata/entity-metadata.ts b/packages/core/src/classes/metadata/entity-metadata.ts
index 07b88c9..a201ca2 100644
--- a/packages/core/src/classes/metadata/entity-metadata.ts
+++ b/packages/core/src/classes/metadata/entity-metadata.ts
@@ -9,6 +9,7 @@ import {
 } from '@typedorm/common';
 import {buildPrimaryKeySchema} from '../../helpers/build-primary-key-schema';
 import {getInterpolatedKeys} from '../../helpers/get-interpolated-keys';
+import {validateKey} from '../../helpers/validate-key';
 import {Connection} from '../connection/connection';
 import {AttributeMetadata} from './attribute-metadata';
 import {AutoGeneratedAttributeMetadata} from './auto-generated-attribute-metadata';
@@ -130,11 +131,9 @@ export class EntityMetadata extends BaseMetadata {
 
       const currentIndex = indexes[key];
 
+      validateKey(currentIndex.sortKey, attributes);
       // validates and gets and fill set indexes interpolations of sort key
-      const sortKeyInterpolations = getInterpolatedKeys(
-        currentIndex.sortKey,
-        attributes
-      );
+      const sortKeyInterpolations = getInterpolatedKeys(currentIndex.sortKey);
 
       if (tableIndexSignature.type === INDEX_TYPE.LSI) {
         if (currentIndex.type !== INDEX_TYPE.LSI) {
@@ -153,10 +152,10 @@ export class EntityMetadata extends BaseMetadata {
         if (currentIndex.type !== INDEX_TYPE.GSI) {
           throw new Error('Index signature mismatch.');
         }
+        validateKey(currentIndex.partitionKey, attributes);
         // validates and gets and fill set indexes interpolations of partition key
         const partitionKeyInterpolations = getInterpolatedKeys(
-          currentIndex.partitionKey,
-          attributes
+          currentIndex.partitionKey
         );
 
         acc[key] = {
diff --git a/packages/core/src/classes/transaction/transaction.ts b/packages/core/src/classes/transaction/transaction.ts
index 5e19837..210179d 100644
--- a/packages/core/src/classes/transaction/transaction.ts
+++ b/packages/core/src/classes/transaction/transaction.ts
@@ -7,12 +7,13 @@ import {
 import {Connection} from '../connection/connection';
 import {EntityManagerUpdateOptions} from '../manager/entity-manager';
 import {DocumentClientRequestTransformer} from '../transformer/document-client-request-transformer';
+import {LazyTransactionWriteItemListLoader} from '../transformer/is-lazy-transaction-write-item-list-loder';
 
 // transaction interfaces
-interface WriteTransactionCreate<Entity> {
+export interface WriteTransactionCreate<Entity> {
   create: {item: Entity};
 }
-interface WriteTransactionUpdate<PrimaryKey, Entity> {
+export interface WriteTransactionUpdate<PrimaryKey, Entity> {
   update: {
     item: EntityTarget<Entity>;
     primaryKey: PrimaryKeyAttributes<PrimaryKey, any>;
@@ -20,32 +21,26 @@ interface WriteTransactionUpdate<PrimaryKey, Entity> {
     options?: EntityManagerUpdateOptions;
   };
 }
+export interface WriteTransactionRemove<PrimaryKey, Entity> {
+  delete: {
+    item: EntityTarget<Entity>;
+    primaryKey: PrimaryKeyAttributes<PrimaryKey, any>;
+  };
+}
 export type WriteTransactionChainItem<PrimaryKey, Entity> =
   | WriteTransactionCreate<Entity>
-  | WriteTransactionUpdate<PrimaryKey, Entity>;
-
-// custom type guards
-export function isCreateTransaction<Entity>(
-  item: any
-): item is WriteTransactionCreate<Entity> {
-  return (item as WriteTransactionCreate<Entity>).create !== undefined;
-}
-
-export function isUpdateTransaction<PrimaryKey, Entity>(
-  item: any
-): item is WriteTransactionUpdate<PrimaryKey, Entity> {
-  return (
-    (item as WriteTransactionUpdate<PrimaryKey, Entity>).update !== undefined
-  );
-}
+  | WriteTransactionUpdate<PrimaryKey, Entity>
+  | WriteTransactionRemove<PrimaryKey, Entity>;
 
 /**
  * Base Transaction
  */
 export abstract class Transaction {
-  protected _items:
-    | DynamoDB.DocumentClient.TransactWriteItemList
-    | DynamoDB.DocumentClient.TransactGetItemList;
+  protected _items: (
+    | DynamoDB.DocumentClient.TransactWriteItem
+    | LazyTransactionWriteItemListLoader
+    | DynamoDB.DocumentClient.TransactGetItem
+  )[];
 
   protected _dcReqTransformer: DocumentClientRequestTransformer;
 
diff --git a/packages/core/src/classes/transaction/type-guards.ts b/packages/core/src/classes/transaction/type-guards.ts
new file mode 100644
index 0000000..61cde95
--- /dev/null
+++ b/packages/core/src/classes/transaction/type-guards.ts
@@ -0,0 +1,40 @@
+import {DynamoDB} from 'aws-sdk';
+import {
+  WriteTransactionCreate,
+  WriteTransactionRemove,
+  WriteTransactionUpdate,
+} from './transaction';
+
+export function isCreateTransaction<Entity>(
+  item: any
+): item is WriteTransactionCreate<Entity> {
+  return (item as WriteTransactionCreate<Entity>).create !== undefined;
+}
+
+export function isUpdateTransaction<PrimaryKey, Entity>(
+  item: any
+): item is WriteTransactionUpdate<PrimaryKey, Entity> {
+  return (
+    (item as WriteTransactionUpdate<PrimaryKey, Entity>).update !== undefined
+  );
+}
+
+export function isRemoveTransaction<PrimaryKey, Entity>(
+  item: any
+): item is WriteTransactionRemove<PrimaryKey, Entity> {
+  return (
+    (item as WriteTransactionRemove<PrimaryKey, Entity>).delete !== undefined
+  );
+}
+
+export const isWriteTransactionItemList = (
+  item: any
+): item is DynamoDB.DocumentClient.TransactWriteItemList =>
+  !!(item as DynamoDB.DocumentClient.TransactWriteItemList).length &&
+  !!(
+    (item as DynamoDB.DocumentClient.TransactWriteItemList)[0]
+      ?.ConditionCheck ||
+    (item as DynamoDB.DocumentClient.TransactWriteItemList)[0]?.Delete ||
+    (item as DynamoDB.DocumentClient.TransactWriteItemList)[0]?.Put ||
+    (item as DynamoDB.DocumentClient.TransactWriteItemList)[0]?.Update
+  );
diff --git a/packages/core/src/classes/transaction/write-transaction.ts b/packages/core/src/classes/transaction/write-transaction.ts
index 315ee5f..b392d12 100644
--- a/packages/core/src/classes/transaction/write-transaction.ts
+++ b/packages/core/src/classes/transaction/write-transaction.ts
@@ -1,14 +1,27 @@
 import {DynamoDB} from 'aws-sdk';
+import {dropProp} from '../../helpers/drop-prop';
 import {Connection} from '../connection/connection';
 import {
-  isCreateTransaction,
-  isUpdateTransaction,
+  isLazyTransactionWriteItemListLoader,
+  LazyTransactionWriteItemListLoader,
+} from '../transformer/is-lazy-transaction-write-item-list-loder';
+import {
   Transaction,
   WriteTransactionChainItem,
+  WriteTransactionCreate,
 } from './transaction';
+import {
+  isCreateTransaction,
+  isRemoveTransaction,
+  isUpdateTransaction,
+  isWriteTransactionItemList,
+} from './type-guards';
 
 export class WriteTransaction extends Transaction {
-  protected _items: DynamoDB.DocumentClient.TransactWriteItemList;
+  protected _items: (
+    | LazyTransactionWriteItemListLoader
+    | DynamoDB.DocumentClient.TransactWriteItem
+  )[];
 
   constructor(
     connection: Connection,
@@ -25,41 +38,77 @@ export class WriteTransaction extends Transaction {
   chian<PrimaryKey, Entity>(
     chainedItem: WriteTransactionChainItem<PrimaryKey, Entity>
   ): WriteTransaction {
+    // create
     if (isCreateTransaction(chainedItem)) {
-      const {
-        create: {item},
-      } = chainedItem;
+      this.items = this.chainCreateTransaction(chainedItem);
+      // update
+    } else if (isUpdateTransaction(chainedItem)) {
+      const {item, body, primaryKey, options} = chainedItem.update;
 
-      const dynamoPutItemInput = this._dcReqTransformer.toDynamoPutItem<Entity>(
-        item
-      );
+      const itemToUpdate = this._dcReqTransformer.toDynamoUpdateItem<
+        PrimaryKey,
+        Entity
+      >(item, primaryKey, body, options);
+      if (!isLazyTransactionWriteItemListLoader(itemToUpdate)) {
+        this.items.push({
+          Update: dropProp(itemToUpdate, 'ReturnValues') as DynamoDB.Update,
+        });
+      } else {
+        this.items.push(itemToUpdate);
+      }
+      // remove
+    } else if (isRemoveTransaction(chainedItem)) {
+      const {item, primaryKey} = chainedItem.delete;
 
-      // when put item is set to array, one or more attributes are marked as unique
-      // to maintain all records consistency, all items must be put into db as a single transaction
-      if (!Array.isArray(dynamoPutItemInput)) {
-        this._items.push({
-          Put: dynamoPutItemInput,
+      const itemToRemove = this._dcReqTransformer.toDynamoDeleteItem<
+        PrimaryKey,
+        Entity
+      >(item, primaryKey);
+      if (!isLazyTransactionWriteItemListLoader(itemToRemove)) {
+        this.items.push({
+          Delete: itemToRemove,
         });
       } else {
-        this._items.push(
-          ...dynamoPutItemInput.map(puttItem => ({Put: puttItem}))
-        );
+        this.items.push(itemToRemove);
       }
-    } else if (isUpdateTransaction(chainedItem)) {
-      const {item, body, primaryKey, options} = chainedItem.update;
-      this._items.push({
-        Update: this._dcReqTransformer.toDynamoUpdateItem<PrimaryKey, Entity>(
-          item,
-          primaryKey,
-          body,
-          options
-        ) as DynamoDB.Update,
-      });
+    } else {
+      return this;
     }
     return this;
   }
 
+  private chainCreateTransaction<Entity>(
+    chainedItem: WriteTransactionCreate<Entity>
+  ) {
+    const {
+      create: {item},
+    } = chainedItem;
+
+    const dynamoPutItemInput = this._dcReqTransformer.toDynamoPutItem<Entity>(
+      item
+    );
+
+    if (!isWriteTransactionItemList(dynamoPutItemInput)) {
+      return [
+        {
+          Put: dynamoPutItemInput,
+        },
+      ] as DynamoDB.DocumentClient.TransactWriteItemList;
+    }
+
+    return [...dynamoPutItemInput];
+  }
+
   get items() {
     return this._items;
   }
+
+  set items(
+    items: (
+      | DynamoDB.DocumentClient.TransactWriteItem
+      | LazyTransactionWriteItemListLoader
+    )[]
+  ) {
+    this._items = [...this.items, ...items];
+  }
 }
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 d7c9bec..5640afe 100644
--- a/packages/core/src/classes/transformer/document-client-request-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-request-transformer.ts
@@ -7,29 +7,34 @@ import {
   INDEX_TYPE,
   PrimaryKeyAttributes,
   QUERY_ORDER,
+  Replace,
   RequireOnlyOne,
   RETURN_VALUES,
   ScalarType,
+  Table,
   UpdateAttributes,
+  TRANSFORM_TYPE,
 } from '@typedorm/common';
 import {DynamoDB} from 'aws-sdk';
+import {dropProp} from '../../helpers/drop-prop';
 import {getConstructorForInstance} from '../../helpers/get-constructor-for-instance';
 import {isEmptyObject} from '../../helpers/is-empty-object';
 import {parseKey} from '../../helpers/parse-key';
-import {Condition} from '../condition/condition';
 import {KeyCondition} from '../condition/key-condition';
 import {Connection} from '../connection/connection';
 import {ExpressionBuilder} from '../expression-builder';
 import {EntityManagerUpdateOptions} from '../manager/entity-manager';
 import {AttributeMetadata} from '../metadata/attribute-metadata';
+import {DynamoEntitySchemaPrimaryKey} from '../metadata/entity-metadata';
 import {BaseTransformer} from './base-transformer';
+import {LazyTransactionWriteItemListLoader} from './is-lazy-transaction-write-item-list-loder';
 
 export interface ManagerToDynamoQueryItemsOptions {
   /**
    * Sort key condition
    * @default none - no sort key condition is applied
    */
-  keyCondition?: RequireOnlyOne<
+  keyCondition: RequireOnlyOne<
     {
       [key in FindKeyScalarOperator]: ScalarType;
     } &
@@ -72,6 +77,13 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
   ): DynamoDB.DocumentClient.GetItemInput {
     const metadata = this.connection.getEntityByTarget(entityClass);
 
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.GET,
+      'Before',
+      metadata.name,
+      primaryKey
+    );
+
     const tableName = this.getTableNameForEntity(entityClass);
 
     const parsedPrimaryKey = this.getParsedPrimaryKey(
@@ -84,12 +96,20 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       throw new Error('Primary could not be resolved');
     }
 
-    return {
+    const transformBody = {
       TableName: tableName,
       Key: {
         ...parsedPrimaryKey,
       },
     };
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.GET,
+      'After',
+      metadata.name,
+      null,
+      transformBody
+    );
+    return transformBody;
   }
 
   toDynamoPutItem<Entity>(
@@ -97,12 +117,21 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     options?: ManagerToDynamoPutItemOptions
   ):
     | DynamoDB.DocumentClient.PutItemInput
-    | DynamoDB.DocumentClient.PutItemInput[] {
+    | DynamoDB.DocumentClient.TransactWriteItemList {
     const entityClass = getConstructorForInstance(entity);
-    const {table, internalAttributes} = this.connection.getEntityByTarget(
+    const {table, internalAttributes, name} = this.connection.getEntityByTarget(
       entityClass
     );
 
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.PUT,
+      'Before',
+      name,
+      null,
+      entity,
+      options
+    );
+
     const uniqueAttributes = this.connection.getUniqueAttributesForEntity(
       entityClass
     ) as AttributeMetadata[];
@@ -114,12 +143,40 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       return acc;
     }, {} as DynamoDB.DocumentClient.PutItemInputAttributeMap);
 
-    let uniqueAttributePutItems = [] as DynamoDB.DocumentClient.PutItemInput[];
+    let dynamoPutItem = {
+      Item: {
+        ...entityInternalAttributes,
+        ...dynamoEntity,
+      },
+      TableName: table.name,
+    } as DynamoDB.DocumentClient.PutItemInput;
+
     // apply attribute not exist condition when creating unique
-    const uniqueRecordConditionExpression = this._expressionBuilder.buildConditionExpression(
-      new Condition().attributeNotExist(table.partitionKey)
+    const uniqueRecordConditionExpression = new ExpressionBuilder().buildUniqueRecordConditionExpression(
+      table
     );
 
+    // always prevent overwriting data until explicitly told to do otherwise
+    if (!options?.overwriteIfExists) {
+      dynamoPutItem = {
+        ...dynamoPutItem,
+        ...uniqueRecordConditionExpression,
+      };
+    }
+
+    if (!uniqueAttributes.length) {
+      this.connection.logger.logTransform(
+        TRANSFORM_TYPE.PUT,
+        'After',
+        name,
+        null,
+        dynamoPutItem
+      );
+      return dynamoPutItem;
+    }
+
+    // if there are unique attributes, return transaction list item
+    let uniqueAttributePutItems: DynamoDB.DocumentClient.TransactWriteItemList = [];
     if (uniqueAttributes.length) {
       uniqueAttributePutItems = uniqueAttributes.map(attr => {
         const attributeValue = (entity as any)[attr.name];
@@ -143,68 +200,67 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
         );
 
         return {
-          Item: uniqueItemPrimaryKey,
-          TableName: table.name,
-          ...uniqueRecordConditionExpression,
-        } as DynamoDB.DocumentClient.PutItemInput;
+          Put: {
+            Item: uniqueItemPrimaryKey,
+            TableName: table.name,
+            ...uniqueRecordConditionExpression,
+          },
+        };
       });
     }
 
-    let dynamoPutItem = {
-      Item: {
-        ...entityInternalAttributes,
-        ...dynamoEntity,
-      },
-      TableName: table.name,
-    } as DynamoDB.DocumentClient.PutItemInput;
-
-    // always prevent overwriting data until explicitly told to do otherwise
-    if (!options?.overwriteIfExists) {
-      dynamoPutItem = {...dynamoPutItem, ...uniqueRecordConditionExpression};
-    }
+    const uniqueAttributesPutItems = [
+      {Put: dynamoPutItem},
+      ...uniqueAttributePutItems,
+    ];
+
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.PUT,
+      'After',
+      name,
+      null,
+      uniqueAttributesPutItems
+    );
 
-    if (uniqueAttributePutItems.length) {
-      return [dynamoPutItem, ...uniqueAttributePutItems];
-    }
-    return dynamoPutItem;
+    return uniqueAttributesPutItems;
   }
 
   toDynamoUpdateItem<PrimaryKey, Entity>(
     entityClass: EntityTarget<Entity>,
-    primaryKey: PrimaryKeyAttributes<PrimaryKey, any>,
+    primaryKeyAttributes: PrimaryKeyAttributes<PrimaryKey, any>,
     body: UpdateAttributes<PrimaryKey, Entity>,
-    options?: EntityManagerUpdateOptions
-  ): DynamoDB.DocumentClient.UpdateItemInput {
+    options: EntityManagerUpdateOptions = {}
+  ):
+    | DynamoDB.DocumentClient.UpdateItemInput
+    | LazyTransactionWriteItemListLoader {
     // default values
-    const {nestedKeySeparator = '.', returnValues = RETURN_VALUES.ALL_NEW} =
-      options ?? {};
+    const {nestedKeySeparator = '.'} = options;
 
     if (!this.connection.hasMetadata(entityClass)) {
       throw new Error(`No metadata found for class "${entityClass.name}".`);
     }
 
     const metadata = this.connection.getEntityByTarget(entityClass);
-
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.UPDATE,
+      'Before',
+      metadata.name,
+      primaryKeyAttributes,
+      body,
+      options
+    );
     const tableName = metadata.table.name;
 
     const parsedPrimaryKey = this.getParsedPrimaryKey(
       metadata.table,
       metadata.schema.primaryKey,
-      primaryKey
+      primaryKeyAttributes
     );
 
     if (isEmptyObject(parsedPrimaryKey)) {
       throw new Error('Primary could not be resolved');
     }
 
-    // TODO: add support for updating unique attributes
-    this.connection.getUniqueAttributesForEntity(entityClass).forEach(attr => {
-      // key that is marked as unique, can not be updated
-      if (body[attr.name]) {
-        throw new Error('');
-      }
-    });
-
     // get all the attributes for entity that are marked as to be auto update
     const autoUpdateAttributes = this.connection.getAutoUpdateAttributes(
       entityClass
@@ -213,11 +269,6 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     // check if auto update attributes are not referenced by primary key
     const formattedAutoUpdateAttributes = autoUpdateAttributes.reduce(
       (acc, attr) => {
-        if ((primaryKey as any)[attr.name]) {
-          throw new Error(
-            `Failed to build update expression, key "${attr.name}" is marked as to up auto updated but is also referenced by primary key`
-          );
-        }
         acc[attr.name] = attr.autoGenerateValue(attr.strategy);
         return acc;
       },
@@ -243,24 +294,66 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       ...affectedIndexes,
     });
 
-    return {
+    const uniqueAttributesToUpdate = this.connection
+      .getUniqueAttributesForEntity(entityClass)
+      .filter(attr => !!body[attr.name]);
+
+    const itemToUpdate: DynamoDB.DocumentClient.UpdateItemInput = {
       TableName: tableName,
       Key: {
         ...parsedPrimaryKey,
       },
       UpdateExpression,
+      // request all new attributes
+      ReturnValues: RETURN_VALUES.ALL_NEW,
       ExpressionAttributeNames,
       ExpressionAttributeValues,
-      ReturnValues: returnValues,
+    };
+
+    // when item does not have any unique attributes to update, return putItemInput
+    if (!uniqueAttributesToUpdate.length) {
+      this.connection.logger.logTransform(
+        TRANSFORM_TYPE.UPDATE,
+        'After',
+        metadata.name,
+        null,
+        itemToUpdate
+      );
+      return itemToUpdate;
+    }
+
+    // if there are unique attributes, return a lazy loader, which will return write item list
+    const lazyLoadTransactionWriteItems = this.lazyToDynamoUpdateItemFactory<
+      PrimaryKey,
+      Entity
+    >(
+      metadata.table,
+      metadata.name,
+      uniqueAttributesToUpdate,
+      dropProp(itemToUpdate, 'ReturnValues'),
+      body
+    );
+
+    return {
+      primaryKeyAttributes,
+      entityClass,
+      lazyLoadTransactionWriteItems,
     };
   }
 
   toDynamoDeleteItem<PrimaryKey, Entity>(
     entityClass: EntityTarget<Entity>,
     primaryKey: PrimaryKey
-  ): DynamoDB.DocumentClient.DeleteItemInput {
+  ):
+    | DynamoDB.DocumentClient.DeleteItemInput
+    | LazyTransactionWriteItemListLoader {
     const metadata = this.connection.getEntityByTarget(entityClass);
-
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.DELETE,
+      'Before',
+      metadata.name,
+      primaryKey
+    );
     const tableName = metadata.table.name;
 
     const parsedPrimaryKey = this.getParsedPrimaryKey(
@@ -273,12 +366,40 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       throw new Error('Primary could not be resolved');
     }
 
-    return {
+    const uniqueAttributesToRemove = this.connection.getUniqueAttributesForEntity(
+      entityClass
+    );
+
+    const mainItemToRemove = {
       TableName: tableName,
       Key: {
         ...parsedPrimaryKey,
       },
     };
+    // if item does not have any unique attributes return it as is
+    if (!uniqueAttributesToRemove?.length) {
+      this.connection.logger.logTransform(
+        TRANSFORM_TYPE.DELETE,
+        'After',
+        metadata.name,
+        primaryKey
+      );
+      return mainItemToRemove;
+    }
+
+    // or return lazy resolver
+    const lazyLoadTransactionWriteItems = this.lazyToDynamoRemoveItemFactory(
+      metadata.table,
+      metadata.name,
+      uniqueAttributesToRemove,
+      mainItemToRemove
+    );
+
+    return {
+      primaryKeyAttributes: primaryKey,
+      entityClass,
+      lazyLoadTransactionWriteItems,
+    };
   }
 
   toDynamoQueryItem<PartitionKeyAttributes, Entity>(
@@ -288,8 +409,16 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     },
     queryOptions?: ManagerToDynamoQueryItemsOptions
   ): DynamoDB.DocumentClient.QueryInput {
-    const {table, schema} = this.connection.getEntityByTarget(entityClass);
-
+    const {table, schema, name} = this.connection.getEntityByTarget(
+      entityClass
+    );
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.QUERY,
+      'Before',
+      name,
+      partitionKeyAttributes,
+      queryOptions
+    );
     const queryIndexName = partitionKeyAttributes.queryIndex ?? '';
     let indexToQuery: IndexOptions | undefined;
     if (partitionKeyAttributes.queryIndex) {
@@ -343,13 +472,23 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
 
     // if no query options are present, resolve key condition expression
     if (!queryOptions || isEmptyObject(queryOptions)) {
-      return {
+      const transformedQueryItem = {
         TableName: table.name,
         IndexName: partitionKeyAttributes.queryIndex,
         ...this._expressionBuilder.buildKeyConditionExpression(
           partitionKeyCondition
         ),
       };
+
+      this.connection.logger.logTransform(
+        TRANSFORM_TYPE.QUERY,
+        'After',
+        name,
+        null,
+        transformedQueryItem
+      );
+
+      return transformedQueryItem;
     }
 
     const parsedSortKey = {} as {name: string};
@@ -406,8 +545,146 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       };
     }
 
-    return {
-      ...queryInputParams,
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.QUERY,
+      'After',
+      name,
+      null,
+      queryInputParams
+    );
+
+    return queryInputParams;
+  }
+
+  /**
+   * 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<PrimaryKey, Entity>(
+    table: Table,
+    entityName: string,
+    uniqueAttributesToUpdate: Replace<
+      AttributeMetadata,
+      'unique',
+      {
+        unique: DynamoEntitySchemaPrimaryKey;
+      }
+    >[],
+    mainItem: DynamoDB.DocumentClient.UpdateItemInput,
+    newBody: UpdateAttributes<PrimaryKey, Entity>
+  ) {
+    // returns transact write item list
+    return (previousItemBody: any) => {
+      // updating unique attributes also require checking if new value exists
+      const uniqueRecordConditionExpression = new ExpressionBuilder().buildUniqueRecordConditionExpression(
+        table
+      );
+
+      // map all unique attributes to [put, delete] item tuple
+      const uniqueAttributeInputs: DynamoDB.DocumentClient.TransactWriteItemList = uniqueAttributesToUpdate.flatMap(
+        attr => {
+          const uniqueAttributeWriteItems: DynamoDB.DocumentClient.TransactWriteItemList = [
+            {
+              Put: {
+                TableName: table.name,
+                Item: {
+                  ...this.getParsedPrimaryKey(table, attr.unique, newBody),
+                },
+                ...uniqueRecordConditionExpression,
+              },
+            },
+          ];
+
+          // if unique attribute previously existed, remove it as part of the same transaction
+          if (previousItemBody[attr.name]) {
+            uniqueAttributeWriteItems.push({
+              Delete: {
+                TableName: table.name,
+                Key: {
+                  ...this.getParsedPrimaryKey(
+                    table,
+                    attr.unique,
+                    previousItemBody
+                  ),
+                },
+              },
+            });
+          }
+
+          return uniqueAttributeWriteItems;
+        }
+      );
+
+      // in order for update express to succeed, all listed must succeed in a transaction
+      const updateTransactionItems = [
+        {Update: mainItem},
+        ...uniqueAttributeInputs,
+      ] as DynamoDB.DocumentClient.TransactWriteItem[];
+      this.connection.logger.logTransform(
+        TRANSFORM_TYPE.UPDATE,
+        'After',
+        entityName,
+        null,
+        updateTransactionItems
+      );
+      return updateTransactionItems;
+    };
+  }
+
+  /**
+   * lazily resolve all unique attribute items to remove
+   * @param table
+   * @param uniqueAttributesToRemove
+   * @param mainItem
+   */
+  private lazyToDynamoRemoveItemFactory(
+    table: Table,
+    entityName: string,
+    uniqueAttributesToRemove: Replace<
+      AttributeMetadata,
+      'unique',
+      {
+        unique: DynamoEntitySchemaPrimaryKey;
+      }
+    >[],
+    mainItem: DynamoDB.DocumentClient.DeleteItemInput
+  ) {
+    return (existingItemBody: any) => {
+      const uniqueAttributeInputs: DynamoDB.DocumentClient.TransactWriteItemList = uniqueAttributesToRemove.map(
+        attr => {
+          return {
+            Delete: {
+              TableName: table.name,
+              Key: {
+                ...this.getParsedPrimaryKey(
+                  table,
+                  attr.unique,
+                  existingItemBody
+                ),
+              },
+            },
+          };
+        }
+      );
+
+      const deleteTransactionItems = [
+        {
+          Delete: mainItem,
+        },
+        ...uniqueAttributeInputs,
+      ] as DynamoDB.DocumentClient.TransactWriteItem[];
+
+      this.connection.logger.logTransform(
+        TRANSFORM_TYPE.DELETE,
+        'After',
+        entityName,
+        null,
+        deleteTransactionItems
+      );
+
+      return deleteTransactionItems;
     };
   }
 }
diff --git a/packages/core/src/classes/transformer/entity-transformer.ts b/packages/core/src/classes/transformer/entity-transformer.ts
index 48efe97..9971e9a 100644
--- a/packages/core/src/classes/transformer/entity-transformer.ts
+++ b/packages/core/src/classes/transformer/entity-transformer.ts
@@ -1,4 +1,4 @@
-import {DynamoEntity, EntityTarget} from '@typedorm/common';
+import {DynamoEntity, EntityTarget, TRANSFORM_TYPE} from '@typedorm/common';
 import {Connection} from '../connection/connection';
 import {BaseTransformer} from './base-transformer';
 
@@ -18,6 +18,13 @@ export class EntityTransformer extends BaseTransformer {
     dynamoEntity: DynamoEntity<Entity>
   ): Entity {
     const entityMetadata = this.connection.getEntityByTarget(entityClass);
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.RESPONSE,
+      'Before',
+      entityMetadata.name,
+      null,
+      dynamoEntity
+    );
 
     const entityPrimaryKeys = Object.keys(entityMetadata.schema.primaryKey);
     const entityInternalAttributeKeys = entityMetadata.internalAttributes.map(
@@ -32,7 +39,7 @@ export class EntityTransformer extends BaseTransformer {
       })
       .flat();
 
-    return Object.keys(dynamoEntity).reduce((acc, key) => {
+    const transformedEntity = Object.keys(dynamoEntity).reduce((acc, key) => {
       if (
         entityPrimaryKeys.includes(key) ||
         entityIndexes.includes(key) ||
@@ -43,5 +50,15 @@ export class EntityTransformer extends BaseTransformer {
       (acc as any)[key] = (dynamoEntity as any)[key];
       return acc;
     }, {} as Entity);
+
+    this.connection.logger.logTransform(
+      TRANSFORM_TYPE.RESPONSE,
+      'After',
+      entityMetadata.name,
+      null,
+      transformedEntity
+    );
+
+    return transformedEntity;
   }
 }
diff --git a/packages/core/src/classes/transformer/is-lazy-transaction-write-item-list-loder.ts b/packages/core/src/classes/transformer/is-lazy-transaction-write-item-list-loder.ts
new file mode 100644
index 0000000..fc6ba01
--- /dev/null
+++ b/packages/core/src/classes/transformer/is-lazy-transaction-write-item-list-loder.ts
@@ -0,0 +1,20 @@
+import {EntityTarget} from '@typedorm/common';
+import {DynamoDB} from 'aws-sdk';
+
+export type LazyTransactionWriteItemListLoader = {
+  lazyLoadTransactionWriteItems: (
+    previousItemBody: any
+  ) => DynamoDB.DocumentClient.TransactWriteItem[];
+  entityClass: EntityTarget<any>;
+  primaryKeyAttributes: any;
+};
+
+export const isLazyTransactionWriteItemListLoader = (
+  response: any
+): response is LazyTransactionWriteItemListLoader =>
+  !!(
+    (response as LazyTransactionWriteItemListLoader)
+      .lazyLoadTransactionWriteItems &&
+    typeof (response as LazyTransactionWriteItemListLoader)
+      .lazyLoadTransactionWriteItems === 'function'
+  );
diff --git a/packages/core/src/helpers/build-primary-key-schema.ts b/packages/core/src/helpers/build-primary-key-schema.ts
index e100572..9ce73a8 100644
--- a/packages/core/src/helpers/build-primary-key-schema.ts
+++ b/packages/core/src/helpers/build-primary-key-schema.ts
@@ -12,8 +12,7 @@ export function buildPrimaryKeySchema({
   attributes: {[key: string]: string};
 }) {
   const partitionKeyInterpolations = getInterpolatedKeys(
-    primaryKey.partitionKey,
-    attributes
+    primaryKey.partitionKey
   );
 
   const tablePartitionKeyName = table.partitionKey;
@@ -30,10 +29,7 @@ export function buildPrimaryKeySchema({
 
     // build primary key
     const tableSortKeyName = table.sortKey ?? '';
-    const sortKeyInterpolations = getInterpolatedKeys(
-      primaryKey.sortKey,
-      attributes
-    );
+    const sortKeyInterpolations = getInterpolatedKeys(primaryKey.sortKey);
     return {
       [tablePartitionKeyName]: primaryKey.partitionKey,
       [tableSortKeyName]: primaryKey.sortKey,
diff --git a/packages/core/src/helpers/drop-prop.ts b/packages/core/src/helpers/drop-prop.ts
new file mode 100644
index 0000000..b92431f
--- /dev/null
+++ b/packages/core/src/helpers/drop-prop.ts
@@ -0,0 +1,5 @@
+export function dropProp<T, K extends keyof T>(object: T, prop: K) {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const {[prop]: propToDrop, ...updatedObj} = object;
+  return updatedObj;
+}
diff --git a/packages/core/src/helpers/get-interpolated-keys.ts b/packages/core/src/helpers/get-interpolated-keys.ts
index cc6d1af..4521c55 100644
--- a/packages/core/src/helpers/get-interpolated-keys.ts
+++ b/packages/core/src/helpers/get-interpolated-keys.ts
@@ -1,12 +1,9 @@
 import {regexInterpolatedWord} from './constants';
 
-export function getInterpolatedKeys(
-  key: string,
-  dict: {[key: string]: string}
-) {
+export function getInterpolatedKeys(key: string) {
   const matchIterator = key.matchAll(regexInterpolatedWord);
   const interpolatedKeys = [] as string[];
-  recursiveFindInterpolatedKeys(key, matchIterator, dict, interpolatedKeys);
+  recursiveFindInterpolatedKeys(key, matchIterator, interpolatedKeys);
 
   // there shouldn't be any duplicates, still to be double sure remove duplicates if there are any
   return [...new Set(interpolatedKeys)];
@@ -15,7 +12,6 @@ export function getInterpolatedKeys(
 function recursiveFindInterpolatedKeys(
   key: string,
   iterator: IterableIterator<RegExpMatchArray>,
-  dict: {[key: string]: string},
   interpolatedKeys: string[]
 ) {
   const next = iterator.next();
@@ -25,25 +21,8 @@ function recursiveFindInterpolatedKeys(
 
   // regex match returns [match, key]
   const variable = next.value[1];
-  const variableType = dict[variable];
-
-  if (!variableType) {
-    throw new Error(
-      `key "${key}" references variable "${variable}" but it could not be resolved`
-    );
-  }
-
-  if (
-    variableType !== 'String' &&
-    variableType !== 'Number' &&
-    variableType !== 'Boolean'
-  ) {
-    throw new Error(
-      `"${variable}" is used in key ${key}, thus it's type must be a "Number" or a "String"`
-    );
-  }
 
   // push interpolated word to array
   interpolatedKeys.push(variable);
-  recursiveFindInterpolatedKeys(key, iterator, dict, interpolatedKeys);
+  recursiveFindInterpolatedKeys(key, iterator, interpolatedKeys);
 }
diff --git a/packages/core/src/helpers/is-used-for-primary-key.ts b/packages/core/src/helpers/is-used-for-primary-key.ts
new file mode 100644
index 0000000..3498808
--- /dev/null
+++ b/packages/core/src/helpers/is-used-for-primary-key.ts
@@ -0,0 +1,42 @@
+import {IsCompositePrimaryKey, PrimaryKey} from '@typedorm/common';
+import {DynamoEntitySchemaPrimaryKey} from '../classes/metadata/entity-metadata';
+import {getInterpolatedKeys} from './get-interpolated-keys';
+
+const IsRawPrimaryKey = (key: unknown): key is PrimaryKey => {
+  return !!(key as PrimaryKey).partitionKey;
+};
+
+export function isUsedForPrimaryKey(
+  primaryKey: DynamoEntitySchemaPrimaryKey | PrimaryKey,
+  attributeName: string
+) {
+  let primaryKeyInterpolations = [] as string[];
+
+  // if raw primary key was provided, resolve all the interpolations for it
+  if (IsRawPrimaryKey(primaryKey)) {
+    primaryKeyInterpolations = [
+      ...primaryKeyInterpolations,
+      ...getInterpolatedKeys(primaryKey.partitionKey),
+    ];
+
+    if (IsCompositePrimaryKey(primaryKey)) {
+      primaryKeyInterpolations = [
+        ...primaryKeyInterpolations,
+        ...getInterpolatedKeys(primaryKey.sortKey),
+      ];
+    }
+    // when parsed primary key is provided, flatten all interpolations to an array
+  } else {
+    const interpolationsToFlatten = primaryKey._interpolations ?? {};
+    primaryKeyInterpolations = [
+      ...primaryKeyInterpolations,
+      ...Object.keys(interpolationsToFlatten).flatMap(key => {
+        const currInterpolation = interpolationsToFlatten[key];
+        return currInterpolation;
+      }),
+    ];
+  }
+
+  // check if there is a matching attribute name
+  return primaryKeyInterpolations.includes(attributeName);
+}
diff --git a/packages/core/src/helpers/validate-key.ts b/packages/core/src/helpers/validate-key.ts
index 63af1f4..a2568a5 100644
--- a/packages/core/src/helpers/validate-key.ts
+++ b/packages/core/src/helpers/validate-key.ts
@@ -29,9 +29,13 @@ function validateMatch(
     );
   }
 
-  if (variableType !== 'String' && variableType !== 'Number') {
+  if (
+    variableType !== 'String' &&
+    variableType !== 'Number' &&
+    variableType !== 'Boolean'
+  ) {
     throw new Error(
-      `"${variable}" is used in key ${key}, thus it's type must be a "Number" or a "String"`
+      `"${variable}" is used in key ${key}, thus it's type must be or scalar type, if attribute type is Enum, please set "isEnum" to true in attribute decorator.`
     );
   }
 


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 ef63d32..cc05aee 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
@@ -1,6 +1,12 @@
 import {UserUniqueEmail} from './../../../../__mocks__/user-unique-email';
 import {table} from './../../../../__mocks__/table';
-import {Entity, Table, Attribute} from '@typedorm/common';
+import {
+  Entity,
+  Table,
+  Attribute,
+  AutoGenerateAttribute,
+  AUTO_GENERATE_ATTRIBUTE_STRATEGY,
+} from '@typedorm/common';
 import {UserAutoGenerateAttributes} from '../../../../__mocks__/user-auto-generate-attributes';
 import {User} from '../../../../__mocks__/user';
 import {AttributesMetadataBuilder} from '../attribute-metadata-builder';
@@ -14,7 +20,7 @@ beforeEach(() => {
 
 test('builds simple attribute metadata', () => {
   const metadata = attributesMetadataBuilder
-    .build(table, User)
+    .build(table, User, User)
     .map(obj => Object.assign({}, obj));
 
   expect(metadata).toEqual([
@@ -46,6 +52,13 @@ test('builds simple attribute metadata', () => {
 });
 
 test('builds attribute metadata for inherited entity', () => {
+  @Entity({
+    name: 'admin',
+    primaryKey: {
+      partitionKey: 'ADMIN#{{id}}',
+      sortKey: 'ADMIN#{{id}}',
+    },
+  })
   class Admin extends User {}
 
   const metadata = attributesMetadataBuilder
@@ -82,7 +95,7 @@ test('builds attribute metadata for inherited entity', () => {
 
 test('builds multi type attribute metadata', () => {
   const metadata = attributesMetadataBuilder
-    .build(table, UserAutoGenerateAttributes)
+    .build(table, UserAutoGenerateAttributes, UserAutoGenerateAttributes)
     .map(obj => Object.assign({}, obj));
 
   expect(metadata).toEqual([
@@ -124,7 +137,7 @@ test('builds metadata for attribute with explicit entity', () => {
   }
 
   const metadata = attributesMetadataBuilder
-    .build(table, Admin)
+    .build(table, Admin, Admin)
     .map(obj => Object.assign({}, obj));
 
   expect(metadata).toEqual([
@@ -147,7 +160,7 @@ test('builds metadata for attribute with explicit entity', () => {
 
 test('builds metadata with implicit primary key for unique attribute', () => {
   const metadata = attributesMetadataBuilder
-    .build(table, UserUniqueEmail)
+    .build(table, UserUniqueEmail, UserUniqueEmail)
     .map(obj => Object.assign({}, obj));
 
   expect(metadata).toEqual([
@@ -185,3 +198,30 @@ test('builds metadata with implicit primary key for unique attribute', () => {
     },
   ]);
 });
+
+test('throws an error when attribute referenced in primary key is also marked as "autoUpdate"', () => {
+  @Entity({
+    name: 'admin',
+    table,
+    primaryKey: {
+      partitionKey: 'ADMIN#{{adminId}}',
+      sortKey: 'ADMIN#{{adminId}}',
+    },
+  })
+  class Admin extends User {
+    @AutoGenerateAttribute({
+      strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4,
+      autoUpdate: true,
+    })
+    adminId: string;
+  }
+
+  const metadata = () =>
+    attributesMetadataBuilder
+      .build(table, Admin, User)
+      .map(obj => Object.assign({}, obj));
+
+  expect(metadata).toThrow(
+    'Failed to build metadata for "adminId", attributes referenced in primary key cannot be auto updated.'
+  );
+});
diff --git a/packages/core/src/classes/connection/__test__/entity-metadata-builder.spec.ts b/packages/core/src/classes/connection/__test__/entity-metadata-builder.spec.ts
index 0868578..ec9fe4f 100644
--- a/packages/core/src/classes/connection/__test__/entity-metadata-builder.spec.ts
+++ b/packages/core/src/classes/connection/__test__/entity-metadata-builder.spec.ts
@@ -169,12 +169,6 @@ test('overrides property of base class if it is defined again on derived class w
 
   const [entityMetadata] = metadataBuilder.build([Customer]);
   expect(entityMetadata.attributes.map(obj => Object.assign({}, obj))).toEqual([
-    {
-      name: 'email',
-      type: 'String',
-      table,
-      entityClass: Customer,
-    },
     {
       name: 'password',
       type: 'String',
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 ef594e8..a1908a6 100644
--- a/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
+++ b/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
@@ -11,17 +11,20 @@ import {
   UserAutoGenerateAttributesPrimaryKey,
   UserAutoGenerateAttributes,
 } from '../../../../__mocks__/user-auto-generate-attributes';
+import {Connection} from '../../connection/connection';
 
 let manager: EntityManager;
+let connection: Connection;
 const dcMock = {
   put: jest.fn(),
   get: jest.fn(),
   update: jest.fn(),
   delete: jest.fn(),
   query: jest.fn(),
+  transactWrite: jest.fn(),
 };
 beforeEach(() => {
-  const connection = createTestConnection({
+  connection = createTestConnection({
     entities: [User, UserUniqueEmail, UserAutoGenerateAttributes],
     documentClient: dcMock,
   });
@@ -60,9 +63,11 @@ test('creates entity', async () => {
       status: 'active',
     },
     TableName: 'test-table',
-    ConditionExpression: 'attribute_not_exists(#CE_PK)',
+    ConditionExpression:
+      'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
     ExpressionAttributeNames: {
       '#CE_PK': 'PK',
+      '#CE_SK': 'SK',
     },
   });
   expect(userEntity).toEqual({
@@ -96,9 +101,11 @@ test('creates entity and returns all attributes, including auto generated ones',
       updatedAt: 1606896235,
     },
     TableName: 'test-table',
-    ConditionExpression: 'attribute_not_exists(#CE_PK)',
+    ConditionExpression:
+      'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
     ExpressionAttributeNames: {
       '#CE_PK': 'PK',
+      '#CE_SK': 'SK',
     },
   });
   expect(userEntity).toEqual({
@@ -224,7 +231,7 @@ test('throws an error if trying to perform exists check with partial primary key
 /**
  * @group update
  */
-test('updates item', async () => {
+test('updates item and return all new attributes', async () => {
   dcMock.update.mockReturnValue({
     promise: () => ({
       Attributes: {
@@ -233,7 +240,7 @@ test('updates item', async () => {
         GSI1PK: 'USER#STATUS#active',
         GSI1SK: 'USER#Me',
         id: '1',
-        name: 'Me',
+        name: 'user',
         status: 'active',
       },
     }),
@@ -269,7 +276,7 @@ test('updates item', async () => {
     UpdateExpression:
       'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2, #attr3 = :val3',
   });
-  expect(updatedItem).toEqual({id: '1', name: 'Me', status: 'active'});
+  expect(updatedItem).toEqual({id: '1', name: 'user', status: 'active'});
 });
 
 test('updates item and attributes marked to be autoUpdated', async () => {
@@ -321,6 +328,116 @@ test('updates item and attributes marked to be autoUpdated', async () => {
   expect(updatedItem).toEqual({id: '1', name: 'Me', status: 'active'});
 });
 
+test('updates item with unique attributes and returns all updated attributes', async () => {
+  manager.findOne = jest
+    .fn()
+    // mock first call to return existing item, this will be called before update is performed
+    .mockImplementationOnce(() => ({
+      id: '1',
+      email: 'old@email.com',
+      status: 'active',
+    }))
+    // mock send call to return new updated item, this will be called after update is performed
+    .mockImplementationOnce(() => ({
+      id: '1',
+      email: 'new@email.com',
+      status: 'active',
+    }));
+
+  const updateOperationSpy = dcMock.transactWrite.mockReturnValue({
+    on: jest.fn(),
+    send: jest.fn().mockImplementation(cb => {
+      cb(null, {
+        ConsumedCapacity: [{}],
+        ItemCollectionMetrics: [{}],
+      });
+    }),
+  });
+
+  const updatedItem = await manager.update<
+    UserUniqueEmailPrimaryKey,
+    UserUniqueEmail
+  >(
+    UserUniqueEmail,
+    {
+      id: '1',
+    },
+    {
+      email: 'new@examil.com',
+    }
+  );
+
+  expect(updateOperationSpy).toHaveBeenCalledTimes(1);
+  expect(updateOperationSpy).toHaveBeenCalledWith({
+    TransactItems: [
+      {
+        Update: {
+          ExpressionAttributeNames: {
+            '#attr0': 'email',
+          },
+          ExpressionAttributeValues: {
+            ':val0': 'new@examil.com',
+          },
+          Key: {
+            PK: 'USER#1',
+            SK: 'USER#1',
+          },
+          TableName: 'test-table',
+          UpdateExpression: 'SET #attr0 = :val0',
+        },
+      },
+      {
+        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@examil.com',
+            SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#new@examil.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',
+        },
+      },
+    ],
+  });
+  expect(updatedItem).toEqual({
+    id: '1',
+    email: 'new@email.com',
+    status: 'active',
+  });
+});
+
+test('does not update an item when failed to get item by key', async () => {
+  manager.findOne = jest.fn();
+
+  const updatedItem = async () =>
+    await manager.update<UserUniqueEmailPrimaryKey, UserUniqueEmail>(
+      UserUniqueEmail,
+      {
+        id: '1',
+      },
+      {
+        email: 'new@examil.com',
+      }
+    );
+
+  await expect(updatedItem).rejects.toThrow(
+    'Failed to update entity, could not find entity with primary key "{"id":"1"}"'
+  );
+});
+
 /**
  * @group delete
  */
@@ -355,6 +472,58 @@ test('throws an error when trying to delete item by non primary key attributes',
   ).rejects.toThrowError('Could not resolve "id" from given dictionary');
 });
 
+test('deletes an item with unique attributes', async () => {
+  manager.findOne = jest.fn().mockReturnValue({
+    id: '1',
+    email: 'old@email.com',
+    status: 'active',
+  });
+
+  const deleteItemOperation = dcMock.transactWrite.mockReturnValue({
+    on: jest.fn(),
+    send: jest.fn().mockImplementation(cb => {
+      cb(null, {
+        ConsumedCapacity: [{}],
+        ItemCollectionMetrics: [{}],
+      });
+    }),
+  });
+
+  const deletedResponse = await manager.delete<
+    UserUniqueEmailPrimaryKey,
+    UserUniqueEmail
+  >(UserUniqueEmail, {
+    id: '1',
+  });
+
+  expect(deleteItemOperation).toHaveBeenCalledTimes(1);
+  expect(deleteItemOperation).toHaveBeenCalledWith({
+    TransactItems: [
+      {
+        Delete: {
+          Key: {
+            PK: 'USER#1',
+            SK: 'USER#1',
+          },
+          TableName: 'test-table',
+        },
+      },
+      {
+        Delete: {
+          Key: {
+            PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#old@email.com',
+            SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#old@email.com',
+          },
+          TableName: 'test-table',
+        },
+      },
+    ],
+  });
+  expect(deletedResponse).toEqual({
+    success: true,
+  });
+});
+
 /**
  * @group find
  */
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 b661bf9..589bbcb 100644
--- a/packages/core/src/classes/manager/__test__/transaction-manager.spec.ts
+++ b/packages/core/src/classes/manager/__test__/transaction-manager.spec.ts
@@ -4,6 +4,10 @@ import {Connection} from '../../connection/connection';
 import {WriteTransaction} from '../../transaction/write-transaction';
 import {TransactionManager} from '../transaction-manager';
 import {User} from '../../../../__mocks__/user';
+import {
+  UserUniqueEmail,
+  UserUniqueEmailPrimaryKey,
+} from '@typedorm/core/__mocks__/user-unique-email';
 
 let manager: TransactionManager;
 const dcMock = {
@@ -13,7 +17,7 @@ const dcMock = {
 let connection: Connection;
 beforeEach(() => {
   connection = createTestConnection({
-    entities: [User],
+    entities: [User, UserUniqueEmail],
     documentClient: dcMock,
   });
   manager = new TransactionManager(connection);
@@ -26,7 +30,7 @@ afterEach(() => {
 /**
  * @group write
  */
-test('performs write transactions', async () => {
+test('performs write transactions for simple writes', async () => {
   dcMock.transactWrite.mockReturnValue({
     promise: jest.fn().mockReturnValue({
       ConsumedCapacity: [{}],
@@ -92,9 +96,11 @@ test('performs write transactions', async () => {
             status: 'inactive',
           },
           TableName: 'test-table',
-          ConditionExpression: 'attribute_not_exists(#CE_PK)',
+          ConditionExpression:
+            'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
           ExpressionAttributeNames: {
             '#CE_PK': 'PK',
+            '#CE_SK': 'SK',
           },
         },
       },
@@ -112,7 +118,6 @@ test('performs write transactions', async () => {
             PK: 'USER#1',
             SK: 'USER#1',
           },
-          ReturnValues: 'ALL_NEW',
           TableName: 'test-table',
           UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
         },
@@ -130,9 +135,11 @@ test('performs write transactions', async () => {
             status: 'inactive',
           },
           TableName: 'test-table',
-          ConditionExpression: 'attribute_not_exists(#CE_PK)',
+          ConditionExpression:
+            'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
           ExpressionAttributeNames: {
             '#CE_PK': 'PK',
+            '#CE_SK': 'SK',
           },
         },
       },
@@ -143,3 +150,248 @@ test('performs write transactions', async () => {
     ItemCollectionMetrics: [{}],
   });
 });
+
+test('performs write transactions for entities with unique attributes ', async () => {
+  connection.entityManager.findOne = jest.fn().mockReturnValue({
+    id: '1',
+    email: 'olduser@example.com',
+  });
+
+  dcMock.transactWrite.mockReturnValue({
+    on: jest.fn(),
+    send: jest.fn().mockImplementation(cb => {
+      cb(null, {
+        ConsumedCapacity: [{}],
+        ItemCollectionMetrics: [{}],
+      });
+    }),
+  });
+
+  const transaction = new WriteTransaction(connection).chian<
+    UserUniqueEmailPrimaryKey,
+    UserUniqueEmail
+  >({
+    update: {
+      item: UserUniqueEmail,
+      primaryKey: {
+        id: '1',
+      },
+      body: {
+        email: 'new@example.com',
+        status: 'active',
+      },
+    },
+  });
+
+  await manager.write(transaction);
+
+  expect(dcMock.transactWrite).toHaveBeenCalledTimes(1);
+  expect(dcMock.transactWrite).toHaveBeenCalledWith({
+    TransactItems: [
+      {
+        Update: {
+          ExpressionAttributeNames: {
+            '#attr0': 'email',
+            '#attr1': 'status',
+            '#attr2': 'GSI1PK',
+          },
+          ExpressionAttributeValues: {
+            ':val0': 'new@example.com',
+            ':val1': 'active',
+            ':val2': 'USER#STATUS#active',
+          },
+          Key: {
+            PK: 'USER#1',
+            SK: 'USER#1',
+          },
+          TableName: 'test-table',
+          UpdateExpression:
+            'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2',
+        },
+      },
+      {
+        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@example.com',
+            SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#new@example.com',
+          },
+          TableName: 'test-table',
+        },
+      },
+      {
+        Delete: {
+          Key: {
+            PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#olduser@example.com',
+            SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#olduser@example.com',
+          },
+          TableName: 'test-table',
+        },
+      },
+    ],
+  });
+});
+
+test('performs write transactions for entities with non existing unique attributes ', async () => {
+  connection.entityManager.findOne = jest.fn().mockReturnValue({
+    id: '1',
+    status: 'inactive',
+  });
+
+  dcMock.transactWrite.mockReturnValue({
+    on: jest.fn(),
+    send: jest.fn().mockImplementation(cb => {
+      cb(null, {
+        ConsumedCapacity: [{}],
+        ItemCollectionMetrics: [{}],
+      });
+    }),
+  });
+
+  const transaction = new WriteTransaction(connection).chian<
+    UserUniqueEmailPrimaryKey,
+    UserUniqueEmail
+  >({
+    update: {
+      item: UserUniqueEmail,
+      primaryKey: {
+        id: '1',
+      },
+      body: {
+        email: 'new@example.com',
+        status: 'active',
+      },
+    },
+  });
+
+  await manager.write(transaction);
+
+  expect(dcMock.transactWrite).toHaveBeenCalledTimes(1);
+  expect(dcMock.transactWrite).toHaveBeenCalledWith({
+    TransactItems: [
+      {
+        Update: {
+          ExpressionAttributeNames: {
+            '#attr0': 'email',
+            '#attr1': 'status',
+            '#attr2': 'GSI1PK',
+          },
+          ExpressionAttributeValues: {
+            ':val0': 'new@example.com',
+            ':val1': 'active',
+            ':val2': 'USER#STATUS#active',
+          },
+          Key: {
+            PK: 'USER#1',
+            SK: 'USER#1',
+          },
+          TableName: 'test-table',
+          UpdateExpression:
+            'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2',
+        },
+      },
+      {
+        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@example.com',
+            SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#new@example.com',
+          },
+          TableName: 'test-table',
+        },
+      },
+    ],
+  });
+});
+
+test('throws an error if no existing item found', async () => {
+  connection.entityManager.findOne = jest.fn();
+
+  const transaction = new WriteTransaction(connection).chian<
+    UserUniqueEmailPrimaryKey,
+    UserUniqueEmail
+  >({
+    update: {
+      item: UserUniqueEmail,
+      primaryKey: {
+        id: '1',
+      },
+      body: {
+        email: 'new@example.com',
+        status: 'active',
+      },
+    },
+  });
+
+  const update = () => manager.write(transaction);
+
+  expect(dcMock.transactWrite).not.toHaveBeenCalled();
+  await expect(update).rejects.toThrow(
+    'Failed to process entity "UserUniqueEmail", could not find entity with primary key "{"id":"1"}"'
+  );
+});
+
+test('performs write transactions when removing entities with unique attributes ', async () => {
+  connection.entityManager.findOne = jest.fn().mockReturnValue({
+    id: '1',
+    email: 'olduser@example.com',
+  });
+
+  dcMock.transactWrite.mockReturnValue({
+    on: jest.fn(),
+    send: jest.fn().mockImplementation(cb => {
+      cb(null, {
+        ConsumedCapacity: [{}],
+        ItemCollectionMetrics: [{}],
+      });
+    }),
+  });
+
+  const transaction = new WriteTransaction(connection).chian<
+    UserUniqueEmailPrimaryKey,
+    UserUniqueEmail
+  >({
+    delete: {
+      item: UserUniqueEmail,
+      primaryKey: {
+        id: '1',
+      },
+    },
+  });
+
+  await manager.write(transaction);
+
+  expect(dcMock.transactWrite).toHaveBeenCalledTimes(1);
+  expect(dcMock.transactWrite).toHaveBeenCalledWith({
+    TransactItems: [
+      {
+        Delete: {
+          Key: {
+            PK: 'USER#1',
+            SK: 'USER#1',
+          },
+          TableName: 'test-table',
+        },
+      },
+      {
+        Delete: {
+          Key: {
+            PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#olduser@example.com',
+            SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#olduser@example.com',
+          },
+          TableName: 'test-table',
+        },
+      },
+    ],
+  });
+});
diff --git a/packages/core/src/classes/transaction/__test__/write-transaction.spec.ts b/packages/core/src/classes/transaction/__test__/write-transaction.spec.ts
index f0d8164..b1f4699 100644
--- a/packages/core/src/classes/transaction/__test__/write-transaction.spec.ts
+++ b/packages/core/src/classes/transaction/__test__/write-transaction.spec.ts
@@ -75,9 +75,11 @@ test('chains simple transaction requests', () => {
           status: 'active',
         },
         TableName: 'test-table',
-        ConditionExpression: 'attribute_not_exists(#CE_PK)',
+        ConditionExpression:
+          'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
         ExpressionAttributeNames: {
           '#CE_PK': 'PK',
+          '#CE_SK': 'SK',
         },
       },
     },
@@ -89,7 +91,6 @@ test('chains simple transaction requests', () => {
           ':val1': 'USER#new name',
         },
         Key: {PK: 'USER#123', SK: 'USER#123'},
-        ReturnValues: 'ALL_NEW',
         TableName: 'test-table',
         UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
       },
@@ -110,9 +111,9 @@ test('chains complex transaction requests', () => {
         item: user,
       },
     })
-    .chian<UserPrimaryKey, User>({
+    .chian<UserPrimaryKey, UserUniqueEmail>({
       update: {
-        item: User,
+        item: UserUniqueEmail,
         primaryKey: {
           id: '123',
         },
@@ -122,12 +123,14 @@ test('chains complex transaction requests', () => {
       },
     });
 
-  expect(transaction.items).toEqual([
+  expect(transaction.items).toMatchObject([
     {
       Put: {
-        ConditionExpression: 'attribute_not_exists(#CE_PK)',
+        ConditionExpression:
+          'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
         ExpressionAttributeNames: {
           '#CE_PK': 'PK',
+          '#CE_SK': 'SK',
         },
         Item: {
           PK: 'USER#1',
@@ -145,9 +148,11 @@ test('chains complex transaction requests', () => {
     },
     {
       Put: {
-        ConditionExpression: 'attribute_not_exists(#CE_PK)',
+        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#user@example.com',
@@ -157,20 +162,9 @@ test('chains complex transaction requests', () => {
       },
     },
     {
-      Update: {
-        ExpressionAttributeNames: {
-          '#attr0': 'email',
-        },
-        ExpressionAttributeValues: {
-          ':val0': 'example@email.com',
-        },
-        Key: {
-          PK: 'USER#123',
-          SK: 'USER#123',
-        },
-        ReturnValues: 'ALL_NEW',
-        TableName: 'test-table',
-        UpdateExpression: 'SET #attr0 = :val0',
+      entityClass: UserUniqueEmail,
+      primaryKeyAttributes: {
+        id: '123',
       },
     },
   ]);
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 617990d..4b65147 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
@@ -5,11 +5,15 @@ import {User, UserGSI1} from '../../../../__mocks__/user';
 import {createTestConnection, resetTestConnection} from '@typedorm/testing';
 import {UserPrimaryKey} from '../../../../__mocks__/user';
 import {DocumentClientRequestTransformer} from '../document-client-request-transformer';
+import {
+  UserUniqueEmail,
+  UserUniqueEmailPrimaryKey,
+} from '../../../../__mocks__/user-unique-email';
 
 let transformer: DocumentClientRequestTransformer;
 beforeEach(async () => {
   const connection = createTestConnection({
-    entities: [User, Customer],
+    entities: [User, Customer, UserUniqueEmail],
   });
   transformer = new DocumentClientRequestTransformer(connection);
 });
@@ -69,9 +73,11 @@ test('transforms put item requests', () => {
       __en: 'user',
       status: 'active',
     },
-    ConditionExpression: 'attribute_not_exists(#CE_PK)',
+    ConditionExpression:
+      'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
     ExpressionAttributeNames: {
       '#CE_PK': 'PK',
+      '#CE_SK': 'SK',
     },
     TableName: 'test-table',
   });
@@ -98,37 +104,43 @@ test('transforms put item request with unique attributes', () => {
     email: string;
   }
 
-  const connection = createTestConnection({
+  const newConnection = createTestConnection({
     entities: [UserUniqueEmail],
   });
-  transformer = new DocumentClientRequestTransformer(connection);
+  const newTransformer = new DocumentClientRequestTransformer(newConnection);
 
   const user = new UserUniqueEmail();
   user.id = '1';
   user.email = 'user@example.com';
 
-  const putItem = transformer.toDynamoPutItem(user);
+  const putItem = newTransformer.toDynamoPutItem(user);
   expect(putItem).toEqual([
     {
-      ConditionExpression: 'attribute_not_exists(#CE_PK)',
-      ExpressionAttributeNames: {'#CE_PK': 'PK'},
-      Item: {
-        PK: 'USER#1',
-        SK: 'USER#1',
-        email: 'user@example.com',
-        id: '1',
-        __en: 'user',
+      Put: {
+        ConditionExpression:
+          'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
+        ExpressionAttributeNames: {'#CE_PK': 'PK', '#CE_SK': 'SK'},
+        Item: {
+          PK: 'USER#1',
+          SK: 'USER#1',
+          email: 'user@example.com',
+          id: '1',
+          __en: 'user',
+        },
+        TableName: 'test-table',
       },
-      TableName: 'test-table',
     },
     {
-      ConditionExpression: 'attribute_not_exists(#CE_PK)',
-      ExpressionAttributeNames: {'#CE_PK': 'PK'},
-      Item: {
-        PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#user@example.com',
-        SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#user@example.com',
+      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#user@example.com',
+          SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#user@example.com',
+        },
+        TableName: 'test-table',
       },
-      TableName: 'test-table',
     },
   ]);
 });
@@ -157,37 +169,43 @@ test('transforms put item request consisting unique attributes with provided pri
     email: string;
   }
 
-  const connection = createTestConnection({
+  const newConnection = createTestConnection({
     entities: [UserUniqueEmail],
   });
-  transformer = new DocumentClientRequestTransformer(connection);
+  const newTransformer = new DocumentClientRequestTransformer(newConnection);
 
   const user = new UserUniqueEmail();
   user.id = '1';
   user.email = 'user@example.com';
 
-  const putItem = transformer.toDynamoPutItem(user);
+  const putItem = newTransformer.toDynamoPutItem(user);
   expect(putItem).toEqual([
     {
-      ConditionExpression: 'attribute_not_exists(#CE_PK)',
-      ExpressionAttributeNames: {'#CE_PK': 'PK'},
-      Item: {
-        PK: 'USER#1',
-        SK: 'USER#1',
-        email: 'user@example.com',
-        id: '1',
-        __en: 'user',
+      Put: {
+        ConditionExpression:
+          'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
+        ExpressionAttributeNames: {'#CE_PK': 'PK', '#CE_SK': 'SK'},
+        Item: {
+          PK: 'USER#1',
+          SK: 'USER#1',
+          email: 'user@example.com',
+          id: '1',
+          __en: 'user',
+        },
+        TableName: 'test-table',
       },
-      TableName: 'test-table',
     },
     {
-      ConditionExpression: 'attribute_not_exists(#CE_PK)',
-      ExpressionAttributeNames: {'#CE_PK': 'PK'},
-      Item: {
-        PK: 'CUSTOM#user@example.com',
-        SK: 'CUSTOM#user@example.com',
+      Put: {
+        ConditionExpression:
+          'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
+        ExpressionAttributeNames: {'#CE_PK': 'PK', '#CE_SK': 'SK'},
+        Item: {
+          PK: 'CUSTOM#user@example.com',
+          SK: 'CUSTOM#user@example.com',
+        },
+        TableName: 'test-table',
       },
-      TableName: 'test-table',
     },
   ]);
 });
@@ -224,6 +242,71 @@ test('transforms update item request', () => {
   });
 });
 
+test('transforms update item record with unique attributes', () => {
+  const updatedItem = transformer.toDynamoUpdateItem<
+    UserPrimaryKey,
+    UserUniqueEmail
+  >(
+    UserUniqueEmail,
+    {
+      id: '1',
+    },
+    {
+      name: 'new name',
+      email: 'new@email.com',
+    }
+  );
+
+  const lazyWriteItemListLoader = (updatedItem as any)
+    .lazyLoadTransactionWriteItems;
+  expect(typeof lazyWriteItemListLoader).toEqual('function');
+
+  const writeItemList = lazyWriteItemListLoader({
+    name: 'new name',
+    email: 'old@email.com',
+  });
+  expect(writeItemList).toEqual([
+    {
+      Update: {
+        ExpressionAttributeNames: {
+          '#attr0': 'name',
+          '#attr1': 'email',
+          '#attr2': 'GSI1SK',
+        },
+        ExpressionAttributeValues: {
+          ':val0': 'new name',
+          ':val1': 'new@email.com',
+          ':val2': 'USER#new name',
+        },
+        Key: {PK: 'USER#1', SK: 'USER#1'},
+        TableName: 'test-table',
+        UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2',
+      },
+    },
+    {
+      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',
+      },
+    },
+  ]);
+});
+
 /**
  * @group toDynamoDeleteItem
  */
@@ -243,6 +326,53 @@ test('transforms delete item request', () => {
   });
 });
 
+test('transforms delete item request with unique attributes', () => {
+  const deleteItemInput = transformer.toDynamoDeleteItem<
+    UserUniqueEmailPrimaryKey,
+    UserUniqueEmail
+  >(UserUniqueEmail, {
+    id: '1',
+  });
+  expect(deleteItemInput).toMatchObject({
+    entityClass: UserUniqueEmail,
+    primaryKeyAttributes: {
+      id: '1',
+    },
+  });
+
+  const lazyWriteItemListLoader = (deleteItemInput as any)
+    .lazyLoadTransactionWriteItems;
+
+  expect(typeof lazyWriteItemListLoader).toEqual('function');
+
+  const deleteItemList = lazyWriteItemListLoader({
+    id: '1',
+    name: 'new name',
+    email: 'old@email.com',
+  });
+
+  expect(deleteItemList).toEqual([
+    {
+      Delete: {
+        TableName: 'test-table',
+        Key: {
+          PK: 'USER#1',
+          SK: 'USER#1',
+        },
+      },
+    },
+    {
+      Delete: {
+        TableName: 'test-table',
+        Key: {
+          PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#old@email.com',
+          SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#old@email.com',
+        },
+      },
+    },
+  ]);
+});
+
 /**
  * @group toDynamoQueryItem
  */
diff --git a/packages/core/src/classes/transformer/__test__/expression-builder.spec.ts b/packages/core/src/classes/transformer/__test__/expression-builder.spec.ts
new file mode 100644
index 0000000..469ef53
--- /dev/null
+++ b/packages/core/src/classes/transformer/__test__/expression-builder.spec.ts
@@ -0,0 +1,37 @@
+import {Table} from '@typedorm/common';
+import {ExpressionBuilder} from '../../expression-builder';
+
+test('builds unique record expression for table with simple key', () => {
+  const simpleTable = new Table({
+    name: 'simple-primary-key-table',
+    partitionKey: 'PK',
+  });
+  const expression = new ExpressionBuilder().buildUniqueRecordConditionExpression(
+    simpleTable
+  );
+  expect(expression).toEqual({
+    ConditionExpression: 'attribute_not_exists(#CE_PK)',
+    ExpressionAttributeNames: {
+      '#CE_PK': 'PK',
+    },
+  });
+});
+
+test('builds unique record expression for table with composite key', () => {
+  const simpleTable = new Table({
+    name: 'simple-primary-key-table',
+    partitionKey: 'PK',
+    sortKey: 'SK',
+  });
+  const expression = new ExpressionBuilder().buildUniqueRecordConditionExpression(
+    simpleTable
+  );
+  expect(expression).toEqual({
+    ConditionExpression:
+      'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
+    ExpressionAttributeNames: {
+      '#CE_PK': 'PK',
+      '#CE_SK': 'SK',
+    },
+  });
+});
diff --git a/packages/core/src/helpers/__test__/drop-prop.spec.ts b/packages/core/src/helpers/__test__/drop-prop.spec.ts
new file mode 100644
index 0000000..df80ddc
--- /dev/null
+++ b/packages/core/src/helpers/__test__/drop-prop.spec.ts
@@ -0,0 +1,30 @@
+import {dropProp} from '../drop-prop';
+
+test('drops given prop from object', () => {
+  const obj = {
+    a: '1',
+    b: '2',
+    c: '3',
+  };
+
+  const updated = dropProp(obj, 'a');
+  expect(updated).toEqual({
+    b: '2',
+    c: '3',
+  });
+});
+
+test('does drops any prop from object if no matches', () => {
+  const obj = {
+    a: '1',
+    b: '2',
+    c: '3',
+  };
+
+  const updated = dropProp(obj, 'd' as any);
+  expect(updated).toEqual({
+    a: '1',
+    b: '2',
+    c: '3',
+  });
+});
diff --git a/packages/core/src/helpers/__test__/is-used-for-primary-key.spec.ts b/packages/core/src/helpers/__test__/is-used-for-primary-key.spec.ts
new file mode 100644
index 0000000..39b0fb8
--- /dev/null
+++ b/packages/core/src/helpers/__test__/is-used-for-primary-key.spec.ts
@@ -0,0 +1,45 @@
+import {isUsedForPrimaryKey} from '../is-used-for-primary-key';
+
+test('checks if given attribute is referenced in raw primary key', () => {
+  const isNotUsed = isUsedForPrimaryKey(
+    {
+      partitionKey: 'USER#{{id}}#NAME#{{name}}:something',
+    },
+    'role'
+  );
+  expect(isNotUsed).toBeFalsy();
+
+  const isUsed = isUsedForPrimaryKey(
+    {
+      partitionKey: 'USER#{{id}}#NAME#{{name}}:something',
+    },
+    'name'
+  );
+  expect(isUsed).toBeTruthy();
+});
+
+test('checks if given attribute is referenced in formatted primary key', () => {
+  const isNotUsed = isUsedForPrimaryKey(
+    {
+      PK: 'USER#{{id}}#NAME#{{name}}:something',
+      _interpolations: {
+        PK: ['id', 'name'],
+      },
+    },
+    'role'
+  );
+  expect(isNotUsed).toBeFalsy();
+
+  const isUsed = isUsedForPrimaryKey(
+    {
+      PK: 'USER#{{id}}#NAME#{{name}}:something',
+      SK: 'USER#{{id}}#ROLE#{{role}}',
+      _interpolations: {
+        PK: ['id', 'name'],
+        SK: ['id', 'role'],
+      },
+    },
+    'role'
+  );
+  expect(isUsed).toBeTruthy();
+});
diff --git a/packages/core/src/helpers/__test__/validate-key.spec.ts b/packages/core/src/helpers/__test__/validate-key.spec.ts
index c77b122..c88dd75 100644
--- a/packages/core/src/helpers/__test__/validate-key.spec.ts
+++ b/packages/core/src/helpers/__test__/validate-key.spec.ts
@@ -25,7 +25,7 @@ describe('validateKey()', () => {
         status: 'Object',
       })
     ).toThrowError(
-      '"status" is used in key USER#{{id}}#STATUS#{{status}}, thus it\'s type must be a "Number" or a "String"'
+      '"status" is used in key USER#{{id}}#STATUS#{{status}}, thus it\'s type must be or scalar type, if attribute type is Enum, please set "isEnum" to true in attribute decorator.'
     );
   });
 });