Repository: typedorm/typedorm (ts)

Issue:
Ability to hide property from Entity
Ability to define hidden option for attribute, when attribute is marked as hidden, do not include it in the response returned to clients

Pull request description:
Exclude hidden props from responses
# Changes
- excludes hidden props from transforms

closes #46 

Patch:
diff --git a/docs/api.md b/docs/api.md
index b796328..7b3b5d4 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -106,6 +106,14 @@ Declare Attribute on an entity
   // @optional
   // Required when attribute type is enum and referenced in key schema
   isEnum
+
+  // @optional
+  // Define default value for current attribute
+  default
+
+  // @optional
+  // Hides property from returned responses
+  hidden
 })
 ```
 
@@ -124,6 +132,10 @@ Declare Auto generated attribute on an entity
   // When true, attribute will be auto updated on write request with specified strategy
   // Useful for defining attributes like `updatedAt`
   autoUpdate
+
+  // @optional
+  // Hides property from returned responses
+  hidden
 })
 ```
 
diff --git a/packages/common/src/decorators/attribute.decorator.ts b/packages/common/src/decorators/attribute.decorator.ts
index e939840..db5f656 100644
--- a/packages/common/src/decorators/attribute.decorator.ts
+++ b/packages/common/src/decorators/attribute.decorator.ts
@@ -26,6 +26,11 @@ export interface AttributeOptions {
    * Assign default value to attribute
    */
   default?: ScalarType | (() => ScalarType);
+  /**
+   * Defines whether the attribute should be hidden from response returned to client
+   * @default false
+   */
+  hidden?: boolean;
 }
 
 export function Attribute(options?: AttributeOptions): PropertyDecorator {
@@ -42,6 +47,7 @@ export function Attribute(options?: AttributeOptions): PropertyDecorator {
       type,
       unique: options?.unique,
       default: options?.default,
+      hidden: options?.hidden,
     } as AttributeRawMetadataOptions;
 
     MetadataManager.metadataStorage.addRawAttribute(
diff --git a/packages/common/src/decorators/auto-generate-attribute.decorator.ts b/packages/common/src/decorators/auto-generate-attribute.decorator.ts
index 12b7aa1..6d52430 100644
--- a/packages/common/src/decorators/auto-generate-attribute.decorator.ts
+++ b/packages/common/src/decorators/auto-generate-attribute.decorator.ts
@@ -1,11 +1,29 @@
+import {AUTO_GENERATE_ATTRIBUTE_STRATEGY} from '../enums';
 import {MetadataManager} from '../metadata/metadata-manager';
 import {AutoGenerateAttributeRawMetadataOptions} from '../metadata/metadata-storage';
 
+export interface AutoGeneratedAttributeOptions {
+  /**
+   * Auto generate strategy to use
+   */
+  strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY;
+
+  /**
+   * Defines whether the attribute values should be auto updated when any write operation occurs
+   * happens (i.e updatedAt on entities)
+   * @default false
+   */
+  autoUpdate?: boolean;
+
+  /**
+   * Defines whether the attribute should be hidden from response returned to client
+   * @default false
+   */
+  hidden?: boolean;
+}
+
 export function AutoGenerateAttribute(
-  options: Pick<
-    AutoGenerateAttributeRawMetadataOptions,
-    'strategy' | 'autoUpdate'
-  >
+  options: AutoGeneratedAttributeOptions
 ): PropertyDecorator {
   return (target, propertyKey) => {
     const type = Reflect.getMetadata('design:type', target, propertyKey);
@@ -14,9 +32,8 @@ export function AutoGenerateAttribute(
       name: propertyKey.toString(),
       type: type.name,
       strategy: options.strategy,
-      // this does not make a lot of sense atm, as all supported auto generate strategy are likely to be unique
-      // but can be useful in cases where we support auto generating value by user input
       autoUpdate: options.autoUpdate,
+      hidden: options.hidden,
     };
 
     if (options.strategy)
diff --git a/packages/common/src/metadata/metadata-storage.ts b/packages/common/src/metadata/metadata-storage.ts
index cd21c24..b1e21f9 100644
--- a/packages/common/src/metadata/metadata-storage.ts
+++ b/packages/common/src/metadata/metadata-storage.ts
@@ -32,6 +32,7 @@ export interface EntityRawMetadataOptions {
 interface BaseAttributeRawMetadataOptions {
   name: string;
   type: any;
+  hidden?: boolean;
 }
 export interface AttributeRawMetadataOptions
   extends BaseAttributeRawMetadataOptions {
diff --git a/packages/core/src/classes/metadata/attribute-metadata.ts b/packages/core/src/classes/metadata/attribute-metadata.ts
index 4bebfb4..ab174a5 100644
--- a/packages/core/src/classes/metadata/attribute-metadata.ts
+++ b/packages/core/src/classes/metadata/attribute-metadata.ts
@@ -31,9 +31,8 @@ export class AttributeMetadata extends BaseAttributeMetadata {
   readonly table: Table;
   readonly entityClass: EntityTarget<any>;
   constructor(options: AttributeMetadataOptions) {
-    const {name, type, entityClass, unique, table} = options;
-
-    super({name, type});
+    const {name, entityClass, unique, table} = options;
+    super(options);
     this.entityClass = entityClass;
     this.table = table;
     this.default = this.getDefaultValue(name, options.default);
diff --git a/packages/core/src/classes/metadata/auto-generated-attribute-metadata.ts b/packages/core/src/classes/metadata/auto-generated-attribute-metadata.ts
index 2b31921..dbc09f5 100644
--- a/packages/core/src/classes/metadata/auto-generated-attribute-metadata.ts
+++ b/packages/core/src/classes/metadata/auto-generated-attribute-metadata.ts
@@ -21,13 +21,9 @@ export class AutoGeneratedAttributeMetadata extends BaseAttributeMetadata {
   readonly autoUpdate: boolean;
   readonly unique: boolean;
 
-  constructor({
-    name,
-    type,
-    strategy,
-    autoUpdate,
-  }: AutoGeneratedAttributeMetadataOptions) {
-    super({name, type});
+  constructor(options: AutoGeneratedAttributeMetadataOptions) {
+    const {strategy, autoUpdate} = options;
+    super(options);
     this.autoUpdate = !!autoUpdate;
     this.strategy = strategy;
   }
diff --git a/packages/core/src/classes/metadata/base-attribute-metadata.ts b/packages/core/src/classes/metadata/base-attribute-metadata.ts
index 565d933..b2068ba 100644
--- a/packages/core/src/classes/metadata/base-attribute-metadata.ts
+++ b/packages/core/src/classes/metadata/base-attribute-metadata.ts
@@ -1,14 +1,17 @@
 export interface BaseAttributeMetadataOptions {
   name: string;
   type: string;
+  hidden?: boolean;
 }
 
 export class BaseAttributeMetadata {
   readonly name: string;
   readonly type: string;
+  readonly hidden?: boolean;
 
-  constructor({name, type}: BaseAttributeMetadataOptions) {
+  constructor({name, type, hidden}: BaseAttributeMetadataOptions) {
     this.name = name;
     this.type = type;
+    this.hidden = hidden;
   }
 }
diff --git a/packages/core/src/classes/transformer/entity-transformer.ts b/packages/core/src/classes/transformer/entity-transformer.ts
index e9ea98d..13cf916 100644
--- a/packages/core/src/classes/transformer/entity-transformer.ts
+++ b/packages/core/src/classes/transformer/entity-transformer.ts
@@ -10,6 +10,7 @@ export class EntityTransformer extends BaseTransformer {
     super(connection);
   }
   /**
+   * Converts dynamodb entity to model defined in entities
    * @param entityClass - Target class to look metadata off
    * @param dynamoEntity
    */
@@ -33,6 +34,10 @@ export class EntityTransformer extends BaseTransformer {
       attr => attr.name
     );
 
+    const entityHiddenAttributeKeys = entityMetadata.attributes
+      .filter(attr => attr.hidden)
+      .map(attr => attr.name);
+
     const entityMetadataSchemaIndexes = entityMetadata.schema.indexes ?? {};
     const entityIndexes = Object.keys(entityMetadataSchemaIndexes)
       .map(key => {
@@ -41,10 +46,12 @@ export class EntityTransformer extends BaseTransformer {
       .flat();
 
     const transformedEntity = Object.keys(dynamoEntity).reduce((acc, key) => {
+      // if any of the below conditions are true, skip adding given attribute from returning response
       if (
         entityPrimaryKeys.includes(key) ||
         entityIndexes.includes(key) ||
-        entityInternalAttributeKeys.includes(key)
+        entityInternalAttributeKeys.includes(key) ||
+        entityHiddenAttributeKeys.includes(key)
       ) {
         return acc;
       }


Test patch:
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 da0d8e5..c3b2855 100644
--- a/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
+++ b/packages/core/src/classes/transformer/__test__/entity-transformer.spec.ts
@@ -1,10 +1,18 @@
 import {UserAutoGenerateAttributes} from '../../../../__mocks__/user-auto-generate-attributes';
-import {Attribute, Entity, INDEX_TYPE, Table} from '@typedorm/common';
+import {
+  Attribute,
+  AutoGenerateAttribute,
+  AUTO_GENERATE_ATTRIBUTE_STRATEGY,
+  Entity,
+  INDEX_TYPE,
+  Table,
+} from '@typedorm/common';
 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';
+import {table} from '@typedorm/core/__mocks__/table';
 
 let transformer: EntityTransformer;
 beforeEach(() => {
@@ -81,6 +89,55 @@ test('excludes internal attributes from transformed object', () => {
   });
 });
 
+test('excludes hidden props from returned response', () => {
+  @Entity({
+    name: 'user-priv',
+    table,
+    primaryKey: {
+      partitionKey: 'USER#{{id}}',
+      sortKey: 'USER#{{id}}',
+    },
+  })
+  class UserPriv {
+    @Attribute()
+    id: string;
+
+    @Attribute()
+    username: string;
+
+    @Attribute({
+      hidden: true,
+    })
+    password: string;
+
+    @AutoGenerateAttribute({
+      strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4,
+      hidden: true,
+    })
+    createdAt: string;
+  }
+  transformer = new EntityTransformer(
+    createTestConnection({
+      entities: [UserPriv],
+      name: 'temp-hidden-props',
+    })
+  );
+
+  const dynamoEntity = {
+    PK: 'USER#1',
+    id: '1',
+    username: 'Me@21',
+    password: '12344',
+    createdAt: '13123123123',
+    __en: 'user-priv',
+  };
+  const transformed = transformer.fromDynamoEntity(UserPriv, dynamoEntity);
+  expect(transformed).toEqual({
+    id: '1',
+    username: 'Me@21',
+  });
+});
+
 /**
  * @group toDynamoEntity
  */