Repository: typedorm/typedorm (ts)

Issue:
Add support for specifying default value for attribute decorator
@Attribute decorator to support additional option to provide default value, this can accept any default static value type or a function that returns a staic value.

- [x] handle incoming default value on decorator, (only scalar type supported atm)
- [x] when running create operation include all default values if they exist
- [x]  if there is both default value and explicitly assigned value, later one should be used 

Pull request description:
12 support specifying default value for attribute
# Changes
- add support for specifying default values on attribute

closes #12 

Patch:
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
index 26cd1ee..c9c658c 100644
--- a/packages/common/src/error/index.ts
+++ b/packages/common/src/error/index.ts
@@ -1,1 +1,2 @@
+export * from './attribute-metadata-unsupported-default-value-error';
 export * from './parse-error';
diff --git a/packages/common/src/metadata/metadata-storage.ts b/packages/common/src/metadata/metadata-storage.ts
index 73a297f..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
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/transformer/document-client-request-transformer.ts b/packages/core/src/classes/transformer/document-client-request-transformer.ts
index 74355b1..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,
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));


Test patch:
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 bf95f37..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({
@@ -99,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/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/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;
+});