Repository: typedorm/typedorm
Pull request #6 (2020-11-26 07:36:32)

Import typedorm from private repo
# Changes
- Import DRM internal package as typeDORM
- fix lint/build errors
- init release packages for core/common/testing




Patch:
diff --git a/.eslintrc.json b/.eslintrc.json
index f95bb33..03723ee 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,3 +1,11 @@
 {
-  "extends": "./node_modules/gts/"
+  "extends": "./node_modules/gts/",
+  "rules": {
+    "@typescript-eslint/no-explicit-any": [
+      0,
+      {
+        "fixToUnknown": true
+      }
+    ]
+  }
 }
diff --git a/.gitignore b/.gitignore
index 7ffb796..038be77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
 dist
 bazel-out
 node_modules
-*.tgz
\ No newline at end of file
+*.tgz
+coverage
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..c04ed0c
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,50 @@
+{
+  // Use IntelliSense to learn about possible attributes.
+  // Hover to view descriptions of existing attributes.
+  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "Current TS File",
+      "type": "node",
+      "request": "launch",
+      "args": ["${relativeFile}"],
+      "runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
+      "sourceMaps": true,
+      "cwd": "${workspaceRoot}",
+      "protocol": "inspector"
+    },
+    {
+      "type": "node",
+      "timeout": 1000000,
+      "request": "attach",
+      "name": "Attach",
+      "port": 9229,
+      "skipFiles": ["<node_internals>/**"]
+    },
+    {
+      "type": "node",
+      "request": "launch",
+      "name": "Jest Debug Current file",
+      "runtimeVersion": "12.14.1",
+      "runtimeArgs": [
+        "--inspect-brk",
+        "--nolazy",
+        "node_modules/jest/bin/jest",
+        "${fileBasename}",
+        "--runInBand",
+        "--watch",
+        "--config=jest.config.js"
+      ],
+      "sourceMaps": true,
+      "disableOptimisticBPs": true,
+      "cwd": "${workspaceFolder}",
+      "console": "integratedTerminal",
+      "internalConsoleOptions": "neverOpen",
+      "port": 9229,
+      "env": {
+        "NODE_ENV": "test"
+      }
+    }
+  ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 32d5c42..56f832e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,9 +1,12 @@
 {
   "cSpell.words": [
+    "KSUID",
     "bazel",
     "commitlint",
+    "dynamodb",
     "posttest",
     "prettierrc",
+    "subpackages",
     "typedorm",
     "typeorm"
   ],
diff --git a/BUILD.bazel b/BUILD.bazel
index 3a13ccc..311f488 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -6,6 +6,6 @@
 #   so that ts_library rules still use it by default.
 #   See https://www.npmjs.com/package/@bazel/typescript#installation
 exports_files(
-    ["tsconfig.json", "LICENSE"],
+    ["tsconfig.json", "LICENSE", ".npmignore"],
     visibility = ["//packages:__subpackages__"],
 )
diff --git a/README.md b/README.md
index 1172c96..0128f7d 100644
--- a/README.md
+++ b/README.md
@@ -3,4 +3,220 @@
 
 # TypeDORM
 
-Object Relational mapper for DynamoDB, inspired by typeorm
+[![NPM Release](https://img.shields.io/npm/v/@typedorm/common?style=for-the-badge&label=@typedorm/common)](https://www.npmjs.com/package/@typedorm/common)
+[![NPM Release](https://img.shields.io/npm/v/@typedorm/core?style=for-the-badge&label=@typedorm/core)](https://www.npmjs.com/package/@typedorm/core)
+[![NPM Release](https://img.shields.io/npm/v/@typedorm/core?style=for-the-badge&label=@typedorm/testing)](https://www.npmjs.com/package/@typedorm/core)
+
+Object Relational mapper for DynamoDB, inspired by typeorm.
+
+TypeDORM is an ORM built from ground up using typescript and latest javascript features to provide an easy gateway when doing complex highly relational data modeling in dynamoDB. TypeDORM is built with [single-table-design](https://www.youtube.com/watch?v=HaEPXoXVf2k) first in mind, but should work as smoothly with regular entity table <-> design pattern. TypeDORM would have not existed without [TypeORM](https://github.com/typeorm/typeorm) and [dynamodb-toolbox](https://github.com/jeremydaly/dynamodb-toolbox), big shout-out to these projects and their awesome contributors.
+
+TypeDORM borrows decorator based syntax from TypeORM and provides fully type safe ORM to with dynamodb. TypeDORM currently only support [Data Mapper](https://en.wikipedia.org/wiki/Data_mapper_pattern).
+
+## Stability
+
+![Stability](https://img.shields.io/badge/Stability-Beta-orange?style=for-the-badge)
+
+TypeDORM is currency in it's very early stage of development so breaking changes are likely. Consider this before using it in production.
+
+Please submit an issue for any bugs or suggestions [here](https://github.com/typedorm/typedorm/issues), or you can reach out to me on twitter [@whimzy_live](https://mobile.twitter.com/whimzy_live).
+
+## Features
+
+- DataMapper development pattern
+- Full type safety
+- Declarative relational schema
+- Entity manager
+- Transaction manager
+- Multiple connections support (Partial)
+- Code follows all possible best practices when modeling for dynamodb
+- Additional higher level features like, ability to declare non key attribute a unique
+- Auto Generated values fro attributes
+- Auto Update values on update
+- Complex update, key condition and condition expression all made easy to work with (Partial)
+- Typescript and javascript support
+
+And many more to come.
+
+## Getting Started
+
+### Installation
+
+1. Install core and common modules from npm.
+
+   `npm install @typedorm/core @typedorm/common --save`
+
+2. Install aws-sdk for nodejs, TypeDORM uses documentClient to interact with dynamodb.
+
+   `npm install aws-sdk --save`
+
+3. Install `reflect-metadata` shim
+
+   `npm install reflect-metadata --save`
+
+   and import it as the first thing in node entry file as
+
+   `import 'reflect-metadata'`
+
+### Typescript configuration
+
+If you are using TypeDORM with typescript, make sure you also have below options enabled in `tsconfig.json`
+
+```shell
+"emitDecoratorMetadata": true,
+"experimentalDecorators": true,
+```
+
+### Developing with TypeDORM
+
+#### Creating Table
+
+First thing to do when working with TypeDORM is to setup dynamodb table config. Currently this needs to be manually setup and have also have it configured in deployed table instance(s).
+
+This guide shows how to setup single-table-design
+
+`my-table.ts`
+
+```typescript
+import {Table} from '@typedorm/common';
+
+// create table
+
+const myGlobalTable = new Table({
+  name: 'test-table',
+  partitionKey: 'PK',
+  sortKey: 'SK',
+  indexes: {
+    GSI1: {
+      type: INDEX_TYPE.GSI,
+      partitionKey: 'GSI1PK',
+      sortKey: 'GSI1SK',
+    },
+    GSI2: {
+      type: INDEX_TYPE.GSI,
+      partitionKey: 'GSI2PK',
+      sortKey: 'GSI2SK',
+    },
+    LSI1: {
+      type: INDEX_TYPE.LSI,
+      sortKey: 'LSI1SK',
+    },
+  },
+});
+```
+
+**Note:** _These indexes must match exactly to what is created in dynamo table instance hosted._
+
+#### Creating an entity
+
+`organisation.entity.ts`
+
+```typescript
+import {Attribute, Entity, AutoGeneratedAttribute} from '@typedorm/common';
+import {AUTO_GENERATE_ATTRIBUTE_STRATEGY} from '@typedorm/common';
+
+@Entity({
+  name: 'organisation',
+  primaryKey: {
+    partitionKey: 'ORG#{{id}}',
+    sortKey: 'ORG#{{id}}',
+  },
+  indexes: {
+    // specify GSI1 key - "GSI1" named global secondary index needs to exist in above table declaration
+    GSI1: {
+      partitionKey: 'ORG#{{id}}#STATUS#{{status}}',
+      sortKey: 'ORG#{{id}}#ACTIVE#{{active}}',
+      type: INDEX_TYPE.GSI,
+    },
+    // specify LSI1 key
+    LSI: {
+      sortKey: 'TICKETS#UPDATED_AT#{{updatedAt}}'
+      type: INDEX_TYPE.LSI
+    }
+  },
+})
+export class Organisation{
+
+  @AutoGeneratedAttribute({
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4,
+  })
+  id: string;
+
+  @Attribute()
+  name: string;
+
+  @Attribute()
+  status: string;
+
+  @Attribute()
+  active: boolean;
+
+  @AutoGeneratedAttribute({
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4,
+    autoUpdate: true // this will make this attribute and any indexes referencing it auto update for any write operation
+  })
+  updatedAt: number;
+}
+```
+
+#### Initialize default connection
+
+```typescript
+import {createConnection} from '@typedorm/core';
+
+// initialize with specifying list of entities
+createConnection({
+  table: myGlobalTable,
+  entities: [Organisation],
+});
+
+// or initialize with specifying path match for entities
+createConnection({
+  table: myGlobalTable,
+  entities: 'path-to-entities/*.entity.ts',
+});
+```
+
+#### Working with entity manager
+
+```typescript
+import {getEntityManager} from '@typedorm/core';
+
+const org = new Organisation();
+org.name = 'My awesome org';
+org.status = 'onboarding';
+org.active = true;
+
+const entityManger = getEntityManager();
+
+// create item
+const response = await entityManger.create(org);
+
+// get item
+const org = await entityManger.findOne(Organisation, {
+  id: response.id,
+  status: 'onboarding',
+  active: true,
+});
+
+// delete item
+await entityManger.delete(Organisation, {
+  id: response.id,
+  status: 'onboarding',
+  active: true,
+});
+```
+
+## More
+
+DynamoDB is different, different than most other no-sql databases, and therefore data in dynamodb should be stored the way dynamodb expects to get the most benefits out of it. While doing this development experience suffers and all data can become a huge mess very quickly, this is specially true with single-table-design patten. To resolve this, TypeDORM let's declaratively define schema and later takes control from there to provide best development experience possible.
+
+To find out more about how the data looks like when it is stored in dynamo have a look at this detailed guide.
+
+### [Step by step guide](./docs/guide.md)
+
+## Current Limitations
+
+- TypDORM currently is not able to connect with dynamodb table using aws credentials and only works within environment like lambda/ec2 that already has access to dynamo table, but this will be resolved very soon.
+- To keep things simple TypeDORM, at the moment WILL NOT create/update table configuration and must be done separately.
+- There is very minimal support for providing different arguments to queries, and will be resolved soon.
diff --git a/docs/guide.md b/docs/guide.md
new file mode 100644
index 0000000..d3be9c5
--- /dev/null
+++ b/docs/guide.md
@@ -0,0 +1,3 @@
+# Step by step guide
+
+// TODO: add guide
diff --git a/jest-tsconfig-path-mapper.js b/jest-tsconfig-path-mapper.js
new file mode 100644
index 0000000..51d9c37
--- /dev/null
+++ b/jest-tsconfig-path-mapper.js
@@ -0,0 +1,16 @@
+const tsConfig = require('./tsconfig.json');
+module.exports = function () {
+  const paths = tsConfig.compilerOptions.paths;
+
+  return Object.keys(paths).reduce((acc, key) => {
+    const jcPathKey = key.replace(/\*/, '(.*)');
+    const pathValue = paths[key];
+
+    const jcPathValues = pathValue.map(path => {
+      return `<rootDir>/${path}`.replace(/\*/, '$1');
+    });
+
+    acc[jcPathKey] = jcPathValues;
+    return acc;
+  }, {});
+};
diff --git a/jest.config.js b/jest.config.js
index 186f80f..732d455 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,8 +1,193 @@
+// For a detailed explanation regarding each configuration property, visit:
+// https://jestjs.io/docs/en/configuration.html
+const jestTsConfigPathMapper = require('./jest-tsconfig-path-mapper');
+
 module.exports = {
-  testEnvironment: 'node',
-  verbose: true,
+  // All imported modules in your tests should be mocked automatically
+  // automock: false,
+
+  // Stop running tests after `n` failures
+  bail: 1,
+
+  // The directory where Jest should store its cached dependency information
+  // cacheDirectory: "/tmp/jest_rs",
+
+  // Automatically clear mock calls and instances between every test
+  clearMocks: true,
+
+  // Indicates whether the coverage information should be collected while executing the test
+  collectCoverage: true,
+
+  // An array of glob patterns indicating a set of files for which coverage information should be collected
+  // collectCoverageFrom: undefined,
+
+  // The directory where Jest should output its coverage files
+  coverageDirectory: 'coverage',
+
+  // An array of regexp pattern strings used to skip coverage collection
+  // coveragePathIgnorePatterns: [
+  //   "/node_modules/"
+  // ],
+
+  // Indicates which provider should be used to instrument code for coverage
+  // coverageProvider: "babel",
+
+  // A list of reporter names that Jest uses when writing coverage reports
+  // coverageReporters: [
+  //   "json",
+  //   "text",
+  //   "lcov",
+  //   "clover"
+  // ],
+
+  // An object that configures minimum threshold enforcement for coverage results
+  // coverageThreshold: undefined,
+
+  // A path to a custom dependency extractor
+  // dependencyExtractor: undefined,
+
+  // Make calling deprecated APIs throw helpful error messages
+  // errorOnDeprecated: false,
+
+  // Force coverage collection from ignored files using an array of glob patterns
+  // forceCoverageMatch: [],
+
+  // A path to a module which exports an async function that is triggered once before all test suites
+  // globalSetup: undefined,
+
+  // A path to a module which exports an async function that is triggered once after all test suites
+  // globalTeardown: undefined,
+
+  // A set of global variables that need to be available in all test environments
+  // globals: {},
+
+  // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
+  // maxWorkers: "50%",
+
+  // An array of directory names to be searched recursively up from the requiring module's location
+  // moduleDirectories: [
+  //   "node_modules"
+  // ],
+
+  // An array of file extensions your modules use
+  // moduleFileExtensions: [
+  //   "js",
+  //   "json",
+  //   "jsx",
+  //   "ts",
+  //   "tsx",
+  //   "node"
+  // ],
+
+  // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
+  moduleNameMapper: jestTsConfigPathMapper(),
+
+  // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
+  // modulePathIgnorePatterns: [],
+
+  // Activates notifications for test results
+  notify: false,
+
+  // An enum that specifies notification mode. Requires { notify: true }
+  // notifyMode: "failure-change",
+
+  // A preset that is used as a base for Jest's configuration
   preset: 'ts-jest',
-  transform: {'^.+\\.jsx?$': 'babel-jest'},
-  testMatch: ['**/*.test.js', '**/*.spec.js', '**/*.test.ts', '**/*.spec.ts'],
-  modulePathIgnorePatterns: ['dist', 'bin', 'bazel-out', 'node_modules'],
+
+  // Run tests from one or more projects
+  // projects: undefined,
+
+  // Use this configuration option to add custom reporters to Jest
+  // reporters: undefined,
+
+  // Automatically reset mock state between every test
+  resetMocks: false,
+
+  // Reset the module registry before running each individual test
+  resetModules: true,
+
+  // A path to a custom resolver
+  // resolver: '<rootDir>/jest-resolver.js',
+
+  // Automatically restore mock state between every test
+  // restoreMocks: false,
+
+  // The root directory that Jest should scan for tests and modules within
+  // rootDir: undefined,
+
+  // A list of paths to directories that Jest should use to search for files in
+  // roots: [
+  //   "<rootDir>"
+  // ],
+
+  // Allows you to use a custom runner instead of Jest's default test runner
+  // runner: "jest-runner",
+
+  // The paths to modules that run some code to configure or set up the testing environment before each test
+  // setupFiles: [],
+
+  // A list of paths to modules that run some code to configure or set up the testing framework before each test
+  // setupFilesAfterEnv: [],
+
+  // The number of seconds after which a test is considered as slow and reported as such in the results.
+  // slowTestThreshold: 5,
+
+  // A list of paths to snapshot serializer modules Jest should use for snapshot testing
+  // snapshotSerializers: [],
+
+  // The test environment that will be used for testing
+  testEnvironment: 'node',
+
+  // Options that will be passed to the testEnvironment
+  // testEnvironmentOptions: {},
+
+  // Adds a location field to test results
+  // testLocationInResults: false,
+
+  // The glob patterns Jest uses to detect test files
+  // testMatch: [
+  //   "**/__tests__/**/*.[jt]s?(x)",
+  //   "**/?(*.)+(spec|test).[tj]s?(x)"
+  // ],
+
+  // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
+  testPathIgnorePatterns: ['/node_modules/', '/dist/'],
+
+  // The regexp pattern or array of patterns that Jest uses to detect test files
+  // testRegex: [],
+
+  // This option allows the use of a custom results processor
+  // testResultsProcessor: undefined,
+
+  // This option allows use of a custom test runner
+  // testRunner: "jasmine2",
+
+  // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
+  // testURL: "http://localhost",
+
+  // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
+  // timers: "real",
+
+  // A map from regular expressions to paths to transformers
+  // transform: {
+  //   '^.+\\.[t|j]sx?$': 'babel-jest',
+  // },
+
+  // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
+  // transformIgnorePatterns: [
+  //   "/node_modules/",
+  //   "\\.pnp\\.[^\\/]+$"
+  // ],
+
+  // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
+  // unmockedModulePathPatterns: undefined,
+
+  // Indicates whether each individual test should be reported during the run
+  // verbose: undefined,
+
+  // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
+  // watchPathIgnorePatterns: [],
+
+  // Whether to use watchman for file crawling
+  // watchman: true,
 };
diff --git a/package-lock.json b/package-lock.json
index f584635..300ea4b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -775,7 +775,6 @@
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
       "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==",
-      "dev": true,
       "requires": {
         "@nodelib/fs.stat": "2.0.3",
         "run-parallel": "^1.1.9"
@@ -784,14 +783,12 @@
     "@nodelib/fs.stat": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
-      "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
-      "dev": true
+      "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA=="
     },
     "@nodelib/fs.walk": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz",
       "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==",
-      "dev": true,
       "requires": {
         "@nodelib/fs.scandir": "2.1.3",
         "fastq": "^1.6.0"
@@ -1298,6 +1295,12 @@
       "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==",
       "dev": true
     },
+    "@types/uuid": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
+      "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
+      "dev": true
+    },
     "@types/yargs": {
       "version": "15.0.9",
       "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz",
@@ -1652,6 +1655,29 @@
       "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
       "dev": true
     },
+    "aws-sdk": {
+      "version": "2.799.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.799.0.tgz",
+      "integrity": "sha512-NYAoiNU+bJXhlJsC0rFqrmD5t5ho7/VxldmziP6HLPYHfOCI9Uvk6UVjfPmhLWPm0mHnIxhsHqmsNGyjhHNYmw==",
+      "requires": {
+        "buffer": "4.9.2",
+        "events": "1.1.1",
+        "ieee754": "1.1.13",
+        "jmespath": "0.15.0",
+        "querystring": "0.2.0",
+        "sax": "1.2.1",
+        "url": "0.10.3",
+        "uuid": "3.3.2",
+        "xml2js": "0.4.19"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.3.2",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+          "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+        }
+      }
+    },
     "aws-sign2": {
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -1802,6 +1828,16 @@
         }
       }
     },
+    "base-convert-int-array": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/base-convert-int-array/-/base-convert-int-array-1.0.1.tgz",
+      "integrity": "sha512-NWqzaoXx8L/SS32R+WmKqnQkVXVYl2PwNJ68QV3RAlRRL1uV+yxJT66abXI1cAvqCXQTyXr7/9NN4Af90/zDVw=="
+    },
+    "base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+    },
     "bcrypt-pbkdf": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -1837,7 +1873,6 @@
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
       "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-      "dev": true,
       "requires": {
         "fill-range": "^7.0.1"
       }
@@ -1866,6 +1901,16 @@
         "node-int64": "^0.4.0"
       }
     },
+    "buffer": {
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+      "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+      "requires": {
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4",
+        "isarray": "^1.0.0"
+      }
+    },
     "buffer-from": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@@ -3070,6 +3115,11 @@
       "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
       "dev": true
     },
+    "events": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
+      "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
+    },
     "exec-sh": {
       "version": "0.3.4",
       "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz",
@@ -3288,7 +3338,6 @@
       "version": "3.2.4",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz",
       "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==",
-      "dev": true,
       "requires": {
         "@nodelib/fs.stat": "^2.0.2",
         "@nodelib/fs.walk": "^1.2.3",
@@ -3314,7 +3363,6 @@
       "version": "1.9.0",
       "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz",
       "integrity": "sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==",
-      "dev": true,
       "requires": {
         "reusify": "^1.0.4"
       }
@@ -3350,7 +3398,6 @@
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
       "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-      "dev": true,
       "requires": {
         "to-regex-range": "^5.0.1"
       }
@@ -3582,7 +3629,6 @@
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
       "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
-      "dev": true,
       "requires": {
         "is-glob": "^4.0.1"
       }
@@ -4159,6 +4205,11 @@
         "safer-buffer": ">= 2.1.2 < 3"
       }
     },
+    "ieee754": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
+    },
     "ignore": {
       "version": "5.1.8",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
@@ -4409,8 +4460,7 @@
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-      "dev": true
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
     },
     "is-fullwidth-code-point": {
       "version": "2.0.0",
@@ -4428,7 +4478,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
       "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
-      "dev": true,
       "requires": {
         "is-extglob": "^2.1.1"
       }
@@ -4436,8 +4485,7 @@
     "is-number": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
     },
     "is-obj": {
       "version": "2.0.0",
@@ -4518,8 +4566,7 @@
     "isarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
-      "dev": true
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
     },
     "isexe": {
       "version": "2.0.0",
@@ -5067,6 +5114,11 @@
         "supports-color": "^7.0.0"
       }
     },
+    "jmespath": {
+      "version": "0.15.0",
+      "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
+      "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
+    },
     "js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -5237,6 +5289,14 @@
       "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
       "dev": true
     },
+    "ksuid": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ksuid/-/ksuid-2.0.0.tgz",
+      "integrity": "sha512-BM5UU56FhzqQN6v83X8JSKWktl0gAYRovV0DnO/mXpuOs/SFMTPP7fr7ViNtzpg3iV4pnk/6pChuLhOkKBZ59A==",
+      "requires": {
+        "base-convert-int-array": "^1.0.1"
+      }
+    },
     "leven": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -5485,14 +5545,12 @@
     "merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
-      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
-      "dev": true
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
     },
     "micromatch": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
       "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
-      "dev": true,
       "requires": {
         "braces": "^3.0.1",
         "picomatch": "^2.0.5"
@@ -9511,8 +9569,7 @@
     "picomatch": {
       "version": "2.2.2",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
-      "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
-      "dev": true
+      "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg=="
     },
     "pirates": {
       "version": "4.0.1",
@@ -9726,6 +9783,11 @@
       "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
       "dev": true
     },
+    "querystring": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+      "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
+    },
     "quick-lru": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
@@ -9823,6 +9885,11 @@
         "esprima": "~4.0.0"
       }
     },
+    "reflect-metadata": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
+      "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
+    },
     "regex-not": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
@@ -10029,8 +10096,7 @@
     "reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
-      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
-      "dev": true
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
     },
     "rsvp": {
       "version": "4.8.5",
@@ -10047,8 +10113,7 @@
     "run-parallel": {
       "version": "1.1.10",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz",
-      "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==",
-      "dev": true
+      "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw=="
     },
     "rxjs": {
       "version": "6.6.3",
@@ -10303,6 +10368,11 @@
         }
       }
     },
+    "sax": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
+      "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
+    },
     "saxes": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
@@ -11193,7 +11263,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
       "requires": {
         "is-number": "^7.0.0"
       }
@@ -11436,6 +11505,22 @@
       "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
       "dev": true
     },
+    "url": {
+      "version": "0.10.3",
+      "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
+      "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
+      "requires": {
+        "punycode": "1.3.2",
+        "querystring": "0.2.0"
+      },
+      "dependencies": {
+        "punycode": {
+          "version": "1.3.2",
+          "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+          "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
+        }
+      }
+    },
     "url-join": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
@@ -11457,9 +11542,7 @@
     "uuid": {
       "version": "8.3.1",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
-      "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==",
-      "dev": true,
-      "optional": true
+      "integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg=="
     },
     "v8-compile-cache": {
       "version": "2.2.0",
@@ -11656,6 +11739,20 @@
       "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
       "dev": true
     },
+    "xml2js": {
+      "version": "0.4.19",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
+      "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
+      "requires": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~9.0.1"
+      }
+    },
+    "xmlbuilder": {
+      "version": "9.0.7",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
+      "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
+    },
     "xmlchars": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
diff --git a/package.json b/package.json
index e253723..34118c7 100644
--- a/package.json
+++ b/package.json
@@ -14,11 +14,17 @@
     "publish:all": "./tools/publish.sh publish",
     "lint": "gts lint",
     "clean": "gts clean",
-    "build": "bazel build //...",
+    "build": "bazel build ${TARGET:-//...}",
     "fix": "gts fix",
     "posttest": "npm run lint"
   },
