Source

persistence/Adapter.ts

import {
  BaseError,
  InternalError,
  OperationKeys,
  BulkCrudOperationKeys,
  PrimaryKeyType,
} from "@decaf-ts/db-decorators";
import type { FlagsOf as ContextualFlagsOf } from "@decaf-ts/db-decorators";
import { type Observer } from "../interfaces/Observer";
import {
  hashObj,
  Model,
  ModelConstructor,
} from "@decaf-ts/decorator-validation";
import { SequenceOptions } from "../interfaces/SequenceOptions";
import { RawExecutor } from "../interfaces/RawExecutor";
import { DefaultAdapterFlags, PersistenceKeys } from "./constants";
import type { Repository } from "../repository/Repository";
import type { Sequence } from "./Sequence";
import { ErrorParser } from "../interfaces";
import { Statement } from "../query/Statement";
import { final, Logger } from "@decaf-ts/logging";
import type { Dispatch } from "./Dispatch";
import {
  type AdapterFlags,
  type EventIds,
  FlagsOf,
  Migration,
  type ObserverFilter,
  PersistenceObservable,
  PersistenceObserver,
  PreparedModel,
} from "./types";
import { ObserverHandler } from "./ObserverHandler";
import { Impersonatable, Logging } from "@decaf-ts/logging";
import { AdapterDispatch } from "./types";
import { Context } from "./Context";
import {
  Decoration,
  DefaultFlavour,
  Metadata,
  type Constructor,
} from "@decaf-ts/decoration";
import { MigrationError } from "./errors";
import {
  ContextualArgs,
  ContextualizedArgs,
  ContextualLoggedClass,
} from "../utils/ContextualLoggedClass";
import { Paginator } from "../query/Paginator";

const flavourResolver = Decoration["flavourResolver"].bind(Decoration);
Decoration["flavourResolver"] = (obj: object) => {
  try {
    const result = flavourResolver(obj);
    if (result && result !== DefaultFlavour) return result;
    const targetCtor =
      typeof obj === "function"
        ? (obj as Constructor)
        : ((obj as { constructor?: Constructor })?.constructor as
            | Constructor
            | undefined);
    const registeredFlavour =
      targetCtor && typeof Metadata["registeredFlavour"] === "function"
        ? Metadata.registeredFlavour(targetCtor)
        : undefined;
    if (registeredFlavour && registeredFlavour !== DefaultFlavour)
      return registeredFlavour;
    const currentFlavour = Adapter["_currentFlavour"];
    if (currentFlavour) {
      const cachedAdapter = Adapter["_cache"]?.[currentFlavour];
      if (cachedAdapter?.flavour) return cachedAdapter.flavour;
      return currentFlavour;
    }
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
  } catch (e: unknown) {
    return DefaultFlavour;
  }
};

export type AdapterSubClass<A> =
  A extends Adapter<any, any, any, any> ? A : never;

