Repository: typedorm/typedorm (ts)

Issue:
Store entity name as an attribute on dynamo item
Store entity name on item

May be also add an prefix, something like 
`__en`: short for entity name

Pull request description:
16 store entity name as separate attribute
# Changes 

- default internal attributes support 

closes #16 

Patch:
diff --git a/README.md b/README.md
index 283a32d..a9f6def 100644
--- a/README.md
+++ b/README.md
@@ -217,6 +217,8 @@ To find out more about how the data looks like when it is stored in dynamo have
 
 ### [Step by step guide](./docs/guide.md)
 
+### [Index](./docs/index.md)
+
 ## Current Limitations
 
 - TypDORM currently is not able to connect with dynamodb table using aws credentials and only works within environment like lambda/ec2 that already has access to dynamo table, but this will be resolved very soon.
diff --git a/packages/common/enums.ts b/packages/common/enums.ts
index 7f310bf..113d863 100644
--- a/packages/common/enums.ts
+++ b/packages/common/enums.ts
@@ -27,3 +27,7 @@ export enum AUTO_GENERATE_ATTRIBUTE_STRATEGY {
   EPOCH_DATE = 'EPOCH_DATE',
   ISO_DATE = 'ISO_DATE',
 }
+
+export enum INTERNAL_ENTITY_ATTRIBUTE {
+  ENTITY_NAME = '__en',
+}
diff --git a/packages/core/classes/metadata/entity-metadata.ts b/packages/core/classes/metadata/entity-metadata.ts
index 264a47b..ab7863c 100644
--- a/packages/core/classes/metadata/entity-metadata.ts
+++ b/packages/core/classes/metadata/entity-metadata.ts
@@ -1,4 +1,11 @@
-import {EntityTarget, INDEX_TYPE} from '@typedorm/common';
+import {
+  EntityTarget,
+  INDEX_TYPE,
+  INTERNAL_ENTITY_ATTRIBUTE,
+  IndexOptions,
+  IsCompositePrimaryKey,
+  Table,
+} from '@typedorm/common';
 import {getInterpolatedKeys} from '../../helpers/get-interpolated-keys';
 import {validateKey} from '../../helpers/validate-key';
 import {Connection} from '../connection/connection';
@@ -7,10 +14,10 @@ import {
   Indexes,
   PrimaryKey,
 } from '@typedorm/common/metadata-storage';
-import {IndexOptions, IsCompositePrimaryKey, Table} from '@typedorm/common';
 import {AttributeMetadata} from './attribute-metadata';
 import {AutoGeneratedAttributeMetadata} from './auto-generated-attribute-metadata';
 import {BaseMetadata} from './base-metadata';
