Repository: typedorm/typedorm (ts)

Issue:
Add support for attribute aliasing
# Idea
Add support for specifying attributes aliases in indexes and primary keys. Basic idea is to be able to have a static one-on-one reference between `index <-> attribute`, in cases where we would like to have indexes be a simple alias of the same type instead of being forced to `string` type.

For more info check out #109.

# Proposed Solution
entity primary key and index attributes to also support specifying entity alias, something like:
```Typescript
@Entity(
  indexes: {
    GSI1: {
      partitionKey: {
        alias: 'numberValue' // <-- this creates an alias to `numberValue` attribute on entity and will have the same type as `numberValue` 
      }, 
      sortKey: 'ORG#{{id}}#ACTIVE#{{active}}',
      type: INDEX_TYPE.GSI,
    },
  },
})
export class Organisation{
  ...
  @Attribute()
  numberValue: number;
}
```

# Tasks
- [x] update entity index primary key type infer types from entity attributes
- [x] check if the index key contains any interpolations, 
  - [x] if it does, parse it as per current impl
  - [x] if not, try to parse it as an alias by default with the same type as an aliased attribute
- [x] add test coverage
- [x] updates docs 

Pull request:
Index attribute aliasing support
# Changes
- adds GSI and LSI attribute aliasing support

closes #112 