-  "dependencies": {},
+  "dependencies": {
+    "aws-sdk": "^2.799.0",
+    "fast-glob": "^3.2.4",
+    "ksuid": "^2.0.0",
+    "reflect-metadata": "^0.1.13",
+    "uuid": "^8.3.1"
+  },
   "devDependencies": {
     "@bazel/bazel": "^2.1.0",
     "@bazel/bazelisk": "^1.7.3",
@@ -29,6 +35,7 @@
     "@semantic-release/changelog": "^5.0.1",
     "@semantic-release/git": "^9.0.0",
     "@types/node": "^14.11.2",
+    "@types/uuid": "^8.3.0",
     "babel": "^6.23.0",
     "gts": "^3.0.2",
     "jest": "^26.6.3",
diff --git a/packages/common/BUILD b/packages/common/BUILD
index 361294e..7855a62 100644
--- a/packages/common/BUILD
+++ b/packages/common/BUILD
@@ -4,7 +4,10 @@ load("//:tools/pkg.bzl", "typedorm_package")
 setup_ts_build(
     name = "library",
     module_name = "@typedorm/common",
-    deps = [],
+    deps = [
+        "@npm//reflect-metadata"
+    ],
+    visibility = ["//packages:__subpackages__"]
 )
 
 typedorm_package(
diff --git a/packages/common/README.md b/packages/common/README.md
index e69de29..af3acf1 100644
--- a/packages/common/README.md
+++ b/packages/common/README.md
@@ -0,0 +1,7 @@
+[![NPM Release](https://img.shields.io/npm/v/@typedorm/common?style=for-the-badge)](https://www.npmjs.com/package/@typedorm/common)
+
+# TypeDORM - Common
+
+Decorators and basic types used by typeDORM
+
+[Read more here](../../README.md)
diff --git a/packages/common/common-types.ts b/packages/common/common-types.ts
new file mode 100644
index 0000000..cb0db1c
--- /dev/null
+++ b/packages/common/common-types.ts
@@ -0,0 +1,16 @@
+import {ObjectType} from './object-type';
+
+export type EntityTarget<Entity> = ObjectType<Entity>;
+
+export type EntityInstance = {
+  constructor: Function;
+};
+
+export type EntityAttributes<Entity> = {
+  [key in keyof Entity]: Entity[key];
+};
+
+export const IsEntityInstance = (
+  variable: unknown
+): variable is EntityInstance =>
+  (variable as EntityInstance).constructor instanceof Function;
diff --git a/packages/common/constants.ts b/packages/common/constants.ts
new file mode 100644
index 0000000..f8a4de7
--- /dev/null
+++ b/packages/common/constants.ts
@@ -0,0 +1,14 @@
+/**
+ * Current transaction write items limit
+ */
+export const TRANSACTION_WRITE_ITEMS_LIMIT = 25;
+
+/**
+ * Prefix to apply to drm generated items
+ */
+export const DYNAMO_ATTRIBUTE_PREFIX = 'DRM_GEN';
+
+/**
+ * Default Limit that is applied when query conditions do not include any explicit limits
+ */
+export const DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT = 3000;
diff --git a/packages/common/decorators/attribute.decorator.ts b/packages/common/decorators/attribute.decorator.ts
new file mode 100644
index 0000000..047b054
--- /dev/null
+++ b/packages/common/decorators/attribute.decorator.ts
@@ -0,0 +1,28 @@
+import 'reflect-metadata';
+import {MetadataManager} from '../metadata-manager';
+import {AttributeRawMetadataOptions} from '../metadata-storage';
+
+export interface AttributeOptions {
+  /**
+   * item will be managed using transaction to ensure it's consistency
+   * @default false
+   */
+  unique: boolean;
+}
+
+export function Attribute(options?: AttributeOptions): PropertyDecorator {
+  return (target, propertyKey): void => {
+    const type = Reflect.getMetadata('design:type', target, propertyKey);
+
+    const attributeProps = {
+      name: propertyKey.toString(),
+      type: type.name,
+      unique: options?.unique,
+    } as AttributeRawMetadataOptions;
+
+    MetadataManager.metadataStorage.addRawAttribute(
+      target.constructor,
+      attributeProps
+    );
+  };
+}
diff --git a/packages/common/decorators/auto-generate-attribute.decorator.ts b/packages/common/decorators/auto-generate-attribute.decorator.ts
new file mode 100644
index 0000000..4898f56
--- /dev/null
+++ b/packages/common/decorators/auto-generate-attribute.decorator.ts
@@ -0,0 +1,29 @@
+import {MetadataManager} from '../metadata-manager';
+import {AutoGenerateAttributeRawMetadataOptions} from '../metadata-storage';
+
+export function AutoGenerateAttribute(
+  options: Pick<
+    AutoGenerateAttributeRawMetadataOptions,
+    'unique' | 'strategy' | 'autoUpdate'
+  >
+): PropertyDecorator {
+  return (target, propertyKey) => {
+    const type = Reflect.getMetadata('design:type', target, propertyKey);
+
+    const attributeProps: AutoGenerateAttributeRawMetadataOptions = {
+      name: propertyKey.toString(),
+      type: type.name,
+      strategy: options.strategy,
+      // this does not make a lot of sense atm, as all supported auto generate strategy are likely to be unique
+      // but can be useful in cases where we support auto generating value by user input
+      unique: options.unique,
+      autoUpdate: options.autoUpdate,
+    };
+
+    if (options.strategy)
+      MetadataManager.metadataStorage.addRawAttribute(
+        target.constructor,
+        attributeProps
+      );
+  };
+}
diff --git a/packages/common/decorators/entity.decorator.ts b/packages/common/decorators/entity.decorator.ts
new file mode 100644
index 0000000..475ec93
--- /dev/null
+++ b/packages/common/decorators/entity.decorator.ts
@@ -0,0 +1,26 @@
+import {MetadataManager} from '../metadata-manager';
+import {EntityRawMetadataOptions} from '../metadata-storage';
+
+export function Entity({
+  table,
+  primaryKey,
+  indexes,
+  name,
+}: Pick<
+  EntityRawMetadataOptions,
+  'name' | 'table' | 'indexes' | 'primaryKey'
+>): ClassDecorator {
+  return target => {
+    const originalTarget = target;
+
+    MetadataManager.metadataStorage.addRawEntity({
+      name,
+      table,
+      target,
+      primaryKey,
+      indexes,
+    });
+
+    return originalTarget;
+  };
+}
diff --git a/packages/common/dynamo-entity-type.ts b/packages/common/dynamo-entity-type.ts
new file mode 100644
index 0000000..13c7803
--- /dev/null
+++ b/packages/common/dynamo-entity-type.ts
@@ -0,0 +1,1 @@
+export type DynamoEntity<Entity> = Entity | {[key: string]: unknown};
diff --git a/packages/common/enums.ts b/packages/common/enums.ts
new file mode 100644
index 0000000..7f310bf
--- /dev/null
+++ b/packages/common/enums.ts
@@ -0,0 +1,29 @@
+export enum RETURN_VALUES {
+  ALL_OLD = 'ALL_OLD',
+  ALL_NEW = 'ALL_NEW',
+  UPDATED_OLD = 'UPDATED_OLD',
+  UPDATED_NEW = 'UPDATED_NEW',
+}
+
+export enum INDEX_TYPE {
+  GSI = 'GLOBAL_SECONDARY_INDEX',
+  LSI = 'LOCAL_SECONDARY_INDEX',
+}
+
+export enum QUERY_ORDER {
+  ASC = 'ASC',
+  DESC = 'DESC',
+}
+
+export enum AUTO_GENERATE_ATTRIBUTE_STRATEGY {
+  /**
+   * @summary Universally Unique Identifier version 4
+   */
+  UUID4 = 'UUID4',
+  /**
+   * @summary K-Sortable Unique Identifier
+   */
+  KSUID = 'KSUID',
+  EPOCH_DATE = 'EPOCH_DATE',
+  ISO_DATE = 'ISO_DATE',
+}
diff --git a/packages/common/find-key-operators.ts b/packages/common/find-key-operators.ts
new file mode 100644
index 0000000..3f07176
--- /dev/null
+++ b/packages/common/find-key-operators.ts
@@ -0,0 +1,5 @@
+export type FindKeySimpleOperator = 'EQ' | 'LE' | 'LT' | 'GE' | 'GT';
+
+export type FindKeyScalarOperator = FindKeySimpleOperator | 'BEGINS_WITH';
+
+export type FindKeyListOperator = 'BETWEEN';
diff --git a/packages/common/index.ts b/packages/common/index.ts
index c143f04..818f074 100644
--- a/packages/common/index.ts
+++ b/packages/common/index.ts
@@ -1,3 +1,21 @@
-export const firstFeature = {
-  message: 'Initial bot release',
-};
+// decorators
+export * from './decorators/attribute.decorator';
+export * from './decorators/auto-generate-attribute.decorator';
+export * from './decorators/entity.decorator';
+
+// utils
+export * from './common-types';
+export * from './constants';
+export * from './dynamo-entity-type';
+export * from './enums';
+export * from './find-key-operators';
+export * from './non-key-attributes-type';
+export * from './object-type';
+export * from './primary-key-attributes-type';
+export * from './require-at-least-one-type';
+export * from './require-only-one-type';
+export * from './scalar-type';
+export * from './update-attributes-type';
+
+// classes
+export * from './table';
diff --git a/packages/common/metadata-manager.ts b/packages/common/metadata-manager.ts
new file mode 100644
index 0000000..f155f56
--- /dev/null
+++ b/packages/common/metadata-manager.ts
@@ -0,0 +1,33 @@
+import {MetadataStorage} from './metadata-storage';
+const metadataKey = Symbol.for('DynamoDBMetadataStorageKey');
+
+declare global {
+  // eslint-disable-next-line @typescript-eslint/no-namespace
+  namespace NodeJS {
+    interface Global {
+      [metadataKey]: MetadataStorage;
+    }
+  }
+}
+
+export class MetadataManager {
+  private constructor() {}
+
+  public static createMetadataStorage() {
+    if (!global[metadataKey]) {
+      global[metadataKey] = new MetadataStorage();
+    }
+    return global[metadataKey];
+  }
+
+  public static get metadataStorage() {
+    if (!global[metadataKey]) {
+      return MetadataManager.createMetadataStorage();
+    }
+    return global[metadataKey];
+  }
+
+  static resetMetadata() {
+    global[metadataKey];
+  }
+}
diff --git a/packages/common/metadata-storage.ts b/packages/common/metadata-storage.ts
new file mode 100644
index 0000000..0058063
--- /dev/null
+++ b/packages/common/metadata-storage.ts
@@ -0,0 +1,100 @@
+import {AUTO_GENERATE_ATTRIBUTE_STRATEGY, EntityTarget} from '@typedorm/common';
+import {IndexOptions, Table} from './table';
+
+export type PrimaryKey = SimplePrimaryKey | CompositePrimaryKey;
+export type SimplePrimaryKey = {
+  partitionKey: string;
+};
+
+export type CompositePrimaryKey = {
+  partitionKey: string;
+  sortKey: string;
+};
+
+export type Indexes = {
+  [key: string]: IndexOptions;
+};
+
+export interface EntityRawMetadataOptions {
+  name: string;
+  target: EntityTarget<any>;
+  primaryKey: PrimaryKey;
+  indexes?: Indexes;
+  table?: Table;
+}
+
+export interface AttributeRawMetadataOptions {
+  name: string;
+  type: any;
+  unique?: boolean;
+}
+
+export interface AutoGenerateAttributeRawMetadataOptions
+  extends AttributeRawMetadataOptions {
+  strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY;
+  autoUpdate?: boolean;
+}
+
+export class MetadataStorage {
+  private _entities: Map<string, EntityRawMetadataOptions>;
+  private _attributes: Map<
+    string,
+    Map<
+      string,
+      AttributeRawMetadataOptions | AutoGenerateAttributeRawMetadataOptions
+    >
+  >;
+
+  constructor() {
+    this._entities = new Map();
+    this._attributes = new Map();
+  }
+
+  getRawAttributesForEntity<Entity>(entityClass: EntityTarget<Entity>) {
+    const attributes = this._attributes.get(entityClass.name)?.values();
+
+    if (!attributes) {
+      throw new Error(`No entity with name "${entityClass.name}" could be resolved, 
+      make sure they have been declared at the connection creation time.`);
+    }
+    return Array.from(attributes);
+  }
+
+  getRawEntityByTarget<Entity>(entityClass: EntityTarget<Entity>) {
+    const entity = this._entities.get(entityClass.name);
+
+    if (!entity) {
+      throw new Error(`No entity with name "${entityClass.name}" could be resolved, 
+      make sure they have been declared at the connection creation time.`);
+    }
+    return entity;
+  }
+
+  addRawAttribute<Entity>(
+    entityClass: EntityTarget<Entity>,
+    attribute: AttributeRawMetadataOptions
+  ) {
+    let attributesForEntity = this._attributes.get(entityClass.name);
+
+    if (!attributesForEntity) {
+      attributesForEntity = new Map();
+    }
+
+    attributesForEntity.set(attribute.name, attribute);
+    this._attributes.set(entityClass.name, attributesForEntity);
+  }
+
+  addRawEntity(entity: EntityRawMetadataOptions) {
+    this._entities.set(entity.target.name, entity);
+  }
+
+  get entities() {
+    return Array.from(this._entities.values());
+  }
+
+  get attributes() {
+    return Array.from(this._attributes.values()).map(attr =>
+      Array.from(attr.values())
+    );
+  }
+}
diff --git a/packages/common/non-key-attributes-type.ts b/packages/common/non-key-attributes-type.ts
new file mode 100644
index 0000000..6408e6b
--- /dev/null
+++ b/packages/common/non-key-attributes-type.ts
@@ -0,0 +1,3 @@
+export type NonKeyAttributes<PrimaryKey, Entity, ValueType> = {
+  [key in keyof Omit<Entity, keyof PrimaryKey>]?: ValueType;
+};
diff --git a/packages/common/object-type.ts b/packages/common/object-type.ts
new file mode 100644
index 0000000..7ed13b5
--- /dev/null
+++ b/packages/common/object-type.ts
@@ -0,0 +1,1 @@
+export type ObjectType<T> = (new () => T) | Function;
diff --git a/packages/common/package.json b/packages/common/package.json
index b618e95..8ea6ff9 100644
--- a/packages/common/package.json
+++ b/packages/common/package.json
@@ -9,7 +9,8 @@
     "orm"
   ],
   "peerDependencies": {
-    "@typedorm/core": "0.0.0-PLACEHOLDER"
+    "@typedorm/core": "0.0.0-PLACEHOLDER",
+    "reflect-metadata": "^0.1.13"
   },
   "publishConfig": {
     "tag": "TAG-PLACEHOLDER",
diff --git a/packages/common/primary-key-attributes-type.ts b/packages/common/primary-key-attributes-type.ts
new file mode 100644
index 0000000..73e75fc
--- /dev/null
+++ b/packages/common/primary-key-attributes-type.ts
@@ -0,0 +1,3 @@
+export type PrimaryKeyAttributes<PrimaryKey, ValueType> = {
+  [key in keyof PrimaryKey]: ValueType;
+};
diff --git a/packages/common/require-at-least-one-type.ts b/packages/common/require-at-least-one-type.ts
new file mode 100644
index 0000000..706c85c
--- /dev/null
+++ b/packages/common/require-at-least-one-type.ts
@@ -0,0 +1,10 @@
+// tslint:disable-next-line: max-line-length
+// Ref: https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist/49725198#49725198
+
+export type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Omit<
+  T,
+  Keys
+> &
+  {
+    [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
+  }[Keys];
diff --git a/packages/common/require-only-one-type.ts b/packages/common/require-only-one-type.ts
new file mode 100644
index 0000000..30c6984
--- /dev/null
+++ b/packages/common/require-only-one-type.ts
@@ -0,0 +1,10 @@
+// tslint:disable-next-line: max-line-length
+// Ref: https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist/49725198#49725198
+
+type InternalPickOne<T, Keys extends keyof T> = {
+  [K in Keys]: Required<Pick<T, K>> &
+    Partial<Record<Exclude<Keys, K>, undefined>>;
+}[Keys];
+
+export type RequireOnlyOne<T, Keys extends keyof T = keyof T> = Omit<T, Keys> &
+  InternalPickOne<T, Keys>;
diff --git a/packages/common/scalar-type.ts b/packages/common/scalar-type.ts
new file mode 100644
index 0000000..a070ead
--- /dev/null
+++ b/packages/common/scalar-type.ts
@@ -0,0 +1,1 @@
+export type ScalarType = string | number | boolean;
diff --git a/packages/common/table.ts b/packages/common/table.ts
new file mode 100644
index 0000000..8a5f46d
--- /dev/null
+++ b/packages/common/table.ts
@@ -0,0 +1,64 @@
+import {INDEX_TYPE} from './enums';
+import {CompositePrimaryKey} from './metadata-storage';
+
+export const IsCompositePrimaryKey = (
+  key: unknown
+): key is CompositePrimaryKey =>
+  !!(
+    (key as CompositePrimaryKey).partitionKey &&
+    (key as CompositePrimaryKey).sortKey
+  );
+
+export interface TableOptions {
+  name: string;
+  partitionKey: string; // identifier of partition key
+  sortKey?: string; // identifier of sort key
+  indexes?: {
+    [key: string]: IndexOptions;
+  };
+}
+
+export interface GSIIndexOptions {
+  type: INDEX_TYPE.GSI;
+  partitionKey: string;
+  sortKey: string;
+}
+
+export interface LSIIndexOptions {
+  type: INDEX_TYPE.LSI;
+  sortKey: string;
+}
+
+export type IndexOptions = GSIIndexOptions | LSIIndexOptions;
+
+export class Table {
+  constructor(private options: TableOptions) {}
+
+  set name(value: string) {
+    this.options.name = value;
+  }
+
+  get name() {
+    return this.options.name;
+  }
+
+  get partitionKey() {
+    return this.options.partitionKey;
+  }
+
+  get sortKey() {
+    if (!IsCompositePrimaryKey(this.options)) {
+      throw new Error('Only Tables using composite keys can have sort keys');
+    }
+    return this.options.sortKey;
+  }
+
+  usesCompositeKey() {
+    return !!(this.options.partitionKey && this.options.sortKey);
+  }
+
+  getIndexByKey(key: string) {
+    const {indexes = {}} = this.options;
+    return indexes[key];
+  }
+}
diff --git a/packages/common/update-attributes-type.ts b/packages/common/update-attributes-type.ts
new file mode 100644
index 0000000..0d077b0
--- /dev/null
+++ b/packages/common/update-attributes-type.ts
@@ -0,0 +1,8 @@
+import {NonKeyAttributes} from './non-key-attributes-type';
+export type UpdateAttributes<PrimaryKey, Entity> = NonKeyAttributes<
+  PrimaryKey,
+  Entity,
+  any
+> & {
+  [key: string]: any;
+};
diff --git a/packages/core/BUILD b/packages/core/BUILD
new file mode 100644
index 0000000..7e1b41b
--- /dev/null
+++ b/packages/core/BUILD
@@ -0,0 +1,25 @@
+load("//:tools/defaults.bzl", "setup_ts_build")
+load("//:tools/pkg.bzl", "typedorm_package")
+
+setup_ts_build(
+    name = "library",
+    module_name = "@typedorm/core",
+    deps = [
+        "//packages/common:library",
+        "@npm//aws-sdk",
+        "@npm//uuid",
+        "@npm//ksuid",
+        "@npm//fast-glob",
+        "@npm//@types/uuid"
+    ],
+    visibility = ["//packages:__subpackages__"]
+
+)
+
+typedorm_package(
+    name = "core",
+    readme_md = "README.md",
+    deps = [
+        ":library"
+    ]
+)
\ No newline at end of file
diff --git a/packages/core/README.md b/packages/core/README.md
new file mode 100644
index 0000000..79738f5
--- /dev/null
+++ b/packages/core/README.md
@@ -0,0 +1,7 @@
+[![NPM Release](https://img.shields.io/npm/v/@typedorm/core?style=for-the-badge)](https://www.npmjs.com/package/@typedorm/core)
+
+# TypeDORM - Core
+
+Contains Core classes and types
+
+[Read more here](../../README.md)
diff --git a/packages/core/__mocks__/organisation.ts b/packages/core/__mocks__/organisation.ts
new file mode 100644
index 0000000..0e606b7
--- /dev/null
+++ b/packages/core/__mocks__/organisation.ts
@@ -0,0 +1,43 @@
+import {Attribute, Entity, INDEX_TYPE} from '@typedorm/common';
+import {table} from './table';
+
+export interface OrganisationPrimaryKey {
+  id: string;
+}
+
+@Entity({
+  table,
+  name: 'organisation',
+  primaryKey: {
+    partitionKey: 'ORG#{{id}}',
+    sortKey: 'ORG#{{id}}',
+  },
+  indexes: {
+    GSI1: {
+      partitionKey: 'ORG#{{id}}#STATUS#{{status}}',
+      sortKey: 'ORG#{{name}}#ACTIVE#{{active}}',
+      type: INDEX_TYPE.GSI,
+    },
+    GSI2: {
+      partitionKey: 'ORG#{{id}}#STATUS#{{status}}',
+      sortKey: 'ORG#{{name}}#TEAM_COUNT#{{teamCount}}',
+      type: INDEX_TYPE.GSI,
+    },
+  },
+})
+export class Organisation implements OrganisationPrimaryKey {
+  @Attribute()
+  id: string;
+
+  @Attribute()
+  name: string;
+
+  @Attribute()
+  status: string;
+
+  @Attribute()
+  active: boolean;
+
+  @Attribute()
+  teamCount: number;
+}
diff --git a/packages/core/__mocks__/table.ts b/packages/core/__mocks__/table.ts
new file mode 100644
index 0000000..f66cccd
--- /dev/null
+++ b/packages/core/__mocks__/table.ts
@@ -0,0 +1,23 @@
+import {INDEX_TYPE, Table} from '@typedorm/common';
+
+export const table = new Table({
+  name: 'test-table',
+  partitionKey: 'PK',
+  sortKey: 'SK',
+  indexes: {
+    GSI1: {
+      type: INDEX_TYPE.GSI,
+      partitionKey: 'GSI1PK',
+      sortKey: 'GSI1SK',
+    },
+    GSI2: {
+      type: INDEX_TYPE.GSI,
+      partitionKey: 'GSI2PK',
+      sortKey: 'GSI2SK',
+    },
+    LSI1: {
+      type: INDEX_TYPE.LSI,
+      sortKey: 'LSI1SK',
+    },
+  },
+});
diff --git a/packages/core/__mocks__/user-auto-generate-attributes.ts b/packages/core/__mocks__/user-auto-generate-attributes.ts
new file mode 100644
index 0000000..0ce9136
--- /dev/null
+++ b/packages/core/__mocks__/user-auto-generate-attributes.ts
@@ -0,0 +1,39 @@
+import {
+  Attribute,
+  AutoGenerateAttribute,
+  AUTO_GENERATE_ATTRIBUTE_STRATEGY,
+  Entity,
+  INDEX_TYPE,
+} from '@typedorm/common';
+import {table} from './table';
+
+export interface UserAutoGenerateAttributesPrimaryKey {
+  id: string;
+}
+
+@Entity({
+  table,
+  name: 'user',
+  primaryKey: {
+    partitionKey: 'USER#{{id}}',
+    sortKey: 'USER#{{id}}',
+  },
+  indexes: {
+    GSI1: {
+      partitionKey: 'USER#UPDATED_AT#{{updatedAt}}',
+      sortKey: 'USER#{{id}}',
+      type: INDEX_TYPE.GSI,
+    },
+  },
+})
+export class UserAutoGenerateAttributes
+  implements UserAutoGenerateAttributesPrimaryKey {
+  @Attribute()
+  id: string;
+
+  @AutoGenerateAttribute({
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE,
+    autoUpdate: true,
+  })
+  private readonly updatedAt: string;
+}
diff --git a/packages/core/__mocks__/user-unique-email.ts b/packages/core/__mocks__/user-unique-email.ts
new file mode 100644
index 0000000..37a76cf
--- /dev/null
+++ b/packages/core/__mocks__/user-unique-email.ts
@@ -0,0 +1,43 @@
+import {Attribute, Entity, INDEX_TYPE} from '@typedorm/common';
+import {table} from './table';
+
+export interface UserUniqueEmailPrimaryKey {
+  id: string;
+}
+
+export interface UserUniqueEmailGSI1 {
+  status: string;
+  name?: string;
+}
+
+@Entity({
+  table,
+  name: 'user',
+  primaryKey: {
+    partitionKey: 'USER#{{id}}',
+    sortKey: 'USER#{{id}}',
+  },
+  indexes: {
+    GSI1: {
+      partitionKey: 'USER#STATUS#{{status}}',
+      sortKey: 'USER#{{name}}',
+      type: INDEX_TYPE.GSI,
+    },
+  },
+})
+export class UserUniqueEmail
+  implements UserUniqueEmailPrimaryKey, UserUniqueEmailGSI1 {
+  @Attribute()
+  id: string;
+
+  @Attribute()
+  name: string;
+
+  @Attribute()
+  status: string;
+
+  @Attribute({
+    unique: true,
+  })
+  email: string;
+}
diff --git a/packages/core/__mocks__/user.ts b/packages/core/__mocks__/user.ts
new file mode 100644
index 0000000..50b1572
--- /dev/null
+++ b/packages/core/__mocks__/user.ts
@@ -0,0 +1,40 @@
+import {Attribute, Entity, INDEX_TYPE} from '@typedorm/common';
+import {table} from './table';
+
+export interface UserPrimaryKey {
+  id: string;
+}
+
+export interface UserGSI1 {
+  status: string;
+  name?: string;
+}
+
+@Entity({
+  table,
+  name: 'user',
+  primaryKey: {
+    partitionKey: 'USER#{{id}}',
+    sortKey: 'USER#{{id}}',
+  },
+  indexes: {
+    GSI1: {
+      partitionKey: 'USER#STATUS#{{status}}',
+      sortKey: 'USER#{{name}}',
+      type: INDEX_TYPE.GSI,
+    },
+  },
+})
+export class User implements UserPrimaryKey, UserGSI1 {
+  @Attribute()
+  id: string;
+
+  @Attribute()
+  name: string;
+
+  @Attribute()
+  status: string;
+
+  @Attribute()
+  age: number;
+}
diff --git a/packages/core/classes/condition/base-condition.ts b/packages/core/classes/condition/base-condition.ts
new file mode 100644
index 0000000..f8aec69
--- /dev/null
+++ b/packages/core/classes/condition/base-condition.ts
@@ -0,0 +1,210 @@
+import {FindKeySimpleOperator, ScalarType} from '@typedorm/common';
+
+const lastCharSpaceMatcher = /\s$/;
+export enum MERGE_STRATEGY {
+  AND = 'AND',
+  OR = 'OR',
+}
+
+export abstract class BaseCondition {
+  expression: string;
+  _names?: {[key: string]: any};
+  _values?: {[key: string]: any};
+
+  constructor() {
+    this.expression = '';
+  }
+
+  set names(value: any) {
+    this._names = {
+      ...this.names,
+      ...value,
+    };
+  }
+
+  get names() {
+    return this._names ?? {};
+  }
+
+  set values(value: any) {
+    this._values = {
+      ...this.values,
+      ...value,
+    };
+  }
+
+  get values() {
+    return this._values ?? {};
+  }
+
+  protected abstract getExpNameKey(key: string): string;
+
+  protected abstract getExpValueKey(key: string): string;
+
+  appendToExpression(segment: string) {
+    if (!segment) {
+      return;
+    }
+
+    if (this.expression.length === 0) {
+      this.expression += segment;
+      return;
+    }
+
+    if (!this.hasSpaceInLastChar(this.expression)) {
+      this.expression += ' '; // append empty space if does not exist
+    }
+
+    this.expression += segment;
+  }
+
+  addExpressionName(name: string) {
+    const expressionPrefixedName = this.getExpNameKey(name);
+    if (this.names[expressionPrefixedName]) {
+      throw new Error(
+        `There is already an expression name with key ${expressionPrefixedName}.`
+      );
+    }
+    this.names = {
+      ...this.names,
+      [expressionPrefixedName]: name,
+    };
+    return expressionPrefixedName;
+  }
+
+  addExpressionValue(name: string, value: any) {
+    const expressionPrefixedValue = this.getExpValueKey(name);
+    if (this.values[expressionPrefixedValue]) {
+      throw new Error(
+        `There is already an expression value with key ${expressionPrefixedValue}.`
+      );
+    }
+    this.values = {
+      ...this.values,
+      [expressionPrefixedValue]: value,
+    };
+    return expressionPrefixedValue;
+  }
+
+  merge(
+    condition: BaseCondition,
+    strategy: MERGE_STRATEGY = MERGE_STRATEGY.AND
+  ): this {
+    const {expression, names, values} = condition;
+
+    // empty condition then return
+    if (!expression) {
+      return this;
+    }
+
+    if (strategy === MERGE_STRATEGY.OR) {
+      this.or().appendToExpression(expression);
+    } else {
+      this.and().appendToExpression(expression);
+    }
+
+    Object.keys(names).forEach(nameKey => {
+      if (this.names[nameKey]) {
+        throw new Error(
+          `Failed to merge expression attribute names, there are multiple attributes names with key "${nameKey}"`
+        );
+      }
+    });
+    Object.keys(values).forEach(valueKey => {
+      if (this.names[valueKey]) {
+        throw new Error(
+          `Failed to merge expression attribute values, there are multiple attributes values with key "${valueKey}"`
+        );
+      }
+    });
+    this.names = {...this.names, ...names};
+    this.values = {...this.values, ...values};
+
+    return this;
+  }
+
+  and(): BaseCondition {
+    this.appendToExpression('AND');
+    return this;
+  }
+
+  or(): BaseCondition {
+    this.appendToExpression('OR');
+    return this;
+  }
+
+  beginsWith(key: string, substring: ScalarType): this {
+    const attrExpName = this.addExpressionName(key);
+    const attrExpValue = this.addExpressionValue(key, substring);
+    this.appendToExpression(`begins_with(${attrExpName}, ${attrExpValue})`);
+    return this;
+  }
+
+  equals(key: string, value: ScalarType): this {
+    return this.addBaseOperatorCondition('EQ', key, value);
+  }
+
+  lessThan(key: string, value: ScalarType): this {
+    return this.addBaseOperatorCondition('LT', key, value);
+  }
+
+  lessThanAndEqualTo(key: string, value: ScalarType): this {
+    return this.addBaseOperatorCondition('LE', key, value);
+  }
+
+  greaterThan(key: string, value: ScalarType): this {
+    return this.addBaseOperatorCondition('GT', key, value);
+  }
+
+  greaterThanAndEqualTo(key: string, value: ScalarType): this {
+    return this.addBaseOperatorCondition('GE', key, value);
+  }
+
+  between(key: string, value: [ScalarType, ScalarType]): this {
+    if (value.length !== 2) {
+      throw new Error(
+        'Incorrect query value for BETWEEN operator, it requires array containing two values.'
+      );
+    }
+    const [startIncluding, endIncluding] = value;
+    const attrExpName = this.addExpressionName(key);
+    const attrExpValueStart = this.addExpressionValue(
+      `${key}_start`,
+      startIncluding
+    );
+    const attrExpValueEnd = this.addExpressionValue(`${key}_end`, endIncluding);
+
+    this.appendToExpression(
+      `${attrExpName} BETWEEN ${attrExpValueStart} AND ${attrExpValueEnd}`
+    );
+    return this;
+  }
+
+  addBaseOperatorCondition(
+    operator: FindKeySimpleOperator,
+    key: string,
+    value: any
+  ): this {
+    const attrExpName = this.addExpressionName(key);
+    const attrExpValue = this.addExpressionValue(key, value);
+    this.appendToExpression(
+      `${attrExpName} ${this.getSymbolForOperator(operator)} ${attrExpValue}`
+    );
+    return this;
+  }
+
+  private getSymbolForOperator(operator: FindKeySimpleOperator): string {
+    const symbolMap = {
+      EQ: '=',
+      LE: '<=',
+      LT: '<',
+      GE: '>=',
+      GT: '>',
+    };
+    return symbolMap[operator];
+  }
+
+  private hasSpaceInLastChar(match: string) {
+    return lastCharSpaceMatcher.test(match);
+  }
+}
diff --git a/packages/core/classes/condition/condition.ts b/packages/core/classes/condition/condition.ts
new file mode 100644
index 0000000..19858c7
--- /dev/null
+++ b/packages/core/classes/condition/condition.ts
@@ -0,0 +1,23 @@
+import {BaseCondition} from './base-condition';
+
+export class Condition extends BaseCondition {
+  constructor(prefix?: string) {
+    super();
+    this.expression = prefix ?? '';
+  }
+
+  attributeNotExist(attributeKey: string): Condition {
+    const attrExpName = this.addExpressionName(attributeKey);
+    this.appendToExpression(`attribute_not_exists(${attrExpName})`);
+
+    return this;
+  }
+
+  getExpNameKey(key: string) {
+    return `#CE_${key}`;
+  }
+
+  getExpValueKey(key: string) {
+    return `:CE_${key}`;
+  }
+}
diff --git a/packages/core/classes/condition/key-condition.ts b/packages/core/classes/condition/key-condition.ts
new file mode 100644
index 0000000..0d262b8
--- /dev/null
+++ b/packages/core/classes/condition/key-condition.ts
@@ -0,0 +1,14 @@
+import {BaseCondition} from './base-condition';
+
+export class KeyCondition extends BaseCondition {
+  constructor() {
+    super();
+  }
+
+  getExpNameKey(key: string): string {
+    return `#KY_CE_${key}`;
+  }
+  getExpValueKey(key: string): string {
+    return `:KY_CE_${key}`;
+  }
+}
diff --git a/packages/core/classes/connection/connection-manager.ts b/packages/core/classes/connection/connection-manager.ts
new file mode 100644
index 0000000..95718be
--- /dev/null
+++ b/packages/core/classes/connection/connection-manager.ts
@@ -0,0 +1,40 @@
+import {ConnectionOptions} from './connection-options';
+import {Connection} from './connection';
+
+export class ConnectionManager {
+  private connections: Map<string, Connection>;
+
+  constructor() {
+    this.connections = new Map<string, Connection>();
+  }
+
+  get(name = 'default') {
+    const connection = this.connections.get(name);
+    if (!connection) {
+      throw new Error(`No such connection with name "${name}" exists`);
+    }
+    return connection;
+  }
+
+  create(options: ConnectionOptions) {
+    const {name = 'default'} = options;
+    if (this.connections.has(name)) {
+      throw new Error(
+        `There is already an existing connection with name "${name}".`
+      );
+    }
+    this.connections.set(name, new Connection(options));
+
+    const createdConnection = this.connections.get(name);
+    if (!createdConnection) {
+      throw new Error(
+        `New connection with name "${name}" was created but could not be found.`
+      );
+    }
+    return createdConnection;
+  }
+
+  clear() {
+    this.connections = new Map<string, Connection>();
+  }
+}
diff --git a/packages/core/classes/connection/connection-metadata-builder.ts b/packages/core/classes/connection/connection-metadata-builder.ts
new file mode 100644
index 0000000..ae023ad
--- /dev/null
+++ b/packages/core/classes/connection/connection-metadata-builder.ts
@@ -0,0 +1,45 @@
+import fg from 'fast-glob';
+import path from 'path';
+import {EntityTarget} from '@typedorm/common';
+import {Connection} from './connection';
+import {EntityMetadataBuilder} from './entity-metadata-builder';
+
+export class ConnectionMetadataBuilder {
+  constructor(private connection: Connection) {}
+
+  buildEntityMetadatas(entities: EntityTarget<any>[] | string) {
+    let entitiesToBuild = [] as Function[];
+    if (typeof entities === 'string') {
+      entitiesToBuild = [...this.loadEntitiesFromDirs(entities)];
+    } else {
+      entitiesToBuild = [...entities];
+    }
+
+    return new EntityMetadataBuilder(this.connection).build(entitiesToBuild);
+  }
+
+  private loadEntitiesFromDirs(pathMatchPattern: string) {
+    const classesDirectory = path.normalize(pathMatchPattern);
+    const allFiles = fg
+      .sync(classesDirectory, {
+        dot: false,
+      })
+      .map(file => require(file));
+    return this.recursiveLoadModulesFromFiles(allFiles, []);
+  }
+
+  private recursiveLoadModulesFromFiles(exported: any, allLoaded: Function[]) {
+    if (typeof exported === 'function') {
+      allLoaded.push(exported);
+    } else if (Array.isArray(exported)) {
+      exported.forEach((file: any) =>
+        this.recursiveLoadModulesFromFiles(file, allLoaded)
+      );
+    } else if (typeof exported === 'object' && exported !== null) {
+      Object.keys(exported).forEach(key =>
+        this.recursiveLoadModulesFromFiles(exported[key], allLoaded)
+      );
+    }
+    return allLoaded;
+  }
+}
diff --git a/packages/core/classes/connection/connection-options.ts b/packages/core/classes/connection/connection-options.ts
new file mode 100644
index 0000000..889a956
--- /dev/null
+++ b/packages/core/classes/connection/connection-options.ts
@@ -0,0 +1,8 @@
+import {EntityTarget, Table} from '@typedorm/common';
+
+export interface ConnectionOptions {
+  table?: Table;
+  name?: string;
+  dynamoQueryItemsImplicitLimit?: number;
+  entities: EntityTarget<any>[] | string;
+}
diff --git a/packages/core/classes/connection/connection.ts b/packages/core/classes/connection/connection.ts
new file mode 100644
index 0000000..573ea55
--- /dev/null
+++ b/packages/core/classes/connection/connection.ts
@@ -0,0 +1,141 @@
+import {
+  DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT,
+  EntityTarget,
+  Table,
+} from '@typedorm/common';
+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,
+} from '../metadata/entity-metadata';
+import {ConnectionMetadataBuilder} from './connection-metadata-builder';
+import {ConnectionOptions} from './connection-options';
+
+export class Connection {
+  readonly name: string;
+  readonly table: Table;
+  readonly entityManager: EntityManager;
+  readonly transactionManger: TransactionManager;
+  readonly defaultConfig: {queryItemsImplicitLimit: number};
+
+  private _entityMetadatas: Map<string, EntityMetadata>;
+  private isConnected: boolean;
+
+  constructor(private options: ConnectionOptions) {
+    const {table, name = 'default'} = options;
+    if (table) {
+      this.table = table;
+    }
+    this.name = name;
+    this.entityManager = new EntityManager(this);
+    this.transactionManger = new TransactionManager();
+    this.defaultConfig = {
+      queryItemsImplicitLimit:
+        options.dynamoQueryItemsImplicitLimit ??
+        DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT,
+    };
+    /**
+     * This makes sure that we only ever build entity metadatas once per connection
+     */
+    this.isConnected = false;
+  }
+
+  connect() {
+    if (this.isConnected) {
+      throw new Error(
+        'There is already an active connection, Connect should only be called once per application.'
+      );
+    }
+
+    this._entityMetadatas = new Map(
+      this.buildMetadatas().map(entityMeta => [
+        entityMeta.target.name,
+        entityMeta,
+      ])
+    );
+
+    this.isConnected = true;
+    return this;
+  }
+
+  get entityMetadatas() {
+    return Array.from(this._entityMetadatas.values());
+  }
+
+  hasMetadata<Entity>(entityClass: EntityTarget<Entity>) {
+    return !!this.getEntityByTarget(entityClass);
+  }
+
+  getAttributesForEntity<Entity>(entityClass: EntityTarget<Entity>) {
+    const attributesMap = this._entityMetadatas.get(entityClass.name);
+    if (!attributesMap) {
+      throw new Error(
+        `Cannot find attributes for entity "${entityClass.name}".`
+      );
+    }
+    return attributesMap.attributes;
+  }
+
+  get globalTable() {
+    return this.table;
+  }
+
+  /**
+   * Returns any attributes marked as unique
+   * If attribute used in a primary key is marked as unique, it is ignored, since all primary key are always unique
+   * @param entityClass
+   */
+  getUniqueAttributesForEntity<Entity>(entityClass: EntityTarget<Entity>) {
+    const entityMetadata = this.getEntityByTarget(entityClass);
+
+    if (!entityMetadata) {
+      throw new Error(
+        'Could not get unique attributes for entity, every class to be used as entity must have @Entity decorator on it.'
+      );
+    }
+
+    return this.getAttributesForEntity<Entity>(entityClass).filter(attr => {
+      // only attributes that are not part of primary key should be included
+      return (
+        (attr as AttributeMetadata)?.unique &&
+        !this.isUsedForPrimaryKey(entityMetadata.schema.primaryKey, attr.name)
+      );
+    });
+  }
+
+  getEntityByTarget<Entity>(entityClass: EntityTarget<Entity>) {
+    const metadata = this._entityMetadatas.get(entityClass.name);
+    if (!metadata) {
+      throw new Error(
+        `No such entity named "${entityClass.name}" is known to TypeDrm, make sure it is declared at the connection creation time.`
+      );
+    }
+    return metadata;
+  }
+
+  getAutoUpdateAttributes<Entity>(entityClass: EntityTarget<Entity>) {
+    return this.getAttributesForEntity(entityClass).filter(
+      attr => (attr as AutoGeneratedAttributeMetadata)?.autoUpdate
+    ) as AutoGeneratedAttributeMetadata[];
+  }
+
+  isUsedForPrimaryKey(
+    primaryKey: DynamoEntitySchemaPrimaryKey,
+    attributeName: string
+  ) {
+    const primaryKeyInterpolations = primaryKey._interpolations ?? {};
+    return Object.keys(primaryKeyInterpolations).some(key => {
+      const currInterpolation = primaryKeyInterpolations[key];
+      return currInterpolation.includes(attributeName);
+    });
+  }
+
+  private buildMetadatas() {
+    return new ConnectionMetadataBuilder(this).buildEntityMetadatas(
+      this.options.entities
+    );
+  }
+}
diff --git a/packages/core/classes/connection/entity-metadata-builder.ts b/packages/core/classes/connection/entity-metadata-builder.ts
new file mode 100644
index 0000000..fe171d8
--- /dev/null
+++ b/packages/core/classes/connection/entity-metadata-builder.ts
@@ -0,0 +1,52 @@
+import {MetadataManager} from '@typedorm/common/metadata-manager';
+import {AttributeMetadata} from '../metadata/attribute-metadata';
+import {AutoGeneratedAttributeMetadata} from '../metadata/auto-generated-attribute-metadata';
+import {EntityMetadata} from '../metadata/entity-metadata';
+import {IsAutoGeneratedAttributeMetadata} from './../metadata/auto-generated-attribute-metadata';
+import {Connection} from './connection';
+
+export class EntityMetadataBuilder {
+  constructor(private connection: Connection) {}
+  build(entityClasses: Function[]): EntityMetadata[] {
+    return entityClasses.map(entity => {
+      const getRawAttributesForEntity = MetadataManager.metadataStorage.getRawAttributesForEntity(
+        entity
+      );
+
+      const attributesMetadata = getRawAttributesForEntity.map(attr => {
+        if (IsAutoGeneratedAttributeMetadata(attr)) {
+          return new AutoGeneratedAttributeMetadata({
+            ...attr,
+            connection: this.connection,
+          });
+        }
+
+        return new AttributeMetadata({
+          connection: this.connection,
+          ...attr,
+        });
+      });
+
+      const {
+        target,
+        table,
+        name,
+        primaryKey,
+        indexes,
+      } = MetadataManager.metadataStorage.getRawEntityByTarget(entity);
+
+      // At the moment we do simple store metadata and retrieve for entities,
+      // this however needs to support extending Entity
+      // TODO: add support for concatenating attributes for all inherited entities
+      return new EntityMetadata({
+        connection: this.connection,
+        table,
+        target,
+        attributes: attributesMetadata,
+        name,
+        primaryKey,
+        indexes,
+      });
+    });
+  }
+}
diff --git a/packages/core/classes/container.ts b/packages/core/classes/container.ts
new file mode 100644
index 0000000..997b2bd
--- /dev/null
+++ b/packages/core/classes/container.ts
@@ -0,0 +1,23 @@
+type Parameters<T> = T extends (...args: infer K) => any ? K : never;
+
+export class Container {
+  private static _instance: Container;
+  private modules: Map<Function, any>;
+  private constructor() {
+    this.modules = new Map<Function, any>();
+  }
+
+  static get<T>(anyClass: new (...args: any) => T, ...args: Parameters<T>): T {
+    if (!this._instance) {
+      this._instance = new Container();
+    }
+
+    const modules = this._instance.modules;
+
+    if (!modules.has(anyClass)) {
+      modules.set(anyClass, new anyClass(...args));
+    }
+
+    return modules.get(anyClass);
+  }
+}
diff --git a/packages/core/classes/expression-builder.ts b/packages/core/classes/expression-builder.ts
new file mode 100644
index 0000000..d27be22
--- /dev/null
+++ b/packages/core/classes/expression-builder.ts
@@ -0,0 +1,101 @@
+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 {
+  buildConditionExpression(condition: Condition) {
+    if (!condition.expression) {
+      return {ConditionExpression: ''};
+    }
+    const expression = {
+      ConditionExpression: condition.expression.trim(),
+      ExpressionAttributeNames: condition.names,
+      ExpressionAttributeValues: condition.values,
+    };
+    return this.removeEmptyFieldsAndReturn(expression);
+  }
+
+  buildUpdateExpression(
+    item: {[key: string]: any},
+    options?: {
+      nestedKeySeparator: string;
+    }
+  ) {
+    if (!isObject(item)) {
+      throw new Error('Item to generate expression must be of type object');
+    }
+
+    if (isEmptyObject(item)) {
+      throw new Error(
+        'Item to generate expression must contain at least one attribute'
+      );
+    }
+    const nestedKeySeparator = options?.nestedKeySeparator || '.';
+
+    // build update set expression
+    const expression = Object.keys(item).reduce(
+      (acc, key, index) => {
+        // only append comma from second key
+        if (index !== 0) {
+          acc.UpdateExpression += ',';
+        }
+
+        let expAttrKey = '';
+
+        if (key.includes(nestedKeySeparator)) {
+          const pathParts = key.split(nestedKeySeparator);
+          expAttrKey = pathParts
+            .map((innerKey, innerIndex) => {
+              const innerKeyExp = `#attr${index}_inner${innerIndex}`;
+              acc.ExpressionAttributeNames[innerKeyExp] = innerKey;
+              return innerKeyExp;
+            })
+            .join('.');
+        } else {
+          expAttrKey = `#attr${index}`;
+          acc.ExpressionAttributeNames[expAttrKey] = key;
+        }
+
+        const expValKey = `:val${index}`;
+        acc.UpdateExpression += ` ${expAttrKey} = ${expValKey}`;
+        acc.ExpressionAttributeValues[expValKey] = item[key];
+        return acc;
+      },
+      {
+        UpdateExpression: 'SET',
+        ExpressionAttributeNames: {} as {[key: string]: string},
+        ExpressionAttributeValues: {} as {[key: string]: any},
+      }
+    );
+    return this.removeEmptyFieldsAndReturn(expression);
+  }
+
+  buildKeyConditionExpression(condition: KeyCondition) {
+    if (!condition.expression) {
+      return {};
+    }
+
+    const expression = {
+      KeyConditionExpression: condition.expression.trim(),
+      ExpressionAttributeNames: condition.names,
+      ExpressionAttributeValues: condition.values,
+    };
+    return this.removeEmptyFieldsAndReturn(expression);
+  }
+
+  private removeEmptyFieldsAndReturn(expression: {
+    ExpressionAttributeNames: any;
+    ExpressionAttributeValues: any;
+    [key: string]: any;
+  }) {
+    if (isEmptyObject(expression.ExpressionAttributeNames)) {
+      delete expression.ExpressionAttributeNames;
+    }
+
+    if (isEmptyObject(expression.ExpressionAttributeValues)) {
+      delete expression.ExpressionAttributeValues;
+    }
+    return expression;
+  }
+}
diff --git a/packages/core/classes/manager/base-manager.ts b/packages/core/classes/manager/base-manager.ts
new file mode 100644
index 0000000..3e9d276
--- /dev/null
+++ b/packages/core/classes/manager/base-manager.ts
@@ -0,0 +1,8 @@
+import {DynamoDB} from 'aws-sdk';
+
+export abstract class BaseManager {
+  protected _dc: DynamoDB.DocumentClient;
+  constructor() {
+    this._dc = new DynamoDB.DocumentClient();
+  }
+}
diff --git a/packages/core/classes/manager/entity-manager.ts b/packages/core/classes/manager/entity-manager.ts
new file mode 100644
index 0000000..98e4b82
--- /dev/null
+++ b/packages/core/classes/manager/entity-manager.ts
@@ -0,0 +1,309 @@
+import {DocumentClient} from 'aws-sdk/clients/dynamodb';
+import {
+  EntityAttributes,
+  EntityTarget,
+  PrimaryKeyAttributes,
+  RETURN_VALUES,
+  UpdateAttributes,
+} from '@typedorm/common';
+import {getDynamoQueryItemsLimit} from '../../helpers/get-dynamo-query-items-limit';
+import {getUniqueAttributePrimaryKey} from '../../helpers/get-unique-attr-primary-key';
+import {isEmptyObject} from '../../helpers/is-empty-object';
+import {Connection} from '../connection/connection';
+import {WriteTransaction} from '../transaction/write-transaction';
+import {
+  DocumentClientRequestTransformer,
+  ManagerToDynamoQueryItemsOptions,
+} from '../transformer/document-client-request-transformer';
+import {EntityTransformer} from '../transformer/entity-transformer';
+import {BaseManager} from './base-manager';
+
+export interface EntityManagerUpdateOptions {
+  /**
+   * @default '.'
+   */
+  nestedKeySeparator?: string;
+
+  /**
+   * @default ALL_NEW
+   */
+  returnValues?: RETURN_VALUES;
+}
+
+export interface EntityManagerQueryOptions
+  extends ManagerToDynamoQueryItemsOptions {
+  cursor?: DocumentClient.Key;
+}
+
+export class EntityManager extends BaseManager {
+  private _dcReqTransformer: DocumentClientRequestTransformer;
+  private _entityTransformer: EntityTransformer;
+
+  constructor(protected connection: Connection) {
+    super();
+    this._dcReqTransformer = new DocumentClientRequestTransformer(connection);
+    this._entityTransformer = new EntityTransformer(connection);
+  }
+
+  /**
+   * Creates new record in table with given entity
+   * @param entity Entity to add to table as a new record
+   */
+  async create<Entity>(
+    entity: Entity,
+    options?: {returnIdentifier: string}
+  ): Promise<Entity> {
+    const identifier = options?.returnIdentifier ?? 'id';
+    const dynamoPutItemInput = this._dcReqTransformer.toDynamoPutItem(entity);
+    let itemToReturn = {...entity};
+
+    if (!Array.isArray(dynamoPutItemInput)) {
+      await this._dc.put(dynamoPutItemInput).promise();
+
+      if (dynamoPutItemInput.Item[identifier]) {
+        itemToReturn = {...itemToReturn, id: dynamoPutItemInput.Item.id};
+      }
+      return {...entity};
+    }
+
+    // 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
+    const transaction = new WriteTransaction(
+      this.connection,
+      dynamoPutItemInput.map(putItem => ({Put: putItem}))
+    );
+    await this.connection.transactionManger.write(transaction);
+
+    // if there are any unique attributes they will be appended after first item,
+    // in that case we get id from
+    if (dynamoPutItemInput[0].Item[identifier]) {
+      itemToReturn = {...itemToReturn, id: dynamoPutItemInput[0].Item.id};
+    }
+
+    return itemToReturn;
+  }
+
+  /**
+   * Finds an record by given primary key, when table uses composite primary key,
+   * props must include both partition and sort key attributes
+   * @param entityClass Entity to get value of
+   * @param props attributes of entity
+   */
+  async findOne<PrimaryKey, Entity>(
+    entityClass: EntityTarget<Entity>,
+    primaryKeyAttributes: PrimaryKey
+  ): Promise<Entity | undefined> {
+    const dynamoGetItem = this._dcReqTransformer.toDynamoGetItem(
+      entityClass,
+      primaryKeyAttributes
+    );
+
+    const response = await this._dc.get(dynamoGetItem).promise();
+
+    const entity = this._entityTransformer.fromDynamoEntity<Entity>(
+      entityClass,
+      response.Item ?? {}
+    );
+    return entity;
+  }
+
+  /**
+   * Checks if item with given attribute/primary key exists in the table
+   * @param entityClass Entity class
+   * @param attributes attributes to find items by, must be primary key attributes or attribute marked as unique
+   */
+  async exists<Entity, KeyAttributes = Partial<Entity>>(
+    entityClass: EntityTarget<Entity>,
+    attributes: KeyAttributes
+  ) {
+    if (isEmptyObject(attributes)) {
+      throw new Error("Attributes are required to check it's existence.");
+    }
+
+    const metadata = this.connection.getEntityByTarget(entityClass);
+
+    const uniqueAttributeNames = this.connection
+      .getUniqueAttributesForEntity(entityClass)
+      .map(attr => attr.name);
+
+    const {primaryKeyAttributes, uniqueAttributes} = Object.entries(
+      attributes
+    ).reduce(
+      (acc, [attrKey, value]) => {
+        if (
+          this.connection.isUsedForPrimaryKey(
+            metadata.schema.primaryKey,
+            attrKey
+          )
+        ) {
+          acc.primaryKeyAttributes[attrKey] = value;
+        } else if (uniqueAttributeNames.includes(attrKey)) {
+          acc.uniqueAttributes[attrKey] = value;
+        } else {
+          // any attributes that are not part of either primary key or is not marked as unique will be rejected
+          throw new Error(
+            `Only attributes that are part of primary key or is marked as unique attribute can be queried, attribute "${attrKey} is neither."`
+          );
+        }
+        return acc;
+      },
+      {
+        primaryKeyAttributes: {} as any,
+        uniqueAttributes: {} as any,
+      }
+    );
+
+    if (
+      !isEmptyObject(primaryKeyAttributes) &&
+      !isEmptyObject(uniqueAttributes)
+    ) {
+      throw new Error(
+        'Can not search both primary key and unique attributes at the same time.'
+      );
+    }
+
+    // find item by primary key if it can be resolved
+    if (!isEmptyObject(primaryKeyAttributes)) {
+      return !!(await this.findOne(entityClass, attributes));
+    }
+
+    // try finding entity by unique attribute
+    if (!isEmptyObject(uniqueAttributes)) {
+      if (Object.keys(uniqueAttributes).length > 1) {
+        throw new Error('Can only query one unique attribute at a time.');
+      }
+      const [attrName, attrValue] = Object.entries(uniqueAttributes)[0];
+      const uniqueAttributePrimaryKey = getUniqueAttributePrimaryKey(
+        metadata.table,
+        entityClass.name,
+        attrName,
+        attrValue
+      );
+      return !!(
+        await this._dc
+          .get({
+            Key: {...uniqueAttributePrimaryKey},
+            TableName: metadata.table.name,
+          })
+          .promise()
+      ).Item;
+    }
+    // if none of the above, item does not exist
+    return false;
+  }
+
+  /**
+   *
+   * @param entityClass Entity class to update
+   * @param primaryKey Primary key
+   * @param body Attributes to update
+   * @param options update options
+   */
+  async update<PrimaryKey, Entity>(
+    entityClass: EntityTarget<Entity>,
+    primaryKey: PrimaryKeyAttributes<PrimaryKey, any>,
+    body: UpdateAttributes<PrimaryKey, Entity>,
+    options?: EntityManagerUpdateOptions
+  ): Promise<Entity> {
+    const dynamoUpdateItem = this._dcReqTransformer.toDynamoUpdateItem<
+      PrimaryKey,
+      Entity
+    >(entityClass, primaryKey, body, options);
+
+    const response = await this._dc.update(dynamoUpdateItem).promise();
+
+    return this._entityTransformer.fromDynamoEntity<Entity>(
+      entityClass,
+      response.Attributes ?? {}
+    );
+  }
+
+  /**
+   * Deletes an entity by primary key
+   * @param entityClass Entity Class to delete
+   * @param primaryKey Entity Primary key
+   */
+  async delete<PrimaryKey, Entity>(
+    entityClass: EntityTarget<Entity>,
+    primaryKey: PrimaryKey
+  ) {
+    const dynamoDeleteItem = this._dcReqTransformer.toDynamoDeleteItem<
+      PrimaryKey,
+      Entity
+    >(entityClass, primaryKey);
+
+    await this._dc.delete(dynamoDeleteItem).promise();
+
+    return {
+      success: true,
+    };
+  }
+
+  /**
+   * Find items items using declarative query options
+   * @param entityClass Entity to query
+   * @param partitionKeyAttributes Partition key attributes, If querying an index,
+   * this is the partition key attributes of that index
+   * @param queryOptions Query Options
+   */
+  async find<
+    Entity,
+    PartitionKeyAttributes = Partial<EntityAttributes<Entity>>
+  >(
+    entityClass: EntityTarget<Entity>,
+    partitionKeyAttributes: PartitionKeyAttributes & {
+      queryIndex?: string;
+    },
+    queryOptions?: EntityManagerQueryOptions
+  ) {
+    const dynamoQueryItem = this._dcReqTransformer.toDynamoQueryItem<
+      PartitionKeyAttributes,
+      Entity
+    >(entityClass, partitionKeyAttributes, queryOptions);
+
+    const response = await this._internalRecursiveQuery({
+      queryInput: dynamoQueryItem,
+      // if no explicit limit is set, always fall back to imposing implicit limit
+      limit: queryOptions?.limit ?? getDynamoQueryItemsLimit(),
+      cursor: queryOptions?.cursor,
+    });
+
+    return {
+      ...response,
+      items: response.items.map(item =>
+        this._entityTransformer.fromDynamoEntity(entityClass, item)
+      ),
+    };
+  }
+
+  /**
+   * Recursively queries all items from table
+   * @param param Query params
+   */
+  private async _internalRecursiveQuery({
+    queryInput,
+    limit,
+    cursor,
+    itemsFetched = [],
+  }: {
+    queryInput: DocumentClient.QueryInput;
+    limit: number;
+    cursor?: DocumentClient.Key;
+    itemsFetched?: DocumentClient.ItemList;
+  }): Promise<{items: DocumentClient.ItemList; cursor?: DocumentClient.Key}> {
+    const {LastEvaluatedKey, Items = []} = await this._dc
+      .query({...queryInput, ExclusiveStartKey: cursor})
+      .promise();
+    itemsFetched = [...itemsFetched, ...Items];
+
+    if (itemsFetched.length < limit && LastEvaluatedKey) {
+      return this._internalRecursiveQuery({
+        queryInput,
+        limit,
+        cursor: LastEvaluatedKey,
+        itemsFetched,
+      });
+    }
+    return {items: itemsFetched, cursor: LastEvaluatedKey};
+  }
+}
diff --git a/packages/core/classes/manager/transaction-manager.ts b/packages/core/classes/manager/transaction-manager.ts
new file mode 100644
index 0000000..59df1f8
--- /dev/null
+++ b/packages/core/classes/manager/transaction-manager.ts
@@ -0,0 +1,62 @@
+import {DocumentClient} from 'aws-sdk/clients/dynamodb';
+import {TRANSACTION_WRITE_ITEMS_LIMIT} from '@typedorm/common';
+import {WriteTransaction} from '../transaction/write-transaction';
+import {BaseManager} from './base-manager';
+
+/**
+ * Performs transactions using document client's writeTransaction
+ */
+export class TransactionManager extends BaseManager {
+  constructor() {
+    super();
+  }
+
+  async write(transaction: WriteTransaction) {
+    if (transaction.items.length > TRANSACTION_WRITE_ITEMS_LIMIT) {
+      throw new Error(
+        `Transaction write limit exceeded, current transaction write limit is "${TRANSACTION_WRITE_ITEMS_LIMIT}"`
+      );
+    }
+
+    const transactionInput: DocumentClient.TransactWriteItemsInput = {
+      TransactItems: transaction.items,
+    };
+
+    // FIXME: current promise implementation of document client transact write does not provide a way
+    // the transaction was failed, as a work around we do following.
+    // refer to this issue for more details https://github.com/aws/aws-sdk-js/issues/2464
+    const transactionRequest = this._dc.transactWrite(transactionInput);
+
+    let cancellationReasons: any[];
+    transactionRequest.on('extractError', response => {
+      try {
+        cancellationReasons = JSON.parse(response.httpResponse.body.toString())
+          .CancellationReasons;
+      } catch (err) {
+        // suppress this just in case some types of errors aren't JSON parseable
+        console.error('Error extracting cancellation error', err);
+      }
+    });
+
+    return new Promise((resolve, reject) => {
+      transactionRequest.send((err, response) => {
+        if (err) {
+          // pull all reasons from response and map them to errors
+          const transactionError = new Error() as any;
+          transactionError.code = err.code;
+          transactionError.message = err.message;
+          transactionError.cancellationReasons = cancellationReasons.map(
+            reason => {
+              return {
+                code: reason.Code,
+                message: reason.Message,
+              };
+            }
+          );
+          return reject(transactionError);
+        }
+        return resolve(response);
+      });
+    });
+  }
+}
diff --git a/packages/core/classes/metadata/attribute-metadata.ts b/packages/core/classes/metadata/attribute-metadata.ts
new file mode 100644
index 0000000..9833119
--- /dev/null
+++ b/packages/core/classes/metadata/attribute-metadata.ts
@@ -0,0 +1,22 @@
+import {Connection} from '../connection/connection';
+import {BaseMetadata} from './base-metadata';
+
+export interface AttributeMetadataOptions {
+  connection: Connection;
+  unique?: boolean;
+  name: string;
+  type: string;
+}
+
+export class AttributeMetadata extends BaseMetadata {
+  readonly unique: boolean;
+  readonly name: string;
+  readonly type: string;
+
+  constructor({connection, unique, name, type}: AttributeMetadataOptions) {
+    super(connection);
+    this.unique = !!unique;
+    this.name = name;
+    this.type = type;
+  }
+}
diff --git a/packages/core/classes/metadata/auto-generated-attribute-metadata.ts b/packages/core/classes/metadata/auto-generated-attribute-metadata.ts
new file mode 100644
index 0000000..fee55fb
--- /dev/null
+++ b/packages/core/classes/metadata/auto-generated-attribute-metadata.ts
@@ -0,0 +1,61 @@
+import KSUID from 'ksuid';
+import {v4} from 'uuid';
+import {AUTO_GENERATE_ATTRIBUTE_STRATEGY} from '@typedorm/common';
+import {Connection} from '../connection/connection';
+import {AutoGenerateAttributeRawMetadataOptions} from '@typedorm/common/metadata-storage';
+import {BaseMetadata} from './base-metadata';
+
+export const IsAutoGeneratedAttributeMetadata = (
+  attr: any
+): attr is AutoGeneratedAttributeMetadata => !!attr.strategy;
+
+export interface AutoGeneratedAttributeMetadataOptions
+  extends AutoGenerateAttributeRawMetadataOptions {
+  connection: Connection;
+}
+
+export class AutoGeneratedAttributeMetadata extends BaseMetadata {
+  readonly value: any;
+  readonly strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY;
+  readonly autoUpdate: boolean;
+  readonly name: string;
+  readonly type: string;
+  readonly unique: boolean;
+
+  constructor({
+    name,
+    connection,
+    type,
+    strategy,
+    autoUpdate,
+    unique,
+  }: AutoGeneratedAttributeMetadataOptions) {
+    super(connection);
+    this.autoUpdate = !!autoUpdate;
+    this.unique = !!unique;
+    this.name = name;
+    this.type = type;
+    this.strategy = strategy;
+    this.value = this.autoGenerateValue(strategy);
+  }
+
+  autoGenerateValue(strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY) {
+    switch (strategy) {
+      case AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4: {
+        return v4();
+      }
+      case AUTO_GENERATE_ATTRIBUTE_STRATEGY.KSUID: {
+        return KSUID.randomSync(new Date()).string;
+      }
+      case AUTO_GENERATE_ATTRIBUTE_STRATEGY.ISO_DATE: {
+        return new Date().toISOString();
+      }
+      case AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE: {
+        return Math.ceil(new Date().valueOf() / 1000);
+      }
+      default: {
+        throw new Error('Unknown attribute strategy.');
+      }
+    }
+  }
+}
diff --git a/packages/core/classes/metadata/base-metadata.ts b/packages/core/classes/metadata/base-metadata.ts
new file mode 100644
index 0000000..16b76ce
--- /dev/null
+++ b/packages/core/classes/metadata/base-metadata.ts
@@ -0,0 +1,5 @@
+import {Connection} from '../connection/connection';
+
+export abstract class BaseMetadata {
+  constructor(public connection: Connection) {}
+}
diff --git a/packages/core/classes/metadata/entity-metadata.ts b/packages/core/classes/metadata/entity-metadata.ts
new file mode 100644
index 0000000..264a47b
--- /dev/null
+++ b/packages/core/classes/metadata/entity-metadata.ts
@@ -0,0 +1,220 @@
+import {EntityTarget, INDEX_TYPE} from '@typedorm/common';
+import {getInterpolatedKeys} from '../../helpers/get-interpolated-keys';
+import {validateKey} from '../../helpers/validate-key';
+import {Connection} from '../connection/connection';
+import {
+  EntityRawMetadataOptions,
+  Indexes,
+  PrimaryKey,
+} from '@typedorm/common/metadata-storage';
+import {IndexOptions, IsCompositePrimaryKey, Table} from '@typedorm/common';
+import {AttributeMetadata} from './attribute-metadata';
+import {AutoGeneratedAttributeMetadata} from './auto-generated-attribute-metadata';
+import {BaseMetadata} from './base-metadata';
+
+export type DynamoEntitySchemaPrimaryKey = {
+  [key: string]: any;
+  _interpolations?: {[key: string]: string[]};
+};
+export type DynamoEntityIndexesSchema = {
+  [key: string]: DynamoEntityIndexSchema;
+};
+
+export type DynamoEntityIndexSchema = {
+  // auto generated
+  _name?: string;
+  // for LSI, only contains interpolations for sort key
+  _interpolations?: {[key: string]: string[]};
+  // entity transformer will inject additional attributes
+  [key: string]: any;
+} & IndexOptions;
+
+export interface DynamoEntitySchema {
+  primaryKey: DynamoEntitySchemaPrimaryKey;
+  indexes?: DynamoEntityIndexesSchema;
+}
+
+export type AttributeMetadataType =
+  | AttributeMetadata
+  | AutoGeneratedAttributeMetadata;
+
+export interface EntityMetadataOptions extends EntityRawMetadataOptions {
+  connection: Connection;
+  attributes: AttributeMetadataType[];
+}
+
+export class EntityMetadata extends BaseMetadata {
+  readonly name: string;
+  readonly table: Table;
+  readonly target: EntityTarget<any>;
+  readonly attributes: AttributeMetadataType[];
+  readonly schema: DynamoEntitySchema;
+  constructor({
+    connection,
+    table,
+    name,
+    target,
+    primaryKey,
+    indexes,
+    attributes,
+  }: EntityMetadataOptions) {
+    super(connection);
+    this.name = name;
+    this.target = target;
+    this.attributes = attributes;
+
+    if (table) {
+      // if no entity level table is defined fallback to global connection table
+      this.table = table;
+    } else {
+      this.table = connection.table;
+    }
+
+    const attributesKeyTypePair = this.attributes.reduce((acc, attr) => {
+      acc[attr.name] = attr.type;
+      return acc;
+    }, {} as {[key: string]: string});
+
+    this.validatePrimaryKey(this.table, primaryKey, attributesKeyTypePair);
+
+    this.schema = {
+      primaryKey: this.buildPrimaryKeySchema({
+        table: this.table,
+        primaryKey,
+        attributes: attributesKeyTypePair,
+      }),
+      indexes: this.buildIndexesSchema({
+        table: this.table,
+        indexes: {...indexes},
+        attributes: attributesKeyTypePair,
+      }),
+    };
+  }
+
+  private validatePrimaryKey(
+    table: Table,
+    primaryKey: PrimaryKey,
+    attributes: {[key: string]: string}
+  ) {
+    if (IsCompositePrimaryKey(primaryKey)) {
+      if (!table.usesCompositeKey()) {
+        throw new Error(
+          `Table "${table.name}" does not use composite key, thus "${primaryKey.sortKey}" should not exist.`
+        );
+      }
+
+      validateKey(primaryKey.partitionKey, attributes);
+      validateKey(primaryKey.sortKey, attributes);
+    } else {
+      if (table.usesCompositeKey()) {
+        throw new Error(
+          `Table "${table.name}" uses composite key as a primary key, thus sort key on entity "${this.target.name}" is required`
+        );
+      }
+      validateKey(primaryKey.partitionKey, attributes);
+    }
+  }
+
+  private buildPrimaryKeySchema({
+    table,
+    primaryKey,
+    attributes,
+  }: {
+    table: Table;
+    primaryKey: PrimaryKey;
+    attributes: {[key: string]: string};
+  }) {
+    const partitionKeyInterpolations = getInterpolatedKeys(
+      primaryKey.partitionKey,
+      attributes
+    );
+
+    const tablePartitionKeyName = table.partitionKey;
+    if (IsCompositePrimaryKey(primaryKey)) {
+      const tableSortKeyName = table.sortKey ?? '';
+      const sortKeyInterpolations = getInterpolatedKeys(
+        primaryKey.sortKey,
+        attributes
+      );
+      return {
+        [tablePartitionKeyName]: primaryKey.partitionKey,
+        [tableSortKeyName]: primaryKey.sortKey,
+        _interpolations: {
+          [tablePartitionKeyName]: partitionKeyInterpolations,
+          [tableSortKeyName]: sortKeyInterpolations,
+        },
+      };
+    } else {
+      return {
+        [tablePartitionKeyName]: primaryKey.partitionKey,
+        _interpolations: {
+          [tablePartitionKeyName]: partitionKeyInterpolations,
+        },
+      };
+    }
+  }
+
+  private buildIndexesSchema({
+    table,
+    indexes,
+    attributes,
+  }: {
+    table: Table;
+    indexes: Indexes;
+    attributes: {[key: string]: string};
+  }) {
+    return Object.keys(indexes).reduce((acc, key) => {
+      const tableIndexSignature = table.getIndexByKey(key);
+      if (!tableIndexSignature) {
+        throw new Error(
+          `No matching index signature found for index "${key}" in table "${table.name}"`
+        );
+      }
+
+      const currentIndex = indexes[key];
+
+      // validates and gets and fill set indexes interpolations of sort key
+      const sortKeyInterpolations = getInterpolatedKeys(
+        currentIndex.sortKey,
+        attributes
+      );
+
+      if (tableIndexSignature.type === INDEX_TYPE.LSI) {
+        if (currentIndex.type !== INDEX_TYPE.LSI) {
+          throw new Error('Index signature mismatch.');
+        }
+        acc[key] = {
+          [tableIndexSignature.sortKey]: currentIndex.sortKey,
+          type: tableIndexSignature.type,
+          _name: key,
+          _interpolations: {
+            [tableIndexSignature.sortKey]: sortKeyInterpolations,
+          },
+        };
+        return acc;
+      } else {
+        if (currentIndex.type !== INDEX_TYPE.GSI) {
+          throw new Error('Index signature mismatch.');
+        }
+        // validates and gets and fill set indexes interpolations of partition key
+        const partitionKeyInterpolations = getInterpolatedKeys(
+          currentIndex.partitionKey,
+          attributes
+        );
+
+        acc[key] = {
+          [tableIndexSignature.partitionKey]: currentIndex.partitionKey,
+          [tableIndexSignature.sortKey]: currentIndex.sortKey,
+          type: tableIndexSignature.type,
+          _name: key,
+          // remove any duplicates from partition or sort keys
+          _interpolations: {
+            [tableIndexSignature.partitionKey]: partitionKeyInterpolations,
+            [tableIndexSignature.sortKey]: sortKeyInterpolations,
+          },
+        };
+        return acc;
+      }
+    }, {} as any);
+  }
+}
diff --git a/packages/core/classes/transaction/transaction.ts b/packages/core/classes/transaction/transaction.ts
new file mode 100644
index 0000000..5e19837
--- /dev/null
+++ b/packages/core/classes/transaction/transaction.ts
@@ -0,0 +1,60 @@
+import {DynamoDB} from 'aws-sdk';
+import {
+  EntityTarget,
+  PrimaryKeyAttributes,
+  UpdateAttributes,
+} from '@typedorm/common';
+import {Connection} from '../connection/connection';
+import {EntityManagerUpdateOptions} from '../manager/entity-manager';
+import {DocumentClientRequestTransformer} from '../transformer/document-client-request-transformer';
+
+// transaction interfaces
+interface WriteTransactionCreate<Entity> {
+  create: {item: Entity};
+}
+interface WriteTransactionUpdate<PrimaryKey, Entity> {
+  update: {
+    item: EntityTarget<Entity>;
+    primaryKey: PrimaryKeyAttributes<PrimaryKey, any>;
+    body: UpdateAttributes<PrimaryKey, Entity>;
+    options?: EntityManagerUpdateOptions;
+  };
+}
+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
+  );
+}
+
+/**
+ * Base Transaction
+ */
+export abstract class Transaction {
+  protected _items:
+    | DynamoDB.DocumentClient.TransactWriteItemList
+    | DynamoDB.DocumentClient.TransactGetItemList;
+
+  protected _dcReqTransformer: DocumentClientRequestTransformer;
+
+  constructor(connection: Connection) {
+    this._items = [];
+    this._dcReqTransformer = new DocumentClientRequestTransformer(connection);
+  }
+
+  abstract chian<PrimaryKey, Entity>(
+    item: WriteTransactionChainItem<PrimaryKey, Entity>
+  ): Transaction;
+}
diff --git a/packages/core/classes/transaction/write-transaction.ts b/packages/core/classes/transaction/write-transaction.ts
new file mode 100644
index 0000000..315ee5f
--- /dev/null
+++ b/packages/core/classes/transaction/write-transaction.ts
@@ -0,0 +1,65 @@
+import {DynamoDB} from 'aws-sdk';
+import {Connection} from '../connection/connection';
+import {
+  isCreateTransaction,
+  isUpdateTransaction,
+  Transaction,
+  WriteTransactionChainItem,
+} from './transaction';
+
+export class WriteTransaction extends Transaction {
+  protected _items: DynamoDB.DocumentClient.TransactWriteItemList;
+
+  constructor(
+    connection: Connection,
+    initialItems?: DynamoDB.DocumentClient.TransactWriteItemList
+  ) {
+    super(connection);
+
+    // initialize items if there are any
+    if (initialItems) {
+      this._items = [...initialItems];
+    }
+  }
+
+  chian<PrimaryKey, Entity>(
+    chainedItem: WriteTransactionChainItem<PrimaryKey, Entity>
+  ): WriteTransaction {
+    if (isCreateTransaction(chainedItem)) {
+      const {
+        create: {item},
+      } = chainedItem;
+
+      const dynamoPutItemInput = this._dcReqTransformer.toDynamoPutItem<Entity>(
+        item
+      );
+
+      // 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,
+        });
+      } else {
+        this._items.push(
+          ...dynamoPutItemInput.map(puttItem => ({Put: puttItem}))
+        );
+      }
+    } 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,
+      });
+    }
+    return this;
+  }
+
+  get items() {
+    return this._items;
+  }
+}
diff --git a/packages/core/classes/transformer/base-transformer.ts b/packages/core/classes/transformer/base-transformer.ts
new file mode 100644
index 0000000..9002b02
--- /dev/null
+++ b/packages/core/classes/transformer/base-transformer.ts
@@ -0,0 +1,225 @@
+import {EntityTarget, INDEX_TYPE} from '@typedorm/common';
+import {Table} from '@typedorm/common';
+import {getConstructorForInstance} from '@typedorm/core/helpers/get-constructor-for-instance';
+import {isEmptyObject} from '@typedorm/core/helpers/is-empty-object';
+import {isObject} from '../../helpers/is-object';
+import {isScalarType} from '../../helpers/is-scalar-type';
+import {parseKey} from '../../helpers/parse-key';
+import {Connection} from '../connection/connection';
+import {
+  DynamoEntityIndexesSchema,
+  DynamoEntitySchema,
+} from '../metadata/entity-metadata';
+import {IsAutoGeneratedAttributeMetadata} from './../metadata/auto-generated-attribute-metadata';
+import {DynamoEntitySchemaPrimaryKey} from './../metadata/entity-metadata';
+
+export abstract class BaseTransformer {
+  constructor(protected connection: Connection) {}
+  /**
+   * Returns table name decorated for given entity class
+   * @param entityClass Entity Class
+   */
+  getTableNameForEntity<Entity>(entityClass: EntityTarget<Entity>) {
+    const metadata = this.connection.getEntityByTarget(entityClass);
+
+    return metadata.table.name;
+  }
+
+  /**
+   * Transforms entity to dynamo db entity schema
+   * @param entity Entity to transform to DynamoDB entity type
+   */
+  toDynamoEntity<Entity>(entity: Entity) {
+    const entityClass = getConstructorForInstance(entity);
+    // retrieve metadata and parse it to schema
+    const metadata = this.connection.getEntityByTarget(entityClass);
+
+    //  auto populate generated values
+    this.connection.getAttributesForEntity(entityClass).forEach(attr => {
+      if (IsAutoGeneratedAttributeMetadata(attr)) {
+        if (attr.value) {
+          entity = Object.assign({...entity, [attr.name]: attr.value});
+        }
+      }
+    });
+
+    const parsedSchema: DynamoEntitySchema = this.recursiveParseEntity(
+      metadata.schema,
+      entity
+    );
+
+    // drop any extra keys that are on entity schema
+    const normalizedPrimaryKey = this.getParsedPrimaryKey(
+      metadata.table,
+      metadata.schema.primaryKey,
+      entity
+    );
+
+    const indexes = {
+      ...parsedSchema.indexes,
+    } as DynamoEntityIndexesSchema;
+
+    // normalize indexes to non nested keys and drop other extra keys
+    const normalizedIndexes = Object.keys(indexes).reduce((acc, key) => {
+      const index = indexes[key];
+
+      const tableIndexSignature = metadata.table.getIndexByKey(
+        index._name ?? ''
+      );
+      // remove all other metadata from indexes
+      const onlyKeysIndex = (index => {
+        if (
+          index.type === INDEX_TYPE.GSI &&
+          tableIndexSignature.type === INDEX_TYPE.GSI
+        ) {
+          return {
+            [tableIndexSignature.partitionKey]:
+              index[tableIndexSignature.partitionKey],
+            [tableIndexSignature.sortKey]: index[tableIndexSignature.sortKey],
+          };
+        } else if (
+          index.type === INDEX_TYPE.LSI &&
+          tableIndexSignature.type === INDEX_TYPE.LSI
+        ) {
+          return {
+            [tableIndexSignature.sortKey]: index[tableIndexSignature.sortKey],
+          };
+        } else {
+          return index;
+        }
+      })(index);
+
+      acc = {...acc, ...onlyKeysIndex};
+      return acc;
+    }, {});
+
+    // clone and cleanup any redundant keys
+    const formattedSchema = {
+      ...normalizedPrimaryKey,
+      ...normalizedIndexes,
+    };
+
+    return {...entity, ...formattedSchema};
+  }
+
+  /**
+   * Returns all affected indexes for given attributes
+   * @param entityClass Entity class
+   * @param attributes Attributes to check affected indexes for
+   * @param options
+   */
+  getAffectedIndexesForAttributes<PrimaryKey, Entity>(
+    entityClass: EntityTarget<Entity>,
+    attributes: {
+      [key in keyof Omit<Entity, keyof PrimaryKey>]?: any;
+    } & {[key: string]: any},
+    options?: {nestedKeySeparator: string}
+  ) {
+    const nestedKeySeparator = options?.nestedKeySeparator ?? '.';
+    const {
+      schema: {indexes},
+    } = this.connection.getEntityByTarget(entityClass);
+
+    const affectedIndexes = Object.keys(attributes).reduce(
+      (acc, attrKey: string) => {
+        const currAttrValue = attributes[attrKey];
+        // if current value is not if scalar type skip checking index
+        if (
+          attrKey.includes(nestedKeySeparator) ||
+          !isScalarType(currAttrValue)
+        ) {
+          return acc;
+        }
+
+        if (!indexes) {
+          return acc;
+        }
+
+        Object.keys(indexes).forEach(key => {
+          const currIndex = indexes[key];
+          const interpolationsForCurrIndex = currIndex._interpolations ?? {};
+
+          // if current index does not have any interpolations to resolve, move onto next one
+          if (isEmptyObject(interpolationsForCurrIndex)) {
+            return acc;
+          }
+
+          // check if attribute we are looking to update is referenced by any index
+          Object.keys(interpolationsForCurrIndex).forEach(interpolationKey => {
+            const currentInterpolation =
+              interpolationsForCurrIndex[interpolationKey];
+
+            if (currentInterpolation.includes(attrKey)) {
+              const parsedIndex = parseKey(
+                currIndex[interpolationKey],
+                attributes
+              );
+              acc[interpolationKey] = parsedIndex;
+            }
+          });
+        });
+
+        return acc;
+      },
+      {} as any
+    );
+    return affectedIndexes;
+  }
+
+  /**
+   * Returns a primary key of an entity
+   * @param entityClass Class of entity
+   * @param attributes Attributes to parse into primary key
+   */
+  getParsedPrimaryKey<Entity>(
+    table: Table,
+    primaryKey: DynamoEntitySchemaPrimaryKey,
+    attributes: {[key in keyof Entity]: any}
+  ) {
+    const normalizedPrimaryKey = this.getNormalizedPrimaryKey(
+      table,
+      primaryKey
+    );
+
+    return this.recursiveParseEntity(normalizedPrimaryKey, attributes);
+  }
+
+  /**
+   * Returns normalized primary key
+   * Normalized as in with only containing partition and sort key
+   * @param table
+   * @param primaryKey
+   */
+  getNormalizedPrimaryKey(
+    table: Table,
+    primaryKey: DynamoEntitySchemaPrimaryKey
+  ) {
+    return Object.keys(primaryKey).reduce((acc, key) => {
+      // remove all not key attributes on schema
+      if (table.partitionKey === key || table.sortKey === key) {
+        acc[key] = primaryKey[key];
+      }
+      return acc;
+    }, {} as Pick<DynamoEntitySchemaPrimaryKey, string | number>);
+  }
+
+  /**
+   * Recursively parses all keys of given object and replaces placeholders with matching values
+   * @private
+   * @param schema schema to resolve
+   * @param entity entity to resolve schema against
+   */
+  protected recursiveParseEntity<Entity>(schema: any, entity: Entity) {
+    const parsedSchema = Object.keys(schema).reduce((acc, key) => {
+      const currentValue = schema[key];
+      if (isObject(currentValue)) {
+        acc[key] = this.recursiveParseEntity(currentValue, entity);
+      } else {
+        acc[key] = parseKey(currentValue, entity);
+      }
+      return acc;
+    }, {} as any);
+
+    return parsedSchema;
+  }
+}
diff --git a/packages/core/classes/transformer/document-client-request-transformer.ts b/packages/core/classes/transformer/document-client-request-transformer.ts
new file mode 100644
index 0000000..c1519f4
--- /dev/null
+++ b/packages/core/classes/transformer/document-client-request-transformer.ts
@@ -0,0 +1,401 @@
+import {
+  EntityTarget,
+  FindKeyListOperator,
+  FindKeyScalarOperator,
+  FindKeySimpleOperator,
+  IndexOptions,
+  INDEX_TYPE,
+  PrimaryKeyAttributes,
+  QUERY_ORDER,
+  RequireOnlyOne,
+  RETURN_VALUES,
+  ScalarType,
+  UpdateAttributes,
+} from '@typedorm/common';
+import {getConstructorForInstance} from '@typedorm/core/helpers/get-constructor-for-instance';
+import {DocumentClient} from 'aws-sdk/clients/dynamodb';
+import {getUniqueAttributePrimaryKey} from '../../helpers/get-unique-attr-primary-key';
+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 {AutoGeneratedAttributeMetadata} from '../metadata/auto-generated-attribute-metadata';
+import {BaseTransformer} from './base-transformer';
+
+export interface ManagerToDynamoQueryItemsOptions {
+  /**
+   * Sort key condition
+   * @default none - no sort key condition is applied
+   */
+  keyCondition?: RequireOnlyOne<
+    {
+      [key in FindKeyScalarOperator]: ScalarType;
+    } &
+      {
+        [key in FindKeyListOperator]: [ScalarType, ScalarType];
+      }
+  >;
+
+  /**
+   * Max number of records to query
+   * @default - implicit dynamo db query limit is applied
+   */
+  limit?: number;
+
+  /**
+   * Order to query items in
+   * @default ASC
+   */
+  orderBy?: QUERY_ORDER;
+}
+
+export interface ManagerToDynamoPutItemOptions {
+  /**
+   * @default false
+   */
+  overwriteIfExists: boolean;
+}
+
+export class DocumentClientRequestTransformer extends BaseTransformer {
+  private _expressionBuilder: ExpressionBuilder;
+
+  constructor(connection: Connection) {
+    super(connection);
+    this._expressionBuilder = new ExpressionBuilder();
+  }
+
+  toDynamoGetItem<PrimaryKey, Entity>(
+    entityClass: EntityTarget<Entity>,
+    primaryKey: PrimaryKey
+  ): DocumentClient.GetItemInput {
+    const metadata = this.connection.getEntityByTarget(entityClass);
+
+    const tableName = this.getTableNameForEntity(entityClass);
+
+    const parsedPrimaryKey = this.getParsedPrimaryKey(
+      metadata.table,
+      metadata.schema.primaryKey,
+      primaryKey
+    );
+
+    if (isEmptyObject(parsedPrimaryKey)) {
+      throw new Error('Primary could not be resolved');
+    }
+
+    return {
+      TableName: tableName,
+      Key: {
+        ...parsedPrimaryKey,
+      },
+    };
+  }
+
+  toDynamoPutItem<Entity>(
+    entity: Entity,
+    options?: ManagerToDynamoPutItemOptions
+  ): DocumentClient.PutItemInput | DocumentClient.PutItemInput[] {
+    const entityClass = getConstructorForInstance(entity);
+    const {table, name} = this.connection.getEntityByTarget(entityClass);
+
+    const uniqueAttributes = this.connection.getUniqueAttributesForEntity(
+      entityClass
+    ) as AttributeMetadata[];
+
+    const dynamoEntity = this.toDynamoEntity(entity);
+    let uniqueAttributePutItems = [] as DocumentClient.PutItemInput[];
+    // apply attribute not exist condition when creating unique
+    const uniqueRecordConditionExpression = this._expressionBuilder.buildConditionExpression(
+      new Condition().attributeNotExist(table.partitionKey)
+    );
+
+    if (uniqueAttributes.length) {
+      uniqueAttributePutItems = uniqueAttributes.map(attr => {
+        const attributeValue =
+          (attr as AutoGeneratedAttributeMetadata)?.value ??
+          (entity as any)[attr.name];
+
+        if (!attributeValue) {
+          throw new Error(
+            `All unique attributes are required, Could not resolve value for unique attribute "${attr.name}."`
+          );
+        }
+
+        const uniqueItemPrimaryKey = getUniqueAttributePrimaryKey(
+          table,
+          name,
+          attr.name,
+          attributeValue
+        );
+
+        return {
+          Item: uniqueItemPrimaryKey,
+          TableName: table.name,
+          ...uniqueRecordConditionExpression,
+        } as DocumentClient.PutItemInput;
+      });
+    }
+
+    let dynamoPutItem = {
+      Item: {
+        ...dynamoEntity,
+      },
+      TableName: table.name,
+    } as DocumentClient.PutItemInput;
+
+    // always prevent overwriting data until explicitly told to do otherwise
+    if (!options?.overwriteIfExists) {
+      dynamoPutItem = {...dynamoPutItem, ...uniqueRecordConditionExpression};
+    }
+
+    if (uniqueAttributePutItems.length) {
+      return [dynamoPutItem, ...uniqueAttributePutItems];
+    }
+    return dynamoPutItem;
+  }
+
+  toDynamoUpdateItem<PrimaryKey, Entity>(
+    entityClass: EntityTarget<Entity>,
+    primaryKey: PrimaryKeyAttributes<PrimaryKey, any>,
+    body: UpdateAttributes<PrimaryKey, Entity>,
+    options?: EntityManagerUpdateOptions
+  ): DocumentClient.UpdateItemInput {
+    // default values
+    const {nestedKeySeparator = '.', returnValues = RETURN_VALUES.ALL_NEW} =
+      options ?? {};
+
+    if (!this.connection.hasMetadata(entityClass)) {
+      throw new Error(`No metadata found for class "${entityClass.name}".`);
+    }
+
+    const metadata = this.connection.getEntityByTarget(entityClass);
+
+    const tableName = metadata.table.name;
+
+    const parsedPrimaryKey = this.getParsedPrimaryKey(
+      metadata.table,
+      metadata.schema.primaryKey,
+      primaryKey
+    );
+
+    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
+    );
+
+    // 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;
+      },
+      {} as {[key: string]: any}
+    );
+
+    const attributesToUpdate = {...body, ...formattedAutoUpdateAttributes};
+
+    // get all affected indexes for attributes
+    const affectedIndexes = this.getAffectedIndexesForAttributes<
+      PrimaryKey,
+      Entity
+    >(entityClass, attributesToUpdate, {
+      nestedKeySeparator,
+    });
+
+    const {
+      UpdateExpression,
+      ExpressionAttributeNames,
+      ExpressionAttributeValues,
+    } = this._expressionBuilder.buildUpdateExpression({
+      ...attributesToUpdate,
+      ...affectedIndexes,
+    });
+
+    return {
+      TableName: tableName,
+      Key: {
+        ...parsedPrimaryKey,
+      },
+      UpdateExpression,
+      ExpressionAttributeNames,
+      ExpressionAttributeValues,
+      ReturnValues: returnValues,
+    };
+  }
+
+  toDynamoDeleteItem<PrimaryKey, Entity>(
+    entityClass: EntityTarget<Entity>,
+    primaryKey: PrimaryKey
+  ): DocumentClient.DeleteItemInput {
+    const metadata = this.connection.getEntityByTarget(entityClass);
+
+    const tableName = metadata.table.name;
+
+    const parsedPrimaryKey = this.getParsedPrimaryKey(
+      metadata.table,
+      metadata.schema.primaryKey,
+      primaryKey
+    );
+
+    if (isEmptyObject(parsedPrimaryKey)) {
+      throw new Error('Primary could not be resolved');
+    }
+
+    return {
+      TableName: tableName,
+      Key: {
+        ...parsedPrimaryKey,
+      },
+    };
+  }
+
+  toDynamoQueryItem<PartitionKeyAttributes, Entity>(
+    entityClass: EntityTarget<Entity>,
+    partitionKeyAttributes: PartitionKeyAttributes & {
+      queryIndex?: string;
+    },
+    queryOptions?: ManagerToDynamoQueryItemsOptions
+  ): DocumentClient.QueryInput {
+    const {table, schema} = this.connection.getEntityByTarget(entityClass);
+
+    const queryIndexName = partitionKeyAttributes.queryIndex ?? '';
+    let indexToQuery: IndexOptions | undefined;
+    if (partitionKeyAttributes.queryIndex) {
+      const matchingIndex = table.getIndexByKey(
+        partitionKeyAttributes.queryIndex
+      );
+      if (!matchingIndex) {
+        throw new Error(
+          `Requested to query items from index "${partitionKeyAttributes.queryIndex}", but no such index exists on table "${table.name}".`
+        );
+      }
+
+      const matchingIndexOnEntity =
+        schema.indexes && schema.indexes[partitionKeyAttributes.queryIndex];
+
+      if (!matchingIndexOnEntity) {
+        throw new Error(
+          `Requested to query items from index "${partitionKeyAttributes.queryIndex}", but no such index exists on entity.`
+        );
+      }
+      indexToQuery = matchingIndex;
+    }
+
+    const parsedPartitionKey = {} as {name: string; value: any};
+    // query will be executed against main table or
+    // if querying local  index, then partition key will be same as main table
+    if (
+      !queryIndexName ||
+      !indexToQuery ||
+      indexToQuery?.type === INDEX_TYPE.LSI
+    ) {
+      parsedPartitionKey.name = table.partitionKey;
+      parsedPartitionKey.value = parseKey(
+        schema.primaryKey[table.partitionKey],
+        partitionKeyAttributes
+      );
+    } else {
+      // query is to be executed against global secondary index
+      parsedPartitionKey.name = indexToQuery.partitionKey;
+
+      const schemaForIndexToQuery = (schema.indexes ?? {})[queryIndexName];
+      parsedPartitionKey.value = parseKey(
+        schemaForIndexToQuery[indexToQuery.partitionKey],
+        partitionKeyAttributes
+      );
+    }
+    const partitionKeyCondition = new KeyCondition().equals(
+      parsedPartitionKey.name,
+      parsedPartitionKey.value
+    );
+
+    // if no query options are present, resolve key condition expression
+    if (!queryOptions || isEmptyObject(queryOptions)) {
+      return {
+        TableName: table.name,
+        IndexName: partitionKeyAttributes.queryIndex,
+        ...this._expressionBuilder.buildKeyConditionExpression(
+          partitionKeyCondition
+        ),
+      };
+    }
+
+    const parsedSortKey = {} as {name: string};
+    // if no we are not querying against index, validate if table is using composite key
+    if (!indexToQuery) {
+      if (!table.usesCompositeKey()) {
+        throw new Error(
+          `Table ${table.name} does not use composite key, thus querying a sort key is not allowed`
+        );
+      }
+
+      parsedSortKey.name = table.sortKey;
+    } else {
+      parsedSortKey.name = indexToQuery.sortKey;
+    }
+
+    // at this point we have resolved partition key and table to query
+    const {keyCondition, limit, orderBy: order} = queryOptions;
+
+    let queryInputParams = {
+      TableName: table.name,
+      IndexName: partitionKeyAttributes.queryIndex,
+      Limit: limit,
+      ScanIndexForward: !order || order === QUERY_ORDER.ASC,
+    } as DocumentClient.QueryInput;
+
+    if (keyCondition && !isEmptyObject(keyCondition)) {
+      // build sort key condition
+      const sortKeyCondition = new KeyCondition();
+      if (keyCondition.BETWEEN && keyCondition.BETWEEN.length) {
+        sortKeyCondition.between(parsedSortKey.name, keyCondition.BETWEEN);
+      } else if (keyCondition.BEGINS_WITH) {
+        sortKeyCondition.beginsWith(
+          parsedSortKey.name,
+          keyCondition.BEGINS_WITH
+        );
+      } else {
+        const operator = Object.keys(keyCondition)[0] as FindKeySimpleOperator;
+        sortKeyCondition.addBaseOperatorCondition(
+          operator,
+          parsedSortKey.name,
+          keyCondition[operator]
+        );
+      }
+
+      // if condition resolution was successful, we can merge both partition and sort key conditions now
+      const keyConditionExpression = this._expressionBuilder.buildKeyConditionExpression(
+        partitionKeyCondition.merge(sortKeyCondition)
+      );
+
+      queryInputParams = {
+        ...queryInputParams,
+        ...keyConditionExpression,
+      };
+    }
+
+    return {
+      ...queryInputParams,
+    };
+  }
+}
diff --git a/packages/core/classes/transformer/entity-transformer.ts b/packages/core/classes/transformer/entity-transformer.ts
new file mode 100644
index 0000000..3f864a0
--- /dev/null
+++ b/packages/core/classes/transformer/entity-transformer.ts
@@ -0,0 +1,40 @@
+import {DynamoEntity, EntityTarget} from '@typedorm/common';
+import {Connection} from '../connection/connection';
+import {BaseTransformer} from './base-transformer';
+
+/**
+ * Note: To use any of the base transformer methods, this default entity transformer should be used
+ */
+export class EntityTransformer extends BaseTransformer {
+  constructor(connection: Connection) {
+    super(connection);
+  }
+  /**
+   * @param entityClass - Target class to look metadata off
+   * @param dynamoEntity
+   */
+  fromDynamoEntity<Entity>(
+    entityClass: EntityTarget<Entity>,
+    dynamoEntity: DynamoEntity<Entity>
+  ): Entity {
+    const entityMetadata = this.connection.getEntityByTarget(entityClass);
+
+    const entityPrimaryKeys = Object.keys(entityMetadata.schema.primaryKey);
+
+    const entityMetadataSchemaIndexes = entityMetadata.schema.indexes ?? {};
+    const entityIndexes = Object.keys(entityMetadataSchemaIndexes)
+      .map(key => {
+        const currentIndex = entityMetadataSchemaIndexes[key];
+        return Object.keys(currentIndex._interpolations ?? {});
+      })
+      .flat();
+
+    return Object.keys(dynamoEntity).reduce((acc, key) => {
+      if (entityPrimaryKeys.includes(key) || entityIndexes.includes(key)) {
+        return acc;
+      }
+      (acc as any)[key] = (dynamoEntity as any)[key];
+      return acc;
+    }, {} as Entity);
+  }
+}
diff --git a/packages/core/helpers/constants.ts b/packages/core/helpers/constants.ts
new file mode 100644
index 0000000..ecc363e
--- /dev/null
+++ b/packages/core/helpers/constants.ts
@@ -0,0 +1,1 @@
+export const regexInterpolatedWord = /\{{(.+?)\}}/g;
diff --git a/packages/core/helpers/get-constructor-for-instance.ts b/packages/core/helpers/get-constructor-for-instance.ts
new file mode 100644
index 0000000..46df1f2
--- /dev/null
+++ b/packages/core/helpers/get-constructor-for-instance.ts
@@ -0,0 +1,11 @@
+import {EntityInstance, EntityTarget, IsEntityInstance} from '@typedorm/common';
+
+export function getConstructorForInstance<Entity>(entity: Entity) {
+  let entityClass: EntityTarget<Entity>;
+  if (IsEntityInstance(entity)) {
+    entityClass = (entity as EntityInstance).constructor;
+  } else {
+    throw new Error('Cannot transform Class to dynamo object');
+  }
+  return entityClass;
+}
diff --git a/packages/core/helpers/get-dynamo-query-items-limit.ts b/packages/core/helpers/get-dynamo-query-items-limit.ts
new file mode 100644
index 0000000..42dbbdb
--- /dev/null
+++ b/packages/core/helpers/get-dynamo-query-items-limit.ts
@@ -0,0 +1,12 @@
+import {DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT} from '@typedorm/common';
+
+export function getDynamoQueryItemsLimit() {
+  try {
+    return JSON.parse(
+      process.env.DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT ?? ''
+    ) as number;
+  } catch {
+    // if something went wrong while getting limit from env
+    return DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT;
+  }
+}
diff --git a/packages/core/helpers/get-interpolated-keys.ts b/packages/core/helpers/get-interpolated-keys.ts
new file mode 100644
index 0000000..cc6d1af
--- /dev/null
+++ b/packages/core/helpers/get-interpolated-keys.ts
@@ -0,0 +1,49 @@
+import {regexInterpolatedWord} from './constants';
+
+export function getInterpolatedKeys(
+  key: string,
+  dict: {[key: string]: string}
+) {
+  const matchIterator = key.matchAll(regexInterpolatedWord);
+  const interpolatedKeys = [] as string[];
+  recursiveFindInterpolatedKeys(key, matchIterator, dict, interpolatedKeys);
+
+  // there shouldn't be any duplicates, still to be double sure remove duplicates if there are any
+  return [...new Set(interpolatedKeys)];
+}
+
+function recursiveFindInterpolatedKeys(
+  key: string,
+  iterator: IterableIterator<RegExpMatchArray>,
+  dict: {[key: string]: string},
+  interpolatedKeys: string[]
+) {
+  const next = iterator.next();
+  if (next.done) {
+    return;
+  }
+
+  // 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);
+}
diff --git a/packages/core/helpers/get-unique-attr-primary-key.ts b/packages/core/helpers/get-unique-attr-primary-key.ts
new file mode 100644
index 0000000..a67558d
--- /dev/null
+++ b/packages/core/helpers/get-unique-attr-primary-key.ts
@@ -0,0 +1,21 @@
+import {DYNAMO_ATTRIBUTE_PREFIX, Table} from '@typedorm/common';
+export function getUniqueAttributePrimaryKey(
+  table: Table,
+  entityName: string,
+  attrName: string,
+  attrValue: any
+) {
+  const item = {} as any;
+  // add prefix to drm generated item to avoid name collisions
+
+  const uniqueKeyValue = `${DYNAMO_ATTRIBUTE_PREFIX}_${entityName.toUpperCase()}.${attrName.toUpperCase()}#${attrValue}`;
+
+  if (table.usesCompositeKey()) {
+    item[table.partitionKey] = uniqueKeyValue;
+    item[table.sortKey ?? ''] = uniqueKeyValue;
+  } else {
+    item[table.partitionKey] = uniqueKeyValue;
+  }
+
+  return item;
+}
diff --git a/packages/core/helpers/is-empty-object.ts b/packages/core/helpers/is-empty-object.ts
new file mode 100644
index 0000000..a910dc3
--- /dev/null
+++ b/packages/core/helpers/is-empty-object.ts
@@ -0,0 +1,5 @@
+import {isObject} from './is-object';
+
+export function isEmptyObject(item: any) {
+  return isObject(item) && !Object.keys(item).length;
+}
diff --git a/packages/core/helpers/is-object.ts b/packages/core/helpers/is-object.ts
new file mode 100644
index 0000000..1f2819e
--- /dev/null
+++ b/packages/core/helpers/is-object.ts
@@ -0,0 +1,3 @@
+export function isObject(item: any) {
+  return typeof item === 'object' && item !== null;
+}
diff --git a/packages/core/helpers/is-scalar-type.ts b/packages/core/helpers/is-scalar-type.ts
new file mode 100644
index 0000000..8a5b481
--- /dev/null
+++ b/packages/core/helpers/is-scalar-type.ts
@@ -0,0 +1,7 @@
+export function isScalarType(item: unknown) {
+  return (
+    typeof item === 'string' ||
+    typeof item === 'number' ||
+    Buffer.isBuffer(item)
+  );
+}
diff --git a/packages/core/helpers/parse-key.ts b/packages/core/helpers/parse-key.ts
new file mode 100644
index 0000000..3b56a19
--- /dev/null
+++ b/packages/core/helpers/parse-key.ts
@@ -0,0 +1,26 @@
+const regexExp = {
+  interpolation: /\{{.+?\}}/g,
+  interpolatedWord: /\{{(.+?)\}}/,
+};
+
+export function parseKey<Entity>(key: string, dict: Entity) {
+  return key.replace(regexExp.interpolation, substr => {
+    const match = regexExp.interpolatedWord.exec(substr);
+    let variable: any;
+    if (match) {
+      variable = match[1];
+    }
+    if (!match || !variable) {
+      throw new Error(
+        `Failed to parse expression: "${key}", could not find a variable inside "${substr}"`
+      );
+    }
+
+    const valueToReplace = (dict as any)[variable];
+    if (valueToReplace === undefined || valueToReplace === null) {
+      throw new Error(`Could not resolve "${variable}" from given dictionary`);
+    }
+
+    return valueToReplace.toString();
+  });
+}
diff --git a/packages/core/helpers/validate-key.ts b/packages/core/helpers/validate-key.ts
new file mode 100644
index 0000000..63af1f4
--- /dev/null
+++ b/packages/core/helpers/validate-key.ts
@@ -0,0 +1,39 @@
+import {regexInterpolatedWord} from './constants';
+
+/**
+ *
+ * @param key key to validate
+ * @param dict dictionary to validate key against, dictionary of attribute { name: type }
+ */
+export function validateKey(key: string, dict: {[key: string]: string}) {
+  const matchIterator = key.matchAll(regexInterpolatedWord);
+  validateMatch(key, matchIterator, dict);
+}
+
+function validateMatch(
+  key: string,
+  iterator: IterableIterator<RegExpMatchArray>,
+  dict: {[key: string]: string}
+) {
+  const next = iterator.next();
+  if (next.done) {
+    return;
+  }
+
+  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') {
+    throw new Error(
+      `"${variable}" is used in key ${key}, thus it's type must be a "Number" or a "String"`
+    );
+  }
+
+  validateMatch(key, iterator, dict);
+}
diff --git a/packages/core/index.ts b/packages/core/index.ts
new file mode 100644
index 0000000..bcd5271
--- /dev/null
+++ b/packages/core/index.ts
@@ -0,0 +1,40 @@
+import {ConnectionOptions} from './classes/connection/connection-options';
+import {Container} from './classes/container';
+import {ConnectionManager} from './classes/connection/connection-manager';
+
+// classes
+export * from './classes/condition/condition';
+export * from './classes/condition/key-condition';
+export * from './classes/connection/connection-options';
+export * from './classes/transaction/write-transaction';
+export * from './classes/manager/entity-manager';
+export * from './classes/manager/transaction-manager';
+
+// public method exports
+
+export function createConnection(options: ConnectionOptions) {
+  const connection = connectionManger().create(options);
+  return connection.connect();
+}
+
+export function createConnections(optionsList: ConnectionOptions[]) {
+  return optionsList.map(options => createConnection(options));
+}
+
+export function getConnection() {
+  return Container.get(ConnectionManager).get();
+}
+
+export function getEntityManager(connectionName?: string) {
+  return connectionManger().get(connectionName).entityManager;
+}
+
+export function getTransactionManger(connectionName?: string) {
+  return connectionManger().get(connectionName).transactionManger;
+}
+
+// private methods
+
+function connectionManger() {
+  return Container.get(ConnectionManager);
+}
diff --git a/packages/core/package.json b/packages/core/package.json
new file mode 100644
index 0000000..939cb57
--- /dev/null
+++ b/packages/core/package.json
@@ -0,0 +1,32 @@
+{
+  "name": "@typedorm/core",
+  "version": "0.0.0-PLACEHOLDER",
+  "description": "TypeDORM - Core classes",
+  "keywords": [
+    "core",
+    "typedorm",
+    "dynamoDb orm",
+    "orm"
+  ],
+  "peerDependencies": {
+    "@typedorm/common": "0.0.0-PLACEHOLDER",
+    "reflect-metadata": "^0.1.13",
+    "aws-sdk": "^2.799.0"
+  },
+  "dependencies": {
+    "uuid": "^8.3.1",
+    "ksuid": "^2.0.0",
+    "fast-glob": "^3.2.4"
+  },
+  "publishConfig": {
+    "tag": "TAG-PLACEHOLDER",
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/typedorm/typedorm.git",
+    "directory": "packages/core"
+  },
+  "author": "typedorm",
+  "license": "MIT"
+}
diff --git a/packages/core/static/created.ts b/packages/core/static/created.ts
new file mode 100644
index 0000000..e8a9937
--- /dev/null
+++ b/packages/core/static/created.ts
@@ -0,0 +1,3 @@
+export class Created {
+  created: boolean;
+}
diff --git a/tools/defaults.bzl b/tools/defaults.bzl
index c487b83..4481de0 100644
--- a/tools/defaults.bzl
+++ b/tools/defaults.bzl
@@ -14,7 +14,7 @@ def setup_ts_build(name, deps = [], **kwargs):
             [
                 "**/*.ts",
             ],