+import {InternalAttributeMetadata} from './internal-attribute-metadata';
 
 export type DynamoEntitySchemaPrimaryKey = {
   [key: string]: any;
@@ -48,6 +55,7 @@ export class EntityMetadata extends BaseMetadata {
   readonly table: Table;
   readonly target: EntityTarget<any>;
   readonly attributes: AttributeMetadataType[];
+  readonly internalAttributes: InternalAttributeMetadata[];
   readonly schema: DynamoEntitySchema;
   constructor({
     connection,
@@ -63,6 +71,15 @@ export class EntityMetadata extends BaseMetadata {
     this.target = target;
     this.attributes = attributes;
 
+    // auto persist internal attributes
+    this.internalAttributes = [
+      new InternalAttributeMetadata({
+        name: INTERNAL_ENTITY_ATTRIBUTE.ENTITY_NAME,
+        type: 'String',
+        value: this.name,
+      }),
+    ];
+
     if (table) {
       // if no entity level table is defined fallback to global connection table
       this.table = table;
@@ -70,7 +87,9 @@ export class EntityMetadata extends BaseMetadata {
       this.table = connection.table;
     }
 
+    // validate attributes and key type pair
     const attributesKeyTypePair = this.attributes.reduce((acc, attr) => {
+      this.validateAttributeMetadata(attr);
       acc[attr.name] = attr.type;
       return acc;
     }, {} as {[key: string]: string});
@@ -91,6 +110,18 @@ export class EntityMetadata extends BaseMetadata {
     };
   }
 
+  private validateAttributeMetadata(
+    attributeToValidate: AttributeMetadataType
+  ) {
+    this.internalAttributes.forEach(internalAttr => {
+      if (attributeToValidate.name === internalAttr.name) {
+        throw new Error(
+          `Attribute name "${attributeToValidate.name}" is reserved by TypeDORM and cannot be used.`
+        );
+      }
+    });
+  }
+
   private validatePrimaryKey(
     table: Table,
     primaryKey: PrimaryKey,
diff --git a/packages/core/classes/metadata/internal-attribute-metadata.ts b/packages/core/classes/metadata/internal-attribute-metadata.ts
new file mode 100644
index 0000000..86a9d4b
--- /dev/null
+++ b/packages/core/classes/metadata/internal-attribute-metadata.ts
@@ -0,0 +1,17 @@
+import {
+  AttributeMetadata,
+  AttributeMetadataOptions,
+} from './attribute-metadata';
+
+export interface InternalAttributeMetadataOptions
+  extends AttributeMetadataOptions {
+  value: any;
+}
+
+export class InternalAttributeMetadata extends AttributeMetadata {
+  readonly value: any;
+  constructor(options: InternalAttributeMetadataOptions) {
+    super(options);
+    this.value = options.value;
+  }
+}
diff --git a/packages/core/classes/transformer/document-client-request-transformer.ts b/packages/core/classes/transformer/document-client-request-transformer.ts
index c1519f4..cadc201 100644
--- a/packages/core/classes/transformer/document-client-request-transformer.ts
+++ b/packages/core/classes/transformer/document-client-request-transformer.ts
@@ -99,13 +99,21 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     options?: ManagerToDynamoPutItemOptions
   ): DocumentClient.PutItemInput | DocumentClient.PutItemInput[] {
     const entityClass = getConstructorForInstance(entity);
-    const {table, name} = this.connection.getEntityByTarget(entityClass);
+    const {table, name, internalAttributes} = this.connection.getEntityByTarget(
+      entityClass
+    );
 
     const uniqueAttributes = this.connection.getUniqueAttributesForEntity(
       entityClass
     ) as AttributeMetadata[];
 
     const dynamoEntity = this.toDynamoEntity(entity);
+
+    const entityInternalAttributes = internalAttributes.reduce((acc, attr) => {
+      acc[attr.name] = attr.value;
+      return acc;
+    }, {} as DocumentClient.PutItemInputAttributeMap);
+
     let uniqueAttributePutItems = [] as DocumentClient.PutItemInput[];
     // apply attribute not exist condition when creating unique
     const uniqueRecordConditionExpression = this._expressionBuilder.buildConditionExpression(
@@ -141,6 +149,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
 
     let dynamoPutItem = {
       Item: {
+        ...entityInternalAttributes,
         ...dynamoEntity,
       },
       TableName: table.name,
diff --git a/packages/core/classes/transformer/entity-transformer.ts b/packages/core/classes/transformer/entity-transformer.ts
index 3f864a0..48efe97 100644
--- a/packages/core/classes/transformer/entity-transformer.ts
+++ b/packages/core/classes/transformer/entity-transformer.ts
@@ -20,6 +20,9 @@ export class EntityTransformer extends BaseTransformer {
     const entityMetadata = this.connection.getEntityByTarget(entityClass);
 
     const entityPrimaryKeys = Object.keys(entityMetadata.schema.primaryKey);
+    const entityInternalAttributeKeys = entityMetadata.internalAttributes.map(
+      attr => attr.name
+    );
 
     const entityMetadataSchemaIndexes = entityMetadata.schema.indexes ?? {};
     const entityIndexes = Object.keys(entityMetadataSchemaIndexes)
@@ -30,7 +33,11 @@ export class EntityTransformer extends BaseTransformer {
       .flat();
 
     return Object.keys(dynamoEntity).reduce((acc, key) => {
-      if (entityPrimaryKeys.includes(key) || entityIndexes.includes(key)) {
+      if (
+        entityPrimaryKeys.includes(key) ||
+        entityIndexes.includes(key) ||
+        entityInternalAttributeKeys.includes(key)
+      ) {
         return acc;
       }
       (acc as any)[key] = (dynamoEntity as any)[key];


Test patch:
diff --git a/packages/core/classes/manager/__test__/entity-manager.spec.ts b/packages/core/classes/manager/__test__/entity-manager.spec.ts
index 63e2782..9414fc1 100644
--- a/packages/core/classes/manager/__test__/entity-manager.spec.ts
+++ b/packages/core/classes/manager/__test__/entity-manager.spec.ts
@@ -55,6 +55,7 @@ test('creates entity', async () => {
       PK: 'USER#1',
       SK: 'USER#1',
       id: '1',
+      __en: 'user',
       name: 'Test User',
       status: 'active',
     },
@@ -91,6 +92,7 @@ test('creates entity and returns all attributes, including auto generated ones',
       PK: 'USER#1',
       SK: 'USER#1',
       id: '1',
+      __en: 'user',
       updatedAt: 1606896235,
     },
     TableName: 'test-table',
diff --git a/packages/core/classes/manager/__test__/transaction-manager.spec.ts b/packages/core/classes/manager/__test__/transaction-manager.spec.ts
index 3cc1b99..5c7e80b 100644
--- a/packages/core/classes/manager/__test__/transaction-manager.spec.ts
+++ b/packages/core/classes/manager/__test__/transaction-manager.spec.ts
@@ -87,6 +87,7 @@ test('performs write transactions', async () => {
             PK: 'USER#1',
             SK: 'USER#1',
             id: '1',
+            __en: 'user',
             name: 'user name',
             status: 'inactive',
           },
@@ -124,6 +125,7 @@ test('performs write transactions', async () => {
             PK: 'USER#2',
             SK: 'USER#2',
             id: '2',
+            __en: 'user',
             name: 'user 2',
             status: 'inactive',
           },
diff --git a/packages/core/classes/transaction/__test__/write-transaction.spec.ts b/packages/core/classes/transaction/__test__/write-transaction.spec.ts
index caee895..3897635 100644
--- a/packages/core/classes/transaction/__test__/write-transaction.spec.ts
+++ b/packages/core/classes/transaction/__test__/write-transaction.spec.ts
@@ -69,6 +69,7 @@ test('chains simple transaction requests', () => {
           GSI1SK: 'USER#user-name',
           PK: 'USER#123',
           SK: 'USER#123',
+          __en: 'user',
           id: '123',
           name: 'user-name',
           status: 'active',
@@ -135,6 +136,7 @@ test('chains complex transaction requests', () => {
           GSI1SK: 'USER#new user',
           email: 'user@example.com',
           id: '1',
+          __en: 'user',
           name: 'new user',
           status: 'inactive',
         },
diff --git a/packages/core/classes/transformer/__test__/dc-request-transformer.spec.ts b/packages/core/classes/transformer/__test__/dc-request-transformer.spec.ts
index d491b60..81663a0 100644
--- a/packages/core/classes/transformer/__test__/dc-request-transformer.spec.ts
+++ b/packages/core/classes/transformer/__test__/dc-request-transformer.spec.ts
@@ -66,6 +66,7 @@ test('transforms put item requests', () => {
       SK: 'USER#1',
       id: '1',
       name: 'Tito',
+      __en: 'user',
       status: 'active',
     },
     ConditionExpression: 'attribute_not_exists(#CE_PK)',
@@ -116,6 +117,7 @@ test('transforms put item request with unique attributes', () => {
         SK: 'USER#1',
         email: 'user@example.com',
         id: '1',
+        __en: 'user',
       },
       TableName: 'test-table',
     },
diff --git a/packages/core/classes/transformer/__test__/entity-transformer.spec.ts b/packages/core/classes/transformer/__test__/entity-transformer.spec.ts
index cb1ca9b..b64ac9b 100644
--- a/packages/core/classes/transformer/__test__/entity-transformer.spec.ts
+++ b/packages/core/classes/transformer/__test__/entity-transformer.spec.ts
@@ -59,6 +59,21 @@ test('transforms inherited dynamo entity to entity model', () => {
   });
 });
 
+test('excludes internal attributes from transformed object', () => {
+  const dynamoEntity = {
+    PK: 'CUS#1',
+    SK: 'CUS#user@example.com',
+    id: '1',
+    name: 'Me',
+    __en: 'user',
+  };
+  const transformed = transformer.fromDynamoEntity(User, dynamoEntity);
+  expect(transformed).toEqual({
+    id: '1',
+    name: 'Me',
+  });
+});
+
 /**
  * @group toDynamoEntity
  */