Repository: typedorm/typedorm (ts)

Issue:
Return auto-generated properties from managers
Hello!

I have a case where I am creating multiple records via `BatchManager`.  Part of the PartitionKey is generated using the `UUID4` strategy. For example:

```typescript
  @AutoGenerateAttribute({
    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4,
  })
  public id!: string;
```


As I'm writing a repository function, I don't see a way to return the item that I've just created (or items), or more importantly, I don't have a great way to find them if I tried to run another query to read them back.

I'm wondering if there's an extension point I can use to "pre-generate" the auto-generated values before sending the entities through the BatchManager , or, alternatively, a way to grab the transformed entities after the fact.

As an alternative, I can probably just pre-populate those values myself, but i was wondering if there was an expected way to accomplish this.

Thanks!

Pull request description:
[#176] Expose attribute value generator helper over the public api
closes #176 

Patch:
diff --git a/packages/common/src/error/index.ts b/packages/common/src/error/index.ts
index 5b3cab0..7b84e26 100644
--- a/packages/common/src/error/index.ts
+++ b/packages/common/src/error/index.ts
@@ -19,3 +19,4 @@ export * from './duplicate-entity-physical-name-error';
 export * from './invalid-unique-attribute-update-error';
 export * from './invalid-parallel-scan-limit-option-error';
 export * from './invalid-dynamic-update-attribute-value';
+export * from './unknown-attribute-value-generation-strategy-error';
diff --git a/packages/common/src/error/unknown-attribute-value-generation-strategy-error.ts b/packages/common/src/error/unknown-attribute-value-generation-strategy-error.ts
new file mode 100644
index 0000000..ac32e39
--- /dev/null
+++ b/packages/common/src/error/unknown-attribute-value-generation-strategy-error.ts
@@ -0,0 +1,15 @@
+import {AUTO_GENERATE_ATTRIBUTE_STRATEGY} from '../enums';
+
+export class UnknownAttributeValueGenerationStrategyError extends Error {
+  name = 'UnknownAttributeValueGenerationStrategyError';
+
+  constructor(strategy: string) {
+    super();
+    this.message = `Unknown attribute value generation strategy provided: "${strategy}".
+    Valid attribute value generation strategies are: ${Object.keys(
+      AUTO_GENERATE_ATTRIBUTE_STRATEGY
+    )
+      .map(key => `"${key}"`)
+      .join(', ')}`;
+  }
+}
diff --git a/packages/core/public-api.ts b/packages/core/public-api.ts
index de6c735..d1aa5f2 100644
--- a/packages/core/public-api.ts
+++ b/packages/core/public-api.ts
@@ -21,6 +21,9 @@ export * from './src/classes/manager/transaction-manager';
 // classes
 export {Connection} from './src/classes/connection/connection';
 
+// helpers
+export {AutoGenerateAttributeValue} from './src/helpers/auto-generate-attribute-value';
+
 // public method exports
 
 export function createConnection(options: ConnectionOptions) {
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 dbc09f5..44630cc 100644
--- a/packages/core/src/classes/metadata/auto-generated-attribute-metadata.ts
+++ b/packages/core/src/classes/metadata/auto-generated-attribute-metadata.ts
@@ -1,6 +1,5 @@
 import {AUTO_GENERATE_ATTRIBUTE_STRATEGY} from '@typedorm/common';
-import KSUID from 'ksuid';
-import {v4} from 'uuid';
+import {autoGenerateValue} from '../../helpers/auto-generate-attribute-value';
 import {
   BaseAttributeMetadata,
   BaseAttributeMetadataOptions,
@@ -29,26 +28,6 @@ export class AutoGeneratedAttributeMetadata extends BaseAttributeMetadata {
   }
 
   get value() {
-    return this.autoGenerateValue(this.strategy);
-  }
-
-  autoGenerateValue(strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY) {
-    switch (strategy) {
-      case AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4: {
-        return v4();
-      }
-      case AUTO_GENERATE_ATTRIBUTE_STRATEGY.KSUID: {
-        return KSUID.randomSync(new Date()).string;
-      }
-      case AUTO_GENERATE_ATTRIBUTE_STRATEGY.ISO_DATE: {
-        return new Date().toISOString();
-      }
-      case AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE: {
-        return Math.ceil(new Date().valueOf() / 1000);
-      }
-      default: {
-        throw new Error('Unknown attribute strategy.');
-      }
-    }
+    return autoGenerateValue(this.strategy);
   }
 }
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 e99052c..3e0e7fb 100644
--- a/packages/core/src/classes/transformer/document-client-request-transformer.ts
+++ b/packages/core/src/classes/transformer/document-client-request-transformer.ts
@@ -29,6 +29,7 @@ import {KeyConditionOptions} from '../expression/key-condition-options-type';
 import {UpdateBody} from '../expression/update-body-type';
 import {isObject} from '../../helpers/is-object';
 import {DocumentClientTypes} from '@typedorm/document-client';
+import {autoGenerateValue} from '../../helpers/auto-generate-attribute-value';
 
 export interface ManagerToDynamoPutItemOptions {
   /**
@@ -404,7 +405,7 @@ export class DocumentClientRequestTransformer extends BaseTransformer {
     // check if auto update attributes are not referenced by primary key
     const formattedAutoUpdateAttributes = autoUpdateAttributes.reduce(
       (acc, attr) => {
-        acc[attr.name] = attr.autoGenerateValue(attr.strategy);
+        acc[attr.name] = autoGenerateValue(attr.strategy);
         return acc;
       },
       {} as {[key: string]: any}
diff --git a/packages/core/src/helpers/auto-generate-attribute-value.ts b/packages/core/src/helpers/auto-generate-attribute-value.ts
new file mode 100644
index 0000000..f2ff3c7
--- /dev/null
+++ b/packages/core/src/helpers/auto-generate-attribute-value.ts
@@ -0,0 +1,48 @@
+import KSUID from 'ksuid';
+import {
+  AUTO_GENERATE_ATTRIBUTE_STRATEGY,
+  UnknownAttributeValueGenerationStrategyError,
+} from '@typedorm/common';
+import {v4} from 'uuid';
+
+export class AutoGenerateAttributeValue {
+  static get UUID4() {
+    return autoGenerateValue(AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4) as string;
+  }
+
+  static get KSUID() {
+    return autoGenerateValue(AUTO_GENERATE_ATTRIBUTE_STRATEGY.KSUID) as string;
+  }
+
+  static get ISO_DATE() {
+    return autoGenerateValue(
+      AUTO_GENERATE_ATTRIBUTE_STRATEGY.ISO_DATE
+    ) as string;
+  }
+
+  static get EPOCH_DATE() {
+    return autoGenerateValue(
+      AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE
+    ) as number;
+  }
+}
+
+export function autoGenerateValue(strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY) {
+  switch (strategy) {
+    case AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4: {
+      return v4();
+    }
+    case AUTO_GENERATE_ATTRIBUTE_STRATEGY.KSUID: {
+      return KSUID.randomSync(new Date()).string;
+    }
+    case AUTO_GENERATE_ATTRIBUTE_STRATEGY.ISO_DATE: {
+      return new Date().toISOString();
+    }
+    case AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE: {
+      return Math.ceil(new Date().valueOf() / 1000);
+    }
+    default: {
+      throw new UnknownAttributeValueGenerationStrategyError(strategy);
+    }
+  }
+}


Test patch:
diff --git a/__test__/github-issues/176/issue-176.spec.ts b/__test__/github-issues/176/issue-176.spec.ts
new file mode 100644
index 0000000..30c7796
--- /dev/null
+++ b/__test__/github-issues/176/issue-176.spec.ts
@@ -0,0 +1,88 @@
+import 'reflect-metadata';
+
+import {createTestConnection, resetTestConnection} from '@typedorm/testing';
+import {
+  AutoGenerateAttributeValue,
+  BatchManager,
+  WriteBatch,
+} from '@typedorm/core';
+import {User} from './user-entity';
+
+jest.mock('uuid', () => {
+  const originalUUID = jest.requireActual('uuid');
+  return {
+    ...originalUUID,
+    v4: jest
+      .fn()
+      .mockImplementationOnce(() => '763bd269-e0fa-409d-97ac-9b64c550746b')
+      .mockImplementationOnce(() => '9cd91838-7ffa-4c3d-9b5a-0b44430c55df')
+      .mockImplementation(originalUUID.v4),
+  };
+});
+
+let batchManager: BatchManager;
+
+const dcMock = {
+  query: jest.fn(),
+  batchWrite: jest.fn(),
+};
+beforeEach(() => {
+  const connection = createTestConnection({
+    entities: [User],
+    documentClient: dcMock,
+  });
+  batchManager = new BatchManager(connection);
+});
+
+afterEach(() => {
+  resetTestConnection();
+});
+
+test('correctly create event record using batch manager', async () => {
+  dcMock.batchWrite.mockReturnValue({
+    promise: () => ({}),
+  });
+
+  const user1 = new User();
+  user1.id = AutoGenerateAttributeValue.UUID4;
+  user1.name = 'USER 1';
+
+  const user2 = new User();
+  user2.id = AutoGenerateAttributeValue.UUID4;
+  user2.name = 'USER 2';
+
+  const batch = new WriteBatch().addCreateItem(user1).addCreateItem(user2);
+
+  await batchManager.write(batch);
+
+  // batch write is called with correctly transformed dynamo entity
+  expect(dcMock.batchWrite).toHaveBeenCalledWith({
+    RequestItems: {
+      'test-table': [
+        {
+          PutRequest: {
+            Item: {
+              PK: 'ID#763bd269-e0fa-409d-97ac-9b64c550746b',
+              SK: 'ID#763bd269-e0fa-409d-97ac-9b64c550746b',
+              __en: 'User',
+              id: '763bd269-e0fa-409d-97ac-9b64c550746b',
+              name: 'USER 1',
+            },
+          },
+        },
+        {
+          PutRequest: {
+            Item: {
+              PK: 'ID#9cd91838-7ffa-4c3d-9b5a-0b44430c55df',
+              SK: 'ID#9cd91838-7ffa-4c3d-9b5a-0b44430c55df',
+              __en: 'User',
+              id: '9cd91838-7ffa-4c3d-9b5a-0b44430c55df',
+              name: 'USER 2',
+            },
+          },
+        },
+      ],
+    },
+    ReturnConsumedCapacity: undefined,
+  });
+});
diff --git a/__test__/github-issues/176/user-entity.ts b/__test__/github-issues/176/user-entity.ts
new file mode 100644
index 0000000..65bcbcc
--- /dev/null
+++ b/__test__/github-issues/176/user-entity.ts
@@ -0,0 +1,25 @@
+import {
+  Attribute,
+  AutoGenerateAttribute,
+  AUTO_GENERATE_ATTRIBUTE_STRATEGY,
+  Entity,
+} from '@typedorm/common';
+import {testTable} from '../../__mocks__/test-table';
+
+@Entity({
+  name: 'User',
+  table: testTable,
+  primaryKey: {
+    partitionKey: 'ID#{{id}}',
+    sortKey: 'ID#{{id}}',
+  },
+})
+export class User {
+  @AutoGenerateAttribute({
+    strategy: AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4,
+  })
+  id!: string;
+
+  @Attribute()
+  name!: string;
+}
diff --git a/packages/core/src/helpers/__test__/auto-generate-attribute-value.spec.ts b/packages/core/src/helpers/__test__/auto-generate-attribute-value.spec.ts
new file mode 100644
index 0000000..91a71fa
--- /dev/null
+++ b/packages/core/src/helpers/__test__/auto-generate-attribute-value.spec.ts
@@ -0,0 +1,43 @@
+import {AUTO_GENERATE_ATTRIBUTE_STRATEGY} from '@typedorm/common';
+import {autoGenerateValue} from '../auto-generate-attribute-value';
+
+jest.mock('uuid', () => ({
+  v4: () => 'c0ac5395-ba7c-41bf-bbc3-09a6087bcca2',
+}));
+jest.mock('ksuid', () => ({
+  randomSync: () => ({
+    string: '1irj9jenZH4PfTWp3NrDgpSF7lg',
+  }),
+}));
+
+test('returns generated attribute value using uuid strategy', () => {
+  const value = autoGenerateValue(AUTO_GENERATE_ATTRIBUTE_STRATEGY.UUID4);
+  expect(value).toEqual('c0ac5395-ba7c-41bf-bbc3-09a6087bcca2');
+});
+
+test('returns generated attribute value using ksuid strategy', () => {
+  const value = autoGenerateValue(AUTO_GENERATE_ATTRIBUTE_STRATEGY.KSUID);
+  expect(value).toEqual('1irj9jenZH4PfTWp3NrDgpSF7lg');
+});
+
+test('returns generated attribute value using epoch strategy', () => {
+  jest.useFakeTimers('modern').setSystemTime(new Date('2020-01-01'));
+
+  const value = autoGenerateValue(AUTO_GENERATE_ATTRIBUTE_STRATEGY.EPOCH_DATE);
+  expect(value).toEqual(1577836800);
+});
+
+test('returns generated attribute value using iso date strategy', () => {
+  jest.useFakeTimers('modern').setSystemTime(new Date('2020-01-01'));
+
+  const value = autoGenerateValue(AUTO_GENERATE_ATTRIBUTE_STRATEGY.ISO_DATE);
+  expect(value).toEqual('2020-01-01T00:00:00.000Z');
+});
+
+test('throws for unknown strategy', () => {
+  const valueCreator = () => autoGenerateValue('INVALID' as any);
+
+  expect(valueCreator)
+    .toThrowError(`Unknown attribute value generation strategy provided: "INVALID".
+    Valid attribute value generation strategies are: "UUID4", "KSUID", "EPOCH_DATE", "ISO_DATE"`);
+});