-            exclude = ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"],
+            exclude = ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "**/__mocks__/*"],
         ),
         tsconfig = "//:tsconfig.json",
         deps = deps + [
diff --git a/tools/pkg.bzl b/tools/pkg.bzl
index 7aa0f29..22493f4 100644
--- a/tools/pkg.bzl
+++ b/tools/pkg.bzl
@@ -20,6 +20,6 @@ def typedorm_package(name, readme_md, data = [], deps = [], **kwargs):
             "0.0.0-PLACEHOLDER": "{BUILD_SCM_VERSION}"
         },
         deps = [
-            ":license_copied",
+            ":license_copied"
         ] + deps,
     )
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 51e1f47..9b36c33 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,14 @@
 {
   "compilerOptions": {
-    "lib": ["es2015.promise", "es5", "es6", "es2017.object", "ES2018.Regexp"],
+    "lib": [
+      "es2015.promise",
+      "es5",
+      "es6",
+      "es2017.object",
+      "ES2018.Regexp",
+      "es2019",
+      "ES2020.String"
+    ],
     "target": "ES2015",
     "module": "commonjs",
     "moduleResolution": "node",
@@ -20,7 +28,11 @@
     "inlineSources": true,
     "experimentalDecorators": true,
     "strictPropertyInitialization": false,
-    "baseUrl": "."
+    "emitDecoratorMetadata": true,
+    "baseUrl": ".",
+    "paths": {
+      "@typedorm/*": ["packages/*"]
+    }
   },
   "exclude": ["dist", "node_modules"]
 }


