Repository: typedorm/typedorm (ts)

Issue:
Add support for sparse indexes
# Proposed
When parsing entity indexes, allow for [sparse indexes](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-indexes-general-sparse-indexes.html)

I.e
for Index with `sortKey: PUB_ID#${id}` do not auto parse, when index is marked as `sparse index`


## Implementation 
- [x] Add sparse index option on @Entity -> indexes (ideally isSparse: boolean)  
- [x] Allow optional values when parsing `sparse` marked key
- [x] If parsing an sparse index and referenced variable is missing value, DO NOT ADD partition or sort key attributes for that index

Pull request description:
18 add support for sparse indexes
# 🚀️ Features 🚀️

- Added support for sparse indexes
  - Indexes marked as isSparse = false will thrown an error when variable referenced in schema is missing a value
  - by default all indexes are sparse (like dynamodb)
  - More user friendly error messages when noSparse index is missing a value of required variable 

closes #18  

Patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 19a6efa..c04ed0c 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -43,8 +43,7 @@
       "internalConsoleOptions": "neverOpen",
       "port": 9229,
       "env": {
-        "NODE_ENV": "test",
-        "DEBUG": "typedorm:*"
+        "NODE_ENV": "test"
       }
     }
   ]
diff --git a/docs/api.md b/docs/api.md
index 59ed3f5..b796328 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -31,7 +31,14 @@ Table({
   sortKey
 
   // Indexes if there are any
-  indexes
+  indexes: {
+    [indexName] : {
+      partitionKey
+      sortKey
+      // Type of index: INDEX_TYPE
+      type
+    }
+  }
 })
 ```
 
@@ -46,7 +53,7 @@ Declare an entity to be registered in TypeDORM domain
   // Name of the entity that will be saved as __en attribute on each record
   name,
 
-  // table where the entity be saved
+  // Table where the entity be saved
   // @optional
   // Required when no global table exits in connection
   table
@@ -61,7 +68,25 @@ Declare an entity to be registered in TypeDORM domain
 
   // Additional indexes to add to entity
   // @optional
-  indexes
+  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
+      // @optional
+      // Required when index type is GSI
+      partitionKey
+
+      // Sort key attribute name for this entity
+      sortKey
+
+      // Type of index: INDEX_TYPE
+      type
+
+      // Defines if the current index should be considered sparse
+      // @default true - all indexes are marked sparse by default
+      isSparse
+    }
+  }
 })
 ```
 
diff --git a/packages/common/private-api.ts b/packages/common/private-api.ts
index d0430a3..3a956bb 100644
--- a/packages/common/private-api.ts
+++ b/packages/common/private-api.ts
@@ -1,3 +1,4 @@
 export * from './src/metadata/metadata-manager';
 export * from './src/metadata/metadata-storage';
 export * from './src/logger/debug-logger';
+export * from './src/error';
diff --git a/packages/common/src/error/index.ts b/packages/common/src/error/index.ts
new file mode 100644
index 0000000..26cd1ee
--- /dev/null
+++ b/packages/common/src/error/index.ts
@@ -0,0 +1,1 @@
+export * from './parse-error';
diff --git a/packages/common/src/error/parse-error.ts b/packages/common/src/error/parse-error.ts
new file mode 100644
index 0000000..8f82ec8
--- /dev/null
+++ b/packages/common/src/error/parse-error.ts
@@ -0,0 +1,8 @@
+export class SparseIndexParseError extends Error {
+  name = 'SparseIndexParseError';
+
+  constructor(key: string) {
+    super();
+    this.message = `Failed to parse "${key}"`;
+  }
+}
diff --git a/packages/common/src/table.ts b/packages/common/src/table.ts
index 30ee43e..a756e00 100644
--- a/packages/common/src/table.ts
+++ b/packages/common/src/table.ts
@@ -22,11 +22,19 @@ 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;
diff --git a/packages/core/__mocks__/organisation.ts b/packages/core/__mocks__/organisation.ts
index 0e606b7..ec2ea3c 100644
--- a/packages/core/__mocks__/organisation.ts
+++ b/packages/core/__mocks__/organisation.ts
@@ -17,11 +17,13 @@ export interface OrganisationPrimaryKey {
       partitionKey: 'ORG#{{id}}#STATUS#{{status}}',
       sortKey: 'ORG#{{name}}#ACTIVE#{{active}}',
       type: INDEX_TYPE.GSI,
+      isSparse: false,
     },
     GSI2: {
       partitionKey: 'ORG#{{id}}#STATUS#{{status}}',
       sortKey: 'ORG#{{name}}#TEAM_COUNT#{{teamCount}}',
       type: INDEX_TYPE.GSI,
+      isSparse: false,
     },
   },
 })
diff --git a/packages/core/__mocks__/user-auto-generate-attributes.ts b/packages/core/__mocks__/user-auto-generate-attributes.ts
index 0ce9136..7ad3f59 100644
--- a/packages/core/__mocks__/user-auto-generate-attributes.ts
+++ b/packages/core/__mocks__/user-auto-generate-attributes.ts
@@ -23,6 +23,7 @@ export interface UserAutoGenerateAttributesPrimaryKey {
       partitionKey: 'USER#UPDATED_AT#{{updatedAt}}',
       sortKey: 'USER#{{id}}',
       type: INDEX_TYPE.GSI,
+      isSparse: false,
     },
   },
 })
