Repository: typedorm/typedorm (ts)

Issue:
InvalidSetType error if the queried item has an attribute of type String Set
When i queried for the following item using findOne API ,it is failing with an InvalidSetType exception

DynamoDB JSON
==============
```json
{
  "PK": {
    "S": "entity#123"
  },
  "SK": {
    "S": "root"
  },
  "stringset": {
    "SS": [
      "test"
    ]
  }
}

```
Lambda Stack trace
===============
```
{
    "errorType": "InvalidSetType",
    "errorMessage": "Sets can contain string, number, or binary values",
    "code": "InvalidSetType",
    "message": "Sets can contain string, number, or binary values",
    "time": "2021-08-02T06:15:53.376Z",
    "stack": [
        "InvalidSetType: Sets can contain string, number, or binary values",
        "    at Set.detectType (/var/runtime/node_modules/aws-sdk/lib/dynamodb/set.js:37:24)",
        "    at Set.initialize (/var/runtime/node_modules/aws-sdk/lib/dynamodb/set.js:28:10)",
        "    at new Set (/var/runtime/node_modules/aws-sdk/lib/dynamodb/set.js:22:10)",
        "    at TransformOperationExecutor.transform (/var/task/node_modules/class-transformer/cjs/TransformOperationExecutor.js:138:32)",
        "    at TransformOperationExecutor.transform (/var/task/node_modules/class-transformer/cjs/TransformOperationExecutor.js:277:47)",
        "    at ClassTransformer.plainToClassFromExist (/var/task/node_modules/class-transformer/cjs/ClassTransformer.js:34:25)",
        "    at Object.plainToClassFromExist (/var/task/node_modules/class-transformer/cjs/index.js:34:29)",
        "    at EntityTransformer.fromDynamoEntity (/var/task/node_modules/@typedorm/core/src/classes/transformer/entity-transformer.js:65:59)",
        "    at EntityManager.<anonymous> (/var/task/node_modules/@typedorm/core/src/classes/manager/entity-manager.js:113:56)",
        "    at Generator.next (<anonymous>)"
    ]
}
```

Pull request description:
[157] Fixed Invalid set type error on set types
# Changes
 - fixes an issue where trying to deserialize a non-native type in JSON fails with `InvalidSetType` error
 
Custom types are emitted by the document client in cases where Dynamodb item was created outside the js ecosystem.
To correctly deserialize the returned values to the relevant Entity, it needs to be in a plain JSON structure.

i.e DynamoDB itself supports `StringSet` type but since js doesn't have a `StringSet` as a native type,
Therefore, DocumentClient wraps it as a custom `Set` type which must be turned into its JSON form before it can
be correctly deserialized by `class-transformer`.

closes #157 

Patch:
diff --git a/package-lock.json b/package-lock.json
index 454adce..b5adb72 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7411,6 +7411,11 @@
           "bundled": true,
           "dev": true
         },
+        "path-parse": {
+          "version": "1.0.6",
+          "bundled": true,
+          "dev": true
+        },
         "performance-now": {
           "version": "2.1.0",
           "bundled": true,
@@ -8159,9 +8164,9 @@
       "dev": true
     },
     "path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
       "dev": true
     },
     "path-type": {
diff --git a/packages/core/src/classes/transformer/entity-transformer.ts b/packages/core/src/classes/transformer/entity-transformer.ts
index 6d20d30..88af9dc 100644
--- a/packages/core/src/classes/transformer/entity-transformer.ts
+++ b/packages/core/src/classes/transformer/entity-transformer.ts
@@ -32,35 +32,30 @@ export class EntityTransformer extends BaseTransformer {
       body: dynamoEntity,
     });
 
-    const vanillaAttributesToInclude = entityMetadata.attributes
-      .filter(attr => !attr.hidden)
-      .map(attr => attr.name);
-
-    const primaryKeyAttributesToInclude = Object.keys(
+    const entityPrimaryKeys = Object.keys(
       entityMetadata.schema.primaryKey.attributes
-    ).filter(attr => !vanillaAttributesToInclude.includes(attr));
-
+    );
     const entityInternalAttributeKeys = entityMetadata.internalAttributes.map(
       attr => attr.name
     );
+
     const entityHiddenAttributeKeys = entityMetadata.attributes
       .filter(attr => attr.hidden)
       .map(attr => attr.name);
 
     const entityMetadataSchemaIndexes = entityMetadata.schema.indexes ?? {};
-    const indexAttributesToInclude = Object.keys(entityMetadataSchemaIndexes)
+    const entityIndexes = Object.keys(entityMetadataSchemaIndexes)
       .map(key => {
         return Object.keys(entityMetadataSchemaIndexes[key].attributes ?? {});
       })