Test patch:
diff --git a/packages/common/decorators/__test__/attribute.decorator.spec.ts b/packages/common/decorators/__test__/attribute.decorator.spec.ts
new file mode 100644
index 0000000..f547ff2
--- /dev/null
+++ b/packages/common/decorators/__test__/attribute.decorator.spec.ts
@@ -0,0 +1,32 @@
+import {MetadataManager} from '../../metadata-manager';
+import {Attribute} from '../attribute.decorator';
+import {Entity} from '../entity.decorator';
+
+beforeEach(() => {
+  MetadataManager.createMetadataStorage();
+});
+
+afterEach(() => {
+  MetadataManager.resetMetadata();
+});
+
+test('adds raw metadata', () => {
+  @Entity({
+    name: 'user',
+    primaryKey: {
+      partitionKey: 'PK',
+    },
+  })
+  class User {
+    @Attribute()
+    readonly role: string;
+  }
+  expect(
+    MetadataManager.metadataStorage.getRawAttributesForEntity(User)
+  ).toEqual([
+    {
+      name: 'role',
+      type: 'String',
+    },
+  ]);
+});
diff --git a/packages/common/decorators/__test__/entity.decorator.spec.ts b/packages/common/decorators/__test__/entity.decorator.spec.ts
new file mode 100644
index 0000000..05c0eae
--- /dev/null
+++ b/packages/common/decorators/__test__/entity.decorator.spec.ts
@@ -0,0 +1,32 @@
+import {MetadataManager} from '../../metadata-manager';
+import {Entity} from '../entity.decorator';
+
+beforeEach(() => {
+  MetadataManager.createMetadataStorage();
+});
+
+afterEach(() => {
+  MetadataManager.resetMetadata();
+});
+
+test('adds raw metadata', () => {
+  @Entity({
+    name: 'user',
+    primaryKey: {
+      partitionKey: 'USER#{{id}}',
+    },
+  })
+  class User {}
+
+  const userRawMetadata = MetadataManager.metadataStorage.getRawEntityByTarget(
+    User
+  );
+
+  expect(userRawMetadata).toEqual({
+    name: 'user',
+    primaryKey: {
+      partitionKey: 'USER#{{id}}',
+    },
+    target: User,
+  });
+});
diff --git a/packages/common/index.test.ts b/packages/common/index.test.ts
deleted file mode 100644
index 6817e8b..0000000
--- a/packages/common/index.test.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-test('Common works', () => {
-  expect(true).toBeTruthy();
-});
diff --git a/packages/core/classes/__test__/container.spec.ts b/packages/core/classes/__test__/container.spec.ts
new file mode 100644
index 0000000..c69c2cd
--- /dev/null
+++ b/packages/core/classes/__test__/container.spec.ts
@@ -0,0 +1,7 @@
+import {Container} from '../container';
+import {ConnectionManager} from '../connection/connection-manager';
+
+test('returns default connection', () => {
+  const connectionInstance = Container.get(ConnectionManager);
+  expect(connectionInstance instanceof ConnectionManager).toBeTruthy();
+});
diff --git a/packages/core/classes/__test__/expression-builder.spec.ts b/packages/core/classes/__test__/expression-builder.spec.ts
new file mode 100644
index 0000000..f81e323
--- /dev/null
+++ b/packages/core/classes/__test__/expression-builder.spec.ts
@@ -0,0 +1,89 @@
+import {Condition} from '../condition/condition';
+import {ExpressionBuilder} from '../expression-builder';
+
+const expressionBuilder = new ExpressionBuilder();
+
+/**
+ * @group buildConditionExpression
+ */
+test('builds condition expression', () => {
+  const conditionExpression = expressionBuilder.buildConditionExpression(
+    new Condition().attributeNotExist('PK')
+  );
+
+  expect(conditionExpression).toEqual({
+    ConditionExpression: 'attribute_not_exists(#CE_PK)',
+    ExpressionAttributeNames: {
+      '#CE_PK': 'PK',
+    },
+  });
+});
+
+/**
+ * @group buildUpdateExpression
+ */
+test('builds update expression', () => {
+  const item = {
+    name: 'new name',
+  };
+  const expression = expressionBuilder.buildUpdateExpression(item);
+  expect(expression).toEqual({
+    UpdateExpression: 'SET #attr0 = :val0',
+    ExpressionAttributeNames: {
+      '#attr0': 'name',
+    },
+    ExpressionAttributeValues: {
+      ':val0': 'new name',
+    },
+  });
+});
+
+test('builds update expression for nested object', () => {
+  const item = {
+    'user.name': 'new name',
+  };
+  const expression = expressionBuilder.buildUpdateExpression(item);
+  expect(expression).toEqual({
+    UpdateExpression: 'SET #attr0_inner0.#attr0_inner1 = :val0',
+    ExpressionAttributeNames: {
+      '#attr0_inner0': 'user',
+      '#attr0_inner1': 'name',
+    },
+    ExpressionAttributeValues: {
+      ':val0': 'new name',
+    },
+  });
+});
+
+test('builds update expression for complex nested object', () => {
+  const item = {
+    application: 'test',
+    'profile.name.last': 'new Last name',
+    data: [1, 2, 3],
+    'address[0]': 'new address portion',
+    'complex.nested.object[1]': 'new value',
+  };
+  const expression = expressionBuilder.buildUpdateExpression(item);
+  expect(expression).toEqual({
+    UpdateExpression:
+      'SET #attr0 = :val0, #attr1_inner0.#attr1_inner1.#attr1_inner2 = :val1, #attr2 = :val2, #attr3 = :val3, #attr4_inner0.#attr4_inner1.#attr4_inner2 = :val4',
+    ExpressionAttributeNames: {
+      '#attr0': 'application',
+      '#attr1_inner0': 'profile',
+      '#attr1_inner1': 'name',
+      '#attr1_inner2': 'last',
+      '#attr2': 'data',
+      '#attr3': 'address[0]',
+      '#attr4_inner0': 'complex',
+      '#attr4_inner1': 'nested',
+      '#attr4_inner2': 'object[1]',
+    },
+    ExpressionAttributeValues: {
+      ':val0': 'test',
+      ':val1': 'new Last name',
+      ':val2': [1, 2, 3],
+      ':val3': 'new address portion',
+      ':val4': 'new value',
+    },
+  });
+});
diff --git a/packages/core/classes/__test__/metadata-storage.spec.ts b/packages/core/classes/__test__/metadata-storage.spec.ts
new file mode 100644
index 0000000..2b8d294
--- /dev/null
+++ b/packages/core/classes/__test__/metadata-storage.spec.ts
@@ -0,0 +1,53 @@
+import {MetadataStorage} from '@typedorm/common/metadata-storage';
+import {Table} from '@typedorm/common';
+
+let storage: MetadataStorage;
+class User {}
+const table = new Table({
+  name: 'test',
+  partitionKey: 'PK',
+});
+
+beforeEach(() => {
+  storage = new MetadataStorage();
+});
+
+test('creates global metadata storage', () => {
+  expect(storage).toBeDefined();
+});
+
+test('stores entity decorator', () => {
+  storage.addRawEntity({
+    name: 'user',
+
+    primaryKey: {
+      partitionKey: 'USER#1',
+    },
+    table,
+    target: User,
+  });
+
+  expect(storage.entities).toEqual([
+    {
+      name: 'user',
+      primaryKey: {partitionKey: 'USER#1'},
+      table: {options: {name: 'test', partitionKey: 'PK'}},
+      target: User,
+    },
+  ]);
+});
+
+test('adds attribute for entity', () => {
+  storage.addRawAttribute(User, {
+    name: 'id',
+    type: 'String',
+  });
+  expect(storage.attributes).toEqual([
+    [
+      {
+        name: 'id',
+        type: 'String',
+      },
+    ],
+  ]);
+});
diff --git a/packages/core/classes/condition/__test__/base-condition.spec.ts b/packages/core/classes/condition/__test__/base-condition.spec.ts
new file mode 100644
index 0000000..0ae647f
--- /dev/null
+++ b/packages/core/classes/condition/__test__/base-condition.spec.ts
@@ -0,0 +1,127 @@
+import {BaseCondition, MERGE_STRATEGY} from '../base-condition';
+
+class TestCondition extends BaseCondition {
+  protected getExpNameKey(key: string): string {
+    return `#TC_${key}`;
+  }
+  protected getExpValueKey(key: string): string {
+    return `:TC_${key}`;
+  }
+  constructor() {
+    super();
+  }
+}
+
+describe('BaseCondition', () => {
+  it('should correctly chain conditions', () => {
+    const condition = new TestCondition()
+      .equals('name', 'Boi')
+      .and()
+      .lessThan('age', 18)
+      .or()
+      .between('score', [12, 100]);
+
+    expect(condition).toEqual({
+      expression:
+        '#TC_name = :TC_name AND #TC_age < :TC_age OR #TC_score BETWEEN :TC_score_start AND :TC_score_end',
+      _names: {
+        '#TC_age': 'age',
+        '#TC_name': 'name',
+        '#TC_score': 'score',
+      },
+      _values: {
+        ':TC_age': 18,
+        ':TC_name': 'Boi',
+        ':TC_score_end': 100,
+        ':TC_score_start': 12,
+      },
+    });
+  });
+  describe('merge()', () => {
+    it('should correctly merge multiple expressions with AND strategy', () => {
+      const firstCondition = new TestCondition().equals('a', 1);
+      const secondCondition = new TestCondition().equals('b', 2);
+      const newCondition = firstCondition.merge(secondCondition);
+      expect(newCondition).toEqual({
+        expression: '#TC_a = :TC_a AND #TC_b = :TC_b',
+        _names: {
+          '#TC_a': 'a',
+          '#TC_b': 'b',
+        },
+        _values: {
+          ':TC_a': 1,
+          ':TC_b': 2,
+        },
+      });
+    });
+
+    it('should correctly merge multiple expressions with OR strategy', () => {
+      const firstCondition = new TestCondition().equals('a', 1);
+      const secondCondition = new TestCondition().equals('b', 2);
+      const newCondition = firstCondition.merge(
+        secondCondition,
+        MERGE_STRATEGY.OR
+      );
+      expect(newCondition).toEqual({
+        expression: '#TC_a = :TC_a OR #TC_b = :TC_b',
+        _names: {
+          '#TC_a': 'a',
+          '#TC_b': 'b',
+        },
+        _values: {
+          ':TC_a': 1,
+          ':TC_b': 2,
+        },
+      });
+    });
+  });
+
+  describe('between()', () => {
+    it('should correctly generate between expression', () => {
+      const condition = new TestCondition().between('date', [
+        'yesterday',
+        'today',
+      ]);
+      expect(condition).toEqual({
+        expression: '#TC_date BETWEEN :TC_date_start AND :TC_date_end',
+        _names: {
+          '#TC_date': 'date',
+        },
+        _values: {
+          ':TC_date_end': 'today',
+          ':TC_date_start': 'yesterday',
+        },
+      });
+    });
+  });
+
+  describe('beginsWith()', () => {
+    it('should correctly generate beginsWith expression', () => {
+      const condition = new TestCondition().beginsWith('date', 'USER#');
+      expect(condition).toEqual({
+        expression: 'begins_with(#TC_date, :TC_date)',
+        _names: {
+          '#TC_date': 'date',
+        },
+        _values: {
+          ':TC_date': 'USER#',
+        },
+      });
+    });
+  });
+
+  describe('equals()', () => {
+    it('should correctly generate equals expression', () => {
+      const condition = new TestCondition().equals('date', '2018-10-22');
+      expect(condition).toEqual({
+        expression: '#TC_date = :TC_date',
+        _names: {
+          '#TC_date': 'date',
+        },
+        _values: {
+          ':TC_date': '2018-10-22',
+        },
+      });
+    });
+  });
+});
diff --git a/packages/core/classes/condition/__test__/condition.spec.ts b/packages/core/classes/condition/__test__/condition.spec.ts
new file mode 100644
index 0000000..1e07b40
--- /dev/null
+++ b/packages/core/classes/condition/__test__/condition.spec.ts
@@ -0,0 +1,11 @@
+import {Condition} from '../condition';
+
+test('create condition expression with given condition', () => {
+  const condition = new Condition().attributeNotExist('PK');
+  expect(condition).toEqual({
+    expression: 'attribute_not_exists(#CE_PK)',
+    _names: {
+      '#CE_PK': 'PK',
+    },
+  });
+});
diff --git a/packages/core/classes/connection/__test__/connection-manager.spec.ts b/packages/core/classes/connection/__test__/connection-manager.spec.ts
new file mode 100644
index 0000000..eba11a8
--- /dev/null
+++ b/packages/core/classes/connection/__test__/connection-manager.spec.ts
@@ -0,0 +1,99 @@
+import {Attribute, Entity, Table} from '@typedorm/common';
+import {getConnection} from '@typedorm/core';
+import {User} from '@typedorm/core/__mocks__/user';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import path from 'path';
+jest.useFakeTimers('modern').setSystemTime(new Date('2020-01-01'));
+
+beforeEach(() => {
+  createTestConnection({
+    entities: [User],
+  });
+});
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('Connection Manager should register metadata correctly', () => {
+  const userMetadata = getConnection().getEntityByTarget(User);
+  expect(userMetadata.schema).toEqual({
+    indexes: {
+      GSI1: {
+        GSI1PK: 'USER#STATUS#{{status}}',
+        GSI1SK: 'USER#{{name}}',
+        _interpolations: {
+          GSI1PK: ['status'],
+          GSI1SK: ['name'],
+        },
+        _name: 'GSI1',
+        type: 'GLOBAL_SECONDARY_INDEX',
+      },
+    },
+    primaryKey: {
+      PK: 'USER#{{id}}',
+      SK: 'USER#{{id}}',
+      _interpolations: {
+        PK: ['id'],
+        SK: ['id'],
+      },
+    },
+  });
+});
+
+test('Connection Manager should register metadata for entity loaded from path', () => {
+  resetTestConnection();
+  createTestConnection({
+    entities: path.resolve(__dirname, ' ../../../../../__mocks__/**.ts'),
+  });
+
+  const userMetadata = getConnection().getEntityByTarget(User);
+  expect(userMetadata.schema).toEqual({
+    indexes: {
+      GSI1: {
+        GSI1PK: 'USER#STATUS#{{status}}',
+        GSI1SK: 'USER#{{name}}',
+        _interpolations: {
+          GSI1PK: ['status'],
+          GSI1SK: ['name'],
+        },
+        _name: 'GSI1',
+        type: 'GLOBAL_SECONDARY_INDEX',
+      },
+    },
+    primaryKey: {
+      PK: 'USER#{{id}}',
+      SK: 'USER#{{id}}',
+      _interpolations: {
+        PK: ['id'],
+        SK: ['id'],
+      },
+    },
+  });
+});
+
+test('Connection manager should properly configure global table instance for single table modeling to work ', () => {
+  @Entity({
+    name: 'anotherUser',
+    primaryKey: {
+      partitionKey: 'ANOTHER_USER#{{id}}',
+    },
+  })
+  class AnotherUser {
+    @Attribute()
+    id: string;
+  }
+
+  const globalTable = new Table({
+    name: 'Global Table',
+    partitionKey: 'PK',
+  });
+  resetTestConnection();
+  createTestConnection({
+    entities: [AnotherUser],
+    table: globalTable,
+  });
+
+  const userMetadata = getConnection().getEntityByTarget(AnotherUser);
+
+  expect(userMetadata.table).toEqual(globalTable);
+});
diff --git a/packages/core/classes/connection/__test__/connection-metadata-builder.spec.ts b/packages/core/classes/connection/__test__/connection-metadata-builder.spec.ts
new file mode 100644
index 0000000..99e1f86
--- /dev/null
+++ b/packages/core/classes/connection/__test__/connection-metadata-builder.spec.ts
@@ -0,0 +1,39 @@
+import {UserUniqueEmail} from '../../../__mocks__/user-unique-email';
+import {User} from '@typedorm/core/__mocks__/user';
+import {Organisation} from '../../../__mocks__/organisation';
+import path from 'path';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {ConnectionMetadataBuilder} from '../connection-metadata-builder';
+
+let metadataBuilder: ConnectionMetadataBuilder;
+beforeEach(() => {
+  const connection = createTestConnection({
+    entities: [],
+  });
+  metadataBuilder = new ConnectionMetadataBuilder(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+/**
+ * @group buildEntityMetadatas
+ */
+test('builds entity metadata from list of entities', () => {
+  const entities = metadataBuilder.buildEntityMetadatas([
+    User,
+    Organisation,
+    UserUniqueEmail,
+  ]);
+
+  expect(entities.length).toEqual(3);
+});
+
+test('builds entity metadata with path match', () => {
+  const entities = metadataBuilder.buildEntityMetadatas(
+    path.resolve(__dirname, '../../../__mocks__/**/*.ts')
+  );
+
+  expect(entities.length).toEqual(4);
+});
diff --git a/packages/core/classes/connection/__test__/entity-metadata-builder.spec.ts b/packages/core/classes/connection/__test__/entity-metadata-builder.spec.ts
new file mode 100644
index 0000000..26f5a4f
--- /dev/null
+++ b/packages/core/classes/connection/__test__/entity-metadata-builder.spec.ts
@@ -0,0 +1,41 @@
+import {User} from '../../../__mocks__/user';
+import {EntityMetadataBuilder} from '../entity-metadata-builder';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+
+let metadataBuilder: EntityMetadataBuilder;
+beforeEach(() => {
+  const connection = createTestConnection({
+    entities: [],
+  });
+  metadataBuilder = new EntityMetadataBuilder(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('builds entity metadata from decorated raw metadatas', () => {
+  const [metadata] = metadataBuilder.build([User]);
+  expect(metadata.schema).toEqual({
+    indexes: {
+      GSI1: {
+        GSI1PK: 'USER#STATUS#{{status}}',
+        GSI1SK: 'USER#{{name}}',
+        _interpolations: {
+          GSI1PK: ['status'],
+          GSI1SK: ['name'],
+        },
+        _name: 'GSI1',
+        type: 'GLOBAL_SECONDARY_INDEX',
+      },
+    },
+    primaryKey: {
+      PK: 'USER#{{id}}',
+      SK: 'USER#{{id}}',
+      _interpolations: {
+        PK: ['id'],
+        SK: ['id'],
+      },
+    },
+  });
+});
diff --git a/packages/core/classes/manager/__test__/entity-manager.spec.ts b/packages/core/classes/manager/__test__/entity-manager.spec.ts
new file mode 100644
index 0000000..799ac13
--- /dev/null
+++ b/packages/core/classes/manager/__test__/entity-manager.spec.ts
@@ -0,0 +1,545 @@
+// This needs to be at the top of the file before
+// any other modules are imported that might
+// bring in original instance of mock
+const dcMock = jest.fn().mockReturnValue({
+  put: jest.fn(),
+  get: jest.fn(),
+  update: jest.fn(),
+  delete: jest.fn(),
+  query: jest.fn(),
+});
+jest.mock('aws-sdk', () => ({
+  DynamoDB: {
+    DocumentClient: dcMock,
+  },
+}));
+
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {EntityManager} from '../entity-manager';
+import {User, UserPrimaryKey} from '../../../__mocks__/user';
+import {
+  UserUniqueEmail,
+  UserUniqueEmailPrimaryKey,
+} from '@typedorm/core/__mocks__/user-unique-email';
+import {
+  UserAutoGenerateAttributesPrimaryKey,
+  UserAutoGenerateAttributes,
+} from '@typedorm/core/__mocks__/user-auto-generate-attributes';
+
+let manager: EntityManager;
+beforeEach(() => {
+  const connection = createTestConnection({
+    entities: [User, UserUniqueEmail, UserAutoGenerateAttributes],
+  });
+  manager = new EntityManager(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+/**
+ * @group create
+ */
+test('creates entity', async () => {
+  dcMock().put.mockReturnValue({
+    promise: () => ({}),
+  });
+
+  const user = new User();
+  user.id = '1';
+  user.name = 'Test User';
+  user.status = 'active';
+
+  const userEntity = await manager.create(user);
+  expect(dcMock().put).toHaveBeenCalledTimes(1);
+  expect(dcMock().put).toHaveBeenCalledWith({
+    Item: {
+      GSI1PK: 'USER#STATUS#active',
+      GSI1SK: 'USER#Test User',
+      PK: 'USER#1',
+      SK: 'USER#1',
+      id: '1',
+      name: 'Test User',
+      status: 'active',
+    },
+    TableName: 'test-table',
+    ConditionExpression: 'attribute_not_exists(#CE_PK)',
+    ExpressionAttributeNames: {
+      '#CE_PK': 'PK',
+    },
+  });
+  expect(userEntity).toEqual({
+    id: '1',
+    name: 'Test User',
+    status: 'active',
+  });
+});
+
+/**
+ * @group findOne
+ */
+test('finds one entity by given primary key', async () => {
+  dcMock().get.mockReturnValue({
+    promise: () => ({
+      Item: {
+        PK: 'USER#1',
+        SK: 'USER#1',
+        GSI1PK: 'USER#STATUS#active',
+        GSI1SK: 'USER#Me',
+        id: '1',
+        name: 'Me',
+        status: 'active',
+      },
+    }),
+  });
+
+  const userEntity = await manager.findOne<UserPrimaryKey, User>(User, {
+    id: '1',
+  });
+  expect(dcMock().get).toHaveBeenCalledTimes(1);
+  expect(dcMock().get).toHaveBeenCalledWith({
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    TableName: 'test-table',
+  });
+  expect(userEntity).toEqual({
+    id: '1',
+    name: 'Me',
+    status: 'active',
+  });
+});
+
+test('throws an error when trying to do a get request with non primary key attributes', async () => {
+  await expect(
+    manager.findOne(User, {
+      name: 'User',
+    })
+  ).rejects.toThrowError('Could not resolve "id" from given dictionary');
+});
+
+/**
+ * @group exists
+ */
+test('checks if given item exists', async () => {
+  dcMock().get.mockReturnValue({
+    promise: () => ({
+      Item: {
+        PK: 'USER#1',
+        SK: 'USER#1',
+        GSI1PK: 'USER#STATUS#active',
+        GSI1SK: 'USER#Me',
+        id: '1',
+        name: 'Me',
+        status: 'active',
+      },
+    }),
+  });
+
+  const userEntity = await manager.exists<User, UserUniqueEmailPrimaryKey>(
+    User,
+    {
+      id: '1',
+    }
+  );
+
+  expect(dcMock().get).toHaveBeenCalledWith({
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    TableName: 'test-table',
+  });
+  expect(userEntity).toEqual(true);
+});
+
+test('checks if item with given unique attribute exists', async () => {
+  const userEntity = await manager.exists<UserUniqueEmail>(UserUniqueEmail, {
+    email: 'user@example.com',
+  });
+
+  expect(dcMock().get).toHaveBeenCalledWith({
+    Key: {
+      PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#user@example.com',
+      SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#user@example.com',
+    },
+    TableName: 'test-table',
+  });
+  expect(userEntity).toEqual(true);
+});
+
+test('throws an error if trying to perform exists check with non key or non unique attributes', async () => {
+  expect(dcMock().get).not.toHaveBeenCalled();
+  await expect(
+    async () =>
+      await manager.exists<UserUniqueEmail>(UserUniqueEmail, {
+        status: 'active',
+      })
+  ).rejects.toThrowError(
+    'Only attributes that are part of primary key or is marked as unique attribute can be queried, attribute "status is neither."'
+  );
+});
+
+test('throws an error if trying to perform exists check with partial primary key', async () => {
+  await expect(
+    manager.findOne(User, {
+      name: 'User',
+    })
+  ).rejects.toThrowError('Could not resolve "id" from given dictionary');
+});
+
+/**
+ * @group update
+ */
+test('updates item', async () => {
+  dcMock().update.mockReturnValue({
+    promise: () => ({
+      Attributes: {
+        PK: 'USER#1',
+        SK: 'USER#1',
+        GSI1PK: 'USER#STATUS#active',
+        GSI1SK: 'USER#Me',
+        id: '1',
+        name: 'Me',
+        status: 'active',
+      },
+    }),
+  });
+  const updatedItem = await manager.update<UserPrimaryKey, User>(
+    User,
+    {id: '1'},
+    {
+      name: 'user',
+      status: 'active',
+    }
+  );
+
+  expect(dcMock().update).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#attr0': 'name',
+      '#attr1': 'status',
+      '#attr2': 'GSI1SK',
+      '#attr3': 'GSI1PK',
+    },
+    ExpressionAttributeValues: {
+      ':val0': 'user',
+      ':val1': 'active',
+      ':val2': 'USER#user',
+      ':val3': 'USER#STATUS#active',
+    },
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    ReturnValues: 'ALL_NEW',
+    TableName: 'test-table',
+    UpdateExpression:
+      'SET #attr0 = :val0, #attr1 = :val1, #attr2 = :val2, #attr3 = :val3',
+  });
+  expect(updatedItem).toEqual({id: '1', name: 'Me', status: 'active'});
+});
+
+test('updates item and attributes marked to be autoUpdated', async () => {
+  jest.useFakeTimers('modern').setSystemTime(new Date('2020-01-01'));
+
+  dcMock().update.mockReturnValue({
+    promise: () => ({
+      Attributes: {
+        PK: 'USER#1',
+        SK: 'USER#1',
+        GSI1PK: 'USER#STATUS#active',
+        GSI1SK: 'USER#Me',
+        id: '1',
+        name: 'Me',
+        status: 'active',
+      },
+    }),
+  });
+
+  const updatedItem = await manager.update<
+    UserAutoGenerateAttributesPrimaryKey,
+    UserAutoGenerateAttributes
+  >(
+    UserAutoGenerateAttributes,
+    {id: '1'},
+    {},
+    {
+      nestedKeySeparator: '.',
+    }
+  );
+
+  expect(dcMock().update).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#attr0': 'updatedAt',
+      '#attr1': 'GSI1PK',
+    },
+    ExpressionAttributeValues: {
+      ':val0': 1577836800,
+      ':val1': 'USER#UPDATED_AT#1577836800',
+    },
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    ReturnValues: 'ALL_NEW',
+    TableName: 'test-table',
+    UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+  });
+  expect(updatedItem).toEqual({id: '1', name: 'Me', status: 'active'});
+});
+
+/**
+ * @group delete
+ */
+test('deletes item by primary key', async () => {
+  dcMock().delete.mockReturnValue({
+    promise: jest.fn().mockReturnValue({
+      Attributes: {},
+    }),
+  });
+
+  const result = await manager.delete<UserPrimaryKey, User>(User, {
+    id: '1',
+  });
+
+  expect(dcMock().delete).toHaveBeenCalledWith({
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    TableName: 'test-table',
+  });
+  expect(result).toEqual({
+    success: true,
+  });
+});
+
+test('throws an error when trying to delete item by non primary key attributes', async () => {
+  await expect(
+    manager.delete(User, {
+      name: 'User',
+    })
+  ).rejects.toThrowError('Could not resolve "id" from given dictionary');
+});
+
+/**
+ * @group find
+ */
+test('finds items matching given query params', async () => {
+  dcMock().query.mockReturnValue({
+    promise: jest.fn().mockReturnValue({
+      Items: [
+        {
+          PK: 'USER#1',
+          SK: 'USER#1',
+          GSI1PK: 'USER#STATUS#active',
+          GSI1SK: 'USER#Me',
+          id: '1',
+          name: 'Me',
+          status: 'active',
+        },
+        {
+          PK: 'USER#2',
+          SK: 'USER#2',
+          GSI1PK: 'USER#STATUS#active',
+          GSI1SK: 'USER#Me2',
+          id: '2',
+          name: 'Me',
+          status: 'active',
+        },
+      ],
+    }),
+  });
+
+  const users = await manager.find<User, UserPrimaryKey>(
+    User,
+    {
+      id: 'aaaa',
+    },
+    {
+      keyCondition: {
+        BEGINS_WITH: 'USER#',
+      },
+      limit: 10,
+    }
+  );
+
+  expect(dcMock().query).toHaveBeenCalledTimes(1);
+  expect(dcMock().query).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#KY_CE_PK': 'PK',
+      '#KY_CE_SK': 'SK',
+    },
+    ExpressionAttributeValues: {
+      ':KY_CE_PK': 'USER#aaaa',
+      ':KY_CE_SK': 'USER#',
+    },
+    KeyConditionExpression:
+      '#KY_CE_PK = :KY_CE_PK AND begins_with(#KY_CE_SK, :KY_CE_SK)',
+    Limit: 10,
+    ScanIndexForward: true,
+    TableName: 'test-table',
+  });
+  expect(users).toEqual({
+    items: [
+      {
+        id: '1',
+        name: 'Me',
+        status: 'active',
+      },
+      {
+        id: '2',
+        name: 'Me',
+        status: 'active',
+      },
+    ],
+  });
+});
+
+test('finds item from given cursor position', async () => {
+  dcMock().query.mockReturnValue({
+    promise: jest.fn().mockReturnValue({
+      Items: [
+        {
+          PK: 'USER#1',
+          SK: 'USER#1',
+          GSI1PK: 'USER#STATUS#active',
+          GSI1SK: 'USER#Me',
+          id: '1',
+          name: 'Me',
+          status: 'active',
+        },
+        {
+          PK: 'USER#2',
+          SK: 'USER#2',
+          GSI1PK: 'USER#STATUS#active',
+          GSI1SK: 'USER#Me2',
+          id: '2',
+          name: 'Me',
+          status: 'active',
+        },
+      ],
+    }),
+  });
+
+  await manager.find<User, UserPrimaryKey>(
+    User,
+    {
+      id: 'aaaa',
+    },
+    {
+      keyCondition: {
+        BEGINS_WITH: 'USER#',
+      },
+      limit: 10,
+      cursor: {
+        partitionKey: 'USER#1',
+        sortKey: 'USER#1',
+      },
+    }
+  );
+
+  expect(dcMock().query).toHaveBeenCalledWith({
+    ExclusiveStartKey: {
+      partitionKey: 'USER#1',
+      sortKey: 'USER#1',
+    },
+    ExpressionAttributeNames: {
+      '#KY_CE_PK': 'PK',
+      '#KY_CE_SK': 'SK',
+    },
+    ExpressionAttributeValues: {
+      ':KY_CE_PK': 'USER#aaaa',
+      ':KY_CE_SK': 'USER#',
+    },
+    KeyConditionExpression:
+      '#KY_CE_PK = :KY_CE_PK AND begins_with(#KY_CE_SK, :KY_CE_SK)',
+    Limit: 10,
+    ScanIndexForward: true,
+    TableName: 'test-table',
+  });
+});
+
+test('queries items until limit is met', async () => {
+  const itemsToReturn: any[] = [];
+  for (let index = 1; index <= 1000; index++) {
+    itemsToReturn.push({
+      id: index.toString(),
+      status: 'active',
+      PK: `USER#${index}`,
+      SK: `USER#${index}`,
+    });
+  }
+  dcMock()
+    .query.mockImplementationOnce(() => ({
+      promise: jest.fn().mockReturnValue({
+        Items: itemsToReturn,
+        Count: itemsToReturn.length,
+        LastEvaluatedKey: {
+          partitionKey: 'USER#1000',
+          sortKey: 'USER#1000',
+        },
+      }),
+    }))
+    .mockImplementationOnce(() => ({
+      promise: jest.fn().mockReturnValue({
+        Items: itemsToReturn,
+        Count: itemsToReturn.length,
+      }),
+    }));
+
+  const users = await manager.find<User, UserPrimaryKey>(
+    User,
+    {
+      id: '1',
+    },
+    {
+      keyCondition: {
+        BEGINS_WITH: 'USER#',
+      },
+      limit: 2000,
+    }
+  );
+
+  expect(dcMock().query).toHaveBeenCalledTimes(2);
+  expect(dcMock().query.mock.calls).toEqual([
+    [
+      {
+        ExpressionAttributeNames: {
+          '#KY_CE_PK': 'PK',
+          '#KY_CE_SK': 'SK',
+        },
+        ExpressionAttributeValues: {
+          ':KY_CE_PK': 'USER#1',
+          ':KY_CE_SK': 'USER#',
+        },
+        KeyConditionExpression:
+          '#KY_CE_PK = :KY_CE_PK AND begins_with(#KY_CE_SK, :KY_CE_SK)',
+        Limit: 2000,
+        ScanIndexForward: true,
+        TableName: 'test-table',
+      },
+    ],
+    [
+      {
+        ExclusiveStartKey: {
+          partitionKey: 'USER#1000',
+          sortKey: 'USER#1000',
+        },
+        ExpressionAttributeNames: {'#KY_CE_PK': 'PK', '#KY_CE_SK': 'SK'},
+        ExpressionAttributeValues: {
+          ':KY_CE_PK': 'USER#1',
+          ':KY_CE_SK': 'USER#',
+        },
+        KeyConditionExpression:
+          '#KY_CE_PK = :KY_CE_PK AND begins_with(#KY_CE_SK, :KY_CE_SK)',
+        Limit: 2000,
+        ScanIndexForward: true,
+        TableName: 'test-table',
+      },
+    ],
+  ]);
+  expect(users.items.length).toEqual(2000);
+});
diff --git a/packages/core/classes/manager/__test__/transaction-manager-write.spec.ts b/packages/core/classes/manager/__test__/transaction-manager-write.spec.ts
new file mode 100644
index 0000000..b3a4b16
--- /dev/null
+++ b/packages/core/classes/manager/__test__/transaction-manager-write.spec.ts
@@ -0,0 +1,150 @@
+// This needs to be at the top of the file before
+// any other modules are imported that might
+// bring in original instance of mock
+const dcMock = jest.fn().mockReturnValue({
+  transactWrite: jest.fn(),
+});
+jest.mock('aws-sdk', () => ({
+  DynamoDB: {
+    DocumentClient: dcMock,
+  },
+}));
+
+import {UserPrimaryKey} from '../../../__mocks__/user';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {Connection} from '../../connection/connection';
+import {WriteTransaction} from '../../transaction/write-transaction';
+import {TransactionManager} from '../transaction-manager';
+import {User} from '../../../__mocks__/user';
+
+let manager: TransactionManager;
+let connection: Connection;
+beforeEach(() => {
+  connection = createTestConnection({
+    entities: [User],
+  });
+  manager = new TransactionManager();
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+/**
+ * @group write
+ */
+test('performs write transactions', async () => {
+  dcMock().transactWrite.mockReturnValue({
+    promise: jest.fn().mockReturnValue({
+      ConsumedCapacity: [{}],
+      ItemCollectionMetrics: [{}],
+    }),
+    on: jest.fn(),
+    send: jest.fn().mockImplementation(cb => {
+      cb(null, {
+        ConsumedCapacity: [{}],
+        ItemCollectionMetrics: [{}],
+      });
+    }),
+  });
+
+  const user = new User();
+  user.id = '1';
+  user.name = 'user name';
+  user.status = 'inactive';
+
+  const newUser = new User();
+  newUser.id = '2';
+  newUser.name = 'user 2';
+  newUser.status = 'inactive';
+
+  const transaction = new WriteTransaction(connection)
+    .chian({
+      create: {
+        item: user,
+      },
+    })
+    .chian<UserPrimaryKey, User>({
+      update: {
+        item: User,
+        primaryKey: {
+          id: '1',
+        },
+        body: {
+          status: 'active',
+        },
+      },
+    })
+    .chian({
+      create: {
+        item: newUser,
+      },
+    });
+
+  const response = await manager.write(transaction);
+
+  expect(dcMock().transactWrite).toHaveBeenCalledTimes(1);
+  expect(dcMock().transactWrite).toHaveBeenCalledWith({
+    TransactItems: [
+      {
+        Put: {
+          Item: {
+            GSI1PK: 'USER#STATUS#inactive',
+            GSI1SK: 'USER#user name',
+            PK: 'USER#1',
+            SK: 'USER#1',
+            id: '1',
+            name: 'user name',
+            status: 'inactive',
+          },
+          TableName: 'test-table',
+          ConditionExpression: 'attribute_not_exists(#CE_PK)',
+          ExpressionAttributeNames: {
+            '#CE_PK': 'PK',
+          },
+        },
+      },
+      {
+        Update: {
+          ExpressionAttributeNames: {
+            '#attr0': 'status',
+            '#attr1': 'GSI1PK',
+          },
+          ExpressionAttributeValues: {
+            ':val0': 'active',
+            ':val1': 'USER#STATUS#active',
+          },
+          Key: {
+            PK: 'USER#1',
+            SK: 'USER#1',
+          },
+          ReturnValues: 'ALL_NEW',
+          TableName: 'test-table',
+          UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+        },
+      },
+      {
+        Put: {
+          Item: {
+            GSI1PK: 'USER#STATUS#inactive',
+            GSI1SK: 'USER#user 2',
+            PK: 'USER#2',
+            SK: 'USER#2',
+            id: '2',
+            name: 'user 2',
+            status: 'inactive',
+          },
+          TableName: 'test-table',
+          ConditionExpression: 'attribute_not_exists(#CE_PK)',
+          ExpressionAttributeNames: {
+            '#CE_PK': 'PK',
+          },
+        },
+      },
+    ],
+  });
+  expect(response).toEqual({
+    ConsumedCapacity: [{}],
+    ItemCollectionMetrics: [{}],
+  });
+});
diff --git a/packages/core/classes/metadata/__test__/attribute-metadata.spec.ts b/packages/core/classes/metadata/__test__/attribute-metadata.spec.ts
new file mode 100644
index 0000000..fc01157
--- /dev/null
+++ b/packages/core/classes/metadata/__test__/attribute-metadata.spec.ts
@@ -0,0 +1,24 @@
+import {Connection} from '../../connection/connection';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {AttributeMetadata} from '../attribute-metadata';
+
+let connection: Connection;
+beforeEach(() => {
+  connection = createTestConnection({
+    entities: [],
+  });
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('creates attribute metadata', () => {
+  const attMetadata = new AttributeMetadata({
+    connection,
+    name: 'id',
+    type: 'String',
+  });
+
+  expect(attMetadata).toBeTruthy();
+});
diff --git a/packages/core/classes/metadata/__test__/auto-generated-metadata.spec.ts b/packages/core/classes/metadata/__test__/auto-generated-metadata.spec.ts
new file mode 100644
index 0000000..306cf52
--- /dev/null
+++ b/packages/core/classes/metadata/__test__/auto-generated-metadata.spec.ts
@@ -0,0 +1,92 @@
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {Connection} from '../../connection/connection';
+import {AutoGeneratedAttributeMetadata} from '../auto-generated-attribute-metadata';
+import {AUTO_GENERATE_ATTRIBUTE_STRATEGY} from '@typedorm/common';
+
+jest.mock('uuid', () => ({
+  v4: () => 'c0ac5395-ba7c-41bf-bbc3-09a6087bcca2',
+}));
+jest.mock('ksuid', () => ({
+  randomSync: () => ({
+    string: '1irj9jenZH4PfTWp3NrDgpSF7lg',
+  }),
+}));
+
+let connection: Connection;
+beforeEach(() => {
+  connection = createTestConnection({
+    entities: [],
+  });
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('creates attribute metadata with uuid strategy', () => {
+  const metadata = new AutoGeneratedAttributeMetadata({
+    connection,
+    name: 'id',
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4,
+    type: 'String',
+  });
+  expect(metadata).toEqual(
+    expect.objectContaining({
+      name: 'id',
+      strategy: 'UUID4',
+      value: 'c0ac5395-ba7c-41bf-bbc3-09a6087bcca2',
+    })
+  );
+});
+
+test('creates attribute metadata with ksuid strategy', () => {
+  const metadata = new AutoGeneratedAttributeMetadata({
+    connection,
+    name: 'id',
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.KSUID,
+    type: 'String',
+  });
+  expect(metadata).toEqual(
+    expect.objectContaining({
+      name: 'id',
+      strategy: 'KSUID',
+      value: '1irj9jenZH4PfTWp3NrDgpSF7lg',
+    })
+  );
+});
+
+test('creates attribute metadata with epoch strategy', () => {
+  jest.useFakeTimers('modern').setSystemTime(new Date('2020-01-01'));
+
+  const metadata = new AutoGeneratedAttributeMetadata({
+    connection,
+    name: 'date',
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE,
+    type: 'String',
+  });
+  expect(metadata).toEqual(
+    expect.objectContaining({
+      name: 'date',
+      strategy: 'EPOCH_DATE',
+      value: 1577836800,
+    })
+  );
+});
+
+test('creates attribute metadata with iso strategy', () => {
+  jest.useFakeTimers('modern').setSystemTime(new Date('2020-01-01'));
+
+  const metadata = new AutoGeneratedAttributeMetadata({
+    connection,
+    name: 'date',
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.ISO_DATE,
+    type: 'String',
+  });
+  expect(metadata).toEqual(
+    expect.objectContaining({
+      name: 'date',
+      strategy: 'ISO_DATE',
+      value: '2020-01-01T00:00:00.000Z',
+    })
+  );
+});
diff --git a/packages/core/classes/metadata/__test__/entity-metadata.spec.ts b/packages/core/classes/metadata/__test__/entity-metadata.spec.ts
new file mode 100644
index 0000000..eef9284
--- /dev/null
+++ b/packages/core/classes/metadata/__test__/entity-metadata.spec.ts
@@ -0,0 +1,201 @@
+import {Attribute, Entity, INDEX_TYPE, Table} from '@typedorm/common';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {Connection} from '../../connection/connection';
+import {AttributeMetadata} from '../attribute-metadata';
+import {EntityMetadata} from '../entity-metadata';
+
+let connection: Connection;
+const table = new Table({
+  name: 'global',
+  partitionKey: 'PK',
+});
+@Entity({
+  name: 'User',
+  primaryKey: {
+    partitionKey: 'USER#{{id}}',
+  },
+  table,
+})
+class UserEntity {
+  @Attribute()
+  id: string;
+}
+
+beforeEach(() => {
+  connection = createTestConnection({
+    entities: [UserEntity],
+  });
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('create entity metadata for per entity table', () => {
+  const userEntityMetadata = new EntityMetadata({
+    connection,
+    table,
+    name: 'user',
+    primaryKey: {
+      partitionKey: 'USER#{{id}}',
+    },
+    target: UserEntity,
+    attributes: [
+      new AttributeMetadata({
+        connection,
+        name: 'id',
+        type: 'String',
+      }),
+    ],
+  });
+
+  expect(userEntityMetadata.schema).toEqual({
+    indexes: {},
+    primaryKey: {
+      PK: 'USER#{{id}}',
+      _interpolations: {
+        PK: ['id'],
+      },
+    },
+  });
+});
+
+test('creates entity metadata with global table config', () => {
+  const globalTable = new Table({
+    name: 'GlobalTable',
+    partitionKey: 'PK',
+  });
+  @Entity({
+    name: 'User',
+    primaryKey: {
+      partitionKey: 'USER#{{id}}',
+    },
+  })
+  class GlobalUserEntity {
+    @Attribute()
+    id: string;
+  }
+
+  connection = createTestConnection({
+    name: 'new-connection-0',
+    table: globalTable,
+    entities: [GlobalUserEntity],
+  });
+
+  const userEntityMetadata = new EntityMetadata({
+    connection,
+    name: 'user',
+    primaryKey: {
+      partitionKey: 'USER#{{id}}',
+    },
+    target: GlobalUserEntity,
+    attributes: [
+      new AttributeMetadata({
+        connection,
+        name: 'id',
+        type: 'String',
+      }),
+    ],
+  });
+
+  expect(userEntityMetadata.schema).toEqual({
+    indexes: {},
+    primaryKey: {
+      PK: 'USER#{{id}}',
+      _interpolations: {
+        PK: ['id'],
+      },
+    },
+  });
+});
+
+test('creates entity metadata with complex composite and indexes key', () => {
+  const complexTable = new Table({
+    name: 'Table',
+    partitionKey: 'PK',
+    sortKey: 'SK',
+    indexes: {
+      GSI1: {
+        partitionKey: 'GSI1PK',
+        sortKey: 'GSI1SK',
+        type: INDEX_TYPE.GSI,
+      },
+      LSI1: {
+        sortKey: 'LSI1SK',
+        type: INDEX_TYPE.LSI,
+      },
+    },
+  });
+
+  class ComplexUserEntity {}
+
+  const complexEntityMetadata = new EntityMetadata({
+    connection,
+    table: complexTable,
+    name: 'user',
+    primaryKey: {
+      partitionKey: 'USER#{{id}}',
+      sortKey: 'USER#{{id}}',
+    },
+    indexes: {
+      GSI1: {
+        partitionKey: 'USER#{{id}}#NAME#{{name}}',
+        sortKey: 'Name#{{name}}',
+        type: INDEX_TYPE.GSI,
+      },
+      LSI1: {
+        sortKey: 'AGE#{{age}}',
+        type: INDEX_TYPE.LSI,
+      },
+    },
+    target: ComplexUserEntity,
+    attributes: [
+      new AttributeMetadata({
+        connection,
+        name: 'id',
+        type: 'String',
+      }),
+      new AttributeMetadata({
+        connection,
+        name: 'name',
+        type: 'String',
+      }),
+      new AttributeMetadata({
+        connection,
+        name: 'age',
+        type: 'String',
+      }),
+    ],
+  });
+
+  expect(complexEntityMetadata.schema).toEqual({
+    indexes: {
+      GSI1: {
+        GSI1PK: 'USER#{{id}}#NAME#{{name}}',
+        GSI1SK: 'Name#{{name}}',
+        _interpolations: {
+          GSI1PK: ['id', 'name'],
+          GSI1SK: ['name'],
+        },
+        _name: 'GSI1',
+        type: 'GLOBAL_SECONDARY_INDEX',
+      },
+      LSI1: {
+        LSI1SK: 'AGE#{{age}}',
+        _interpolations: {
+          LSI1SK: ['age'],
+        },
+        _name: 'LSI1',
+        type: 'LOCAL_SECONDARY_INDEX',
+      },
+    },
+    primaryKey: {
+      PK: 'USER#{{id}}',
+      SK: 'USER#{{id}}',
+      _interpolations: {
+        PK: ['id'],
+        SK: ['id'],
+      },
+    },
+  });
+});
diff --git a/packages/core/classes/transaction/__test__/write-transaction.spec.ts b/packages/core/classes/transaction/__test__/write-transaction.spec.ts
new file mode 100644
index 0000000..caee895
--- /dev/null
+++ b/packages/core/classes/transaction/__test__/write-transaction.spec.ts
@@ -0,0 +1,175 @@
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {User, UserPrimaryKey} from '../../../__mocks__/user';
+import {UserUniqueEmail} from '../../../__mocks__/user-unique-email';
+import {WriteTransaction} from '../write-transaction';
+import {Connection} from '../../connection/connection';
+
+let writeTransaction: WriteTransaction;
+let connection: Connection;
+beforeEach(() => {
+  connection = createTestConnection({
+    entities: [User, UserUniqueEmail],
+  });
+  writeTransaction = new WriteTransaction(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('creates transaction item with existing items', () => {
+  const transaction = new WriteTransaction(connection, [
+    {
+      Put: {
+        Item: {
+          name: '1',
+        },
+        TableName: 'table',
+      },
+    },
+  ]);
+
+  expect(transaction.items).toEqual([
+    {Put: {Item: {name: '1'}, TableName: 'table'}},
+  ]);
+});
+
+/**
+ * @group chain
+ */
+test('chains simple transaction requests', () => {
+  const user = new User();
+  user.name = 'user-name';
+  user.id = '123';
+  user.status = 'active';
+
+  const transaction = writeTransaction
+    .chian({
+      create: {
+        item: user,
+      },
+    })
+    .chian<UserPrimaryKey, User>({
+      update: {
+        item: User,
+        primaryKey: {
+          id: '123',
+        },
+        body: {
+          name: 'new name',
+        },
+      },
+    });
+
+  expect(transaction.items).toEqual([
+    {
+      Put: {
+        Item: {
+          GSI1PK: 'USER#STATUS#active',
+          GSI1SK: 'USER#user-name',
+          PK: 'USER#123',
+          SK: 'USER#123',
+          id: '123',
+          name: 'user-name',
+          status: 'active',
+        },
+        TableName: 'test-table',
+        ConditionExpression: 'attribute_not_exists(#CE_PK)',
+        ExpressionAttributeNames: {
+          '#CE_PK': 'PK',
+        },
+      },
+    },
+    {
+      Update: {
+        ExpressionAttributeNames: {'#attr0': 'name', '#attr1': 'GSI1SK'},
+        ExpressionAttributeValues: {
+          ':val0': 'new name',
+          ':val1': 'USER#new name',
+        },
+        Key: {PK: 'USER#123', SK: 'USER#123'},
+        ReturnValues: 'ALL_NEW',
+        TableName: 'test-table',
+        UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+      },
+    },
+  ]);
+});
+
+test('chains complex transaction requests', () => {
+  const user = new UserUniqueEmail();
+  user.id = '1';
+  user.email = 'user@example.com';
+  user.status = 'inactive';
+  user.name = 'new user';
+
+  const transaction = writeTransaction
+    .chian({
+      create: {
+        item: user,
+      },
+    })
+    .chian<UserPrimaryKey, User>({
+      update: {
+        item: User,
+        primaryKey: {
+          id: '123',
+        },
+        body: {
+          email: 'example@email.com',
+        },
+      },
+    });
+
+  expect(transaction.items).toEqual([
+    {
+      Put: {
+        ConditionExpression: 'attribute_not_exists(#CE_PK)',
+        ExpressionAttributeNames: {
+          '#CE_PK': 'PK',
+        },
+        Item: {
+          PK: 'USER#1',
+          SK: 'USER#1',
+          GSI1PK: 'USER#STATUS#inactive',
+          GSI1SK: 'USER#new user',
+          email: 'user@example.com',
+          id: '1',
+          name: 'new user',
+          status: 'inactive',
+        },
+        TableName: 'test-table',
+      },
+    },
+    {
+      Put: {
+        ConditionExpression: 'attribute_not_exists(#CE_PK)',
+        ExpressionAttributeNames: {
+          '#CE_PK': 'PK',
+        },
+        Item: {
+          PK: 'DRM_GEN_USER.EMAIL#user@example.com',
+          SK: 'DRM_GEN_USER.EMAIL#user@example.com',
+        },
+        TableName: 'test-table',
+      },
+    },
+    {
+      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',
+      },
+    },
+  ]);
+});
diff --git a/packages/core/classes/transformer/__test__/dc-request-transformer.spec.ts b/packages/core/classes/transformer/__test__/dc-request-transformer.spec.ts
new file mode 100644
index 0000000..3484bdf
--- /dev/null
+++ b/packages/core/classes/transformer/__test__/dc-request-transformer.spec.ts
@@ -0,0 +1,269 @@
+import {Attribute, Entity, QUERY_ORDER} from '@typedorm/common';
+import {table} from '@typedorm/core/__mocks__/table';
+import {User, UserGSI1} from '@typedorm/core/__mocks__/user';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {UserPrimaryKey} from '../../../__mocks__/user';
+import {DocumentClientRequestTransformer} from '../document-client-request-transformer';
+
+let transformer: DocumentClientRequestTransformer;
+beforeEach(async () => {
+  const connection = createTestConnection({
+    entities: [User],
+  });
+  transformer = new DocumentClientRequestTransformer(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+/**
+ * @group toDynamoGetItem
+ */
+test('transforms get item requests', () => {
+  const getItem = transformer.toDynamoGetItem<UserPrimaryKey, User>(User, {
+    id: '1',
+  });
+  expect(getItem).toEqual({
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    TableName: 'test-table',
+  });
+});
+
+/**
+ * @group toDynamoPutItem
+ */
+test('transforms put item requests', () => {
+  const user = new User();
+  user.id = '1';
+  user.name = 'Tito';
+  user.status = 'active';
+
+  const putItem = transformer.toDynamoPutItem(user);
+  expect(putItem).toEqual({
+    Item: {
+      GSI1PK: 'USER#STATUS#active',
+      GSI1SK: 'USER#Tito',
+      PK: 'USER#1',
+      SK: 'USER#1',
+      id: '1',
+      name: 'Tito',
+      status: 'active',
+    },
+    ConditionExpression: 'attribute_not_exists(#CE_PK)',
+    ExpressionAttributeNames: {
+      '#CE_PK': 'PK',
+    },
+    TableName: 'test-table',
+  });
+});
+
+test('transforms put item request with unique attributes', () => {
+  resetTestConnection();
+
+  @Entity({
+    table,
+    name: 'user',
+    primaryKey: {
+      partitionKey: 'USER#{{id}}',
+      sortKey: 'USER#{{id}}',
+    },
+  })
+  class UserUniqueEmail {
+    @Attribute()
+    id: string;
+
+    @Attribute({
+      unique: true,
+    })
+    email: string;
+  }
+
+  const connection = createTestConnection({
+    entities: [UserUniqueEmail],
+  });
+  transformer = new DocumentClientRequestTransformer(connection);
+
+  const user = new UserUniqueEmail();
+  user.id = '1';
+  user.email = 'user@example.com';
+
+  const putItem = transformer.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',
+      },
+      TableName: 'test-table',
+    },
+    {
+      ConditionExpression: 'attribute_not_exists(#CE_PK)',
+      ExpressionAttributeNames: {'#CE_PK': 'PK'},
+      Item: {
+        PK: 'DRM_GEN_USER.EMAIL#user@example.com',
+        SK: 'DRM_GEN_USER.EMAIL#user@example.com',
+      },
+      TableName: 'test-table',
+    },
+  ]);
+});
+
+/**
+ * @group toDynamoUpdateItem
+ */
+test('transforms update item request', () => {
+  const updatedItem = transformer.toDynamoUpdateItem<UserPrimaryKey, User>(
+    User,
+    {
+      id: '1',
+    },
+    {
+      name: 'new name',
+    }
+  );
+  expect(updatedItem).toEqual({
+    ExpressionAttributeNames: {
+      '#attr0': 'name',
+      '#attr1': 'GSI1SK',
+    },
+    ExpressionAttributeValues: {
+      ':val0': 'new name',
+      ':val1': 'USER#new name',
+    },
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    ReturnValues: 'ALL_NEW',
+    TableName: 'test-table',
+    UpdateExpression: 'SET #attr0 = :val0, #attr1 = :val1',
+  });
+});
+
+/**
+ * @group toDynamoDeleteItem
+ */
+test('transforms delete item request', () => {
+  const deleteItemInput = transformer.toDynamoDeleteItem<UserPrimaryKey, User>(
+    User,
+    {
+      id: '1',
+    }
+  );
+  expect(deleteItemInput).toEqual({
+    Key: {
+      PK: 'USER#1',
+      SK: 'USER#1',
+    },
+    TableName: 'test-table',
+  });
+});
+
+/**
+ * @group toDynamoQueryItem
+ */
+test('transforms simple query item request', () => {
+  const queryItem = transformer.toDynamoQueryItem<UserPrimaryKey, User>(User, {
+    id: '1',
+  });
+  expect(queryItem).toEqual({
+    ExpressionAttributeNames: {
+      '#KY_CE_PK': 'PK',
+    },
+    ExpressionAttributeValues: {
+      ':KY_CE_PK': 'USER#1',
+    },
+    KeyConditionExpression: '#KY_CE_PK = :KY_CE_PK',
+    TableName: 'test-table',
+  });
+});
+
+test('transforms complex query item request', () => {
+  const queryItem = transformer.toDynamoQueryItem<UserPrimaryKey, User>(
+    User,
+    {
+      id: '1',
+    },
+    {
+      keyCondition: {
+        BEGINS_WITH: 'USER#',
+      },
+      limit: 12,
+      orderBy: QUERY_ORDER.DESC,
+    }
+  );
+  expect(queryItem).toEqual({
+    ExpressionAttributeNames: {
+      '#KY_CE_PK': 'PK',
+      '#KY_CE_SK': 'SK',
+    },
+    ExpressionAttributeValues: {
+      ':KY_CE_PK': 'USER#1',
+      ':KY_CE_SK': 'USER#',
+    },
+    KeyConditionExpression:
+      '#KY_CE_PK = :KY_CE_PK AND begins_with(#KY_CE_SK, :KY_CE_SK)',
+    Limit: 12,
+    ScanIndexForward: false,
+    TableName: 'test-table',
+  });
+});
+
+test('transforms index based query item request', () => {
+  const queryItem = transformer.toDynamoQueryItem<UserGSI1, User>(
+    User,
+    {
+      status: '13',
+      queryIndex: 'GSI1',
+    },
+    {
+      keyCondition: {
+        BETWEEN: ['jay', 'joe'],
+      },
+      orderBy: QUERY_ORDER.ASC,
+    }
+  );
+  expect(queryItem).toEqual({
+    TableName: 'test-table',
+    ExpressionAttributeNames: {
+      '#KY_CE_GSI1PK': 'GSI1PK',
+      '#KY_CE_GSI1SK': 'GSI1SK',
+    },
+    ExpressionAttributeValues: {
+      ':KY_CE_GSI1PK': 'USER#STATUS#13',
+      ':KY_CE_GSI1SK_end': 'joe',
+      ':KY_CE_GSI1SK_start': 'jay',
+    },
+    IndexName: 'GSI1',
+    KeyConditionExpression:
+      '#KY_CE_GSI1PK = :KY_CE_GSI1PK AND #KY_CE_GSI1SK BETWEEN :KY_CE_GSI1SK_start AND :KY_CE_GSI1SK_end',
+    ScanIndexForward: true,
+  });
+});
+
+test('errors when querying unknown index', () => {
+  expect(() =>
+    transformer.toDynamoQueryItem(
+      User,
+      {
+        status: '13',
+        queryIndex: 'LSI1',
+      },
+      {
+        keyCondition: {
+          EQ: 'joe',
+        },
+      }
+    )
+  ).toThrowError(
+    'Requested to query items from index "LSI1", but no such index exists on entity.'
+  );
+});
diff --git a/packages/core/classes/transformer/__test__/entity-transformer.spec.ts b/packages/core/classes/transformer/__test__/entity-transformer.spec.ts
new file mode 100644
index 0000000..d58dde3
--- /dev/null
+++ b/packages/core/classes/transformer/__test__/entity-transformer.spec.ts
@@ -0,0 +1,164 @@
+import {Attribute, Entity, INDEX_TYPE, Table} from '@typedorm/common';
+import {Organisation} from '@typedorm/core/__mocks__/organisation';
+import {User} from '@typedorm/core/__mocks__/user';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {EntityTransformer} from '../entity-transformer';
+
+let transformer: EntityTransformer;
+beforeEach(() => {
+  const connection = createTestConnection({
+    entities: [User, Organisation],
+  });
+  transformer = new EntityTransformer(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+/**
+ * @group fromDynamoEntity
+ */
+test('transforms dynamo entity to entity model', () => {
+  const dynamoEntity = {
+    PK: 'USER#1',
+    SK: 'USER#1',
+    GSI1PK: 'USER#STATUS#active',
+    GSI1SK: 'USER#Me',
+    id: '1',
+    name: 'Me',
+    status: 'active',
+  };
+  const transformed = transformer.fromDynamoEntity(User, dynamoEntity);
+  expect(transformed).toEqual({
+    id: '1',
+    name: 'Me',
+    status: 'active',
+  });
+});
+
+/**
+ * @group toDynamoEntity
+ */
+test('transforms simple model to dynamo entity', () => {
+  const user = new User();
+  user.id = '111';
+  user.name = 'Test User';
+  user.status = 'inactive';
+
+  const response = transformer.toDynamoEntity(user);
+  expect(response).toEqual({
+    GSI1PK: 'USER#STATUS#inactive',
+    GSI1SK: 'USER#Test User',
+    PK: 'USER#111',
+    SK: 'USER#111',
+    id: '111',
+    name: 'Test User',
+    status: 'inactive',
+  });
+});
+
+test('transforms complex model model to dynamo entity', () => {
+  resetTestConnection();
+  const table = new Table({
+    name: 'user-table',
+    partitionKey: 'PK',
+    sortKey: 'SK',
+    indexes: {
+      GSI1: {
+        partitionKey: 'GSI1PK',
+        sortKey: 'GSI1SK',
+        type: INDEX_TYPE.GSI,
+      },
+      GSI2: {
+        partitionKey: 'GSI2PK',
+        sortKey: 'GSI2SK',
+        type: INDEX_TYPE.GSI,
+      },
+      LSI1: {
+        sortKey: 'LSI1SK',
+        type: INDEX_TYPE.LSI,
+      },
+    },
+  });
+
+  @Entity({
+    name: 'User',
+    primaryKey: {
+      partitionKey: 'USER#{{id}}#NAME#{{name}}',
+      sortKey: 'USER#{{id}}',
+    },
+    indexes: {
+      GSI1: {
+        partitionKey: 'USER#{{id}}#AGE#{{age}}',
+        sortKey: 'USER#{{name}}',
+        type: INDEX_TYPE.GSI,
+      },
+      LSI1: {
+        sortKey: 'USER#{{id}}',
+        type: INDEX_TYPE.LSI,
+      },
+    },
+    table,
+  })
+  class ComplexUser {
+    @Attribute()
+    id: string;
+
+    @Attribute()
+    name: string;
+
+    @Attribute()
+    age: number;
+  }
+
+  const connection = createTestConnection({
+    entities: [ComplexUser],
+  });
+
+  transformer = new EntityTransformer(connection);
+
+  const user = new ComplexUser();
+  user.id = '111';
+  user.name = 'Test User';
+  user.age = 12;
+
+  const response = transformer.toDynamoEntity(user);
+  expect(response).toEqual({
+    id: '111',
+    GSI1PK: 'USER#111#AGE#12',
+    GSI1SK: 'USER#Test User',
+    LSI1SK: 'USER#111',
+    PK: 'USER#111#NAME#Test User',
+    SK: 'USER#111',
+    name: 'Test User',
+    age: 12,
+  });
+});
+
+/**
+ * @group getAffectedIndexesForAttributes
+ */
+test('returns all affected indexes for simple attributes', () => {
+  const affectedIndexes = transformer.getAffectedIndexesForAttributes(User, {
+    name: 'new updated name',
+  });
+  expect(affectedIndexes).toEqual({
+    GSI1SK: 'USER#new updated name',
+  });
+});
+
+test('returns all affected indexes for complex attributes', () => {
+  const affectedIndexes = transformer.getAffectedIndexesForAttributes(
+    Organisation,
+    {
+      name: 'Updated name',
+      teamCount: 12,
+      active: false,
+    }
+  );
+  expect(affectedIndexes).toEqual({
+    GSI1SK: 'ORG#Updated name#ACTIVE#false',
+    GSI2SK: 'ORG#Updated name#TEAM_COUNT#12',
+  });
+});
diff --git a/packages/core/helpers/__test__/parse-key.spec.ts b/packages/core/helpers/__test__/parse-key.spec.ts
new file mode 100644
index 0000000..0ce4306
--- /dev/null
+++ b/packages/core/helpers/__test__/parse-key.spec.ts
@@ -0,0 +1,34 @@
+import {parseKey} from '../parse-key';
+
+describe('parseKey()', () => {
+  it('should parse key and replace interpolation values with actual value', () => {
+    const parsed = parseKey('USER#{{id}}', {
+      id: '1111-2222',
+    });
+
+    expect(parsed).toEqual('USER#1111-2222');
+  });
+
+  it('should parse key with number', () => {
+    const parsed = parseKey('USER#{{id}}', {
+      id: 1111,
+    });
+
+    expect(parsed).toEqual('USER#1111');
+  });
+
+  it('should parse key multiple interpolation occurrences', () => {
+    const parsed = parseKey('USER#{{id}}#CLASS#{{classId}}', {
+      id: 1111,
+      classId: 'class-1232',
+    });
+
+    expect(parsed).toEqual('USER#1111#CLASS#class-1232');
+  });
+
+  it('should thrown an error when no mapping found in dictionary', () => {
+    expect(() => parseKey('USER#{{id}}', {})).toThrowError(
+      'Could not resolve "id" from given dictionary'
+    );
+  });
+});
diff --git a/packages/core/helpers/__test__/validate-key.spec.ts b/packages/core/helpers/__test__/validate-key.spec.ts
new file mode 100644
index 0000000..c77b122
--- /dev/null
+++ b/packages/core/helpers/__test__/validate-key.spec.ts
@@ -0,0 +1,31 @@
+import {validateKey} from '../validate-key';
+
+describe('validateKey()', () => {
+  it('should validate key type', () => {
+    expect(() =>
+      validateKey('USER#{{id}}', {
+        id: 'String',
+      })
+    ).not.toThrowError();
+  });
+
+  it('should validate string with multiple args', () => {
+    expect(() =>
+      validateKey('USER#{{id}}#STATUS#{{status}}', {
+        id: 'String',
+        status: 'String',
+      })
+    ).not.toThrowError();
+  });
+
+  it('should thrown an error when type of a key variable is non compatible', () => {
+    expect(() =>
+      validateKey('USER#{{id}}#STATUS#{{status}}', {
+        id: 'String',
+        status: 'Object',
+      })
+    ).toThrowError(
+      '"status" is used in key USER#{{id}}#STATUS#{{status}}, thus it\'s type must be a "Number" or a "String"'
+    );
+  });
+});
diff --git a/packages/testing/BUILD b/packages/testing/BUILD
new file mode 100644
index 0000000..74df0d0
--- /dev/null
+++ b/packages/testing/BUILD
@@ -0,0 +1,20 @@
+load("//:tools/defaults.bzl", "setup_ts_build")
+load("//:tools/pkg.bzl", "typedorm_package")
+
+setup_ts_build(
+    name = "library",
+    module_name = "@typedorm/testing",
+    deps = [
+        "//packages/core:library"
+    ],
+    visibility = ["//packages:__subpackages__"]
+
+)
+
+typedorm_package(
+    name = "testing",
+    readme_md = "README.md",
+    deps = [
+        ":library"
+    ]
+)
\ No newline at end of file
diff --git a/packages/testing/README.md b/packages/testing/README.md
new file mode 100644
index 0000000..ed6c8f6
--- /dev/null
+++ b/packages/testing/README.md
@@ -0,0 +1,7 @@
+[![NPM Release](https://img.shields.io/npm/v/@typedorm/core?style=for-the-badge)](https://www.npmjs.com/package/@typedorm/core)
+
+# TypeDORM - Testing
+
+Contains TypeDORM testing utilities and helper functions. Also used by TypeDORM itself.
+
+[Read more here](../../README.md)
diff --git a/packages/testing/index.ts b/packages/testing/index.ts
new file mode 100644
index 0000000..118ac9a
--- /dev/null
+++ b/packages/testing/index.ts
@@ -0,0 +1,11 @@
+import {createConnection, ConnectionOptions} from '@typedorm/core';
+import {ConnectionManager} from '@typedorm/core/classes/connection/connection-manager';
+import {Container} from '@typedorm/core/classes/container';
+
+export function createTestConnection(connectionOptions: ConnectionOptions) {
+  return createConnection(connectionOptions);
+}
+
+export function resetTestConnection() {
+  Container.get(ConnectionManager).clear();
+}
diff --git a/packages/testing/package.json b/packages/testing/package.json
new file mode 100644
index 0000000..86fd853
--- /dev/null
+++ b/packages/testing/package.json
@@ -0,0 +1,27 @@
+{
+  "name": "@typedorm/testing",
+  "version": "0.0.0-PLACEHOLDER",
+  "description": "TypeDORM - testing utilities",
+  "keywords": [
+    "core",
+    "typedorm",
+    "dynamoDb orm",
+    "orm"
+  ],
+  "peerDependencies": {
+    "@typedorm/common": "0.0.0-PLACEHOLDER",
+    "@typedorm/core": "0.0.0-PLACEHOLDER",
+    "reflect-metadata": "^0.1.13"
+  },
+  "publishConfig": {
+    "tag": "TAG-PLACEHOLDER",
+    "access": "public"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/typedorm/typedorm.git",
+    "directory": "packages/testing"
+  },
+  "author": "typedorm",
+  "license": "MIT"
+}