Repository: typedorm/typedorm (ts)

Issue:
Improve `AND`, `OR` and `NOT` logical operations to support one or more attributes
# Current 
As per current implementation, logical operations such as `OR` enforce users to provide more than one condition, if done otherwise an error is thrown `Value for operator "OR" must contain more than 1 attribute.`. 

# Expectation
Instead of throwing an error when 1 or fewer conditions are provided, parse the input to be single or no condition.

Pull request:
Feature/142 improve logical operations
closes #142 

Patch:
diff --git a/packages/common/src/helpers/resolve-scalar-type.ts b/packages/common/src/helpers/resolve-scalar-type.ts
index ac8d0e9..11bddfb 100644
--- a/packages/common/src/helpers/resolve-scalar-type.ts
+++ b/packages/common/src/helpers/resolve-scalar-type.ts
@@ -2,7 +2,7 @@ import {InvalidType} from './invalid-type';
 import {ScalarType} from './scalar-type';
 
 export type ResolveScalarType<T extends any> = T extends ScalarType
-  ? ScalarType
+  ? T
   : T extends any[]
   ? T[0] extends ScalarType
     ? T[0]
diff --git a/packages/core/src/classes/expression/condition-options-type.ts b/packages/core/src/classes/expression/condition-options-type.ts
index 6751c73..e7f533d 100644
--- a/packages/core/src/classes/expression/condition-options-type.ts
+++ b/packages/core/src/classes/expression/condition-options-type.ts
@@ -3,7 +3,6 @@ import {
   RequireOnlyOne,
   ConditionType,
   ATTRIBUTE_TYPE,
-  RequireAtLeastOne,
   ResolveScalarType,
 } from '@typedorm/common';
 
@@ -64,26 +63,27 @@ type AttributeConditionOptions<Entity> =
 
 type RecursiveConditionOptions<Entity> = {
   // for `AND` and `OR` logical operators require at least one of defined options or other self
-  [key in Extract<
-    ConditionType.LogicalOperator,
-    'OR' | 'AND'
-  >]: RequireAtLeastOne<
-    AttributeConditionOptions<Entity> &
-      // manually infer recursive type
-      RecursiveConditionOptions<Entity> extends infer R
-      ? R
-      : never
-  >;
+  [key in Extract<ConditionType.LogicalOperator, 'OR' | 'AND'>]:
+    | Partial<
+        AttributeConditionOptions<Entity> &
+          // manually infer recursive type
+          RecursiveConditionOptions<Entity>
+      >
+    | {} extends infer R
+    ? R
+    : never;
 } &
   // for `NOT` logical operators require one from defined options or other self
   {
-    [key in Extract<ConditionType.LogicalOperator, 'NOT'>]: RequireOnlyOne<
-      AttributeConditionOptions<Entity> &
-        // manually infer recursive type
-        RecursiveConditionOptions<Entity> extends infer R
-        ? R
-        : never
-    >;
+    [key in Extract<ConditionType.LogicalOperator, 'NOT'>]:
+      | Partial<
+          AttributeConditionOptions<Entity> &
+            // manually infer recursive type
+            RecursiveConditionOptions<Entity>
+        >
+      | {} extends infer R
+      ? R
+      : never;
   } &
   // require attribute filter
   AttributeConditionOptions<Entity>;
