Repository: typedorm/typedorm
Pull request #62 (2021-02-02 01:19:18)

Release 1.7
# 🚀  Features 🚀 
- added sparse indexes support 
- ability to provide default value for attribute



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/public-api.ts b/packages/common/public-api.ts
index 5b574c2..ebe221a 100644
--- a/packages/common/public-api.ts
+++ b/packages/common/public-api.ts
@@ -18,7 +18,6 @@ export * from './src/helpers/scalar-type';
 export * from './src/helpers/update-attributes-type';
 export * from './src/helpers/replace-type';
 export * from './src/helpers/is-primary-key';
-export * from './src/utils/get-entity-definition';
 
 // classes
 export * from './src/table';
diff --git a/packages/common/src/decorators/attribute.decorator.ts b/packages/common/src/decorators/attribute.decorator.ts
index 5d6f191..e939840 100644
--- a/packages/common/src/decorators/attribute.decorator.ts
+++ b/packages/common/src/decorators/attribute.decorator.ts
@@ -1,4 +1,5 @@
 import 'reflect-metadata';
+import {ScalarType} from '../helpers/scalar-type';
 import {MetadataManager} from '../metadata/metadata-manager';
 import {
   AttributeRawMetadataOptions,
@@ -21,6 +22,10 @@ export interface AttributeOptions {
    * @default false
    */
   isEnum?: boolean;
+  /**
+   * Assign default value to attribute
+   */
+  default?: ScalarType | (() => ScalarType);
 }
 
 export function Attribute(options?: AttributeOptions): PropertyDecorator {
@@ -36,6 +41,7 @@ export function Attribute(options?: AttributeOptions): PropertyDecorator {
       name: propertyKey.toString(),
       type,
       unique: options?.unique,
+      default: options?.default,
     } as AttributeRawMetadataOptions;
 
     MetadataManager.metadataStorage.addRawAttribute(
diff --git a/packages/common/src/error/attribute-metadata-unsupported-default-value-error.ts b/packages/common/src/error/attribute-metadata-unsupported-default-value-error.ts
new file mode 100644
index 0000000..22fd2a8
--- /dev/null
+++ b/packages/common/src/error/attribute-metadata-unsupported-default-value-error.ts
@@ -0,0 +1,9 @@
+export class AttributeMetadataUnsupportedDefaultValueError extends Error {
+  name = 'AttributeMetadataUnsupportedDefaultValueError';
+
+  constructor(attrName: string, value: any) {
+    super();
+    this.message = `Default value for attribute "${attrName}" can not be of type "${typeof value}", 
+    currently only scalar type and function returning scalar type are supported.`;
+  }
+}
diff --git a/packages/common/src/error/index.ts b/packages/common/src/error/index.ts
new file mode 100644
index 0000000..c9c658c
--- /dev/null
+++ b/packages/common/src/error/index.ts
@@ -0,0 +1,2 @@
+export * from './attribute-metadata-unsupported-default-value-error';
+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/metadata/metadata-storage.ts b/packages/common/src/metadata/metadata-storage.ts
index 5ddf0c8..8019596 100644
--- a/packages/common/src/metadata/metadata-storage.ts
+++ b/packages/common/src/metadata/metadata-storage.ts
@@ -1,6 +1,7 @@
 import {AUTO_GENERATE_ATTRIBUTE_STRATEGY, EntityTarget} from '@typedorm/common';
 import {IndexOptions, Table} from '../table';
 import {AttributeOptionsUniqueType} from '../decorators/attribute.decorator';
+import {ScalarType} from '../helpers/scalar-type';
 
 export const IsAutoGenerateAttributeRawMetadataOptions = (
   attr: any
@@ -35,6 +36,7 @@ interface BaseAttributeRawMetadataOptions {
 export interface AttributeRawMetadataOptions
   extends BaseAttributeRawMetadataOptions {
   unique?: AttributeOptionsUniqueType;
+  default?: ScalarType | (() => ScalarType);
 }
 
 export interface AutoGenerateAttributeRawMetadataOptions
@@ -58,10 +60,6 @@ export class MetadataStorage {
     this._attributes = new Map();
   }
 
-  getEntityByName(name: string) {
-    return this.entities.find(en => en.name === name);
-  }
-
   hasKnownEntity<Entity>(entityClass: EntityTarget<Entity>) {
     return this._entities.has(entityClass);
   }
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/common/src/utils/get-entity-definition.ts b/packages/common/src/utils/get-entity-definition.ts
deleted file mode 100644
index 506d1ac..0000000
--- a/packages/common/src/utils/get-entity-definition.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import {MetadataManager} from '../metadata/metadata-manager';
-
-export function getEntityDefinition<Entity>(input: string | Entity) {
-  if (typeof input === 'string') {
-    return MetadataManager.metadataStorage.getEntityByName(input);
-  }
-
-  if (typeof input === 'object' && input !== null) {
-    const entityName = (input as any).__en;
-
-    if (!entityName) {
-      return;
-    }
-    return MetadataManager.metadataStorage.getEntityByName(entityName);
-  }
-  return;
-}
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/attribute-metadata.ts b/packages/core/src/classes/metadata/attribute-metadata.ts
index 5c2f3d1..4bebfb4 100644
--- a/packages/core/src/classes/metadata/attribute-metadata.ts
+++ b/packages/core/src/classes/metadata/attribute-metadata.ts
@@ -7,6 +7,8 @@ import {
   SimplePrimaryKey,
   Table,
   IsPrimaryKey,
+  ScalarType,
+  AttributeMetadataUnsupportedDefaultValueError,
 } from '@typedorm/common';
 import {buildPrimaryKeySchema} from '../../helpers/build-primary-key-schema';
 import {DynamoEntitySchemaPrimaryKey} from './entity-metadata';
@@ -14,33 +16,59 @@ import {
   BaseAttributeMetadataOptions,
   BaseAttributeMetadata,
 } from './base-attribute-metadata';
+import {isScalarType} from '../../helpers/is-scalar-type';
 
 export interface AttributeMetadataOptions extends BaseAttributeMetadataOptions {
   table: Table;
   entityClass: EntityTarget<any>;
   unique?: AttributeOptionsUniqueType;
+  default?: ScalarType | (() => ScalarType);
 }
 
 export class AttributeMetadata extends BaseAttributeMetadata {
   readonly unique?: DynamoEntitySchemaPrimaryKey;
+  readonly default?: ScalarType;
   readonly table: Table;
   readonly entityClass: EntityTarget<any>;
-  constructor({
-    name,
-    type,
-    entityClass,
-    unique,
-    table,
-  }: AttributeMetadataOptions) {
+  constructor(options: AttributeMetadataOptions) {
+    const {name, type, entityClass, unique, table} = options;
+
     super({name, type});
     this.entityClass = entityClass;
     this.table = table;
+    this.default = this.getDefaultValue(name, options.default);
 
     if (unique) {
       this.unique = this.buildUniqueAttributesPrimaryKey(unique);
     }
   }
 
+  private getDefaultValue(
+    attrName: string,
+    defaultValue: AttributeMetadataOptions['default']
+  ) {
+    let scalarDefaultValue: ScalarType | undefined = undefined;
+    if (!defaultValue) {
+      return;
+    }
+
+    // if a factory function was provided get returned value
+    if (typeof defaultValue === 'function') {
+      scalarDefaultValue = defaultValue();
+    } else {
+      scalarDefaultValue = defaultValue as ScalarType;
+    }
+
+    if (isScalarType(scalarDefaultValue)) {
+      return scalarDefaultValue;
+    }
+
+    throw new AttributeMetadataUnsupportedDefaultValueError(
+      attrName,
+      defaultValue
+    );
+  }
+
   private buildUniqueAttributesPrimaryKey(unique: AttributeOptionsUniqueType) {
     if (IsPrimaryKey(unique)) {
       return buildPrimaryKeySchema({
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..4d3fe37 100644
--- a/packages/core/src/classes/transformer/document-client-request-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-request-transformer.ts
@@ -119,9 +119,12 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     | DynamoDB.DocumentClient.PutItemInput
     | DynamoDB.DocumentClient.TransactWriteItemList {
     const entityClass = getConstructorForInstance(entity);
-    const {table, internalAttributes, name} = this.connection.getEntityByTarget(
-      entityClass
-    );
+    const {
+      table,
+      internalAttributes,
+      name,
+      attributes,
+    } = this.connection.getEntityByTarget(entityClass);
 
     this.connection.logger.logTransform(
       TRANSFORM_TYPE.PUT,
@@ -143,9 +146,17 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
       return acc;
     }, {} as DynamoDB.DocumentClient.PutItemInputAttributeMap);
 
+    const attributesWithDefaultValues = attributes
+      .filter(attr => !!(attr as AttributeMetadata)?.default)
+      .reduce((acc, attr) => {
+        acc[attr.name] = (attr as AttributeMetadata).default;
+        return acc;
+      }, {} as DynamoDB.DocumentClient.PutItemInputAttributeMap);
+
     let dynamoPutItem = {
       Item: {
         ...entityInternalAttributes,
+        ...attributesWithDefaultValues,
         ...dynamoEntity,
       },
       TableName: table.name,
@@ -452,7 +463,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 +472,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-scalar-type.ts b/packages/core/src/helpers/is-scalar-type.ts
index 8a5b481..22f49ab 100644
--- a/packages/core/src/helpers/is-scalar-type.ts
+++ b/packages/core/src/helpers/is-scalar-type.ts
@@ -1,7 +1,8 @@
-export function isScalarType(item: unknown) {
-  return (
-    typeof item === 'string' ||
+import {ScalarType} from '@typedorm/common';
+
+export const isScalarType = (item: any): item is ScalarType =>
+  item &&
+  (typeof item === 'string' ||
     typeof item === 'number' ||
-    Buffer.isBuffer(item)
-  );
-}
+    typeof item === 'boolean' ||
+    Buffer.isBuffer(item));
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/common/src/utils/__test__/get-entity-definition.spec.ts b/packages/common/src/utils/__test__/get-entity-definition.spec.ts
deleted file mode 100644
index 7b2763d..0000000
--- a/packages/common/src/utils/__test__/get-entity-definition.spec.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import {Entity} from '@typedorm/common';
-import {Attribute} from '../../decorators/attribute.decorator';
-import {getEntityDefinition} from '../get-entity-definition';
-
-@Entity({
-  name: 'user',
-  primaryKey: {
-    partitionKey: '{{id}}',
-  },
-})
-class User {
-  @Attribute()
-  id: string;
-
-  @Attribute()
-  name: string;
-}
-
-test('returns entity definition by name', () => {
-  const definition = getEntityDefinition('user');
-
-  expect(definition).toEqual({
-    name: 'user',
-    primaryKey: {
-      partitionKey: '{{id}}',
-    },
-    target: User,
-  });
-});
-
-test('returns entity definition by entity data', () => {
-  // this is what is stored to dynamodb
-  const definition = getEntityDefinition({
-    id: '1',
-    name: 'test',
-    __en: 'user',
-  });
-
-  expect(definition).toEqual({
-    name: 'user',
-    primaryKey: {
-      partitionKey: '{{id}}',
-    },
-    target: User,
-  });
-});
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..e4fdc4c 100644
--- a/packages/core/src/classes/metadata/__test__/attribute-metadata.spec.ts
+++ b/packages/core/src/classes/metadata/__test__/attribute-metadata.spec.ts
@@ -1,6 +1,7 @@
 import {User} from './../../../../__mocks__/user';
 import {table} from './../../../../__mocks__/table';
 import {AttributeMetadata} from '../attribute-metadata';
+import {AttributeMetadataUnsupportedDefaultValueError} from '@typedorm/common';
 
 test('attribute has required metadata', () => {
   const attMetadata = new AttributeMetadata({
@@ -35,11 +36,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 +69,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'],
+        },
       },
     },
   });
@@ -91,3 +100,56 @@ test('fails when given primary key schema for unique references unknown variable
     'key "USER.NAME#{{email}}" references variable "email" but it could not be resolved'
   );
 });
+
+test('default value of scalar type is properly assigned', () => {
+  const attrMetadata = new AttributeMetadata({
+    name: 'name',
+    entityClass: User,
+    table,
+    type: 'String',
+    default: 'some default value',
+  });
+
+  expect(Object.assign({}, attrMetadata)).toEqual({
+    entityClass: User,
+    table,
+    type: 'String',
+    name: 'name',
+    default: 'some default value',
+  });
+});
+
+test('default value of scalar factory type is properly assigned', () => {
+  const attrMetadata = new AttributeMetadata({
+    name: 'id',
+    entityClass: User,
+    table,
+    type: 'Number',
+    default: () => 2,
+  });
+
+  expect(Object.assign({}, attrMetadata)).toEqual({
+    entityClass: User,
+    table,
+    type: 'Number',
+    name: 'id',
+    default: 2,
+  });
+});
+
+test('fails when type of default value is not supported one', () => {
+  const attrMetadata = () =>
+    new AttributeMetadata({
+      name: 'id',
+      entityClass: User,
+      table,
+      type: 'Number',
+      default: {
+        id: '2',
+      } as any,
+    });
+
+  expect(attrMetadata).toThrow(
+    typeof AttributeMetadataUnsupportedDefaultValueError
+  );
+});
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__/dc-request-transformer.spec.ts b/packages/core/src/classes/transformer/__test__/dc-request-transformer.spec.ts
index 4b65147..a7bd0ea 100644
--- a/packages/core/src/classes/transformer/__test__/dc-request-transformer.spec.ts
+++ b/packages/core/src/classes/transformer/__test__/dc-request-transformer.spec.ts
@@ -145,6 +145,59 @@ test('transforms put item request with unique attributes', () => {
   ]);
 });
 
+test('transforms put item request with default values ', () => {
+  resetTestConnection();
+
+  @Entity({
+    table,
+    name: 'product',
+    primaryKey: {
+      partitionKey: 'PRD#{{id}}',
+      sortKey: 'PRD#{{id}}',
+    },
+  })
+  class Product {
+    @Attribute()
+    id: string;
+
+    @Attribute({
+      default: () => 'available',
+    })
+    status: string;
+  }
+
+  const newConnection = createTestConnection({
+    entities: [Product],
+  });
+  const newTransformer = new DocumentClientRequestTransformer(newConnection);
+
+  const product = new Product();
+  product.id = '1';
+
+  const putItem = newTransformer.toDynamoPutItem(product);
+  expect(putItem).toEqual({
+    ConditionExpression:
+      'attribute_not_exists(#CE_PK) AND attribute_not_exists(#CE_SK)',
+    ExpressionAttributeNames: {
+      '#CE_PK': 'PK',
+      '#CE_SK': 'SK',
+    },
+    Item: {
+      PK: 'PRD#1',
+      SK: 'PRD#1',
+      __en: 'product',
+      id: '1',
+      status: 'available',
+    },
+    TableName: 'test-table',
+  });
+
+  // when overriding item, this can
+  product.status = 'unavailable';
+  const overriddenPutItem = newTransformer.toDynamoPutItem(product) as any;
+  expect(overriddenPutItem.Item.status).toEqual('unavailable');
+});
+
 test('transforms put item request consisting unique attributes with provided primary key', () => {
   resetTestConnection();
 
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__/get-dynamo-query-limit.spec.ts b/packages/core/src/helpers/__test__/get-dynamo-query-limit.spec.ts
new file mode 100644
index 0000000..3b2ff22
--- /dev/null
+++ b/packages/core/src/helpers/__test__/get-dynamo-query-limit.spec.ts
@@ -0,0 +1,15 @@
+import {getDynamoQueryItemsLimit} from '../get-dynamo-query-items-limit';
+
+test('returns default dynamo query items limit', () => {
+  const limit = getDynamoQueryItemsLimit();
+  expect(limit).toEqual(3000);
+});
+
+test('returns configured dynamo query items limit', () => {
+  process.env.DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT = '200';
+
+  const limit = getDynamoQueryItemsLimit();
+  expect(limit).toEqual(200);
+
+  delete process.env.DYNAMO_QUERY_ITEMS_IMPLICIT_LIMIT;
+});
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.'
     );
   });
 });