Source

query/Condition.ts

import { AttributeOption, ConditionBuilderOption } from "./options";
import {
  Model,
  ModelErrorDefinition,
  required,
} from "@decaf-ts/decorator-validation";
import { GroupOperator, Operator } from "./constants";
import { QueryError } from "./errors";

/**
 * @description Represents a logical condition for database queries
 * @summary A class that encapsulates query conditions with support for complex logical operations.
 * This class allows for building and combining query conditions using logical operators (AND, OR, NOT)
 * and comparison operators (equals, not equals, greater than, etc.).
 * @template M - The model type this condition operates on
 * @param {string | Condition<M>} attr1 - The attribute name or a nested condition
 * @param {Operator | GroupOperator} operator - The operator to use for the condition
 * @param {any} comparison - The value to compare against or another condition
 * @class Condition
 * @example
 * // Create a simple condition
 * const nameCondition = Condition.attribute("name").eq("John");
 *
 * // Create a complex condition
 * const complexCondition = Condition.attribute("age").gt(18)
 *   .and(Condition.attribute("status").eq("active"));
 *
 * // Use the builder pattern
 * const userQuery = Condition.builder()
 *   .attribute("email").regexp(".*@example.com")
 *   .and(Condition.attribute("lastLogin").gt(new Date("2023-01-01")));
 */
export class Condition<M extends Model> extends Model {
  @required()
  protected attr1?: string | Condition<M> = undefined;
  @required()
  protected operator?: Operator | GroupOperator = undefined;
  @required()
  protected comparison?: any = undefined;

  private constructor(
    attr1: string | Condition<M>,
    operator: Operator | GroupOperator,
    comparison: any
  ) {
    super();
    this.attr1 = attr1;
    this.operator = operator;
    this.comparison = comparison;
  }

  /**
   * @description Combines this condition with another using logical AND
   * @summary Joins two conditions with an AND operator, requiring both to be true
   * @param {Condition<M>} condition - The condition to combine with this one
   * @return {Condition<M>} A new condition representing the AND operation
   */
  and(condition: Condition<M>): Condition<M> {
    return Condition.and(this, condition);
  }

  /**
   * @description Combines this condition with another using logical OR
   * @summary Joins two conditions with an OR operator, requiring at least one to be true
   * @param {Condition<M>} condition - The condition to combine with this one
   * @return {Condition<M>} A new condition representing the OR operation
   */
  or(condition: Condition<M>): Condition<M> {
    return Condition.or(this, condition);
  }

  /**
   * @description Creates a negation condition
   * @summary Excludes a value from the result by applying a NOT operator
   * @param {any} val - The value to negate
   * @return {Condition<M>} A new condition representing the NOT operation
   */
  not(val: any): Condition<M> {
    return new Condition(this, Operator.NOT, val);
  }

  /**
   * @description Validates the condition and checks for errors
   * @summary Extends the base validation to ensure the condition is properly formed
   * @param {...string[]} exceptions - Fields to exclude from validation
   * @return {ModelErrorDefinition | undefined} Error definition if validation fails, undefined otherwise
   */
  override hasErrors(
    ...exceptions: string[]
  ): ModelErrorDefinition | undefined {
    const errors = super.hasErrors(...exceptions);
    if (errors) return errors;

    const invalidOpMessage = `Invalid operator ${this.operator}}`;

    if (typeof this.attr1 === "string") {
      if (this.comparison instanceof Condition)
        return {
          comparison: {
            condition: "Both sides of the comparison must be of the same type",
          },
        } as ModelErrorDefinition;
      if (Object.values(Operator).indexOf(this.operator as Operator) === -1)
        return {
          operator: {
            condition: invalidOpMessage,
          },
        } as ModelErrorDefinition;
    }

    if (this.attr1 instanceof Condition) {
      if (
        !(this.comparison instanceof Condition) &&
        this.operator !== Operator.NOT
      )
        return {
          comparison: {
            condition: invalidOpMessage,
          },
        } as ModelErrorDefinition;
      if (
        Object.values(GroupOperator).indexOf(this.operator as GroupOperator) ===
          -1 &&
        this.operator !== Operator.NOT
      )
        return {
          operator: {
            condition: invalidOpMessage,
          },
        } as ModelErrorDefinition;
      // if (this.operator !== Operator.NOT && typeof this.attr1.attr1 !== "string")
      //     return {
      //         attr1: {
      //             condition: stringFormat("Parent condition attribute must be a string")
      //         }
      //     } as ModelErrorDefinition
    }
  }