diff --git a/packages/core/src/classes/expression/expression-input-parser.ts b/packages/core/src/classes/expression/expression-input-parser.ts
index cb13340..c550632 100644
--- a/packages/core/src/classes/expression/expression-input-parser.ts
+++ b/packages/core/src/classes/expression/expression-input-parser.ts
@@ -129,28 +129,23 @@ export class ExpressionInputParser {
           );
           const base = parsedExpList.shift();
           if (!base) {
-            throw new Error(
-              `Value for operator "${operatorOrAttr}" can not be empty`
-            );
+            return new ExpClass();
           }
           switch (operatorOrAttr) {
             case 'AND': {
               if (!parsedExpList?.length) {
-                throw new Error(
-                  `Value for operator "${operatorOrAttr}" must contain more that 1 attributes.`
-                );
+                return base;
               }
               return base.mergeMany(parsedExpList as T[], MERGE_STRATEGY.AND);
             }
             case 'OR': {
               if (!parsedExpList?.length) {
-                throw new Error(
-                  `Value for operator "${operatorOrAttr}" must contain more that 1 attributes.`
-                );
+                return base;
               }
               return base.mergeMany(parsedExpList as T[], MERGE_STRATEGY.OR);
             }
             case 'NOT': {
+              // not can not contain more than one items
               if (parsedExpList?.length) {
                 throw new Error(
                   `Value for operator "${operatorOrAttr}" can not contain more than 1 attributes.`
diff --git a/packages/core/src/classes/expression/filter-options-type.ts b/packages/core/src/classes/expression/filter-options-type.ts
index 6126999..0533fe2 100644
--- a/packages/core/src/classes/expression/filter-options-type.ts
+++ b/packages/core/src/classes/expression/filter-options-type.ts
@@ -2,7 +2,6 @@ import {
   ATTRIBUTE_TYPE,
   FilterType,
   NonKeyAttributesWithReturnType,
-  RequireAtLeastOne,
   RequireOnlyOne,
   ResolveScalarType,
 } from '@typedorm/common';
@@ -60,23 +59,27 @@ type AttributeFilterOptions<Entity, PrimaryKey> =
 
 type RecursiveFilterOptions<Entity, PrimaryKey> = {
   // for `AND` and `OR` logical operators require at least one of defined options or other self
-  [key in Extract<FilterType.LogicalOperator, 'OR' | 'AND'>]: RequireAtLeastOne<
-    AttributeFilterOptions<Entity, PrimaryKey> &
-      // manually infer recursive type
-      RecursiveFilterOptions<Entity, PrimaryKey> extends infer R
-      ? R
-      : never
-  >;
+  [key in Extract<FilterType.LogicalOperator, 'OR' | 'AND'>]:
+    | Partial<
+        AttributeFilterOptions<Entity, PrimaryKey> &
+          // manually infer recursive type
+          RecursiveFilterOptions<Entity, PrimaryKey>
+      >
+    | {} extends infer R
+    ? R
+    : never;
 } &
   // for `NOT` logical operators require one from defined options or other self
   {
-    [key in Extract<FilterType.LogicalOperator, 'NOT'>]: RequireOnlyOne<
-      AttributeFilterOptions<Entity, PrimaryKey> &
-        // manually infer recursive type
-        RecursiveFilterOptions<Entity, PrimaryKey> extends infer R
-        ? R
-        : never
-    >;
+    [key in Extract<FilterType.LogicalOperator, 'NOT'>]:
+      | Partial<
+          AttributeFilterOptions<Entity, PrimaryKey> &
+            // manually infer recursive type
+            RecursiveFilterOptions<Entity, PrimaryKey>
+        >
+      | {} extends infer R
+      ? R
+      : never;
   } &
   // require attribute filter
   AttributeFilterOptions<Entity, PrimaryKey>;


Test patch:
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 c173dda..489a3a9 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
@@ -142,6 +142,42 @@ test('parses filter with single logical operator', () => {
   });
 });
 
+test('parses logical operator with no attributes', () => {
+  const parsedFilter = expInputParser.parseToFilter<User, UserPrimaryKey>({
+    AND: {},
+  });
+  expect(parsedFilter).toBeInstanceOf(Filter);
+  expect(parsedFilter?.expression).toEqual('');
+});
+
+test('parses logical operator with single attribute', () => {
+  const parsedFilter = expInputParser.parseToFilter<User, UserPrimaryKey>({
+    AND: {
+      age: {
+        EQ: 2,
+      },
+      // eslint-disable-next-line no-constant-condition
+      ...(true
+        ? {}
+        : {
+            name: {
+              BEGINS_WITH: 'mys',
+            },
+          }),
+    },
+  });
+  expect(parsedFilter).toBeInstanceOf(Filter);
+  expect(parsedFilter).toEqual({
+    _names: {
+      '#FE_age': 'age',
+    },
+    _values: {
+      ':FE_age': 2,
+    },
+    expression: '#FE_age = :FE_age',
+  });
+});
+
 test('parses filter with `NOT` logical operator', () => {
   const parsedFilter = expInputParser.parseToFilter<User, UserPrimaryKey>({
     NOT: {
@@ -217,6 +253,9 @@ test('parses filter with complex nested logical operators', () => {
   });
 });
 
+/**
+ * @group parseToCondition
+ */
 test('parses deep nested condition', () => {
   const parsedCondition = expInputParser.parseToCondition<User>({
     NOT: {
@@ -248,6 +287,35 @@ test('parses deep nested condition', () => {
   });
 });
 
+test('parses logical condition expression with no attributes', () => {
+  const parsedCondition = expInputParser.parseToCondition<User>({
+    NOT: {},
+  });
+  expect(parsedCondition).toBeInstanceOf(Condition);
+  expect(parsedCondition?.expression).toEqual('');
+});
+
+test('parses logical condition expression with single attribute value', () => {
+  const parsedCondition = expInputParser.parseToCondition<User>({
+    OR: {
+      age: {
+        BETWEEN: [1, 3],
+      },
+    },
+  });
+  expect(parsedCondition).toBeInstanceOf(Condition);
+  expect(parsedCondition).toEqual({
+    _names: {
+      '#CE_age': 'age',
+    },
+    _values: {
+      ':CE_age_end': 3,
+      ':CE_age_start': 1,
+    },
+    expression: '#CE_age BETWEEN :CE_age_start AND :CE_age_end',
+  });
+});
+
 /**
  * @group parseToProjection
  */