Repository: typedorm/typedorm (ts)

Issue:
Expression with path to nested set value type builds incorrectly
# Description

When trying to build an expression for input containing nested key path such as `.setTo('complex.nested.attr[1]', 'new value')`, attr[1] is treated as a continuing attribute that generates expression and its values like the following:

## Current Exp

exp:  `... #complex_nested_object[1] = :complex_nested_object[1]`
names:
```
{
  #complex_nested_object[1]: 'complex_nested_object[1]'
}
```
values: 
```
{
  :complex_nested_object[1]: '1234'
}
```


## Expected Exp
exp:  `... #complex_nested_object[1] = :complex_nested_object`
names 
```
{
  #complex_nested_object: 'complex_nested_object'
}
```
values: 
```
{
  :complex_nested_object: '1234'
}
```

Pull request description:
fix expression input parser
# Changes
- fix expression input parser to correctly build expressions for nested attr references

closes #139 

Patch:
diff --git a/packages/core/src/classes/expression/base-expression-input.ts b/packages/core/src/classes/expression/base-expression-input.ts
index 0d36322..8c9431a 100644
--- a/packages/core/src/classes/expression/base-expression-input.ts
+++ b/packages/core/src/classes/expression/base-expression-input.ts
@@ -1,4 +1,5 @@
 import {ATTRIBUTE_TYPE, ScalarType, SimpleOperator} from '@typedorm/common';