  /**
   * @description Creates a new condition that combines two conditions with logical AND
   * @summary Static method that joins two conditions with an AND operator, requiring both to be true
   * @template M - The model type this condition operates on
   * @param {Condition<M>} condition1 - The first condition
   * @param {Condition<M>} condition2 - The second condition
   * @return {Condition<M>} A new condition representing the AND operation
   */
  static and<M extends Model>(
    condition1: Condition<M>,
    condition2: Condition<M>
  ): Condition<M> {
    return Condition.group(condition1, GroupOperator.AND, condition2);
  }

  /**
   * @description Creates a new condition that combines two conditions with logical OR
   * @summary Static method that joins two conditions with an OR operator, requiring at least one to be true
   * @template M - The model type this condition operates on
   * @param {Condition<M>} condition1 - The first condition
   * @param {Condition<M>} condition2 - The second condition
   * @return {Condition<M>} A new condition representing the OR operation
   */
  static or<M extends Model>(
    condition1: Condition<M>,
    condition2: Condition<M>
  ): Condition<M> {
    return Condition.group(condition1, GroupOperator.OR, condition2);
  }

  /**
   * @description Creates a new condition that groups two conditions with a specified operator
   * @summary Private static method that combines two conditions using the specified group operator
   * @template M - The model type this condition operates on
   * @param {Condition<M>} condition1 - The first condition
   * @param {GroupOperator} operator - The group operator to use (AND, OR)
   * @param {Condition<M>} condition2 - The second condition
   * @return {Condition<M>} A new condition representing the grouped operation
   */
  private static group<M extends Model>(
    condition1: Condition<M>,
    operator: GroupOperator,
    condition2: Condition<M>
  ): Condition<M> {
    return new Condition(condition1, operator, condition2);
  }

  /**
   * @description Creates a condition builder for a specific model attribute
   * @summary Static method that initializes a condition builder with the specified attribute
   * @template M - The model type this condition operates on
   * @param attr - The model attribute to build a condition for
   * @return {AttributeOption<M>} A condition builder initialized with the attribute
   */
  static attribute<M extends Model>(attr: keyof M) {
    return new Condition.Builder<M>().attribute(attr);
  }

  /**
   * @description Alias for the attribute method
   * @summary Shorthand method that initializes a condition builder with the specified attribute
   * @template M - The model type this condition operates on
   * @param attr - The model attribute to build a condition for
   * @return {AttributeOption<M>} A condition builder initialized with the attribute
   */
  static attr<M extends Model>(attr: keyof M) {
    return this.attribute(attr);
  }

