; export interface IBaseRepository { //Query function findAll(options?: FindOptionsQL): Promise ; export interface IBaseRepository { //Query function findAll(options?: FindOptionsQL): Promise ; export interface IBaseRepository { //Query function findAll(options?: FindOptionsQL): Promise
import { SQLWrapper } from "drizzle-orm";
import { PgColumn, PgTable } from "drizzle-orm/pg-core";
import { z } from "zod";
import { FilterRuleGroupSchema, OrderDirection } from "./FIlterBuilder";

export type ID = number | string;
export type OrderDirection = "asc" | "desc";

export type FindOptionsQL = {
  where?: SQLWrapper;
  limit?: number;
  offset?: number;
  orderBy?: {
    column: PgColumn;
    direction: OrderDirection;
  }[];
};

export const FindOptionsSchema = z
  .object({
    where: FilterRuleGroupSchema,
    limit: z.number().default(10), //todo - use app configaration for default value
    offset: z.number().default(0),
    orderBy: z.array(
      z.object({
        column: z.string(),
        direction: z.enum(OrderDirection),
      })
    ),
  })
  .partial();

export type FindOptions = z.infer<typeof FindOptionsSchema>;

export interface IBaseRepository<TTable extends PgTable & { id: SQLWrapper }> {
  //Query function
  findAll(options?: FindOptionsQL): Promise<TTable["$inferSelect"][]>;
  findById(id: ID): Promise<TTable["$inferSelect"] | null>;
  findOne(where: SQLWrapper): Promise<TTable["$inferSelect"] | null>;
  findAndCount(
    where?: FindOptionsQL
  ): Promise<[TTable["$inferSelect"][], number]>;

  count(where?: SQLWrapper): Promise<number>;
  checkExist(where: SQLWrapper): Promise<boolean>;
  checkExistById(id: ID): Promise<boolean>;

  //Mutation function
  create(data: TTable["$inferInsert"]): Promise<TTable["$inferSelect"]>;
  createMany(data: TTable["$inferInsert"][]): Promise<TTable["$inferSelect"][]>;

  //Update function
  update(
    id: ID,
    data: Partial<TTable["$inferInsert"]>
  ): Promise<TTable["$inferSelect"] | null>;

  updateMany(
    data: Partial<TTable["$inferInsert"] & { id: ID }>[]
  ): Promise<TTable["$inferSelect"][] | null>;

  //Delete function
  delete(id: ID): Promise<void>;
  deleteMany(ids: ID[]): Promise<void>;
  getTable(): TTable;
}

import { asc, desc, eq, inArray, sql, SQLWrapper } from "drizzle-orm";
import { PgTable } from "drizzle-orm/pg-core";
import { IDatabaseClient } from "../db/IDatabasClient";
import { FindOptionsQL, IBaseRepository, ID } from "./IBaseRepository";

export abstract class BaseRepository<
  TTable extends PgTable & { id: SQLWrapper }
> implements IBaseRepository<TTable>
{
  constructor(
    protected readonly db: IDatabaseClient,
    protected readonly table: TTable
  ) {}

  //Query function
  async findAll(options?: FindOptionsQL): Promise<TTable["$inferSelect"][]> {
    const result = await this.db.executeQuery("FindAll", async (db) => {
      let query = db.select().from(this.table).$dynamic();

      if (options?.where) {
        query = query.where(options.where.getSQL());
      }

      if (options?.orderBy && options.orderBy.length > 0) {
        const orderClause = options.orderBy.map((order) =>
          order.direction === "asc" ? asc(order.column) : desc(order.column)
        );
        query = query.orderBy(...orderClause);
      }

      if (options?.limit) {
        query = query.limit(options.limit);
      }

      if (options?.offset) {
        query = query.offset(options.offset);
      }

      const records = await query.execute();
      return records;
    });

    return result;
  }

  async findById(id: ID): Promise<TTable["$inferSelect"] | null> {
    const result = await this.db.executeQuery("FindById", async (db) => {
      const records = await db
        .select()
        .from(this.table)
        .where(eq(this.table.id, id))
        .execute();

      return records[0] ?? null;
    });

    return result;
  }

  async findOne(where: SQLWrapper): Promise<TTable["$inferSelect"] | null> {
    const result = await this.db.executeQuery("FindById", async (db) => {
      const records = await db
        .select()
        .from(this.table)
        .where(where.getSQL())
        .execute();
      return records[0] ?? null;
    });

    return result;
  }

  async findAndCount(
    options?: FindOptionsQL
  ): Promise<[TTable["$inferSelect"][], number]> {
    const result = await this.db.executeQuery("FindAndCount", async (db) => {
      const recordsPromise = this.findAll(options);
      const countPromise = this.count(options?.where);

      const [records, count] = await Promise.all([
        recordsPromise,
        countPromise,
      ]);

      return [records, count] as [TTable["$inferSelect"][], number];
    });

    return result;
  }

  async count(where?: SQLWrapper): Promise<number> {
    const result = await this.db.executeQuery("Count", async (db) => {
      let query = db
        .select({ count: sql`count(*)` })
        .from(this.table)
        .$dynamic();

      if (where) {
        query = query.where(where.getSQL());
      }

      const records = await query.execute();
      return Number(records[0]?.count);
    });
    return result;
  }

  async checkExist(where: SQLWrapper): Promise<boolean> {
    const count = await this.count(where);
    return count > 0;
  }

  async checkExistById(id: ID): Promise<boolean> {
    return await this.checkExist(eq(this.table.id, id));
  }

  //Mutation function
  async create(data: TTable["$inferInsert"]): Promise<TTable["$inferSelect"]> {
    const result = await this.db.executeQuery("Create", async (db) => {
      const records = await db.insert(this.table).values(data).returning();

      return records[0];
    });
    return result as TTable["$inferSelect"];
  }

  async createMany(
    data: TTable["$inferInsert"][]
  ): Promise<TTable["$inferSelect"][]> {
    const result = await this.db.executeQuery("CreateMany", async (db) => {
      const records = await db
        .insert(this.table)
        .values(data)
        .returning()
        .execute();

      return records;
    });

    return result as TTable["$inferSelect"][];
  }

  //Update function
  async update(
    id: ID,
    data: Partial<TTable["$inferInsert"]>
  ): Promise<TTable["$inferSelect"] | null> {
    const result = await this.db.executeQuery("Update", async (db) => {
      const records = await db
        .update(this.table)
        .set(data)
        .where(eq(this.table.id, id))
        .returning()
        .execute();
      return records;
    });

    if (!result) {
      return null;
    }

    return (result as TTable["$inferSelect"][])[0];
  }

  async updateMany(
    data: Partial<TTable["$inferInsert"] & { id: ID }>[]
  ): Promise<TTable["$inferSelect"][]> {
    const result = await this.db.executeQuery("UpdateMany", async (db) => {
      const records = await db
        .update(this.table)
        .set(data)
        .where(eq(this.table.id, data[0].id))
        .returning()
        .execute();
      return records;
    });

    return result as TTable["$inferSelect"][];
  }

  //Delete function
  async delete(id: ID): Promise<void> {
    await this.db.executeQuery("Delete", async (db) => {
      await db.delete(this.table).where(eq(this.table.id, id)).execute();
    });
  }

  async deleteMany(ids: ID[]): Promise<void> {
    await this.db.executeQuery("DeleteMany", async (db) => {
      await db.delete(this.table).where(inArray(this.table.id, ids));
    });
  }

  getTable(): TTable {
    return this.table;
  }

}


import chalk from "chalk";
import { SQLWrapper } from "drizzle-orm";
import { PgColumn, PgTable } from "drizzle-orm/pg-core";
import { BaseRepository } from "./BaseRepository";
import { FilterBuilder } from "./FIlterBuilder";
import { FindOptions, ID, OrderDirection } from "./IBaseRepository";
import { IBaserService } from "./IBaserService";

export abstract class BaseService<
  TTable extends PgTable & { id: SQLWrapper },
  TRepository extends BaseRepository<TTable> = BaseRepository<TTable>
> implements IBaserService<TTable>
{
  constructor(
    private readonly repository: TRepository
  ) // protected readonly config: AppConfig; // todo - add config later
  {}

  async findAll(options?: FindOptions) {
    const filter = options?.where
      ? FilterBuilder.build(options.where)
      : undefined;

    try {
      return await this.repository.findAll({
        where: filter,
        limit: options?.limit ?? 10, // this.config.pagination.limit,
        offset: options?.offset ?? 0,
        orderBy: this.transformOrderBy(options?.orderBy),
      });
    } catch (err) {
      this.handleError(err, "FindAll");
    }
  }

  async findById(id: ID): Promise<TTable["$inferSelect"] | null> {
    try {
      const item = await this.repository.findById(id);

      //Todo - Create Custom Error with status code
      if (!item) {
        throw new Error("Item not found");
      }

      return item;
    } catch (error) {
      this.handleError(error, "FindById");
    }
  }

  async create(data: TTable["$inferInsert"]): Promise<TTable["$inferSelect"]> {
    try {
      const item = await this.repository.create(data);
      return item;
    } catch (error) {
      this.handleError(error, "Create");
    }
  }

  async update(
    id: ID,
    data: Partial<TTable["$inferInsert"]>
  ): Promise<TTable["$inferSelect"]> {
    try {
      const item = await this.repository.update(id, data);

      if (!item) {
        throw new Error("Item not found");
      }

      return item;
    } catch (error) {
      this.handleError(error, "Update");
    }
  }

  async delete(id: ID): Promise<void> {
    try {
      await this.repository.delete(id);
    } catch (error) {
      this.handleError(error, "Delete");
    }
  }

  async checkExists(id: ID): Promise<boolean> {
    try {
      const exists = await this.repository.checkExistById(id);
      return exists;
    } catch (error) {
      this.handleError(error, "CheckExists");
    }
  }

  // Private method: complete later
  protected handleError(error: unknown, label: string): never {
    console.error(chalk.red(`${label} Error`), error);

    if (error instanceof Error) {
      throw error;
    }

    throw new Error(
      typeof error === "string" ? error : "An unexpected error occurred"
    );
  }

  protected async catchError(callback: () => Promise<unknown>) {
    try {
      return await callback();
    } catch (error) {
      console.error(chalk.red("Error"), error);

      if (error instanceof Error) {
        throw error;
      }

      throw new Error(
        typeof error === "string" ? error : "An unexpected error occurred"
      );
    }
  }

  protected transformOrderBy(orderBy: FindOptions['orderBy']) {
		if (!orderBy) return undefined;
		const table = this.repository.getTable();

		return orderBy
			.filter((order) => order.column in table)
			.map((order) => ({
				column: table[order.column as keyof typeof table] as PgColumn,
				direction: order.direction as OrderDirection,
			}));
	}
}


import { and, not, or, sql, SQLWrapper } from "drizzle-orm";
import { z } from "zod";

export const Operators = {
  // Comparison Operators
  EQUALS: "=",
  NOT_EQUALS: "!=",
  GREATER: ">",
  GREATER_EQUALS: ">=",
  LESS: "<",
  LESS_EQUALS: "<=",

  // Text Operators
  CONTAINS: "contains",
  NOT_CONTAINS: "not_contains",
  STARTS_WITH: "starts_with",
  ENDS_WITH: "ends_with",
  LIKE: "like",
  ILIKE: "ilike",

  // Null checks
  IS_NULL: "is_null",
  IS_NOT_NULL: "is_not_null",

  // Lists
  IN: "in",
  NOT_IN: "not_in",

  // Ranges
  BETWEEN: "between",
  NOT_BETWEEN: "not_between",
} as const;

export type Operator = (typeof Operators)[keyof typeof Operators];

export type Combinator = "and" | "or";

export const  OrderDirection = ["asc" , "desc"] as const;

export type FilterRule = {
  field: string;
  operator: Operator; //todo - check operator which value which operator can have used
  value?: unknown;
};
export const FilterRuleSchema = z.object({
  field: z.string(),
  operator: z.nativeEnum(Operators),
  value: z.any(),
});

export type FilterRuleGroup = {
  combinator: Combinator;
  not?: boolean;
  rules: Array<FilterRule | FilterRuleGroup>;
};

export const FilterRuleGroupSchema: z.ZodType<FilterRuleGroup> = z.lazy(() =>
  z.object({
    combinator: z.enum(["and", "or"]),
    not: z.boolean().optional(),
    rules: z.array(z.union([FilterRuleSchema, FilterRuleGroupSchema])),
  })
);

export class FilterBuilder {
  static build(filter: FilterRuleGroup): SQLWrapper {
    return sql`1=1`;
  }

  static raw(query: SQLWrapper) {
    return query.getSQL();
  }

  /**
   * ===== Private Methods =====
   */

  private static processRuleGroup(group: FilterRuleGroup): SQLWrapper {
    const conditions = group.rules.map((rule) =>
      this.isRuleGroup(rule)
        ? this.processRuleGroup(rule)
        : this.processRule(rule)
    );

    if (conditions.length === 0) {
      return sql`1=1`;
    }

    const result = (
      group.combinator === "and" ? and(...conditions) : or(...conditions)
    ) as SQLWrapper;

    return group.not ? not(result) : result;
  }

  private static isRuleGroup(
    rule: FilterRule | FilterRuleGroup
  ): rule is FilterRuleGroup {
    return "rules" in rule && rule.rules !== undefined;
  }

  private static processRule(rule: FilterRule): SQLWrapper {
    const column = sql.identifier(rule.field);

    switch (rule.operator) {
      case Operators.EQUALS:
        return sql`${column} = ${rule.value}`;

      case Operators.NOT_EQUALS:
        return sql`${column} != ${rule.value}`;

      case Operators.GREATER:
        return sql`${column} > ${rule.value}`;

      case Operators.GREATER_EQUALS:
        return sql`${column} >= ${rule.value}`;

      case Operators.LESS:
        return sql`${column} < ${rule.value}`;

      case Operators.LESS_EQUALS:
        return sql`${column} <= ${rule.value}`;

      case Operators.CONTAINS:
        return sql`${column} ILIKE %${rule.value}%`;

      case Operators.NOT_CONTAINS:
        return sql`${column} NOT ILIKE %${rule.value}%`;

      case Operators.STARTS_WITH:
        return sql`${column} ILIKE ${rule.value}%`;

      case Operators.ENDS_WITH:
        return sql`${column} ILIKE %${rule.value}`;

      case Operators.LIKE:
        return sql`${column} LIKE ${rule.value}`;

      case Operators.ILIKE:
        return sql`${column} ILIKE ${rule.value}`;

      case Operators.IS_NULL:
        return sql`${column} IS NULL`;

      case Operators.IS_NOT_NULL:
        return sql`${column} IS NOT NULL`;

      case Operators.IN:
        if (!Array.isArray(rule.value)) {
          return sql`${column} = ANY(${rule.value})`;
        }
        return sql`${column} = ANY(ARRAY[${sql.join(rule.value)}])`;

      case Operators.NOT_IN:
        if (!Array.isArray(rule.value)) {
          return sql`${column} != ANY(${rule.value})`;
        }
        return sql`${column} != ANY(ARRAY[${sql.join(rule.value)}])`;

      case Operators.BETWEEN:
        if (!Array.isArray(rule.value) || rule.value.length !== 2) {
          throw new Error("Between operator requires two values");
        }
        return sql`${column} BETWEEN ${rule.value[0]} AND ${rule.value[1]}`;

      case Operators.NOT_BETWEEN:
        if (!Array.isArray(rule.value) || rule.value.length !== 2) {
          throw new Error("Between operator requires two values");
        }
        return sql`${column} NOT BETWEEN ${rule.value[0]} AND ${rule.value[1]}`;

      default:
        throw new Error(`Unknown operator: ${rule.operator}`);
    }
  }
}