diff --git a/packages/core/__mocks__/user-sparse-indexes.ts b/packages/core/__mocks__/user-sparse-indexes.ts
new file mode 100644
index 0000000..d5ee024
--- /dev/null
+++ b/packages/core/__mocks__/user-sparse-indexes.ts
@@ -0,0 +1,47 @@
+import {Attribute, Entity, INDEX_TYPE} from '@typedorm/common';
+import {table} from './table';
+
+export interface UserSparseIndexesPrimaryKey {
+  id: string;
+}
+
+export interface UserSparseIndexesGSI1 {
+  status: string;
+  name?: string;
+}
+
+@Entity({
+  table,
+  name: 'user-sparse-indexes',
+  primaryKey: {
+    partitionKey: 'USER_SPARSE_INDEXES#{{id}}',
+    sortKey: 'USER_SPARSE_INDEXES#{{id}}',
+  },
+  indexes: {
+    GSI1: {
+      partitionKey: 'USER_SPARSE_INDEXES#STATUS#{{status}}',
+      sortKey: 'USER_SPARSE_INDEXES#{{name}}',
+      type: INDEX_TYPE.GSI,
+      isSparse: true,
+    },
+    LSI1: {
+      // isSparse: true, all indexes are sparse by default
+      sortKey: 'AGE#{{age}}',
+      type: INDEX_TYPE.LSI,
+    },
+  },
+})
+export class UserSparseIndexes
+  implements UserSparseIndexesPrimaryKey, UserSparseIndexesGSI1 {
+  @Attribute()
+  id: string;
+
+  @Attribute()
+  name: string;
+
+  @Attribute()
+  status: string;
+
+  @Attribute()
+  age: number;
+}
diff --git a/packages/core/__mocks__/user-unique-email.ts b/packages/core/__mocks__/user-unique-email.ts
index 37a76cf..d89f62e 100644
--- a/packages/core/__mocks__/user-unique-email.ts
+++ b/packages/core/__mocks__/user-unique-email.ts
@@ -22,6 +22,7 @@ export interface UserUniqueEmailGSI1 {
       partitionKey: 'USER#STATUS#{{status}}',
       sortKey: 'USER#{{name}}',
       type: INDEX_TYPE.GSI,
+      isSparse: false,
     },
   },
 })
diff --git a/packages/core/__mocks__/user.ts b/packages/core/__mocks__/user.ts
index 50b1572..2f2dfd7 100644
--- a/packages/core/__mocks__/user.ts
+++ b/packages/core/__mocks__/user.ts
@@ -22,6 +22,7 @@ export interface UserGSI1 {
       partitionKey: 'USER#STATUS#{{status}}',
       sortKey: 'USER#{{name}}',
       type: INDEX_TYPE.GSI,
+      isSparse: false,
     },
   },
 })