/**
 * @description Abstract Facade class for persistence adapters
 * @summary Provides the foundation for all database adapters in the persistence layer. This class
 * implements several interfaces to provide a consistent API for database operations, observer
 * pattern support, and error handling. It manages adapter registration, CRUD operations, and
 * observer notifications.
 * @template CONFIG - The underlying persistence driver config
 * @template QUERY - The query object type used by the adapter
 * @template FLAGS - The repository flags type
 * @template CONTEXT - The context type
 * @param {CONFIG} _config - The underlying persistence driver config
 * @param {string} flavour - The identifier for this adapter type
 * @param {string} [_alias] - Optional alternative name for this adapter
 * @class Adapter
 * @example
 * ```typescript
 * // Implementing a concrete adapter
 * class PostgresAdapter extends Adapter<pg.PoolConfig, pg.Query, PostgresFlags, PostgresContext> {
 *   constructor(client: pg.PoolConfig) {
 *     super(client, 'postgres');
 *   }
 *
 *   async initialize() {
 *     // Set up the adapter
 *     await this.native.connect();
 *   }
 *
 *   async create(tableName, id, model) {
 *     // Implementation for creating records
 *     const columns = Object.keys(model).join(', ');
 *     const values = Object.values(model);
 *     const placeholders = values.map((_, i) => `$${i+1}`).join(', ');
 *
 *     const query = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
 *     const result = await this.native.query(query, values);
 *     return result.rows[0];
 *   }
 *
 *   // Other required method implementations...
 * }
 *
 * // Using the adapter
 * const pgClient = new pg.Client(connectionString);
 * const adapter = new PostgresAdapter(pgClient);
 * await adapter.initialize();
 *
 * // Set as the default adapter
 * Adapter.setCurrent('postgres');
 *
 * // Perform operations
 * const user = await adapter.create('users', 1, { name: 'John', email: 'john@example.com' });
 * ```
 * @mermaid
 * classDiagram
 *   class Adapter {
 *     +Y native
 *     +string flavour
 *     +string alias
 *     +create(tableName, id, model)
 *     +read(tableName, id)
 *     +update(tableName, id, model)
 *     +delete(tableName, id)
 *     +observe(observer, filter)
 *     +unObserve(observer)
 *     +static current
 *     +static get(flavour)
 *     +static setCurrent(flavour)
 *   }
 *
 *   class RawExecutor {
 *     +raw(query)
 *   }
 *
 *   class Observable {
 *     +observe(observer, filter)
 *     +unObserve(observer)
 *     +updateObservers(table, event, id)
 *   }
 *
 *   class Observer {
 *     +refresh(table, event, id)
 *   }
 *
 *   class ErrorParser {
 *     +parseError(err)
 *   }
 *
 *   Adapter --|> RawExecutor
 *   Adapter --|> Observable
 *   Adapter --|> Observer
 *   Adapter --|> ErrorParser
 */