+import {nestedKeyAccessRegex} from '../../helpers/constants';
 
 const lastCharSpaceMatcher = /\s$/;
 export enum MERGE_STRATEGY {
@@ -70,7 +71,9 @@ export abstract class BaseExpressionInput {
     const topLevelPropKey = this.innerAddExpressionName(topKey);
     return nestedKeys.reduce(
       (acc, keySeg) => {
-        const {prefix} = acc;
+        let {prefix} = acc;
+        // make sure that prefix does not contain any nested value reference
+        prefix = prefix.replace(nestedKeyAccessRegex, '');
 
         const currentSegPropKey = this.innerAddExpressionName(
           `${prefix}_${keySeg}`,
@@ -86,6 +89,16 @@ export abstract class BaseExpressionInput {
   }
 
   private innerAddExpressionName(nameKey: string, nameValue?: string) {
+    // match any nested list item reference, and update it to be valid expression
+    // i.e key such as addresses[0] will be name #addresses with expression #addresses[0]
+
+    let match = '';
+    nameKey = nameKey.replace(nestedKeyAccessRegex, substr => {
+      match = substr;
+      return '';
+    });
+    nameValue = nameValue?.replace(match, '');
+
     const expressionPrefixedName = this.getExpNameKey(nameKey);
     if (this.names[expressionPrefixedName]) {
       throw new Error(
@@ -96,7 +109,7 @@ export abstract class BaseExpressionInput {
       ...this.names,
       [expressionPrefixedName]: nameValue ?? nameKey,
     };
-    return expressionPrefixedName;
+    return expressionPrefixedName + match;
   }
 
   protected addExpressionValue(name: string, value: any) {
@@ -105,6 +118,10 @@ export abstract class BaseExpressionInput {
   }
 
   private innerAddExpressionValue(name: string, value: any) {
+    // remove any nested list item reference, it will be handled by names matcher
+    // i.e key such as addresses[0]
+    name = name.replace(nestedKeyAccessRegex, '');
+
     const expressionPrefixedValue = this.getExpValueKey(name);
     if (this.values[expressionPrefixedValue]) {
       throw new Error(
diff --git a/packages/core/src/classes/expression/expression-input-parser.ts b/packages/core/src/classes/expression/expression-input-parser.ts
index 2e38947..e6a1057 100644
--- a/packages/core/src/classes/expression/expression-input-parser.ts
+++ b/packages/core/src/classes/expression/expression-input-parser.ts
@@ -24,6 +24,7 @@ import {Update} from './update/update';
 import {DeleteUpdate} from './update/delete-update';
 import {RemoveUpdate} from './update/remove-update';
 import {isObject} from '../../helpers/is-object';
+import {nestedKeyAccessRegex} from '../../helpers/constants';
 
 /**
  * Parses expression input to expression instances
@@ -64,7 +65,6 @@ export class ExpressionInputParser {
     attr: string,
     value: any
   ): {value: any; type: 'static' | 'dynamic'} {
-    const nestedKeyAccessRegex = /[\d+]/g;
     if (isObject(value) && !isEmptyObject(value)) {
       const [operator, operatorValue] = Object.entries(value as any)[0];
 
diff --git a/packages/core/src/helpers/constants.ts b/packages/core/src/helpers/constants.ts
index ecc363e..340cd83 100644
--- a/packages/core/src/helpers/constants.ts
+++ b/packages/core/src/helpers/constants.ts
@@ -1,1 +1,3 @@
 export const regexInterpolatedWord = /\{{(.+?)\}}/g;
+
+export const nestedKeyAccessRegex = /\[\d+\]/g;


Test patch:
diff --git a/packages/core/src/classes/expression/__test__/expression-builder.spec.ts b/packages/core/src/classes/expression/__test__/expression-builder.spec.ts
index a0bc23d..069ec02 100644
--- a/packages/core/src/classes/expression/__test__/expression-builder.spec.ts
+++ b/packages/core/src/classes/expression/__test__/expression-builder.spec.ts
@@ -76,22 +76,22 @@ test('builds update expression for complex nested object', () => {
   const expression = expressionBuilder.buildUpdateExpression(item);
   expect(expression).toEqual({
     UpdateExpression:
-      'SET #UE_application = :UE_application, #UE_profile.#UE_profile_name.#UE_profile_name_last = :UE_profile_name_last, #UE_data = :UE_data, #UE_address[0] = :UE_address[0], #UE_complex.#UE_complex_nested.#UE_complex_nested_object[1] = :UE_complex_nested_object[1]',
+      'SET #UE_application = :UE_application, #UE_profile.#UE_profile_name.#UE_profile_name_last = :UE_profile_name_last, #UE_data = :UE_data, #UE_address[0] = :UE_address, #UE_complex.#UE_complex_nested.#UE_complex_nested_object[1] = :UE_complex_nested_object',
     ExpressionAttributeNames: {
-      '#UE_address[0]': 'address[0]',
+      '#UE_address': 'address',
       '#UE_application': 'application',
       '#UE_complex': 'complex',
       '#UE_complex_nested': 'nested',
-      '#UE_complex_nested_object[1]': 'object[1]',
+      '#UE_complex_nested_object': 'object',
       '#UE_data': 'data',
       '#UE_profile': 'profile',
       '#UE_profile_name': 'name',
       '#UE_profile_name_last': 'last',
     },
     ExpressionAttributeValues: {
-      ':UE_address[0]': 'new address portion',
+      ':UE_address': 'new address portion',
       ':UE_application': 'test',
-      ':UE_complex_nested_object[1]': 'new value',
+      ':UE_complex_nested_object': 'new value',
       ':UE_data': [1, 2, 3],
       ':UE_profile_name_last': 'new Last name',
     },
diff --git a/packages/core/src/classes/expression/__test__/expression-input-parser.spec.ts b/packages/core/src/classes/expression/__test__/expression-input-parser.spec.ts
index 06a6ef1..5f1dc3a 100644
--- a/packages/core/src/classes/expression/__test__/expression-input-parser.spec.ts
+++ b/packages/core/src/classes/expression/__test__/expression-input-parser.spec.ts
@@ -40,6 +40,38 @@ test('parses simple filter input', () => {
   expect(parsedFilter?.expression).toEqual('#FE_age = :FE_age');
 });
 
+/**
+ * Issue #139
+ */
+test('parses nested key filter input to valid expression', () => {
+  const parsedFilter = expInputParser.parseToFilter<any, {}>({
+    AND: {
+      'contact.addresses[0]': {
+        EQ: 12,
+      },
+      'bio[0].age': {
+        LE: 2,
+      },
+    },
+  });
+
+  expect(parsedFilter).toBeInstanceOf(Filter);
+  expect(parsedFilter).toEqual({
+    _names: {
+      '#FE_contact': 'contact',
+      '#FE_contact_addresses': 'addresses',
+      '#FE_bio': 'bio',
+      '#FE_bio_age': 'age',
+    },
+    _values: {
+      ':FE_contact_addresses': 12,
+      ':FE_bio_age': 2,
+    },
+    expression:
+      '(#FE_contact.#FE_contact_addresses[0] = :FE_contact_addresses) AND (#FE_bio[0].#FE_bio_age <= :FE_bio_age)',
+  });
+});
+
 test('parses filter with range operator', () => {
   const parsedFilter = expInputParser.parseToFilter<User, UserPrimaryKey>({
     name: {
diff --git a/packages/core/src/classes/expression/__test__/remove-update.spec.ts b/packages/core/src/classes/expression/__test__/remove-update.spec.ts
index e4c32e7..975e778 100644
--- a/packages/core/src/classes/expression/__test__/remove-update.spec.ts
+++ b/packages/core/src/classes/expression/__test__/remove-update.spec.ts
@@ -5,7 +5,7 @@ test('creates a update expression for removing attributes from item', () => {
   expect(update).toEqual({
     _names: {
       '#UE_age': 'age',
-      '#UE_items[2]': 'items[2]',
+      '#UE_items': 'items',
     },
     expression: '#UE_age, #UE_items[2]',
     prefix: 'REMOVE',