diff --git a/packages/core/src/classes/connection/connection.ts b/packages/core/src/classes/connection/connection.ts
index ec28630..588076e 100644
--- a/packages/core/src/classes/connection/connection.ts
+++ b/packages/core/src/classes/connection/connection.ts
@@ -152,7 +152,7 @@ export class Connection {
     primaryKey: DynamoEntitySchemaPrimaryKey,
     attributeName: string
   ) {
-    const primaryKeyInterpolations = primaryKey._interpolations ?? {};
+    const primaryKeyInterpolations = primaryKey.metadata._interpolations ?? {};
     return Object.keys(primaryKeyInterpolations).some(key => {
       const currInterpolation = primaryKeyInterpolations[key];
       return currInterpolation.includes(attributeName);
diff --git a/packages/core/src/classes/metadata/entity-metadata.ts b/packages/core/src/classes/metadata/entity-metadata.ts
index a201ca2..2923175 100644
--- a/packages/core/src/classes/metadata/entity-metadata.ts
+++ b/packages/core/src/classes/metadata/entity-metadata.ts
@@ -2,7 +2,6 @@ import {
   EntityRawMetadataOptions,
   EntityTarget,
   Indexes,
-  IndexOptions,
   INDEX_TYPE,
   INTERNAL_ENTITY_ATTRIBUTE,
   Table,
@@ -17,11 +16,16 @@ import {BaseMetadata} from './base-metadata';
 import {InternalAttributeMetadata} from './internal-attribute-metadata';
 
 export type DynamoEntitySchemaPrimaryKey = {
-  [key: string]: any;
-  _interpolations?: {[key: string]: string[]};
+  attributes: {[key: string]: any};
+  metadata: {
+    _interpolations?: {[key: string]: string[]};
+  };
 };
 export type DynamoEntityIndexesSchema = {
-  [key: string]: DynamoEntityIndexSchema;
+  [key: string]: {
+    attributes: {[key: string]: any};
+    metadata: DynamoEntityIndexSchema;
+  };
 };
 
 export type DynamoEntityIndexSchema = {
@@ -29,9 +33,9 @@ export type DynamoEntityIndexSchema = {
   _name?: string;
   // for LSI, only contains interpolations for sort key
   _interpolations?: {[key: string]: string[]};
-  // entity transformer will inject additional attributes
-  [key: string]: any;
-} & IndexOptions;
+  isSparse: boolean;
+  type: INDEX_TYPE;
+};
 
 export interface DynamoEntitySchema {
   primaryKey: DynamoEntitySchemaPrimaryKey;
@@ -120,7 +124,7 @@ export class EntityMetadata extends BaseMetadata {
     table: Table;
     indexes: Indexes;
     attributes: {[key: string]: string};
-  }) {
+  }): DynamoEntityIndexesSchema {
     return Object.keys(indexes).reduce((acc, key) => {
       const tableIndexSignature = table.getIndexByKey(key);
       if (!tableIndexSignature) {
@@ -140,13 +144,23 @@ export class EntityMetadata extends BaseMetadata {
           throw new Error('Index signature mismatch.');
         }
         acc[key] = {
-          [tableIndexSignature.sortKey]: currentIndex.sortKey,
-          type: tableIndexSignature.type,
-          _name: key,
-          _interpolations: {
-            [tableIndexSignature.sortKey]: sortKeyInterpolations,
+          attributes: {
+            [tableIndexSignature.sortKey]: currentIndex.sortKey,
+          },
+          metadata: {
+            type: tableIndexSignature.type,
+            isSparse:
+              currentIndex.isSparse === undefined ||
+              currentIndex.isSparse === null
+                ? true // by default all indexes are sparse
+                : !!currentIndex.isSparse,
+            _name: key,
+            _interpolations: {
+              [tableIndexSignature.sortKey]: sortKeyInterpolations,
+            },
           },
         };
+
         return acc;
       } else {
         if (currentIndex.type !== INDEX_TYPE.GSI) {
@@ -159,18 +173,27 @@ export class EntityMetadata extends BaseMetadata {
         );
 
         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,
+          attributes: {
+            [tableIndexSignature.partitionKey]: currentIndex.partitionKey,
+            [tableIndexSignature.sortKey]: currentIndex.sortKey,
+          },
+          metadata: {
+            isSparse:
+              currentIndex.isSparse === undefined ||
+              currentIndex.isSparse === null
+                ? true // by default all indexes are sparse
+                : !!currentIndex.isSparse,
+            type: tableIndexSignature.type,
+            _name: key,
+            // remove any duplicates from partition or sort keys
+            _interpolations: {
+              [tableIndexSignature.partitionKey]: partitionKeyInterpolations,
+              [tableIndexSignature.sortKey]: sortKeyInterpolations,
+            },
           },
         };
         return acc;
       }
-    }, {} as any);
+    }, {} as DynamoEntityIndexesSchema);
   }
 }
diff --git a/packages/core/src/classes/transformer/base-transformer.ts b/packages/core/src/classes/transformer/base-transformer.ts
index 7789127..5427472 100644
--- a/packages/core/src/classes/transformer/base-transformer.ts
+++ b/packages/core/src/classes/transformer/base-transformer.ts
@@ -1,16 +1,16 @@
-import {EntityTarget, INDEX_TYPE, Table} from '@typedorm/common';
+import {
+  DynamoEntityIndexesSchema,
+  DynamoEntityIndexSchema,
+} from './../metadata/entity-metadata';
+import {EntityTarget, Table, SparseIndexParseError} from '@typedorm/common';
 import {getConstructorForInstance} from '../../helpers/get-constructor-for-instance';
 import {isEmptyObject} from '../../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 {IsAutoGeneratedAttributeMetadata} from '../metadata/auto-generated-attribute-metadata';
-import {
-  DynamoEntityIndexesSchema,
-  DynamoEntitySchema,
-  DynamoEntitySchemaPrimaryKey,
-} from '../metadata/entity-metadata';
+import {DynamoEntitySchemaPrimaryKey} from '../metadata/entity-metadata';
+import {isDynamoEntityKeySchema} from '../../helpers/is-dynamo-entity-key-schma';
 
 export abstract class BaseTransformer {
   constructor(protected connection: Connection) {}
@@ -19,9 +19,9 @@ export abstract class BaseTransformer {
    * @param entityClass Entity Class
    */
   getTableNameForEntity<Entity>(entityClass: EntityTarget<Entity>) {
-    const metadata = this.connection.getEntityByTarget(entityClass);
+    const entityMetadata = this.connection.getEntityByTarget(entityClass);
 
-    return metadata.table.name;
+    return entityMetadata.table.name;
   }
 
   /**
@@ -31,7 +31,7 @@ export abstract class BaseTransformer {
   toDynamoEntity<Entity>(entity: Entity) {
     const entityClass = getConstructorForInstance(entity);
     // retrieve metadata and parse it to schema
-    const metadata = this.connection.getEntityByTarget(entityClass);
+    const entityMetadata = this.connection.getEntityByTarget(entityClass);
 
     //  auto populate generated values
     this.connection.getAttributesForEntity(entityClass).forEach(attr => {
@@ -40,60 +40,54 @@ export abstract class BaseTransformer {
       }
     });
 
-    const parsedSchema: DynamoEntitySchema = this.recursiveParseEntity(
-      metadata.schema,
+    const parsedPrimaryKey = this.recursiveParseEntity(
+      entityMetadata.schema.primaryKey.attributes,
       entity
     );
 
-    // drop any extra keys that are on entity schema
-    const normalizedPrimaryKey = this.getParsedPrimaryKey(
-      metadata.table,
-      metadata.schema.primaryKey,
-      entity
-    );
+    const indexesToParse = entityMetadata.schema.indexes ?? {};
+    const rawParsedIndexes = this.recursiveParseEntity(indexesToParse, entity);
 
-    const indexes = {
-      ...parsedSchema.indexes,
-    } as DynamoEntityIndexesSchema;
+    const parsedIndexes = Object.keys(rawParsedIndexes).reduce(
+      (acc, currIndexKey) => {
+        const {metadata, attributes} = indexesToParse[currIndexKey];
+        const currentParsedIndex = rawParsedIndexes[currIndexKey];
 
-    // normalize indexes to non nested keys and drop other extra keys
-    const normalizedIndexes = Object.keys(indexes).reduce((acc, key) => {
-      const index = indexes[key];
+        // validate if there are any duplicated attribute names
+        Object.keys(currentParsedIndex).forEach(attr => {
+          if (acc[attr]) {
+            throw new Error(
+              `Failed to parse entity "${entityMetadata.name}", duplicate attribute "${attr}".`
+            );
+          }
+        });
 
-      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;
+        // if current index marked as sparse and one or more attribute is missing value, do not add it to schema
+        if (metadata.isSparse) {
+          const doesAllAttributesHaveValue = Object.keys(attributes).every(
+            attr => {
+              if (!currentParsedIndex[attr]) {
+                return false;
+              }
+              return true;
+            }
+          );
+
+          if (!doesAllAttributesHaveValue) {
+            return acc;
+          }
         }
-      })(index);
 
-      acc = {...acc, ...onlyKeysIndex};
-      return acc;
-    }, {});
+        acc = {...acc, ...currentParsedIndex};
+        return acc;
+      },
+      {} as {[key: string]: string}
+    );
 
     // clone and cleanup any redundant keys
     const formattedSchema = {
-      ...normalizedPrimaryKey,
-      ...normalizedIndexes,
+      ...parsedPrimaryKey,
+      ...parsedIndexes,
     };
 
     return {...entity, ...formattedSchema};
@@ -134,7 +128,8 @@ export abstract class BaseTransformer {
 
         Object.keys(indexes).forEach(key => {
           const currIndex = indexes[key];
-          const interpolationsForCurrIndex = currIndex._interpolations ?? {};
+          const interpolationsForCurrIndex =
+            currIndex.metadata._interpolations ?? {};
 
           // if current index does not have any interpolations to resolve, move onto next one
           if (isEmptyObject(interpolationsForCurrIndex)) {
@@ -148,7 +143,7 @@ export abstract class BaseTransformer {
 
             if (currentInterpolation.includes(attrKey)) {
               const parsedIndex = parseKey(
-                currIndex[interpolationKey],
+                currIndex.attributes[interpolationKey],
                 attributes
               );
               acc[interpolationKey] = parsedIndex;
@@ -173,31 +168,7 @@ export abstract class BaseTransformer {
     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>);
+    return this.recursiveParseEntity(primaryKey.attributes, attributes);
   }
 
   /**
@@ -206,13 +177,33 @@ export abstract class BaseTransformer {
    * @param schema schema to resolve
    * @param entity entity to resolve schema against
    */
-  protected recursiveParseEntity<Entity>(schema: any, entity: Entity) {
+  protected recursiveParseEntity<Entity>(
+    schema: DynamoEntitySchemaPrimaryKey | DynamoEntityIndexesSchema,
+    entity: Entity,
+    isSparse = false
+  ) {
     const parsedSchema = Object.keys(schema).reduce((acc, key) => {
-      const currentValue = schema[key];
-      if (isObject(currentValue)) {
-        acc[key] = this.recursiveParseEntity(currentValue, entity);
+      const currentValue = (schema as any)[key];
+
+      if (typeof currentValue === 'string') {
+        try {
+          acc[key] = parseKey(currentValue, entity, {
+            isSparseIndex: isSparse,
+          });
+        } catch (err) {
+          // if there was an error parsing sparse index, ignore
+          if (!(err instanceof SparseIndexParseError)) {
+            throw err;
+          }
+        }
+      } else if (isDynamoEntityKeySchema(currentValue)) {
+        acc[key] = this.recursiveParseEntity(
+          currentValue.attributes,
+          entity,
+          !!(currentValue.metadata as DynamoEntityIndexSchema)?.isSparse
+        );
       } else {
-        acc[key] = parseKey(currentValue, entity);
+        acc[key] = this.recursiveParseEntity(currentValue, entity);
       }
       return acc;
     }, {} as any);
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 5640afe..74355b1 100644
--- a/packages/core/src/classes/transformer/document-client-request-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-request-transformer.ts
@@ -452,7 +452,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     ) {
       parsedPartitionKey.name = table.partitionKey;
       parsedPartitionKey.value = parseKey(
-        schema.primaryKey[table.partitionKey],
+        schema.primaryKey.attributes[table.partitionKey],
         partitionKeyAttributes
       );
     } else {
@@ -461,7 +461,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
 
       const schemaForIndexToQuery = (schema.indexes ?? {})[queryIndexName];
       parsedPartitionKey.value = parseKey(
-        schemaForIndexToQuery[indexToQuery.partitionKey],
+        schemaForIndexToQuery.attributes[indexToQuery.partitionKey],
         partitionKeyAttributes
       );
     }
diff --git a/packages/core/src/classes/transformer/entity-transformer.ts b/packages/core/src/classes/transformer/entity-transformer.ts
index 9971e9a..e9ea98d 100644
--- a/packages/core/src/classes/transformer/entity-transformer.ts
+++ b/packages/core/src/classes/transformer/entity-transformer.ts
@@ -26,7 +26,9 @@ export class EntityTransformer extends BaseTransformer {
       dynamoEntity
     );
 
-    const entityPrimaryKeys = Object.keys(entityMetadata.schema.primaryKey);
+    const entityPrimaryKeys = Object.keys(
+      entityMetadata.schema.primaryKey.attributes
+    );
     const entityInternalAttributeKeys = entityMetadata.internalAttributes.map(
       attr => attr.name
     );
@@ -34,8 +36,7 @@ export class EntityTransformer extends BaseTransformer {
     const entityMetadataSchemaIndexes = entityMetadata.schema.indexes ?? {};
     const entityIndexes = Object.keys(entityMetadataSchemaIndexes)
       .map(key => {
-        const currentIndex = entityMetadataSchemaIndexes[key];
-        return Object.keys(currentIndex._interpolations ?? {});
+        return Object.keys(entityMetadataSchemaIndexes[key].attributes ?? {});
       })
       .flat();
 
diff --git a/packages/core/src/helpers/build-primary-key-schema.ts b/packages/core/src/helpers/build-primary-key-schema.ts
index 9ce73a8..c32c768 100644
--- a/packages/core/src/helpers/build-primary-key-schema.ts
+++ b/packages/core/src/helpers/build-primary-key-schema.ts
@@ -10,7 +10,12 @@ export function buildPrimaryKeySchema({
   table: Table;
   primaryKey: PrimaryKey;
   attributes: {[key: string]: string};
-}) {
+}): {
+  attributes: {[key: string]: string};
+  metadata: {
+    _interpolations: {[key: string]: any};
+  };
+} {
   const partitionKeyInterpolations = getInterpolatedKeys(
     primaryKey.partitionKey
   );
@@ -31,11 +36,15 @@ export function buildPrimaryKeySchema({
     const tableSortKeyName = table.sortKey ?? '';
     const sortKeyInterpolations = getInterpolatedKeys(primaryKey.sortKey);
     return {
-      [tablePartitionKeyName]: primaryKey.partitionKey,
-      [tableSortKeyName]: primaryKey.sortKey,
-      _interpolations: {
-        [tablePartitionKeyName]: partitionKeyInterpolations,
-        [tableSortKeyName]: sortKeyInterpolations,
+      attributes: {
+        [tablePartitionKeyName]: primaryKey.partitionKey,
+        [tableSortKeyName]: primaryKey.sortKey,
+      },
+      metadata: {
+        _interpolations: {
+          [tablePartitionKeyName]: partitionKeyInterpolations,
+          [tableSortKeyName]: sortKeyInterpolations,
+        },
       },
     };
     // when current primary key is a simple key
@@ -50,9 +59,13 @@ export function buildPrimaryKeySchema({
 
     // build primary key
     return {
-      [tablePartitionKeyName]: primaryKey.partitionKey,
-      _interpolations: {
-        [tablePartitionKeyName]: partitionKeyInterpolations,
+      attributes: {
+        [tablePartitionKeyName]: primaryKey.partitionKey,
+      },
+      metadata: {
+        _interpolations: {
+          [tablePartitionKeyName]: partitionKeyInterpolations,
+        },
       },
     };
   }
diff --git a/packages/core/src/helpers/is-dynamo-entity-key-schma.ts b/packages/core/src/helpers/is-dynamo-entity-key-schma.ts
new file mode 100644
index 0000000..6383ad1
--- /dev/null
+++ b/packages/core/src/helpers/is-dynamo-entity-key-schma.ts
@@ -0,0 +1,8 @@
+import {DynamoEntitySchemaPrimaryKey} from '../classes/metadata/entity-metadata';
+export const isDynamoEntityKeySchema = (
+  item: any
+): item is DynamoEntitySchemaPrimaryKey =>
+  !!(
+    (item as DynamoEntitySchemaPrimaryKey).attributes &&
+    (item as DynamoEntitySchemaPrimaryKey).metadata
+  );
diff --git a/packages/core/src/helpers/is-used-for-primary-key.ts b/packages/core/src/helpers/is-used-for-primary-key.ts
index 3498808..910cf41 100644
--- a/packages/core/src/helpers/is-used-for-primary-key.ts
+++ b/packages/core/src/helpers/is-used-for-primary-key.ts
@@ -27,7 +27,7 @@ export function isUsedForPrimaryKey(
     }
     // when parsed primary key is provided, flatten all interpolations to an array
   } else {
-    const interpolationsToFlatten = primaryKey._interpolations ?? {};
+    const interpolationsToFlatten = primaryKey.metadata._interpolations ?? {};
     primaryKeyInterpolations = [
       ...primaryKeyInterpolations,
       ...Object.keys(interpolationsToFlatten).flatMap(key => {
diff --git a/packages/core/src/helpers/parse-key.ts b/packages/core/src/helpers/parse-key.ts
index 3b56a19..d60c473 100644
--- a/packages/core/src/helpers/parse-key.ts
+++ b/packages/core/src/helpers/parse-key.ts
@@ -1,9 +1,15 @@
+import {SparseIndexParseError} from '@typedorm/common';
+
 const regexExp = {
   interpolation: /\{{.+?\}}/g,
   interpolatedWord: /\{{(.+?)\}}/,
 };
 
-export function parseKey<Entity>(key: string, dict: Entity) {
+export function parseKey<Entity>(
+  key: string,
+  dict: Entity,
+  {isSparseIndex}: {isSparseIndex: boolean} = {isSparseIndex: false}
+) {
   return key.replace(regexExp.interpolation, substr => {
     const match = regexExp.interpolatedWord.exec(substr);
     let variable: any;
@@ -17,8 +23,17 @@ export function parseKey<Entity>(key: string, dict: Entity) {
     }
 
     const valueToReplace = (dict as any)[variable];
+
     if (valueToReplace === undefined || valueToReplace === null) {
-      throw new Error(`Could not resolve "${variable}" from given dictionary`);
+      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();


Test patch:
diff --git a/packages/core/src/classes/connection/__test__/attributes-metadata-builder.spec.ts b/packages/core/src/classes/connection/__test__/attributes-metadata-builder.spec.ts
index cc05aee..df5f1b4 100644
--- a/packages/core/src/classes/connection/__test__/attributes-metadata-builder.spec.ts
+++ b/packages/core/src/classes/connection/__test__/attributes-metadata-builder.spec.ts
@@ -147,11 +147,15 @@ test('builds metadata for attribute with explicit entity', () => {
       entityClass: Admin,
       table,
       unique: {
-        PK: 'USER.EMAIL#{{email}}',
-        SK: 'USER.EMAIL#{{email}}',
-        _interpolations: {
-          PK: ['email'],
-          SK: ['email'],
+        attributes: {
+          PK: 'USER.EMAIL#{{email}}',
+          SK: 'USER.EMAIL#{{email}}',
+        },
+        metadata: {
+          _interpolations: {
+            PK: ['email'],
+            SK: ['email'],
+          },
         },
       },
     },
@@ -188,11 +192,15 @@ test('builds metadata with implicit primary key for unique attribute', () => {
       entityClass: UserUniqueEmail,
       table,
       unique: {
-        PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#{{email}}',
-        SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#{{email}}',
-        _interpolations: {
-          PK: ['email'],
-          SK: ['email'],
+        attributes: {
+          PK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#{{email}}',
+          SK: 'DRM_GEN_USERUNIQUEEMAIL.EMAIL#{{email}}',
+        },
+        metadata: {
+          _interpolations: {
+            PK: ['email'],
+            SK: ['email'],
+          },
         },
       },
     },
diff --git a/packages/core/src/classes/connection/__test__/connection-manager.spec.ts b/packages/core/src/classes/connection/__test__/connection-manager.spec.ts
index 3104275..f55ba92 100644
--- a/packages/core/src/classes/connection/__test__/connection-manager.spec.ts
+++ b/packages/core/src/classes/connection/__test__/connection-manager.spec.ts
@@ -20,22 +20,31 @@ test('Connection Manager should register metadata correctly', () => {
   expect(userMetadata.schema).toEqual({
     indexes: {
       GSI1: {
-        GSI1PK: 'USER#STATUS#{{status}}',
-        GSI1SK: 'USER#{{name}}',
-        _interpolations: {
-          GSI1PK: ['status'],
-          GSI1SK: ['name'],
+        attributes: {
+          GSI1PK: 'USER#STATUS#{{status}}',
+          GSI1SK: 'USER#{{name}}',
+        },
+        metadata: {
+          _interpolations: {
+            GSI1PK: ['status'],
+            GSI1SK: ['name'],
+          },
+          _name: 'GSI1',
+          isSparse: false,
+          type: 'GLOBAL_SECONDARY_INDEX',
         },
-        _name: 'GSI1',
-        type: 'GLOBAL_SECONDARY_INDEX',
       },
     },
     primaryKey: {
-      PK: 'USER#{{id}}',
-      SK: 'USER#{{id}}',
-      _interpolations: {
-        PK: ['id'],
-        SK: ['id'],
+      attributes: {
+        PK: 'USER#{{id}}',
+        SK: 'USER#{{id}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id'],
+          SK: ['id'],
+        },
       },
     },
   });
@@ -51,22 +60,31 @@ test('Connection Manager should register metadata for entity loaded from path',
   expect(userMetadata.schema).toEqual({
     indexes: {
       GSI1: {
-        GSI1PK: 'USER#STATUS#{{status}}',
-        GSI1SK: 'USER#{{name}}',
-        _interpolations: {
-          GSI1PK: ['status'],
-          GSI1SK: ['name'],
+        attributes: {
+          GSI1PK: 'USER#STATUS#{{status}}',
+          GSI1SK: 'USER#{{name}}',
+        },
+        metadata: {
+          _interpolations: {
+            GSI1PK: ['status'],
+            GSI1SK: ['name'],
+          },
+          _name: 'GSI1',
+          isSparse: false,
+          type: 'GLOBAL_SECONDARY_INDEX',
         },
-        _name: 'GSI1',
-        type: 'GLOBAL_SECONDARY_INDEX',
       },
     },
     primaryKey: {
-      PK: 'USER#{{id}}',
-      SK: 'USER#{{id}}',
-      _interpolations: {
-        PK: ['id'],
-        SK: ['id'],
+      attributes: {
+        PK: 'USER#{{id}}',
+        SK: 'USER#{{id}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id'],
+          SK: ['id'],
+        },
       },
     },
   });
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 010fadb..756bb05 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(5);
+  expect(entities.length).toEqual(6);
 });
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 ec9fe4f..7ead6fc 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
@@ -1,4 +1,5 @@
 import {User} from '../../../../__mocks__/user';
+import {UserSparseIndexes} from '../../../../__mocks__/user-sparse-indexes';
 import {EntityMetadataBuilder} from '../entity-metadata-builder';
 import {createTestConnection, resetTestConnection} from '@typedorm/testing';
 import {
@@ -9,6 +10,7 @@ import {
   Table,
 } from '@typedorm/common';
 import {table} from '../../../../__mocks__/table';
+
 jest.mock('uuid', () => ({
   v4: jest.fn().mockReturnValue('12-3-1-23-12'),
 }));
@@ -47,22 +49,79 @@ test('builds simple entity metadata', () => {
   expect(metadata.schema).toEqual({
     indexes: {
       GSI1: {
-        GSI1PK: 'USER#STATUS#{{status}}',
-        GSI1SK: 'USER#{{name}}',
+        attributes: {
+          GSI1PK: 'USER#STATUS#{{status}}',
+          GSI1SK: 'USER#{{name}}',
+        },
+        metadata: {
+          isSparse: false,
+          _interpolations: {
+            GSI1PK: ['status'],
+            GSI1SK: ['name'],
+          },
+          _name: 'GSI1',
+          type: 'GLOBAL_SECONDARY_INDEX',
+        },
+      },
+    },
+    primaryKey: {
+      attributes: {
+        PK: 'USER#{{id}}',
+        SK: 'USER#{{id}}',
+      },
+      metadata: {
         _interpolations: {
-          GSI1PK: ['status'],
-          GSI1SK: ['name'],
+          PK: ['id'],
+          SK: ['id'],
+        },
+      },
+    },
+  });
+});
+
+test('builds metadata for entity with sparse indexes', () => {
+  const [metadata] = metadataBuilder.build([UserSparseIndexes]);
+  expect(metadata.schema).toEqual({
+    indexes: {
+      GSI1: {
+        attributes: {
+          GSI1PK: 'USER_SPARSE_INDEXES#STATUS#{{status}}',
+          GSI1SK: 'USER_SPARSE_INDEXES#{{name}}',
+        },
+        metadata: {
+          isSparse: true,
+          _interpolations: {
+            GSI1PK: ['status'],
+            GSI1SK: ['name'],
+          },
+          _name: 'GSI1',
+          type: 'GLOBAL_SECONDARY_INDEX',
+        },
+      },
+      LSI1: {
+        attributes: {
+          LSI1SK: 'AGE#{{age}}',
+        },
+        metadata: {
+          _interpolations: {
+            LSI1SK: ['age'],
+          },
+          _name: 'LSI1',
+          isSparse: true,
+          type: 'LOCAL_SECONDARY_INDEX',
         },
-        _name: 'GSI1',
-        type: 'GLOBAL_SECONDARY_INDEX',
       },
     },
     primaryKey: {
-      PK: 'USER#{{id}}',
-      SK: 'USER#{{id}}',
-      _interpolations: {
-        PK: ['id'],
-        SK: ['id'],
+      attributes: {
+        PK: 'USER_SPARSE_INDEXES#{{id}}',
+        SK: 'USER_SPARSE_INDEXES#{{id}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id'],
+          SK: ['id'],
+        },
       },
     },
   });
@@ -144,9 +203,13 @@ test('builds entity metadata with global table config', () => {
   expect(entityMetadata.schema).toEqual({
     indexes: {},
     primaryKey: {
-      PK: 'USER#{{id}}',
-      _interpolations: {
-        PK: ['id'],
+      attributes: {
+        PK: 'USER#{{id}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id'],
+        },
       },
     },
   });
diff --git a/packages/core/src/classes/manager/__test__/entity-manager.spec.ts b/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
index a1908a6..7247ca1 100644
--- a/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
+++ b/packages/core/src/classes/manager/__test__/entity-manager.spec.ts
@@ -155,7 +155,9 @@ test('throws an error when trying to do a get request with non primary key attri
     manager.findOne(User, {
       name: 'User',
     })
-  ).rejects.toThrowError('Could not resolve "id" from given dictionary');
+  ).rejects.toThrowError(
+    '"id" was referenced in USER#{{id}} but it\'s value could not be resolved.'
+  );
 });
 
 /**
@@ -225,7 +227,9 @@ test('throws an error if trying to perform exists check with partial primary key
     manager.findOne(User, {
       name: 'User',
     })
-  ).rejects.toThrowError('Could not resolve "id" from given dictionary');
+  ).rejects.toThrowError(
+    '"id" was referenced in USER#{{id}} but it\'s value could not be resolved.'
+  );
 });
 
 /**
@@ -469,7 +473,9 @@ test('throws an error when trying to delete item by non primary key attributes',
     manager.delete(User, {
       name: 'User',
     })
-  ).rejects.toThrowError('Could not resolve "id" from given dictionary');
+  ).rejects.toThrowError(
+    '"id" was referenced in USER#{{id}} but it\'s value could not be resolved.'
+  );
 });
 
 test('deletes an item with unique attributes', async () => {
diff --git a/packages/core/src/classes/metadata/__test__/attribute-metadata.spec.ts b/packages/core/src/classes/metadata/__test__/attribute-metadata.spec.ts
index 699b3d5..bf95f37 100644
--- a/packages/core/src/classes/metadata/__test__/attribute-metadata.spec.ts
+++ b/packages/core/src/classes/metadata/__test__/attribute-metadata.spec.ts
@@ -35,11 +35,15 @@ test('unique attribute has required metadata', () => {
     name: 'id',
     type: 'String',
     unique: {
-      PK: 'DRM_GEN_USER.ID#{{id}}',
-      SK: 'DRM_GEN_USER.ID#{{id}}',
-      _interpolations: {
-        PK: ['id'],
-        SK: ['id'],
+      attributes: {
+        PK: 'DRM_GEN_USER.ID#{{id}}',
+        SK: 'DRM_GEN_USER.ID#{{id}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id'],
+          SK: ['id'],
+        },
       },
     },
   });
@@ -64,11 +68,15 @@ test('unique attribute has required metadata with specified primary key', () =>
     name: 'name',
     type: 'String',
     unique: {
-      PK: 'USER.NAME#{{name}}',
-      SK: 'USER.NAME#{{name}}',
-      _interpolations: {
-        PK: ['name'],
-        SK: ['name'],
+      attributes: {
+        PK: 'USER.NAME#{{name}}',
+        SK: 'USER.NAME#{{name}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['name'],
+          SK: ['name'],
+        },
       },
     },
   });
diff --git a/packages/core/src/classes/metadata/__test__/entity-metadata.spec.ts b/packages/core/src/classes/metadata/__test__/entity-metadata.spec.ts
index 2b48d0f..d8e44d5 100644
--- a/packages/core/src/classes/metadata/__test__/entity-metadata.spec.ts
+++ b/packages/core/src/classes/metadata/__test__/entity-metadata.spec.ts
@@ -53,9 +53,13 @@ test('create entity metadata for per entity table', () => {
   expect(userEntityMetadata.schema).toEqual({
     indexes: {},
     primaryKey: {
-      PK: 'USER#{{id}}',
-      _interpolations: {
-        PK: ['id'],
+      attributes: {
+        PK: 'USER#{{id}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id'],
+        },
       },
     },
   });
@@ -126,30 +130,44 @@ test('creates entity metadata with complex composite and indexes key', () => {
   expect(complexEntityMetadata.schema).toEqual({
     indexes: {
       GSI1: {
-        GSI1PK: 'USER#{{id}}#NAME#{{name}}',
-        GSI1SK: 'Name#{{name}}',
-        _interpolations: {
-          GSI1PK: ['id', 'name'],
-          GSI1SK: ['name'],
+        attributes: {
+          GSI1PK: 'USER#{{id}}#NAME#{{name}}',
+          GSI1SK: 'Name#{{name}}',
+        },
+        metadata: {
+          isSparse: true,
+          _interpolations: {
+            GSI1PK: ['id', 'name'],
+            GSI1SK: ['name'],
+          },
+          _name: 'GSI1',
+          type: 'GLOBAL_SECONDARY_INDEX',
         },
-        _name: 'GSI1',
-        type: 'GLOBAL_SECONDARY_INDEX',
       },
       LSI1: {
-        LSI1SK: 'AGE#{{age}}',
-        _interpolations: {
-          LSI1SK: ['age'],
+        attributes: {
+          LSI1SK: 'AGE#{{age}}',
+        },
+        metadata: {
+          isSparse: true,
+          _interpolations: {
+            LSI1SK: ['age'],
+          },
+          _name: 'LSI1',
+          type: 'LOCAL_SECONDARY_INDEX',
         },
-        _name: 'LSI1',
-        type: 'LOCAL_SECONDARY_INDEX',
       },
     },
     primaryKey: {
-      PK: 'USER#{{id}}',
-      SK: 'USER#{{id}}',
-      _interpolations: {
-        PK: ['id'],
-        SK: ['id'],
+      attributes: {
+        PK: 'USER#{{id}}',
+        SK: 'USER#{{id}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id'],
+          SK: ['id'],
+        },
       },
     },
   });
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 6827522..da0d8e5 100644
--- a/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
+++ b/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
@@ -4,11 +4,17 @@ import {Organisation} from '../../../../__mocks__/organisation';
 import {User} from '../../../../__mocks__/user';
 import {createTestConnection, resetTestConnection} from '@typedorm/testing';
 import {EntityTransformer} from '../entity-transformer';
+import {UserSparseIndexes} from '../../../../__mocks__/user-sparse-indexes';
 
 let transformer: EntityTransformer;
 beforeEach(() => {
   const connection = createTestConnection({
-    entities: [User, Organisation, UserAutoGenerateAttributes],
+    entities: [
+      User,
+      Organisation,
+      UserAutoGenerateAttributes,
+      UserSparseIndexes,
+    ],
   });
   transformer = new EntityTransformer(connection);
 });
@@ -96,6 +102,32 @@ test('transforms simple model to dynamo entity', () => {
   });
 });
 
+test('transforms entity with sparse index when variable referenced in sort key is missing a value', () => {
+  const user = new UserSparseIndexes();
+  user.id = '111';
+  user.status = 'active';
+
+  const response = transformer.toDynamoEntity(user);
+  expect(response).toEqual({
+    PK: 'USER_SPARSE_INDEXES#111',
+    SK: 'USER_SPARSE_INDEXES#111',
+    id: '111',
+    status: 'active',
+  });
+});
+
+test('transforms entity with sparse LSI index when variable referenced is missing a value', () => {
+  const user = new UserSparseIndexes();
+  user.id = '111';
+
+  const response = transformer.toDynamoEntity(user);
+  expect(response).toEqual({
+    PK: 'USER_SPARSE_INDEXES#111',
+    SK: 'USER_SPARSE_INDEXES#111',
+    id: '111',
+  });
+});
+
 /**
  * Issue #37
  */
diff --git a/packages/core/src/helpers/__test__/build-primary-key-schema.spec.ts b/packages/core/src/helpers/__test__/build-primary-key-schema.spec.ts
index 2154370..995638c 100644
--- a/packages/core/src/helpers/__test__/build-primary-key-schema.spec.ts
+++ b/packages/core/src/helpers/__test__/build-primary-key-schema.spec.ts
@@ -19,9 +19,13 @@ test('builds primary key schema for composite table', () => {
   });
 
   expect(schema).toEqual({
-    PK: 'DEMO#{{id}}',
-    _interpolations: {
-      PK: ['id'],
+    attributes: {
+      PK: 'DEMO#{{id}}',
+    },
+    metadata: {
+      _interpolations: {
+        PK: ['id'],
+      },
     },
   });
 });
@@ -63,11 +67,15 @@ test('builds primary key schema for composite table', () => {
   });
 
   expect(schema).toEqual({
-    PK: 'DEMO#{{id}}',
-    SK: 'DEMO#{{id}}',
-    _interpolations: {
-      PK: ['id'],
-      SK: ['id'],
+    attributes: {
+      PK: 'DEMO#{{id}}',
+      SK: 'DEMO#{{id}}',
+    },
+    metadata: {
+      _interpolations: {
+        PK: ['id'],
+        SK: ['id'],
+      },
     },
   });
 });
diff --git a/packages/core/src/helpers/__test__/is-used-for-primary-key.spec.ts b/packages/core/src/helpers/__test__/is-used-for-primary-key.spec.ts
index 39b0fb8..264059d 100644
--- a/packages/core/src/helpers/__test__/is-used-for-primary-key.spec.ts
+++ b/packages/core/src/helpers/__test__/is-used-for-primary-key.spec.ts
@@ -21,9 +21,13 @@ test('checks if given attribute is referenced in raw primary key', () => {
 test('checks if given attribute is referenced in formatted primary key', () => {
   const isNotUsed = isUsedForPrimaryKey(
     {
-      PK: 'USER#{{id}}#NAME#{{name}}:something',
-      _interpolations: {
-        PK: ['id', 'name'],
+      attributes: {
+        PK: 'USER#{{id}}#NAME#{{name}}:something',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id', 'name'],
+        },
       },
     },
     'role'
@@ -32,11 +36,15 @@ test('checks if given attribute is referenced in formatted primary key', () => {
 
   const isUsed = isUsedForPrimaryKey(
     {
-      PK: 'USER#{{id}}#NAME#{{name}}:something',
-      SK: 'USER#{{id}}#ROLE#{{role}}',
-      _interpolations: {
-        PK: ['id', 'name'],
-        SK: ['id', 'role'],
+      attributes: {
+        PK: 'USER#{{id}}#NAME#{{name}}:something',
+        SK: 'USER#{{id}}#ROLE#{{role}}',
+      },
+      metadata: {
+        _interpolations: {
+          PK: ['id', 'name'],
+          SK: ['id', 'role'],
+        },
       },
     },
     'role'
diff --git a/packages/core/src/helpers/__test__/parse-key.spec.ts b/packages/core/src/helpers/__test__/parse-key.spec.ts
index 0ce4306..4afa825 100644
--- a/packages/core/src/helpers/__test__/parse-key.spec.ts
+++ b/packages/core/src/helpers/__test__/parse-key.spec.ts
@@ -1,4 +1,5 @@
 import {parseKey} from '../parse-key';
+import {SparseIndexParseError} from '@typedorm/common';
 
 describe('parseKey()', () => {
   it('should parse key and replace interpolation values with actual value', () => {
@@ -17,6 +18,19 @@ describe('parseKey()', () => {
     expect(parsed).toEqual('USER#1111');
   });
 
+  it('should parse sparseIndex key', () => {
+    const parsed = () =>
+      parseKey(
+        'USER#{{id}}#{{status}}',
+        {
+          id: 1111,
+        },
+        {isSparseIndex: true}
+      );
+
+    expect(parsed).toThrowError(SparseIndexParseError);
+  });
+
   it('should parse key multiple interpolation occurrences', () => {
     const parsed = parseKey('USER#{{id}}#CLASS#{{classId}}', {
       id: 1111,
@@ -28,7 +42,7 @@ describe('parseKey()', () => {
 
   it('should thrown an error when no mapping found in dictionary', () => {
     expect(() => parseKey('USER#{{id}}', {})).toThrowError(
-      'Could not resolve "id" from given dictionary'
+      '"id" was referenced in USER#{{id}} but it\'s value could not be resolved.'
     );
   });
 });