-      .flat()
-      .filter(attr => !vanillaAttributesToInclude.includes(attr));
+      .flat();
 
     const plainEntityAttributes = Object.keys(dynamoEntity).reduce(
       (acc, key) => {
         // if any of the below conditions are true, skip adding given attribute from returning response
         if (
-          primaryKeyAttributesToInclude.includes(key) ||
-          indexAttributesToInclude.includes(key) ||
+          entityPrimaryKeys.includes(key) ||
+          entityIndexes.includes(key) ||
           entityInternalAttributeKeys.includes(key) ||
           entityHiddenAttributeKeys.includes(key)
         ) {
@@ -74,9 +69,22 @@ export class EntityTransformer extends BaseTransformer {
 
     // get reflected constructor to avoid initialization issues with custom constructor
     const reflectedConstructor = Reflect.construct(Object, [], entityClass);
+
+    // Perform a deep-copy of item returned by Document client to drop all the custom function types
+
+    // Custom types are emitted by the document client in cases where dynamodb item was created outside the js ecosystem.
+    // To correctly deserialize the returned values to the relevant Entity, it needs to be in a plain JSON structure.
+    // i.e DynamoDB itself supports `StringSet` type but since js doesn't have a `StringSet` as a native type,
+    // Therefore, DocumentClient wraps it as a custom `Set` type which must be turned into its JSON form before it can
+    // be correctly deserialized by `class-transformer`.
+
+    const deserializedEntityAttributes = JSON.parse(
+      JSON.stringify(plainEntityAttributes)
+    );
+
     const transformedEntity = plainToClassFromExist(
       reflectedConstructor,
-      plainEntityAttributes
+      deserializedEntityAttributes
     );
 
     this.connection.logger.logTransform({


Test patch:
diff --git a/__test__/github-issues/157/issue-157.spec.ts b/__test__/github-issues/157/issue-157.spec.ts
new file mode 100644
index 0000000..cf588e5
--- /dev/null
+++ b/__test__/github-issues/157/issue-157.spec.ts
@@ -0,0 +1,76 @@
+// assuming there is a EMAIL_INDEX loaded before anything else
+process.env.EMAIL_INDEX = 'email-index-v2';
+
+import {testTable} from './test-table';
+import {EntityManager} from '@typedorm/core';
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import EntityData from './test-entity';
+import {DocumentClient} from 'aws-sdk/clients/dynamodb';
+let entityManager: EntityManager;
+
+const dcMock = {
+  query: jest.fn(),
+};
+beforeEach(() => {
+  const connection = createTestConnection({
+    entities: [EntityData],
+    table: testTable,
+    documentClient: dcMock,
+  });
+  entityManager = new EntityManager(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('correctly queries matching items when values for set type is not a native js type ', async () => {
+  dcMock.query.mockReturnValue({
+    promise: () => ({
+      Items: [
+        {
+          SK: 'root',
+          // Here string set contains a non-js-native type `Set()`
+          // This type is emitted by document client in cases where dynamodb item was created outside the js ecosystem
+          // i.e DynamoDB it self supports `StringSet` type but since js doesn't have a `StringSet` as a native type,
+          // DocumentClient wraps it as a custom `Set` type
+          stringSet: new DocumentClient().createSet(['test']),
+          email: 'test-entity@gmail.com',
+          PK: 'entity#12345678',
+          name: 'ABCD',
+        },
+      ],
+    }),
+  });
+
+  const response = await entityManager.find(
+    EntityData,
+    'test-entity@gmail.com',
+    {
+      queryIndex: process.env.EMAIL_INDEX!,
+      limit: 1,
+    }
+  );
+
+  expect(dcMock.query).toHaveBeenCalledWith({
+    ExpressionAttributeNames: {
+      '#KY_CE_email': 'email',
+    },
+    ExpressionAttributeValues: {
+      ':KY_CE_email': 'test-entity@gmail.com',
+    },
+    IndexName: 'email-index-v2',
+    KeyConditionExpression: '#KY_CE_email = :KY_CE_email',
+    Limit: 1,
+    ScanIndexForward: true,
+    TableName: 'user-v2',
+  });
+
+  const entityData = new EntityData();
+  entityData.name = 'ABCD';
+  (entityData as any).stringSet = ['test'];
+
+  expect(response).toEqual({
+    items: [entityData],
+  });
+});
diff --git a/__test__/github-issues/158/test-entity.ts b/__test__/github-issues/157/test-entity.ts
similarity index 100%
rename from __test__/github-issues/158/test-entity.ts
rename to __test__/github-issues/157/test-entity.ts
diff --git a/__test__/github-issues/158/test-table.ts b/__test__/github-issues/157/test-table.ts
similarity index 100%
rename from __test__/github-issues/158/test-table.ts
rename to __test__/github-issues/157/test-table.ts
diff --git a/__test__/github-issues/158/issue-158.spec.ts b/__test__/github-issues/158/issue-158.spec.ts
deleted file mode 100644
index 7ed608e..0000000
--- a/__test__/github-issues/158/issue-158.spec.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-// assuming there is a EMAIL_INDEX loaded before anything else
-process.env.EMAIL_INDEX = 'email-index-v2';
-
-import {testTable} from './test-table';
-import {EntityManager} from '@typedorm/core';
-import {createTestConnection, resetTestConnection} from '@typedorm/testing';
-import EntityData from './test-entity';
-let entityManager: EntityManager;
-
-const dcMock = {
-  query: jest.fn(),
-};
-beforeEach(() => {
-  const connection = createTestConnection({
-    entities: [EntityData],
-    table: testTable,
-    documentClient: dcMock,
-  });
-  entityManager = new EntityManager(connection);
-});
-
-afterEach(() => {
-  resetTestConnection();
-});
-
-test('entityManager find returns attributes that has the same name as key', async () => {
-  dcMock.query.mockReturnValue({
-    promise: () => ({
-      Items: [
-        {
-          SK: 'root',
-          stringSet: ['test'],
-          email: 'test-entity@gmail.com',
-          PK: 'entity#12345678',
-          name: 'ABCD',
-        },
-      ],
-    }),
-  });
-
-  const response = await entityManager.find(
-    EntityData,
-    'test-entity@gmail.com',
-    {
-      queryIndex: process.env.EMAIL_INDEX!,
-      limit: 1,
-    }
-  );
-
-  expect(response.items[0]).toBeInstanceOf(EntityData);
-  expect(response).toEqual({
-    items: [
-      {
-        name: 'ABCD',
-        stringSet: ['test'],
-        email: 'test-entity@gmail.com',
-      },
-    ],
-  });
-});