  /**
   * @description Provides a fluent API to build query conditions
   * @summary A builder class that simplifies the creation of database query conditions
   * with a chainable interface for setting attributes and operators
   * @template M - The model type this condition builder operates on
   * @class ConditionBuilder
   */
  private static Builder = class ConditionBuilder<M extends Model>
    implements ConditionBuilderOption<M>, AttributeOption<M>
  {
    attr1?: keyof M | Condition<M> = undefined;
    operator?: Operator | GroupOperator = undefined;
    comparison?: any = undefined;

    /**
     * @description Sets the attribute for the condition
     * @summary Specifies which model attribute the condition will operate on
     * @param attr - The model attribute to use in the condition
     * @return {AttributeOption<M>} This builder instance for method chaining
     */
    attribute(attr: keyof M): AttributeOption<M> {
      this.attr1 = attr;
      return this;
    }

    /**
     * @description Alias for the attribute method
     * @summary Shorthand method to specify which model attribute the condition will operate on
     * @param attr - The model attribute to use in the condition
     * @return {AttributeOption<M>} This builder instance for method chaining
     */
    attr(attr: keyof M) {
      return this.attribute(attr);
    }

    /**
     * @description Creates an equality condition
     * @summary Builds a condition that checks if the attribute equals the specified value
     * @param {any} val - The value to compare the attribute against
     * @return {Condition<M>} A new condition representing the equality comparison
     */
    eq(val: any) {
      return this.setOp(Operator.EQUAL, val);
    }

    /**
     * @description Creates an inequality condition
     * @summary Builds a condition that checks if the attribute is different from the specified value
     * @param {any} val - The value to compare the attribute against
     * @return {Condition<M>} A new condition representing the inequality comparison
     */
    dif(val: any) {
      return this.setOp(Operator.DIFFERENT, val);
    }

    /**
     * @description Creates a greater than condition
     * @summary Builds a condition that checks if the attribute is greater than the specified value
     * @param {any} val - The value to compare the attribute against
     * @return {Condition<M>} A new condition representing the greater than comparison
     */
    gt(val: any) {
      return this.setOp(Operator.BIGGER, val);
    }

    /**
     * @description Creates a less than condition
     * @summary Builds a condition that checks if the attribute is less than the specified value
     * @param {any} val - The value to compare the attribute against
     * @return {Condition<M>} A new condition representing the less than comparison
     */
    lt(val: any) {
      return this.setOp(Operator.SMALLER, val);
    }

    /**
     * @description Creates a greater than or equal to condition
     * @summary Builds a condition that checks if the attribute is greater than or equal to the specified value
     * @param {any} val - The value to compare the attribute against
     * @return {Condition<M>} A new condition representing the greater than or equal comparison
     */
    gte(val: any) {
      return this.setOp(Operator.BIGGER_EQ, val);
    }

    /**
     * @description Creates a less than or equal to condition
     * @summary Builds a condition that checks if the attribute is less than or equal to the specified value
     * @param {any} val - The value to compare the attribute against
     * @return {Condition<M>} A new condition representing the less than or equal comparison
     */
    lte(val: any) {
      return this.setOp(Operator.SMALLER_EQ, val);
    }

    /**
     * @description Creates an inclusion condition
     * @summary Builds a condition that checks if the attribute value is included in the specified array
     * @param {any[]} arr - The array of values to check against
     * @return {Condition<M>} A new condition representing the inclusion comparison
     */
    in(arr: any[]) {
      return this.setOp(Operator.IN, arr);
    }

    /**
     * @description Creates a regular expression condition
     * @summary Builds a condition that checks if the attribute matches the specified regular expression pattern
     * @param {any} val - The regular expression pattern to match against
     * @return {Condition<M>} A new condition representing the regular expression comparison
     */
    regexp(val: any) {
      return this.setOp(Operator.REGEXP, new RegExp(val).source);
    }

    /**
     * @description Sets the operator and comparison value for the condition
     * @summary Private method that configures the condition with the specified operator and value
     * @param {Operator} op - The operator to use for the condition
     * @param {any} val - The value to compare against
     * @return {Condition<M>} A new condition with the specified operator and value
     */
    private setOp(op: Operator, val: any) {
      this.operator = op;
      this.comparison = val;
      return this.build();
    }

    /**
     * @description Constructs a Condition instance from the builder's state
     * @summary Finalizes the condition building process by creating a new Condition instance
     * @throws {QueryError} If the condition cannot be built due to invalid parameters
     * @return {Condition<M>} A new condition instance with the configured attributes
     */
    private build(): Condition<M> {
      try {
        return new Condition(
          this.attr1 as string | Condition<M>,
          this.operator as Operator,
          this.comparison as any
        );
      } catch (e: any) {
        throw new QueryError(e);
      }
    }
  };

  /**
   * @description Creates a new condition builder
   * @summary Factory method that returns a new instance of the condition builder
   * @template M - The model type this condition builder will operate on
   * @return {ConditionBuilderOption<M>} A new condition builder instance
   */
  static builder<M extends Model>(): ConditionBuilderOption<M> {
    return new Condition.Builder<M>();
  }
}