export abstract class Adapter<
    CONF,
    CONN,
    QUERY,
    CONTEXT extends Context<AdapterFlags> = Context,
  >
  extends ContextualLoggedClass<CONTEXT>
  implements
    RawExecutor<QUERY>,
    PersistenceObservable<CONTEXT>,
    PersistenceObserver<CONTEXT>,
    Impersonatable<any, [Partial<CONF>, ...any[]]>,
    ErrorParser
{
  private static _currentFlavour: string;
  private static _cache: Record<string, Adapter<any, any, any, any>> = {};
  private static _baseRepository: Constructor<Repository<any, any>>;
  private static _baseSequence: Constructor<Sequence>;
  private static _baseDispatch: Constructor<
    Dispatch<Adapter<any, any, any, any>>
  >;

  protected dispatch?: AdapterDispatch<typeof this>;

  protected readonly observerHandler?: ObserverHandler;

  protected _client?: CONN;

  /**
   * @description Gets the native persistence config
   * @summary Provides access to the underlying persistence driver config
   * @template CONF
   * @return {CONF} The native persistence driver config
   */
  get config(): CONF {
    return this._config;
  }

  /**
   * @description Gets the adapter's alias or flavor name
   * @summary Returns the alias if set, otherwise returns the flavor name
   * @return {string} The adapter's identifier
   */
  get alias(): string {
    return this._alias || this.flavour;
  }

  /**
   * @description Gets the repository constructor for this adapter
   * @summary Returns the constructor for creating repositories that work with this adapter
   * @template M - The model type
   * @return {Constructor<Repository<any, Adapter<CONF, CONN, QUERY, CONTEXT>>>} The repository constructor
   */
  repository<
    R extends Repository<any, Adapter<CONF, CONN, QUERY, CONTEXT>>,
  >(): Constructor<R> {
    if (!Adapter._baseRepository)
      throw new InternalError(
        `This should be overridden when necessary. Otherwise it will be replaced lazily`
      );
    return Adapter._baseRepository as Constructor<R>;
  }

  @final()
  protected async shutdownProxies(k?: string) {
    if (!this.proxies) return;
    if (k && !(k in this.proxies))
      throw new InternalError(`No proxy found for ${k}`);
    if (!k) {
      for (const key in this.proxies) {
        try {
          await this.proxies[key].shutdown();
        } catch (e: unknown) {
          this.log.error(`Failed to shutdown proxied adapter ${key}: ${e}`);
          continue;
        }
        delete this.proxies[key];
      }
    } else {
      try {
        await this.proxies[k].shutdown();
        delete this.proxies[k];
      } catch (e: unknown) {
        this.log.error(`Failed to shutdown proxied adapter ${k}: ${e}`);
      }
    }
  }

  /**
   * @description Shuts down the adapter
   * @summary Performs any necessary cleanup tasks, such as closing connections
   * When overriding this method, ensure to call the base method first
   * @return {Promise<void>} A promise that resolves when shutdown is complete
   */
  async shutdown(): Promise<void> {
    await this.shutdownProxies();
    if (this.dispatch) await this.dispatch.close();
  }

  /**
   * @description Creates a new adapter instance
   * @summary Initializes the adapter with the native driver and registers it in the adapter cache
   */
  protected constructor(
    private readonly _config: CONF,
    readonly flavour: string,
    private readonly _alias?: string
  ) {
    super();
    if (this.alias in Adapter._cache)
      throw new InternalError(
        `${this.alias} persistence adapter ${this._alias ? `(${this.flavour}) ` : ""} already registered`
      );
    Adapter._cache[this.alias] = this;
    this.log.info(
      `Created ${this.alias} persistence adapter ${this._alias ? `(${this.flavour}) ` : ""} persistence adapter`
    );
    if (!Adapter._currentFlavour) {
      this.log.verbose(`Defined ${this.alias} persistence adapter as current`);
      Adapter._currentFlavour = this.alias;
    }
  }

  /**
   * @description Creates a new statement builder for a model
   * @summary Returns a statement builder that can be used to construct queries for a specific model
   * @template M - The model type
   * @return {Statement} A statement builder for the model
   */
  abstract Statement<M extends Model>(): Statement<
    M,
    Adapter<CONF, CONN, QUERY, CONTEXT>,
    any
  >;

  abstract Paginator<M extends Model>(
    query: QUERY,
    size: number,
    clazz: Constructor<M>
  ): Paginator<M, any, QUERY>;

  /**
   * @description Creates a new dispatch instance
   * @summary Factory method that creates a dispatch instance for this adapter
   * @return {Dispatch} A new dispatch instance
   */
  protected Dispatch(): Dispatch<Adapter<CONF, CONN, QUERY, CONTEXT>> {
    return new Adapter._baseDispatch() as Dispatch<
      Adapter<CONF, CONN, QUERY, CONTEXT>
    >;
  }

  /**
   * @description Creates a new observer handler
   * @summary Factory method that creates an observer handler for this adapter
   * @return {ObserverHandler} A new observer handler instance
   */
  protected ObserverHandler(): ObserverHandler {
    return new ObserverHandler();
  }

  /**
   * @description Checks if an attribute name is reserved
   * @summary Determines if a given attribute name is reserved and cannot be used as a column name
   * @param {string} attr - The attribute name to check
   * @return {boolean} True if the attribute is reserved, false otherwise
   */
  protected isReserved(attr: string): boolean {
    return !attr;
  }

  /**
   * @description Parses a database error into a standardized error
   * @summary Converts database-specific errors into standardized application errors
   * @param {Error} err - The original database error
   * @param args
   * @return {BaseError} A standardized error
   */
  abstract parseError<E extends BaseError>(err: Error, ...args: any[]): E;

  /**
   * @description Initializes the adapter
   * @summary Performs any necessary setup for the adapter, such as establishing connections
   * @param {...any[]} args - Initialization arguments
   * @return {Promise<void>} A promise that resolves when initialization is complete
   */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  async initialize(...args: any[]): Promise<void> {}

  /**
   * @description Creates a sequence generator
   * @summary Factory method that creates a sequence generator for generating sequential values
   * @param {SequenceOptions} options - Configuration options for the sequence
   * @return {Promise<Sequence>} A promise that resolves to a new sequence instance
   */
  async Sequence(options: SequenceOptions): Promise<Sequence> {
    return new Adapter._baseSequence(options, this);
  }

  /**
   * @description Creates repository flags for an operation
   * @summary Generates a set of flags that describe a database operation, combining default flags with overrides
   * @template F - The Repository Flags type
   * @template M - The model type
   * @param {OperationKeys} operation - The type of operation being performed
   * @param {Constructor<M>} model - The model constructor
   * @param {Partial<F>} flags - Custom flag overrides
   * @param {...any[]} args - Additional arguments
   * @return {Promise<F>} The complete set of flags
   */
  protected async flags<M extends Model>(
    operation: OperationKeys | string,
    model: Constructor<M> | Constructor<M>[],
    flags: Partial<FlagsOf<CONTEXT>>,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    ...args: any[]
  ): Promise<FlagsOf<CONTEXT>> {
    let log = (flags.logger || Logging.for(this.toString())) as Logger;
    if (flags.correlationId)
      log = log.for({ correlationId: flags.correlationId });

    return Object.assign({}, DefaultAdapterFlags, flags, {
      affectedTables: (Array.isArray(model) ? model : [model]).map(
        Model.tableName
      ),
      writeOperation: operation !== OperationKeys.READ,
      timestamp: new Date(),
      operation: operation,
      ignoredValidationProperties: Array.isArray(model)
        ? []
        : Metadata.validationExceptions(model, operation as any),
      logger: log,
    }) as unknown as FlagsOf<CONTEXT>;
  }

  /**
   * @description The context constructor for this adapter
   * @summary Reference to the context class constructor used by this adapter
   */
  protected readonly Context: Constructor<CONTEXT> = Context<
    FlagsOf<CONTEXT>
  > as unknown as Constructor<CONTEXT>;

  /**
   * @description Creates a context for a database operation
   * @summary Generates a context object that describes a database operation, used for tracking and auditing
   * @template F - The Repository flags type
   * @template M - The model type
   * @param {OperationKeys.CREATE|OperationKeys.READ|OperationKeys.UPDATE|OperationKeys.DELETE} operation - The type of operation
   * @param {Partial<F>} overrides - Custom flag overrides
   * @param {Constructor<M>} model - The model constructor
   * @param {...any[]} args - Additional arguments
   * @return {Promise<C>} A promise that resolves to the context object
   */
  @final()
  async context<M extends Model>(
    operation:
      | OperationKeys.CREATE
      | OperationKeys.READ
      | OperationKeys.UPDATE
      | OperationKeys.DELETE
      | string,
    overrides: Partial<ContextualFlagsOf<CONTEXT>>,
    model: Constructor<M> | Constructor<M>[],
    ...args: any[]
  ): Promise<CONTEXT> {
    const log = this.log.for(this.context);
    log.debug(
      `Creating new context for ${operation} operation on ${Array.isArray(model) ? model.map((m) => m.name) : model.name} model with flag overrides: ${JSON.stringify(overrides)}`
    );
    const flags = await this.flags(
      operation,
      model,
      overrides as Partial<FlagsOf<CONTEXT>>,
      ...args
    );
    return new this.Context().accumulate(flags) as unknown as CONTEXT;
  }

  /**
   * @description Prepares a model for persistence
   * @summary Converts a model instance into a format suitable for database storage,
   * handling column mapping and separating transient properties
   * handling column mapping and separating transient properties
   * @template M - The model type
   * @param {M} model - The model instance to prepare
   * @param args - optional args for subclassing purposes
   * @return The prepared data
   */
  prepare<M extends Model>(
    model: M,
    ...args: ContextualArgs<CONTEXT>
  ): PreparedModel {
    const { log } = this.logCtx(args, this.prepare);
    const split = model.segregate();
    const result = Object.entries(split.model).reduce(
      (accum: Record<string, any>, [key, val]) => {
        if (typeof val === "undefined") return accum;
        const mappedProp: string = Model.columnName(
          model.constructor as Constructor<M>,
          key as keyof M
        );
        if (this.isReserved(mappedProp))
          throw new InternalError(`Property name ${mappedProp} is reserved`);
        accum[mappedProp] = val;
        return accum;
      },
      {}
    );
    if ((model as any)[PersistenceKeys.METADATA]) {
      // TODO movo to couchdb
      log.silly(
        `Passing along persistence metadata for ${(model as any)[PersistenceKeys.METADATA]}`
      );
      Object.defineProperty(result, PersistenceKeys.METADATA, {
        enumerable: false,
        writable: true,
        configurable: true,
        value: (model as any)[PersistenceKeys.METADATA],
      });
    }

    return {
      record: result,
      id: model[Model.pk(model.constructor as Constructor<M>)] as string,
      transient: split.transient,
    };
  }

  /**
   * @description Converts database data back into a model instance
   * @summary Reconstructs a model instance from database data, handling column mapping
   * and reattaching transient properties
   * @template M - The model type
   * @param obj - The database record
   * @param {Constructor<M>} clazz - The model class or name
   * @param pk - The primary key property name
   * @param {string|number|bigint} id - The primary key value
   * @param [transient] - Transient properties to reattach
   * @param [args] - options args for subclassing purposes
   * @return {M} The reconstructed model instance
   */
  revert<M extends Model>(
    obj: Record<string, any>,
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    transient?: Record<string, any>,
    ...args: ContextualArgs<CONTEXT>
  ): M {
    const { log, ctx } = this.logCtx(args, this.revert);
    const ob: Record<string, any> = {};
    const pk = Model.pk(clazz);
    ob[pk as string] = id;
    const m = new clazz(ob) as M;
    log.silly(`Rebuilding model ${m.constructor.name} id ${id}`);
    const metadata = obj[PersistenceKeys.METADATA]; // TODO move to couchdb
    const result = Object.keys(m).reduce((accum: M, key) => {
      if (key === pk) return accum;
      (accum as Record<string, any>)[key] =
        obj[Model.columnName(clazz, key as keyof M)];
      return accum;
    }, m);

    if (ctx.get("rebuildWithTransient") && transient) {
      log.verbose(
        `re-adding transient properties: ${Object.keys(transient).join(", ")}`
      );
      Object.entries(transient).forEach(([key, val]) => {
        if (key in result)
          throw new InternalError(
            `Transient property ${key} already exists on model ${m.constructor.name}. should be impossible`
          );
        result[key as keyof M] = val;
      });
    }

    if (metadata) {
      // TODO move to couchdb
      log.silly(
        `Passing along ${this.flavour} persistence metadata for ${m.constructor.name} id ${id}: ${metadata}`
      );
      Object.defineProperty(result, PersistenceKeys.METADATA, {
        enumerable: false,
        configurable: true,
        writable: true,
        value: metadata,
      });
    }

    return result;
  }

  /**
   * @description Creates a new record in the database
   * @summary Inserts a new record with the given ID and data into the specified table
   * @param {string} clazz - The name of the table to insert into
   * @param {PrimaryKeyType} id - The identifier for the new record
   * @param model - The data to insert
   * @param {any[]} args - Additional arguments specific to the adapter implementation
   * @return A promise that resolves to the created record
   */
  abstract create<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: ContextualArgs<CONTEXT>
  ): Promise<Record<string, any>>;

  /**
   * @description Creates multiple records in the database
   * @summary Inserts multiple records with the given IDs and data into the specified table
   * @param {string} tableName - The name of the table to insert into
   * @param id - The identifiers for the new records
   * @param model - The data to insert for each record
   * @param {...any[]} args - Additional arguments specific to the adapter implementation
   * @return A promise that resolves to an array of created records
   */
  async createAll<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType[],
    model: Record<string, any>[],
    ...args: ContextualArgs<CONTEXT>
  ): Promise<Record<string, any>[]> {
    if (id.length !== model.length)
      throw new InternalError("Ids and models must have the same length");
    const { log, ctxArgs } = this.logCtx(args, this.createAll);
    const tableLabel = Model.tableName(clazz);
    log.debug(`Creating ${id.length} entries ${tableLabel} table`);
    return Promise.all(
      id.map((i, count) => this.create(clazz, i, model[count], ...ctxArgs))
    );
  }

  /**
   * @description Retrieves a record from the database
   * @summary Fetches a record with the given ID from the specified table
   * @param {string} tableName - The name of the table to read from
   * @param {string|number|bigint} id - The identifier of the record to retrieve
   * @param {...any[]} args - Additional arguments specific to the adapter implementation
   * @return A promise that resolves to the retrieved record
   */
  abstract read<M extends Model>(
    tableName: Constructor<M>,
    id: PrimaryKeyType,
    ...args: ContextualArgs<CONTEXT>
  ): Promise<Record<string, any>>;

  /**
   * @description Retrieves multiple records from the database
   * @summary Fetches multiple records with the given IDs from the specified table
   * @param {string} tableName - The name of the table to read from
   * @param id - The identifiers of the records to retrieve
   * @param {...any[]} args - Additional arguments specific to the adapter implementation
   * @return A promise that resolves to an array of retrieved records
   */
  async readAll<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType[],
    ...args: ContextualArgs<CONTEXT>
  ): Promise<Record<string, any>[]> {
    const { log, ctxArgs } = this.logCtx(args, this.readAll);
    const tableName = Model.tableName(clazz);
    log.debug(`Reading ${id.length} entries ${tableName} table`);
    return Promise.all(id.map((i) => this.read(clazz, i, ...ctxArgs)));
  }

  /**
   * @description Updates a record in the database
   * @summary Modifies an existing record with the given ID in the specified table
   * @template M - The model type
   * @param {Constructor<M>} tableName - The name of the table to update
   * @param {PrimaryKeyType} id - The identifier of the record to update
   * @param  model - The new data for the record
   * @param {...any[]} args - Additional arguments specific to the adapter implementation
   * @return A promise that resolves to the updated record
   */
  abstract update<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: ContextualArgs<CONTEXT>
  ): Promise<Record<string, any>>;

  /**
   * @description Updates multiple records in the database
   * @summary Modifies multiple existing records with the given IDs in the specified table
   * @param {Constructor<M>} tableName - The name of the table to update
   * @param {string[]|number[]} id - The identifiers of the records to update
   * @param model - The new data for each record
   * @param {...any[]} args - Additional arguments specific to the adapter implementation
   * @return A promise that resolves to an array of updated records
   */
  async updateAll<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType[],
    model: Record<string, any>[],
    ...args: ContextualArgs<CONTEXT>
  ): Promise<Record<string, any>[]> {
    if (id.length !== model.length)
      throw new InternalError("Ids and models must have the same length");
    const { log, ctxArgs } = this.logCtx(args, this.updateAll);
    const tableLabel = Model.tableName(clazz);
    log.debug(`Updating ${id.length} entries ${tableLabel} table`);
    return Promise.all(
      id.map((i, count) => this.update(clazz, i, model[count], ...ctxArgs))
    );
  }

  /**
   * @description Deletes a record from the database
   * @summary Removes a record with the given ID from the specified table
   * @param {string} tableName - The name of the table to delete from
   * @param {string|number|bigint} id - The identifier of the record to delete
   * @param {...any[]} args - Additional arguments specific to the adapter implementation
   * @return A promise that resolves to the deleted record
   */
  abstract delete<M extends Model>(
    tableName: Constructor<M>,
    id: PrimaryKeyType,
    ...args: ContextualArgs<CONTEXT>
  ): Promise<Record<string, any>>;

  /**
   * @description Deletes multiple records from the database
   * @summary Removes multiple records with the given IDs from the specified table
   * @param {string} tableName - The name of the table to delete from
   * @param id - The identifiers of the records to delete
   * @param {...any[]} args - Additional arguments specific to the adapter implementation
   * @return A promise that resolves to an array of deleted records
   */
  async deleteAll<M extends Model>(
    tableName: Constructor<M>,
    id: PrimaryKeyType[],
    ...args: ContextualArgs<CONTEXT>
  ): Promise<Record<string, any>[]> {
    const { log, ctxArgs } = Adapter.logCtx<CONTEXT, ContextualArgs<CONTEXT>>(
      args,
      this.deleteAll
    );
    log.verbose(`Deleting ${id.length} entries from ${tableName} table`);
    return Promise.all(id.map((i) => this.delete(tableName, i, ...ctxArgs)));
  }

  /**
   * @description Executes a raw query against the database
   * @summary Allows executing database-specific queries directly
   * @template Q - The raw query type
   * @template R - The return type of the query
   * @param {Q} rawInput - The query to execute
   * @param {...any[]} args - Additional arguments specific to the adapter implementation
   * @return {Promise<R>} A promise that resolves to the query result
   */
  abstract raw<R>(
    rawInput: QUERY,
    ...args: ContextualArgs<CONTEXT>
  ): Promise<R>;

  /**
   * @description Registers an observer for database events
   * @summary Adds an observer to be notified about database changes. The observer can optionally
   * provide a filter function to receive only specific events.
   * @param {Observer} observer - The observer to register
   * @param {ObserverFilter} [filter] - Optional filter function to determine which events the observer receives
   * @return {void}
   */
  @final()
  observe(observer: Observer, filter?: ObserverFilter): void {
    if (!this.observerHandler)
      Object.defineProperty(this, "observerHandler", {
        value: this.ObserverHandler(),
        writable: false,
      });
    this.observerHandler!.observe(observer, filter);
    const log = this.log.for(this.observe);
    log.verbose(`Registering new observer ${observer.toString()}`);
    if (!this.dispatch) {
      log.info(`Creating dispatch for ${this.alias}`);
      this.dispatch = this.Dispatch();
      this.dispatch.observe(this);
    }
  }

  /**
   * @description Unregisters an observer
   * @summary Removes a previously registered observer so it no longer receives database event notifications
   * @param {Observer} observer - The observer to unregister
   * @return {void}
   */
  @final()
  unObserve(observer: Observer): void {
    if (!this.observerHandler)
      throw new InternalError(
        "ObserverHandler not initialized. Did you register any observables?"
      );
    this.observerHandler.unObserve(observer);
    this.log
      .for(this.unObserve)
      .verbose(`Observer ${observer.toString()} removed`);
  }

  /**
   * @description Notifies all observers about a database event
   * @summary Sends notifications to all registered observers about a change in the database,
   * filtering based on each observer's filter function
   * @param {string} table - The name of the table where the change occurred
   * @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of operation that occurred
   * @param {EventIds} id - The identifier(s) of the affected record(s)
   * @param {...any[]} args - Additional arguments to pass to the observers
   * @return {Promise<void>} A promise that resolves when all observers have been notified
   */
  async updateObservers<M extends Model>(
    table: Constructor<M> | string,
    event: OperationKeys | BulkCrudOperationKeys | string,
    id: EventIds,
    ...args: ContextualArgs<CONTEXT>
  ): Promise<void> {
    if (!this.observerHandler)
      throw new InternalError(
        "ObserverHandler not initialized. Did you register any observables?"
      );
    const { log, ctxArgs } = Adapter.logCtx<CONTEXT, ContextualArgs<CONTEXT>>(
      args,
      this.updateObservers
    );

    log.verbose(
      `Updating ${this.observerHandler.count()} observers for adapter ${this.alias}: Event: `
    );
    await this.observerHandler.updateObservers(table, event, id, ...ctxArgs);
  }

  /**
   * @description Refreshes data based on a database event
   * @summary Implementation of the Observer interface method that delegates to updateObservers
   * @param {string} table - The name of the table where the change occurred
   * @param {OperationKeys|BulkCrudOperationKeys|string} event - The type of operation that occurred
   * @param {EventIds} id - The identifier(s) of the affected record(s)
   * @param {...any[]} args - Additional arguments related to the event
   * @return {Promise<void>} A promise that resolves when the refresh is complete
   */
  async refresh<M extends Model>(
    table: Constructor<M> | string,
    event: OperationKeys | BulkCrudOperationKeys | string,
    id: EventIds,
    ...args: ContextualArgs<CONTEXT>
  ) {
    return this.updateObservers(table, event, id, ...args);
  }

  /**
   * @description Gets a string representation of the adapter
   * @summary Returns a human-readable string identifying this adapter
   * @return {string} A string representation of the adapter
   */
  override toString(): string {
    return `${this.flavour} adapter`;
  }

  /**
   * @description Gets the adapter flavor associated with a model
   * @summary Retrieves the adapter flavor that should be used for a specific model class
   * @template M - The model type
   * @param {Constructor<M>} model - The model constructor
   * @return {string} The adapter flavor name
   */
  static flavourOf<M extends Model>(model: Constructor<M>): string {
    return Metadata.flavourOf(model);
  }

  static get currentFlavour() {
    if (!Adapter._currentFlavour)
      throw new InternalError(
        `No persistence flavour set. Please initialize your adapter`
      );
    return Adapter._currentFlavour;
  }

  /**
   * @description Gets the current default adapter
   * @summary Retrieves the adapter that is currently set as the default for operations
   * @return {Adapter<any, any, any, any>} The current adapter
   */
  static get current(): Adapter<any, any, any, any> | undefined {
    return Adapter.get(this.currentFlavour);
  }

  /**
   * @description Gets an adapter by flavor
   * @summary Retrieves a registered adapter by its flavor name
   * @template CONF - The database driver config
   * @template CONN - The database driver instance
   * @template QUERY - The query type
   * @template CONTEXT - The context type
   * @param {string} flavour - The flavor name of the adapter to retrieve
   * @return {Adapter<CONF, CONN, QUERY, CONTEXT> | undefined} The adapter instance or undefined if not found
   */
  static get<A extends Adapter<any, any, any, any>>(
    flavour?: any
  ): A | undefined {
    if (!flavour) return Adapter.get(this._currentFlavour);
    if (flavour in this._cache) return this._cache[flavour] as A;
    throw new InternalError(`No Adapter registered under ${flavour}.`);
  }

  /**
   * @description Sets the current default adapter
   * @summary Changes which adapter is used as the default for operations
   * @param {string} flavour - The flavor name of the adapter to set as current
   * @return {void}
   */
  static setCurrent(flavour: string): void {
    this._currentFlavour = flavour;
  }

  /**
   * @description Gets all models associated with an adapter flavor
   * @summary Retrieves all model constructors that are configured to use a specific adapter flavor
   * @template M - The model type
   * @param {string} flavour - The adapter flavor to find models for
   * @return An array of model constructors
   */
  static models<M extends Model>(flavour: string): ModelConstructor<M>[] {
    try {
      return Metadata.flavouredAs(flavour) as ModelConstructor<M>[];
    } catch (e: any) {
      throw new InternalError(e);
    }
  }

  static decoration(): void {}

  /**
   * @description retrieves the context from args and returns it, the logger and the args (with context at the end)
   * @summary NOTE: if the last argument was a context, this removes the context from the arg list
   * @param args
   * @param method
   */
  static override logCtx<
    CONTEXT extends Context<any>,
    ARGS extends any[] = any[],
  >(this: any, args: ARGS, method: string): ContextualizedArgs<CONTEXT, ARGS>;
  static override logCtx<
    CONTEXT extends Context<any>,
    ARGS extends any[] = any[],
  >(
    this: any,
    args: ARGS,
    method: (...args: any[]) => any
  ): ContextualizedArgs<CONTEXT, ARGS>;
  static override logCtx<
    CONTEXT extends Context<any>,
    ARGS extends any[] = any[],
  >(
    this: any,
    args: ARGS,
    method: ((...args: any[]) => any) | string
  ): ContextualizedArgs<CONTEXT, ARGS> {
    return super.logCtx<CONTEXT, ARGS>(args, method as any);
  }

  protected proxies?: Record<string, typeof this>;

  /**
   * @description Returns the client instance for the adapter
   * @summary This method should be overridden by subclasses to return the client instance for the adapter.
   * @template CON - The type of the client instance
   * @return {CON} The client instance for the adapter
   * @abstract
   * @function getClient
   * @memberOf module:core
   * @instance
   * @protected
   */
  protected abstract getClient(): CONN;

  @final()
  get client(): CONN {
    if (!this._client) {
      this._client = this.getClient();
    }
    return this._client;
  }

  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  for(config: Partial<CONF>, ...args: any[]): this {
    if (!this.proxies) this.proxies = {};
    const key = `${this.alias} - ${hashObj(config)}`;
    if (key in this.proxies) return this.proxies[key] as typeof this;

    let client: any;
    const proxy = new Proxy(this, {
      get: (target: typeof this, p: string | symbol, receiver: any) => {
        if (p === "_config") {
          const originalConf: CONF = Reflect.get(target, p, receiver);
          return Object.assign({}, originalConf, config);
        }
        if (p === "_client") {
          return client;
        }
        return Reflect.get(target, p, receiver);
      },
      set: (target: any, p: string | symbol, value: any, receiver: any) => {
        if (p === "_client") {
          client = value;
          return true;
        }
        return Reflect.set(target, p, value, receiver);
      },
    });
    this.proxies[key] = proxy;
    return proxy as typeof this;
  }

  migrations() {
    return Metadata.migrationsFor(this);
  }

  protected async getQueryRunner(): Promise<CONN> {
    return this as unknown as CONN;
  }

  async migrate(...args: [...any[], CONTEXT]): Promise<void>;
  async migrate(
    migrations:
      | Constructor<Migration<any, any>>[]
      | CONTEXT = this.migrations(),
    ...args: [...any[], CONTEXT]
  ): Promise<void> {
    if (migrations instanceof Context) {
      args = [migrations as unknown as CONTEXT];
      migrations = this.migrations();
    }
    const { ctx } = Adapter.logCtx<CONTEXT>(args, this.migrate);
    const qr = await this.getQueryRunner();
    for (const migration of migrations) {
      try {
        const m = new migration();
        await m.up(qr, this, ctx);
        await m.down(qr, this, ctx);
      } catch (e: unknown) {
        throw new MigrationError(e as Error);
      }
    }
  }
}