Patch:
diff --git a/docs/api.md b/docs/api.md
index 9566682..9f48927 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -110,12 +110,12 @@ Declare an entity to be registered in TypeDORM domain
   indexes: {
     // Each index specified here can only exist if it is also declared on attached table instance
     [indexName] : {
-      // Partition key attribute name for this entity
+      // Partition key attribute pattern or alias schema for this entity
       // @optional
       // Required when index type is GSI
       partitionKey
 
-      // Sort key attribute name for this entity
+      // Sort key attribute pattern or alias schema for this entity
       sortKey
 
       // Type of index: INDEX_TYPE
diff --git a/docs/how-to-recipes.md b/docs/how-to-recipes.md
index b1b49fb..005f045 100644
--- a/docs/how-to-recipes.md
+++ b/docs/how-to-recipes.md
@@ -3,6 +3,9 @@
 This page will walk you through some of the unique recipes to enhance your development workflow when working with TypeDORM.
 
 - [How to recipes](#how-to-recipes)
+  - [Entity configuration](#entity-configuration)
+    - [Define a simple entity](#define-a-simple-entity)
+    - [Define an entity with index](#define-an-entity-with-index)
   - [Define default values](#define-default-values)
     - [Static Default values](#static-default-values)
     - [Dynamic Default values](#dynamic-default-values)
@@ -21,6 +24,60 @@ This page will walk you through some of the unique recipes to enhance your devel
     - [Read items](#read-items)
     - [Retry unprocessed read items in batches](#retry-unprocessed-read-items-in-batches)
 
+## Entity configuration
+
+For any class to be a valid TypeDORM entity, it must be annotated with `@Entity`. While annotating with `@Entity` different
+configs can be provided to customize how the entity is parsed and treated by TypeDORM.
+
+### Define a simple entity
+
+```Typescript
+@Entity(
+  name: 'user', // name of the entity that will be added to each item as an attribute
+  // primary key
+  primaryKey: {
+    partitionKey: 'USER#{{id}}',
+    sortKey: 'USER#{{id}}',
+  }
+)
+class User {
+  @Attribute()
+  id: string  // <- this attribute is required as it is referenced by primary key
+
+  // ... other optional attributes
+}
+```
+
+### Define an entity with index
+
+```Typescript
+@Entity<User>(
+  name: 'user', // name of the entity that will be added to each item as an attribute
+  // primary key
+  primaryKey: {
+    partitionKey: 'USER#{{id}}',
+    sortKey: 'USER#{{id}}',
+  },
+  indexes: {
+    GSI1: {
+      type: INDEX_TYPE.GSI
+      partitionKey: {
+        alias: 'age' // <- this tells TypeDORM to auto infer "type" and "value" for partition key from age attribute.
+      },
+      sortKey: 'USER#{{age}}', // <- here, interpolated "value" will be auto inferred by TypeDORM at runtime based on the value of age
+    }
+  }
+)
+class User {
+  @Attribute()
+  id: string  // <- this attribute is required as it is referenced by primary key
+
+  @Attribute()
+  age: number
+
+}
+```
+
 ## Define default values
 
 `@Attribute` supports specifying default values by providing default values to add to entity at the creation time.
diff --git a/packages/common/private-api.ts b/packages/common/private-api.ts
index 3a956bb..730a06b 100644
--- a/packages/common/private-api.ts
+++ b/packages/common/private-api.ts
@@ -2,3 +2,4 @@ export * from './src/metadata/metadata-manager';
 export * from './src/metadata/metadata-storage';
 export * from './src/logger/debug-logger';
 export * from './src/error';
+export * from './src/index-options';
diff --git a/packages/common/src/decorators/entity.decorator.ts b/packages/common/src/decorators/entity.decorator.ts
index 47ec764..d3a5802 100644
--- a/packages/common/src/decorators/entity.decorator.ts
+++ b/packages/common/src/decorators/entity.decorator.ts
@@ -1,16 +1,18 @@
 import {MetadataManager} from '../metadata/metadata-manager';
 import {EntityRawMetadataOptions} from '../metadata/metadata-storage';
 
-export function Entity({
+type Constructor = {new (...args: any[]): {}};
+
+export function Entity<E>({
   table,
   primaryKey,
   indexes,
   name,
 }: Pick<
-  EntityRawMetadataOptions,
+  EntityRawMetadataOptions<E>,
   'name' | 'table' | 'indexes' | 'primaryKey'
->): ClassDecorator {
-  return target => {
+>) {
+  return function <E extends Constructor>(target: E) {
     const originalTarget = target;
 
     MetadataManager.metadataStorage.addRawEntity({
diff --git a/packages/common/src/error/index.ts b/packages/common/src/error/index.ts
index 18621bb..8b4c31c 100644
--- a/packages/common/src/error/index.ts
+++ b/packages/common/src/error/index.ts
@@ -8,3 +8,5 @@ export * from './invalid-transaction-write-item-error';
 export * from './write-transaction-item-limit-exceeded-error';
 export * from './invalid-transaction-read-item-error';
 export * from './read-transaction-item-limit-exceeded-error';
+export * from './invalid-attribute-alias-schema-error';
+export * from './no-such-attribute-exists-error';
diff --git a/packages/common/src/error/invalid-attribute-alias-schema-error.ts b/packages/common/src/error/invalid-attribute-alias-schema-error.ts
new file mode 100644
index 0000000..e03ddb4
--- /dev/null
+++ b/packages/common/src/error/invalid-attribute-alias-schema-error.ts
@@ -0,0 +1,11 @@
+export class InvalidAttributeAliasSchemaError extends Error {
+  name = 'InvalidAttributeAliasSchemaError';
+
+  constructor(attributeName: any) {
+    super();
+    this.message = `Attribute "${JSON.stringify(
+      attributeName
+    )}" did not satisfy alias attribute type constraint.
+    Attribute alias must always be of type string.`;
+  }
+}
diff --git a/packages/common/src/error/no-such-attribute-exists-error.ts b/packages/common/src/error/no-such-attribute-exists-error.ts
new file mode 100644
index 0000000..54e550c
--- /dev/null
+++ b/packages/common/src/error/no-such-attribute-exists-error.ts
@@ -0,0 +1,10 @@
+export class NoSuchAttributeExistsError extends Error {
+  name = 'NoSuchAttributeExistsError';
+
+  constructor(attributeName: string, entityName?: string) {
+    super();
+    this.message = `No such attribute "${attributeName}" exists on entity${
+      entityName ? ` ${entityName}.` : '.'
+    }`;
+  }
+}
diff --git a/packages/common/src/index-options.ts b/packages/common/src/index-options.ts
new file mode 100644
index 0000000..645f078
--- /dev/null
+++ b/packages/common/src/index-options.ts
@@ -0,0 +1,44 @@
+import {INDEX_TYPE} from './enums';
+import {Replace} from './helpers/replace-type';
+
+export interface GSIIndexOptions {
+  type: INDEX_TYPE.GSI;
+  partitionKey: string;
+  sortKey: string;
+  /**
+   * DynamoDB only adds item to index if both Partition Key and Sort Key defined
+   * for any given GSI contains value
+   *
+   * Indexes missing sort key value will not be added to index.
+   */
+  isSparse?: boolean;
+}
+
+export interface LSIIndexOptions {
+  type: INDEX_TYPE.LSI;
+  sortKey: string;
+  isSparse?: boolean;
+}
+export type IndexOptions = GSIIndexOptions | LSIIndexOptions;
+
+export type KeyAliasSchema<Entity> = {
+  alias: keyof Entity extends infer R ? R : never;
+};
+export type EntityAliasOrString<Entity> = string | KeyAliasSchema<Entity>;
+
+export type IndexOptionsWithAlias<Entity> =
+  | Replace<
+      GSIIndexOptions,
+      'partitionKey' | 'sortKey',
+      {
+        partitionKey: EntityAliasOrString<Entity>;
+        sortKey: EntityAliasOrString<Entity>;
+      }
+    >
+  | Replace<
+      LSIIndexOptions,
+      'sortKey',
+      {
+        sortKey: EntityAliasOrString<Entity>;
+      }
+    >;
diff --git a/packages/common/src/metadata/metadata-storage.ts b/packages/common/src/metadata/metadata-storage.ts
index c2d5f17..8634d5c 100644
--- a/packages/common/src/metadata/metadata-storage.ts
+++ b/packages/common/src/metadata/metadata-storage.ts
@@ -1,7 +1,8 @@
 import {AUTO_GENERATE_ATTRIBUTE_STRATEGY, EntityTarget} from '@typedorm/common';
-import {IndexOptions, Table} from '../table';
+import {Table} from '../table';
 import {AttributeOptionsUniqueType} from '../decorators/attribute.decorator';
 import {ScalarType} from '../helpers/scalar-type';
+import {IndexOptionsWithAlias} from '../index-options';
 
 export const IsAutoGenerateAttributeRawMetadataOptions = (
   attr: any
@@ -17,15 +18,15 @@ export type CompositePrimaryKey = {
   sortKey: string;
 };
 
-export type Indexes = {
-  [key: string]: IndexOptions;
+export type Indexes<Entity = any> = {
+  [key: string]: IndexOptionsWithAlias<Entity>;
 };
 
-export interface EntityRawMetadataOptions {
+export interface EntityRawMetadataOptions<Entity = any> {
   name: string;
-  target: EntityTarget<any>;
+  target: EntityTarget<Entity>;
   primaryKey: PrimaryKey;
-  indexes?: Indexes;
+  indexes?: Indexes<Entity>;
   table?: Table;
 }
 
diff --git a/packages/common/src/table.ts b/packages/common/src/table.ts
index a756e00..e23875f 100644
--- a/packages/common/src/table.ts
+++ b/packages/common/src/table.ts
@@ -1,4 +1,4 @@
-import {INDEX_TYPE} from './enums';
+import {IndexOptions} from './index-options';
 import {CompositePrimaryKey} from './metadata/metadata-storage';
 
 export const IsCompositePrimaryKey = (
@@ -18,27 +18,6 @@ export interface TableOptions {
   };
 }
 
-export interface GSIIndexOptions {
-  type: INDEX_TYPE.GSI;
-  partitionKey: string;
-  sortKey: string;
-  /**
-   * DynamoDB only adds item to index if both Partition Key and Sort Key defined
-   * for any given GSI contains value
-   *
-   * Indexes missing sort key value will not be added to index.
-   */
-  isSparse?: boolean;
-}
-
-export interface LSIIndexOptions {
-  type: INDEX_TYPE.LSI;
-  sortKey: string;
-  isSparse?: boolean;
-}
-
-export type IndexOptions = GSIIndexOptions | LSIIndexOptions;
-
 export class Table {
   constructor(private options: TableOptions) {}
 
diff --git a/packages/core/__mocks__/user-with-attribute-alias.ts b/packages/core/__mocks__/user-with-attribute-alias.ts
new file mode 100644
index 0000000..fb2fff4
--- /dev/null
+++ b/packages/core/__mocks__/user-with-attribute-alias.ts
@@ -0,0 +1,48 @@
+import {Attribute, Entity, INDEX_TYPE} from '@typedorm/common';
+import {table} from './table';
+
+export interface UserAttrAliasPrimaryKey {
+  id: string;
+}
+
+export interface UserAttrAliasGSI1 {
+  name?: string;
+}
+
+@Entity<UserAttrAlias>({
+  table,
+  name: 'user-attr-alias',
+  primaryKey: {
+    partitionKey: 'USER#{{id}}',
+    sortKey: 'USER#{{id}}',
+  },
+  indexes: {
+    GSI1: {
+      partitionKey: {
+        alias: 'status',
+      },
+      sortKey: 'USER#{{name}}',
+      type: INDEX_TYPE.GSI,
+    },
+    LSI1: {
+      type: INDEX_TYPE.LSI,
+      sortKey: {
+        alias: 'age',
+      },
+    },
+  },
+})
+export class UserAttrAlias
+  implements UserAttrAliasPrimaryKey, UserAttrAliasGSI1 {
+  @Attribute()
+  id: string;
+
+  @Attribute()
+  name: string;
+
+  @Attribute()
+  status: string;
+
+  @Attribute()
+  age: number;
+}
diff --git a/packages/core/src/classes/metadata/entity-metadata.ts b/packages/core/src/classes/metadata/entity-metadata.ts
index 2923175..db6517c 100644
--- a/packages/core/src/classes/metadata/entity-metadata.ts
+++ b/packages/core/src/classes/metadata/entity-metadata.ts
@@ -135,7 +135,7 @@ export class EntityMetadata extends BaseMetadata {
 
       const currentIndex = indexes[key];
 
-      validateKey(currentIndex.sortKey, attributes);
+      validateKey(currentIndex.sortKey, attributes, this.name);
       // validates and gets and fill set indexes interpolations of sort key
       const sortKeyInterpolations = getInterpolatedKeys(currentIndex.sortKey);
 
diff --git a/packages/core/src/classes/transformer/base-transformer.ts b/packages/core/src/classes/transformer/base-transformer.ts
index 4c27e6f..a5f70ec 100644
--- a/packages/core/src/classes/transformer/base-transformer.ts
+++ b/packages/core/src/classes/transformer/base-transformer.ts
@@ -15,7 +15,8 @@ import {parseKey} from '../../helpers/parse-key';
 import {Connection} from '../connection/connection';
 import {IsAutoGeneratedAttributeMetadata} from '../metadata/auto-generated-attribute-metadata';
 import {DynamoEntitySchemaPrimaryKey} from '../metadata/entity-metadata';
-import {isDynamoEntityKeySchema} from '../../helpers/is-dynamo-entity-key-schma';
+import {isDynamoEntityKeySchema} from '../../helpers/is-dynamo-entity-key-schema';
+import {isKeyOfTypeAliasSchema} from '../../helpers/is-key-of-type-alias-schema';
 
 export abstract class BaseTransformer {
   constructor(protected connection: Connection) {}
@@ -145,11 +146,18 @@ export abstract class BaseTransformer {
               interpolationsForCurrIndex[interpolationKey];
 
             if (currentInterpolation.includes(attrKey)) {
-              const parsedIndex = parseKey(
-                currIndex.attributes[interpolationKey],
-                attributes
-              );
-              acc[interpolationKey] = parsedIndex;
+              try {
+                const parsedIndex = parseKey(
+                  currIndex.attributes[interpolationKey],
+                  attributes
+                );
+                acc[interpolationKey] = parsedIndex;
+              } catch (err) {
+                // if there was an error parsing sparse index, ignore
+                if (!(err instanceof SparseIndexParseError)) {
+                  throw err;
+                }
+              }
             }
           });
         });
@@ -180,7 +188,7 @@ export abstract class BaseTransformer {
    * @param schema schema to resolve
    * @param entity entity to resolve schema against
    */
-  protected recursiveParseEntity<Entity>(
+  protected recursiveParseEntity<Entity = any>(
     schema: DynamoEntitySchemaPrimaryKey | DynamoEntityIndexesSchema,
     entity: Entity,
     isSparse = false
@@ -188,7 +196,10 @@ export abstract class BaseTransformer {
     const parsedSchema = Object.keys(schema).reduce((acc, key) => {
       const currentValue = (schema as any)[key];
 
-      if (typeof currentValue === 'string') {
+      if (
+        typeof currentValue === 'string' ||
+        isKeyOfTypeAliasSchema(currentValue)
+      ) {
         try {
           acc[key] = parseKey(currentValue, entity, {
             isSparseIndex: isSparse,
diff --git a/packages/core/src/classes/transformer/document-client-request-transformer.ts b/packages/core/src/classes/transformer/document-client-request-transformer.ts
index 58493dd..ff8dabb 100644
--- a/packages/core/src/classes/transformer/document-client-request-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-request-transformer.ts
@@ -1,6 +1,5 @@
 import {
   EntityTarget,
-  IndexOptions,
   INDEX_TYPE,
   QUERY_ORDER,
   Replace,
@@ -8,6 +7,7 @@ import {
   Table,
   UpdateAttributes,
   TRANSFORM_TYPE,
+  IndexOptions,
 } from '@typedorm/common';
 import {DynamoDB} from 'aws-sdk';
 import {dropProp} from '../../helpers/drop-prop';
diff --git a/packages/core/src/helpers/get-interpolated-keys.ts b/packages/core/src/helpers/get-interpolated-keys.ts
index 4521c55..3b2111a 100644
--- a/packages/core/src/helpers/get-interpolated-keys.ts
+++ b/packages/core/src/helpers/get-interpolated-keys.ts
@@ -1,6 +1,13 @@
+import {EntityAliasOrString} from '@typedorm/common';
 import {regexInterpolatedWord} from './constants';
+import {isKeyOfTypeAliasSchema} from './is-key-of-type-alias-schema';
+
+export function getInterpolatedKeys(key: EntityAliasOrString<any>) {
+  // early return when referenced key is of type alias schema
+  if (isKeyOfTypeAliasSchema(key)) {
+    return [key.alias as string];
+  }
 
-export function getInterpolatedKeys(key: string) {
   const matchIterator = key.matchAll(regexInterpolatedWord);
   const interpolatedKeys = [] as string[];
   recursiveFindInterpolatedKeys(key, matchIterator, interpolatedKeys);
diff --git a/packages/core/src/helpers/is-dynamo-entity-key-schma.ts b/packages/core/src/helpers/is-dynamo-entity-key-schema.ts
similarity index 100%
rename from packages/core/src/helpers/is-dynamo-entity-key-schma.ts
rename to packages/core/src/helpers/is-dynamo-entity-key-schema.ts
diff --git a/packages/core/src/helpers/is-key-of-type-alias-schema.ts b/packages/core/src/helpers/is-key-of-type-alias-schema.ts
new file mode 100644
index 0000000..313ada7
--- /dev/null
+++ b/packages/core/src/helpers/is-key-of-type-alias-schema.ts
@@ -0,0 +1,10 @@
+import {KeyAliasSchema} from '@typedorm/common';
+
+export function isKeyOfTypeAliasSchema<Entity = any>(
+  key: any
+): key is KeyAliasSchema<Entity> {
+  return !!(
+    typeof (key as KeyAliasSchema<Entity>) === 'object' &&
+    (key as KeyAliasSchema<Entity>).alias
+  );
+}
diff --git a/packages/core/src/helpers/parse-key.ts b/packages/core/src/helpers/parse-key.ts
index d60c473..efc0c44 100644
--- a/packages/core/src/helpers/parse-key.ts
+++ b/packages/core/src/helpers/parse-key.ts
@@ -1,4 +1,5 @@
-import {SparseIndexParseError} from '@typedorm/common';
+import {EntityAliasOrString, SparseIndexParseError} from '@typedorm/common';
+import {isKeyOfTypeAliasSchema} from './is-key-of-type-alias-schema';
 
 const regexExp = {
   interpolation: /\{{.+?\}}/g,
@@ -6,36 +7,56 @@ const regexExp = {
 };
 
 export function parseKey<Entity>(
-  key: string,
+  key: EntityAliasOrString<any>,
   dict: Entity,
   {isSparseIndex}: {isSparseIndex: boolean} = {isSparseIndex: false}
 ) {
-  return key.replace(regexExp.interpolation, substr => {
-    const match = regexExp.interpolatedWord.exec(substr);
-    let variable: any;
-    if (match) {
-      variable = match[1];
+  if (isKeyOfTypeAliasSchema(key)) {
+    const aliasAttr = key.alias as string;
+    const aliasValue = (dict as any)[aliasAttr];
+    if (aliasValue) {
+      return aliasValue;
     }
-    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) {
+    if (!aliasValue) {
       const error = new Error(
-        `"${variable}" was referenced in ${key} but it's value could not be resolved.`
+        `"attribute alias ${key} was found but it's value could not be resolved.`
       );
 
-      // this is what tells transformer to ignore including parsed values
       if (isSparseIndex) {
-        throw new SparseIndexParseError(key);
+        throw new SparseIndexParseError(aliasAttr);
       }
       throw error;
     }
+    return aliasValue;
+  } else {
+    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) {
+        const error = new Error(
+          `"${variable}" was referenced in ${key} but it's value could not be resolved.`
+        );
+
+        // this is what tells transformer to ignore including parsed values
+        if (isSparseIndex) {
+          throw new SparseIndexParseError(key);
+        }
+        throw error;
+      }
 
-    return valueToReplace.toString();
-  });
+      return valueToReplace.toString();
+    });
+  }
 }
diff --git a/packages/core/src/helpers/validate-key.ts b/packages/core/src/helpers/validate-key.ts
index a2568a5..ad3be65 100644
--- a/packages/core/src/helpers/validate-key.ts
+++ b/packages/core/src/helpers/validate-key.ts
@@ -1,13 +1,38 @@
+import {
+  EntityAliasOrString,
+  InvalidAttributeAliasSchemaError,
+  NoSuchAttributeExistsError,
+} from '@typedorm/common';
 import {regexInterpolatedWord} from './constants';
+import {isKeyOfTypeAliasSchema} from './is-key-of-type-alias-schema';
 
 /**
  *
  * @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}) {
+export function validateKey(
+  key: EntityAliasOrString<any>,
+  dict: {[key: string]: string},
+  entityName?: string
+) {
+  // validate aliases
+  if (isKeyOfTypeAliasSchema(key)) {
+    if (typeof key.alias !== 'string') {
+      throw new InvalidAttributeAliasSchemaError(key.alias as any);
+    }
+
+    const aliasType = dict[key.alias];
+
+    if (!aliasType) {
+      throw new NoSuchAttributeExistsError(key.alias, entityName);
+    }
+    // return when successfully validated
+    return;
+  }
   const matchIterator = key.matchAll(regexInterpolatedWord);
   validateMatch(key, matchIterator, dict);
+  return;
 }
 
 function validateMatch(


Test patch:
diff --git a/packages/core/src/classes/connection/__test__/connection-metadata-builder.spec.ts b/packages/core/src/classes/connection/__test__/connection-metadata-builder.spec.ts
index 756bb05..c060e67 100644
--- a/packages/core/src/classes/connection/__test__/connection-metadata-builder.spec.ts
+++ b/packages/core/src/classes/connection/__test__/connection-metadata-builder.spec.ts
@@ -35,5 +35,5 @@ test('builds entity metadata with path match', () => {
     path.resolve(__dirname, '../../../../__mocks__/**/*.ts')
   );
 
-  expect(entities.length).toEqual(6);
+  expect(entities.length).toEqual(7);
 });
diff --git a/packages/core/src/classes/connection/__test__/entity-metadata-builder.spec.ts b/packages/core/src/classes/connection/__test__/entity-metadata-builder.spec.ts
index 44debc6..dd05b6b 100644
--- a/packages/core/src/classes/connection/__test__/entity-metadata-builder.spec.ts
+++ b/packages/core/src/classes/connection/__test__/entity-metadata-builder.spec.ts
@@ -11,6 +11,7 @@ import {
   Table,
 } from '@typedorm/common';
 import {table} from '../../../../__mocks__/table';
+import {UserAttrAlias} from '@typedorm/core/__mocks__/user-with-attribute-alias';
 
 jest.mock('uuid', () => ({
   v4: jest.fn().mockReturnValue('12-3-1-23-12'),
@@ -80,6 +81,58 @@ test('builds simple entity metadata', () => {
   });
 });
 
+test('builds entity metadata with aliased attributes', () => {
+  const [metadata] = metadataBuilder.build([UserAttrAlias]);
+  expect(metadata.schema).toEqual({
+    indexes: {
+      GSI1: {
+        attributes: {
+          GSI1PK: {
+            alias: 'status',
+          },
+          GSI1SK: 'USER#{{name}}',
+        },
+        metadata: {
+          _interpolations: {
+            GSI1PK: ['status'],
+            GSI1SK: ['name'],
+          },
+          _name: 'GSI1',
+          isSparse: true,
+          type: 'GLOBAL_SECONDARY_INDEX',
+        },
+      },
+      LSI1: {
+        attributes: {
+          LSI1SK: {
+            alias: 'age',
+          },
+        },
+        metadata: {
+          _interpolations: {
+            LSI1SK: ['age'],
+          },
+          _name: 'LSI1',
+          isSparse: true,
+          type: 'LOCAL_SECONDARY_INDEX',
+        },
+      },
+    },
+    primaryKey: {
+      attributes: {
+        PK: 'USER#{{id}}',
+        SK: 'USER#{{id}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id'],
+          SK: ['id'],
+        },
+      },
+    },
+  });
+});
+
 test('builds metadata for entity with sparse indexes', () => {
   const [metadata] = metadataBuilder.build([UserSparseIndexes]);
   expect(metadata.schema).toEqual({
diff --git a/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts b/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
index c8efa7e..c621545 100644
--- a/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
+++ b/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
@@ -13,6 +13,7 @@ import {createTestConnection, resetTestConnection} from '@typedorm/testing';
 import {EntityTransformer} from '../entity-transformer';
 import {UserSparseIndexes} from '../../../../__mocks__/user-sparse-indexes';
 import {table} from '@typedorm/core/__mocks__/table';
+import {UserAttrAlias} from '@typedorm/core/__mocks__/user-with-attribute-alias';
 
 let transformer: EntityTransformer;
 beforeEach(() => {
@@ -22,6 +23,7 @@ beforeEach(() => {
       Organisation,
       UserAutoGenerateAttributes,
       UserSparseIndexes,
+      UserAttrAlias,
     ],
   });
   transformer = new EntityTransformer(connection);
@@ -52,6 +54,27 @@ test('transforms dynamo entity to entity model', () => {
   });
 });
 
+test('transforms dynamo entity with aliased attributes to entity model', () => {
+  const dynamoEntity = {
+    PK: 'USER#1',
+    SK: 'USER#1',
+    GSI1PK: 'USER#STATUS#active',
+    GSI1SK: 'USER#Me',
+    id: '1',
+    name: 'Me',
+    age: 1,
+  };
+  const transformed = transformer.fromDynamoEntity(
+    UserSparseIndexes,
+    dynamoEntity
+  );
+  expect(transformed).toEqual({
+    id: '1',
+    name: 'Me',
+    age: 1,
+  });
+});
+
 test('transforms inherited dynamo entity to entity model', () => {
   const dynamoEntity = {
     PK: 'CUS#1',
@@ -159,6 +182,43 @@ test('transforms simple model to dynamo entity', () => {
   });
 });
 
+test('transforms entity with attribute alias to dynamo entity', () => {
+  const user = new UserAttrAlias();
+  user.id = '111';
+  user.name = 'Test User';
+  user.status = 'inactive';
+  user.age = 10;
+
+  const response = transformer.toDynamoEntity(user);
+  expect(response).toEqual({
+    GSI1PK: 'inactive',
+    GSI1SK: 'USER#Test User',
+    PK: 'USER#111',
+    SK: 'USER#111',
+    LSI1SK: 10, // <-- aliased indexes are correctly persisting attribute types
+    age: 10,
+    id: '111',
+    name: 'Test User',
+    status: 'inactive',
+  });
+});
+
+test('transforms entity with attribute alias to dynamo entity', () => {
+  const user = new UserAttrAlias();
+  user.id = '111';
+
+  user.age = 10;
+
+  const response = transformer.toDynamoEntity(user);
+  expect(response).toEqual({
+    PK: 'USER#111',
+    SK: 'USER#111',
+    LSI1SK: 10, // <-- aliased indexes are correctly persisting attribute types
+    age: 10,
+    id: '111',
+  });
+});
+
 test('transforms entity with sparse index when variable referenced in sort key is missing a value', () => {
   const user = new UserSparseIndexes();
   user.id = '111';
@@ -295,6 +355,20 @@ test('returns all affected indexes for simple attributes', () => {
   });
 });
 
+test('returns all affected indexes for alias attributes', () => {
+  const affectedIndexes = transformer.getAffectedIndexesForAttributes(
+    UserAttrAlias,
+    {
+      age: 10,
+      status: 'inactive',
+    }
+  );
+  expect(affectedIndexes).toEqual({
+    LSI1SK: 10, // <-- aliased indexes are correctly persisting attribute types
+    GSI1PK: 'inactive',
+  });
+});
+
 test('returns all affected indexes for complex attributes', () => {
   const affectedIndexes = transformer.getAffectedIndexesForAttributes(
     Organisation,
diff --git a/packages/core/src/helpers/__test__/get-interpolated-keys.spec.ts b/packages/core/src/helpers/__test__/get-interpolated-keys.spec.ts
new file mode 100644
index 0000000..fdc2ad0
--- /dev/null
+++ b/packages/core/src/helpers/__test__/get-interpolated-keys.spec.ts
@@ -0,0 +1,27 @@
+import {getInterpolatedKeys} from '../get-interpolated-keys';
+
+test('returns all interpolated keys', () => {
+  const keys = getInterpolatedKeys('USER#{{id}}#other@5{{name}}');
+
+  expect(keys).toEqual(['id', 'name']);
+});
+
+test('returns distinct interpolated keys when schema contains duplications', () => {
+  const keys = getInterpolatedKeys('USER#{{id}}#other@5{{id}}');
+
+  expect(keys).toEqual(['id']);
+});
+
+test('returns empty list when no interpolation was found', () => {
+  const keys = getInterpolatedKeys('USER#{id}#other@5');
+
+  expect(keys).toEqual([]);
+});
+
+test('returns empty list when attribute alias schema type is referenced', () => {
+  const keys = getInterpolatedKeys({
+    alias: 'name',
+  });
+
+  expect(keys).toEqual(['name']);
+});
diff --git a/packages/core/src/helpers/__test__/validate-key.spec.ts b/packages/core/src/helpers/__test__/validate-key.spec.ts
index c88dd75..66b2f03 100644
--- a/packages/core/src/helpers/__test__/validate-key.spec.ts
+++ b/packages/core/src/helpers/__test__/validate-key.spec.ts
@@ -1,3 +1,4 @@
+import {NoSuchAttributeExistsError} from '@typedorm/common';
 import {validateKey} from '../validate-key';
 
 describe('validateKey()', () => {
@@ -9,6 +10,34 @@ describe('validateKey()', () => {
     ).not.toThrowError();
   });
 
+  it('should validate key of type alias schema', () => {
+    expect(() =>
+      validateKey(
+        {
+          alias: 'name',
+        },
+        {
+          id: 'String',
+          name: 'Number',
+        }
+      )
+    ).not.toThrowError();
+  });
+
+  it('should validate key of type alias schema and return appropriate error when referencing unknown attribute', () => {
+    expect(() =>
+      validateKey(
+        {
+          alias: 'someRandomAttribute',
+        },
+        {
+          id: 'String',
+          name: 'Number',
+        }
+      )
+    ).toThrow(NoSuchAttributeExistsError);
+  });
+
   it('should validate string with multiple args', () => {
     expect(() =>
       validateKey('USER#{{id}}#STATUS#{{status}}', {