Repository: typedorm/typedorm (ts)

Issue:
Add support using TypeDORM with explicit aws credentials
Currently TypeDORM can only be used on other services running on AWS that can access dynamodb using IAM roles, EC2s and Lambdas. 

- [x] Add support for specifying aws credentials `accessKeyId`
`secretAccessKey` when creating connection, and use provided credentials to connect to dynamodb table
- [x] Update docs with new connection options

Pull request description:
24 add support for loading explicite creds
# Changes

- add support for custom document client to use for a connection
- add support for providing mock `documentClient` when creating test connections
- add docs

closes #24 

Patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 56f832e..3078d02 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,7 @@
 {
   "cSpell.words": [
     "KSUID",
+    "Metadatas",
     "bazel",
     "commitlint",
     "dynamodb",
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..4967587
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,8 @@
+# Index
+
+Document index that covers, list of pages available
+
+- [How it works](./how-it-works.md)
+- [Step by step guide](./guide.md)
+- [Entity inheritance](./entity-inheritance.md)
+- [Working with multiple connections](./multiple-connection.md)
diff --git a/docs/multiple-connection.md b/docs/multiple-connection.md
new file mode 100644
index 0000000..6523a38
--- /dev/null
+++ b/docs/multiple-connection.md
@@ -0,0 +1,49 @@
+# Working with multiple connections
+
+Let's assume that we have a two different tables running two different workloads, one is for `app1` workload and other is for `app2` workload. In order to be able to interact with these two tables, we have two user with different permissions, `app1User` and `app2User`.
+
+Now, we have a service that needs to be able to work with both tables in that are registered in separate connections. Working with different connections using different credentials, will look something like this.
+
+First we create two separate connections both using different credentials
+
+```Typescript
+// connection for app1
+createConnection({
+  name: 'app1'
+  table: app1Table,
+  entities: [...entities],
+  documentClient: {
+    credentials: {
+      accessKeyId: process.env.APP_1_ACCESS_KEY_ID,
+      secretAccessKey: process.env.APP_1_SECRET_ACCESS_KEY
+    }
+  }
+});
+
+// connection for app2
+createConnection({
+  name: 'app2'
+  table: app1Table,
+  entities: [...entities],
+  documentClient: {
+    credentials: {
+      accessKeyId: process.env.APP_2_ACCESS_KEY_ID,
+      secretAccessKey: process.env.APP_2_SECRET_ACCESS_KEY
+    }
+  }
+});
+```
+
+Now we can use multiple connections to different actions, that are using completely different credentials,
+
+```Typescript
+// update record in both table in different connections
+
+// create user in connection 1
+getEntityManager('app1').create(User, {...})
+
+// update record in connection2
+getEntityManager('app2').create(Record, {...})
+```
+
+TypeDORM makes it super easy to configure multiple connections with different connection options.
diff --git a/packages/common/index.ts b/packages/common/index.ts
index 818f074..d97d051 100644
--- a/packages/common/index.ts
+++ b/packages/common/index.ts
@@ -16,6 +16,7 @@ export * from './require-at-least-one-type';
 export * from './require-only-one-type';
 export * from './scalar-type';
 export * from './update-attributes-type';
+export * from './replace-type';
 
 // classes
 export * from './table';
diff --git a/packages/common/replace-type.ts b/packages/common/replace-type.ts
new file mode 100644
index 0000000..f784556
--- /dev/null
+++ b/packages/common/replace-type.ts
@@ -0,0 +1,1 @@
+export type Replace<T, K extends keyof T, R> = Omit<T, K> & R;
diff --git a/packages/core/classes/connection/connection-options.ts b/packages/core/classes/connection/connection-options.ts
index 889a956..f4c326c 100644
--- a/packages/core/classes/connection/connection-options.ts
+++ b/packages/core/classes/connection/connection-options.ts
@@ -1,8 +1,29 @@
 import {EntityTarget, Table} from '@typedorm/common';
+import {DocumentClient} from 'aws-sdk/clients/dynamodb';
 
+/**
+ * @param table - Global table to use for all entities, entity scoped table takes precedence over this
+ * @param name - Connection name
+ * @param dynamoQueryItemsImplicitLimit - Implicit limit to apply when querying items, this will be overridden by explicit limit
+ * @param entities - List of entity classes or global pattern string matching all entities
+ * @param documentClient - Document client to use for this connection
+ */
 export interface ConnectionOptions {
+  /**
+   * @default - none
+   */
   table?: Table;
+  /**
+   * @default - 'default'
+   */
   name?: string;
+  /**
+   * @default 3000
+   */
   dynamoQueryItemsImplicitLimit?: number;
   entities: EntityTarget<any>[] | string;
+  /**
+   * @default - new document client instance with default config will be instantiated
+   */
+  documentClient?: DocumentClient;
 }
diff --git a/packages/core/classes/connection/connection.ts b/packages/core/classes/connection/connection.ts
index 573ea55..eaa1e88 100644
--- a/packages/core/classes/connection/connection.ts
+++ b/packages/core/classes/connection/connection.ts
@@ -3,6 +3,7 @@ import {
   EntityTarget,
   Table,
 } from '@typedorm/common';
+import {DocumentClient} from 'aws-sdk/clients/dynamodb';
 import {EntityManager} from '../manager/entity-manager';
 import {TransactionManager} from '../manager/transaction-manager';
 import {AttributeMetadata} from '../metadata/attribute-metadata';
@@ -20,6 +21,7 @@ export class Connection {
   readonly entityManager: EntityManager;
   readonly transactionManger: TransactionManager;
   readonly defaultConfig: {queryItemsImplicitLimit: number};
+  readonly documentClient: DocumentClient;
 
   private _entityMetadatas: Map<string, EntityMetadata>;
   private isConnected: boolean;
@@ -31,12 +33,18 @@ export class Connection {
     }
     this.name = name;
     this.entityManager = new EntityManager(this);
-    this.transactionManger = new TransactionManager();
+    this.transactionManger = new TransactionManager(this);
     this.defaultConfig = {
       queryItemsImplicitLimit:
         options.dynamoQueryItemsImplicitLimit ??
         DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT,
     };
+
+    if (options.documentClient) {
+      this.documentClient = options.documentClient;
+    } else {
+      this.documentClient = new DocumentClient();
+    }
     /**
      * This makes sure that we only ever build entity metadatas once per connection
      */
diff --git a/packages/core/classes/manager/base-manager.ts b/packages/core/classes/manager/base-manager.ts
deleted file mode 100644
index 3e9d276..0000000
--- a/packages/core/classes/manager/base-manager.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-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
index 7c8a60b..2f57740 100644
--- a/packages/core/classes/manager/entity-manager.ts
+++ b/packages/core/classes/manager/entity-manager.ts
@@ -16,7 +16,6 @@ import {
   ManagerToDynamoQueryItemsOptions,
 } from '../transformer/document-client-request-transformer';
 import {EntityTransformer} from '../transformer/entity-transformer';
-import {BaseManager} from './base-manager';
 import {getConstructorForInstance} from '../../helpers/get-constructor-for-instance';
 
 export interface EntityManagerUpdateOptions {
@@ -36,12 +35,11 @@ export interface EntityManagerQueryOptions
   cursor?: DocumentClient.Key;
 }
 
-export class EntityManager extends BaseManager {
+export class EntityManager {
   private _dcReqTransformer: DocumentClientRequestTransformer;
   private _entityTransformer: EntityTransformer;
 
   constructor(protected connection: Connection) {
-    super();
     this._dcReqTransformer = new DocumentClientRequestTransformer(connection);
     this._entityTransformer = new EntityTransformer(connection);
   }
@@ -55,7 +53,7 @@ export class EntityManager extends BaseManager {
     const entityClass = getConstructorForInstance(entity);
 
     if (!Array.isArray(dynamoPutItemInput)) {
-      await this._dc.put(dynamoPutItemInput).promise();
+      await this.connection.documentClient.put(dynamoPutItemInput).promise();
       const itemToReturn = this._entityTransformer.fromDynamoEntity<Entity>(
         entityClass,
         dynamoPutItemInput.Item
@@ -96,7 +94,9 @@ export class EntityManager extends BaseManager {
       primaryKeyAttributes
     );
 
-    const response = await this._dc.get(dynamoGetItem).promise();
+    const response = await this.connection.documentClient
+      .get(dynamoGetItem)
+      .promise();
 
     const entity = this._entityTransformer.fromDynamoEntity<Entity>(
       entityClass,
@@ -178,7 +178,7 @@ export class EntityManager extends BaseManager {
         attrValue
       );
       return !!(
-        await this._dc
+        await this.connection.documentClient
           .get({
             Key: {...uniqueAttributePrimaryKey},
             TableName: metadata.table.name,
@@ -208,7 +208,9 @@ export class EntityManager extends BaseManager {
       Entity
     >(entityClass, primaryKey, body, options);
 
-    const response = await this._dc.update(dynamoUpdateItem).promise();
+    const response = await this.connection.documentClient
+      .update(dynamoUpdateItem)
+      .promise();
 
     return this._entityTransformer.fromDynamoEntity<Entity>(
       entityClass,
@@ -230,7 +232,7 @@ export class EntityManager extends BaseManager {
       Entity
     >(entityClass, primaryKey);
 
-    await this._dc.delete(dynamoDeleteItem).promise();
+    await this.connection.documentClient.delete(dynamoDeleteItem).promise();
 
     return {
       success: true,
@@ -289,7 +291,10 @@ export class EntityManager extends BaseManager {
     cursor?: DocumentClient.Key;
     itemsFetched?: DocumentClient.ItemList;
   }): Promise<{items: DocumentClient.ItemList; cursor?: DocumentClient.Key}> {
-    const {LastEvaluatedKey, Items = []} = await this._dc
+    const {
+      LastEvaluatedKey,
+      Items = [],
+    } = await this.connection.documentClient
       .query({...queryInput, ExclusiveStartKey: cursor})
       .promise();
     itemsFetched = [...itemsFetched, ...Items];
diff --git a/packages/core/classes/manager/transaction-manager.ts b/packages/core/classes/manager/transaction-manager.ts
index 59df1f8..2355a07 100644
--- a/packages/core/classes/manager/transaction-manager.ts
+++ b/packages/core/classes/manager/transaction-manager.ts
@@ -1,15 +1,13 @@
 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';
+import {Connection} from '../connection/connection';
 
 /**
  * Performs transactions using document client's writeTransaction
  */
-export class TransactionManager extends BaseManager {
-  constructor() {
-    super();
-  }
+export class TransactionManager {
+  constructor(private connection: Connection) {}
 
   async write(transaction: WriteTransaction) {
     if (transaction.items.length > TRANSACTION_WRITE_ITEMS_LIMIT) {
@@ -25,7 +23,9 @@ export class TransactionManager extends BaseManager {
     // 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);
+    const transactionRequest = this.connection.documentClient.transactWrite(
+      transactionInput
+    );
 
     let cancellationReasons: any[];
     transactionRequest.on('extractError', response => {


Test patch:
diff --git a/packages/core/classes/manager/__test__/entity-manager.spec.ts b/packages/core/classes/manager/__test__/entity-manager.spec.ts
index d6c78ae..63e2782 100644
--- a/packages/core/classes/manager/__test__/entity-manager.spec.ts
+++ b/packages/core/classes/manager/__test__/entity-manager.spec.ts
@@ -1,18 +1,3 @@
-// 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,
-  },
-}));
 jest.useFakeTimers('modern').setSystemTime(new Date(1606896235000));
 
 import {createTestConnection, resetTestConnection} from '@typedorm/testing';
@@ -28,10 +13,19 @@ import {
 } from '@typedorm/core/__mocks__/user-auto-generate-attributes';
 
 let manager: EntityManager;
+const dcMock = {
+  put: jest.fn(),
+  get: jest.fn(),
+  update: jest.fn(),
+  delete: jest.fn(),
+  query: jest.fn(),
+};
 beforeEach(() => {
   const connection = createTestConnection({
     entities: [User, UserUniqueEmail, UserAutoGenerateAttributes],
+    documentClient: dcMock,
   });
+
   manager = new EntityManager(connection);
 });
 
@@ -43,7 +37,7 @@ afterEach(() => {
  * @group create
  */
 test('creates entity', async () => {
-  dcMock().put.mockReturnValue({
+  dcMock.put.mockReturnValue({
     promise: () => ({}),
   });
 
@@ -53,8 +47,8 @@ test('creates entity', async () => {
   user.status = 'active';
 
   const userEntity = await manager.create(user);
-  expect(dcMock().put).toHaveBeenCalledTimes(1);
-  expect(dcMock().put).toHaveBeenCalledWith({
+  expect(dcMock.put).toHaveBeenCalledTimes(1);
+  expect(dcMock.put).toHaveBeenCalledWith({
     Item: {
       GSI1PK: 'USER#STATUS#active',
       GSI1SK: 'USER#Test User',
@@ -81,7 +75,7 @@ test('creates entity', async () => {
  * Issue: #11
  */
 test('creates entity and returns all attributes, including auto generated ones', async () => {
-  dcMock().put.mockReturnValue({
+  dcMock.put.mockReturnValue({
     promise: () => ({}),
   });
 
@@ -89,8 +83,8 @@ test('creates entity and returns all attributes, including auto generated ones',
   user.id = '1';
 
   const userEntity = await manager.create(user);
-  expect(dcMock().put).toHaveBeenCalledTimes(1);
-  expect(dcMock().put).toHaveBeenCalledWith({
+  expect(dcMock.put).toHaveBeenCalledTimes(1);
+  expect(dcMock.put).toHaveBeenCalledWith({
     Item: {
       GSI1PK: 'USER#UPDATED_AT#1606896235',
       GSI1SK: 'USER#1',
@@ -115,7 +109,7 @@ test('creates entity and returns all attributes, including auto generated ones',
  * @group findOne
  */
 test('finds one entity by given primary key', async () => {
-  dcMock().get.mockReturnValue({
+  dcMock.get.mockReturnValue({
     promise: () => ({
       Item: {
         PK: 'USER#1',
@@ -132,8 +126,8 @@ test('finds one entity by given primary key', async () => {
   const userEntity = await manager.findOne<UserPrimaryKey, User>(User, {
     id: '1',
   });
-  expect(dcMock().get).toHaveBeenCalledTimes(1);
-  expect(dcMock().get).toHaveBeenCalledWith({
+  expect(dcMock.get).toHaveBeenCalledTimes(1);
+  expect(dcMock.get).toHaveBeenCalledWith({
     Key: {
       PK: 'USER#1',
       SK: 'USER#1',
@@ -159,7 +153,7 @@ test('throws an error when trying to do a get request with non primary key attri
  * @group exists
  */
 test('checks if given item exists', async () => {
-  dcMock().get.mockReturnValue({
+  dcMock.get.mockReturnValue({
     promise: () => ({
       Item: {
         PK: 'USER#1',
@@ -180,7 +174,7 @@ test('checks if given item exists', async () => {
     }
   );
 
-  expect(dcMock().get).toHaveBeenCalledWith({
+  expect(dcMock.get).toHaveBeenCalledWith({
     Key: {
       PK: 'USER#1',
       SK: 'USER#1',
@@ -195,7 +189,7 @@ test('checks if item with given unique attribute exists', async () => {
     email: 'user@example.com',
   });
 
-  expect(dcMock().get).toHaveBeenCalledWith({
+  expect(dcMock.get).toHaveBeenCalledWith({
     Key: {
       PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#user@example.com',
       SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#user@example.com',
@@ -206,7 +200,7 @@ test('checks if item with given unique attribute exists', async () => {
 });
 
 test('throws an error if trying to perform exists check with non key or non unique attributes', async () => {
-  expect(dcMock().get).not.toHaveBeenCalled();
+  expect(dcMock.get).not.toHaveBeenCalled();
   await expect(
     async () =>
       await manager.exists<UserUniqueEmail>(UserUniqueEmail, {
@@ -229,7 +223,7 @@ test('throws an error if trying to perform exists check with partial primary key
  * @group update
  */
 test('updates item', async () => {
-  dcMock().update.mockReturnValue({
+  dcMock.update.mockReturnValue({
     promise: () => ({
       Attributes: {
         PK: 'USER#1',
@@ -251,7 +245,7 @@ test('updates item', async () => {
     }
   );
 
-  expect(dcMock().update).toHaveBeenCalledWith({
+  expect(dcMock.update).toHaveBeenCalledWith({
     ExpressionAttributeNames: {
       '#attr0': 'name',
       '#attr1': 'status',
@@ -279,7 +273,7 @@ test('updates item', async () => {
 test('updates item and attributes marked to be autoUpdated', async () => {
   jest.useFakeTimers('modern').setSystemTime(new Date('2020-01-01'));
 
-  dcMock().update.mockReturnValue({
+  dcMock.update.mockReturnValue({
     promise: () => ({
       Attributes: {
         PK: 'USER#1',
@@ -305,7 +299,7 @@ test('updates item and attributes marked to be autoUpdated', async () => {
     }
   );
 
-  expect(dcMock().update).toHaveBeenCalledWith({
+  expect(dcMock.update).toHaveBeenCalledWith({
     ExpressionAttributeNames: {
       '#attr0': 'updatedAt',
       '#attr1': 'GSI1PK',
@@ -329,7 +323,7 @@ test('updates item and attributes marked to be autoUpdated', async () => {
  * @group delete
  */
 test('deletes item by primary key', async () => {
-  dcMock().delete.mockReturnValue({
+  dcMock.delete.mockReturnValue({
     promise: jest.fn().mockReturnValue({
       Attributes: {},
     }),
@@ -339,7 +333,7 @@ test('deletes item by primary key', async () => {
     id: '1',
   });
 
-  expect(dcMock().delete).toHaveBeenCalledWith({
+  expect(dcMock.delete).toHaveBeenCalledWith({
     Key: {
       PK: 'USER#1',
       SK: 'USER#1',
@@ -363,7 +357,7 @@ test('throws an error when trying to delete item by non primary key attributes',
  * @group find
  */
 test('finds items matching given query params', async () => {
-  dcMock().query.mockReturnValue({
+  dcMock.query.mockReturnValue({
     promise: jest.fn().mockReturnValue({
       Items: [
         {
@@ -401,8 +395,8 @@ test('finds items matching given query params', async () => {
     }
   );
 
-  expect(dcMock().query).toHaveBeenCalledTimes(1);
-  expect(dcMock().query).toHaveBeenCalledWith({
+  expect(dcMock.query).toHaveBeenCalledTimes(1);
+  expect(dcMock.query).toHaveBeenCalledWith({
     ExpressionAttributeNames: {
       '#KY_CE_PK': 'PK',
       '#KY_CE_SK': 'SK',
@@ -434,7 +428,7 @@ test('finds items matching given query params', async () => {
 });
 
 test('finds item from given cursor position', async () => {
-  dcMock().query.mockReturnValue({
+  dcMock.query.mockReturnValue({
     promise: jest.fn().mockReturnValue({
       Items: [
         {
@@ -476,7 +470,7 @@ test('finds item from given cursor position', async () => {
     }
   );
 
-  expect(dcMock().query).toHaveBeenCalledWith({
+  expect(dcMock.query).toHaveBeenCalledWith({
     ExclusiveStartKey: {
       partitionKey: 'USER#1',
       sortKey: 'USER#1',
@@ -507,8 +501,8 @@ test('queries items until limit is met', async () => {
       SK: `USER#${index}`,
     });
   }
-  dcMock()
-    .query.mockImplementationOnce(() => ({
+  dcMock.query
+    .mockImplementationOnce(() => ({
       promise: jest.fn().mockReturnValue({
         Items: itemsToReturn,
         Count: itemsToReturn.length,
@@ -538,8 +532,8 @@ test('queries items until limit is met', async () => {
     }
   );
 
-  expect(dcMock().query).toHaveBeenCalledTimes(2);
-  expect(dcMock().query.mock.calls).toEqual([
+  expect(dcMock.query).toHaveBeenCalledTimes(2);
+  expect(dcMock.query.mock.calls).toEqual([
     [
       {
         ExpressionAttributeNames: {
diff --git a/packages/core/classes/manager/__test__/transaction-manager-write.spec.ts b/packages/core/classes/manager/__test__/transaction-manager.spec.ts
similarity index 87%
rename from packages/core/classes/manager/__test__/transaction-manager-write.spec.ts
rename to packages/core/classes/manager/__test__/transaction-manager.spec.ts
index b3a4b16..3cc1b99 100644
--- a/packages/core/classes/manager/__test__/transaction-manager-write.spec.ts
+++ b/packages/core/classes/manager/__test__/transaction-manager.spec.ts
@@ -1,15 +1,3 @@
-// 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';
@@ -18,12 +6,17 @@ import {TransactionManager} from '../transaction-manager';
 import {User} from '../../../__mocks__/user';
 
 let manager: TransactionManager;
+const dcMock = {
+  transactWrite: jest.fn(),
+};
+
 let connection: Connection;
 beforeEach(() => {
   connection = createTestConnection({
     entities: [User],
+    documentClient: dcMock,
   });
-  manager = new TransactionManager();
+  manager = new TransactionManager(connection);
 });
 
 afterEach(() => {
@@ -34,7 +27,7 @@ afterEach(() => {
  * @group write
  */
 test('performs write transactions', async () => {
-  dcMock().transactWrite.mockReturnValue({
+  dcMock.transactWrite.mockReturnValue({
     promise: jest.fn().mockReturnValue({
       ConsumedCapacity: [{}],
       ItemCollectionMetrics: [{}],
@@ -83,8 +76,8 @@ test('performs write transactions', async () => {
 
   const response = await manager.write(transaction);
 
-  expect(dcMock().transactWrite).toHaveBeenCalledTimes(1);
-  expect(dcMock().transactWrite).toHaveBeenCalledWith({
+  expect(dcMock.transactWrite).toHaveBeenCalledTimes(1);
+  expect(dcMock.transactWrite).toHaveBeenCalledWith({
     TransactItems: [
       {
         Put: {
diff --git a/packages/testing/BUILD b/packages/testing/BUILD
index 74df0d0..15fccd8 100644
--- a/packages/testing/BUILD
+++ b/packages/testing/BUILD
@@ -5,7 +5,10 @@ setup_ts_build(
     name = "library",
     module_name = "@typedorm/testing",
     deps = [
-        "//packages/core:library"
+        "//packages/core:library",
+        "//packages/common:library",
+        "@npm//aws-sdk",
+        "@npm//@types/jest"
     ],
     visibility = ["//packages:__subpackages__"]
 
diff --git a/packages/testing/index.ts b/packages/testing/index.ts
index 118ac9a..cf42201 100644
--- a/packages/testing/index.ts
+++ b/packages/testing/index.ts
@@ -1,9 +1,21 @@
+import {Replace} from '@typedorm/common';
 import {createConnection, ConnectionOptions} from '@typedorm/core';
 import {ConnectionManager} from '@typedorm/core/classes/connection/connection-manager';
 import {Container} from '@typedorm/core/classes/container';
+import {DynamoDB} from 'aws-sdk';
 
-export function createTestConnection(connectionOptions: ConnectionOptions) {
-  return createConnection(connectionOptions);
+export function createTestConnection(
+  connectionOptions: Replace<
+    ConnectionOptions,
+    'documentClient',
+    {
+      documentClient?: {
+        [key in keyof DynamoDB.DocumentClient]?: jest.SpyInstance;
+      };
+    }
+  >
+) {
+  return createConnection(connectionOptions as ConnectionOptions);
 }
 
 export function resetTestConnection() {
diff --git a/packages/testing/package.json b/packages/testing/package.json
index 86fd853..fea233d 100644
--- a/packages/testing/package.json
+++ b/packages/testing/package.json
@@ -11,7 +11,8 @@
   "peerDependencies": {
     "@typedorm/common": "0.0.0-PLACEHOLDER",
     "@typedorm/core": "0.0.0-PLACEHOLDER",
-    "reflect-metadata": "^0.1.13"
+    "reflect-metadata": "^0.1.13",
+    "aws-sdk": "^2.799.0"
   },
   "publishConfig": {
     "tag": "TAG-PLACEHOLDER",