Source

contracts/ContractAdapter.ts

import {
  CouchDBAdapter,
  CouchDBKeys,
  MangoQuery,
  ViewResponse,
  requireGeneratedUseIndex,
  ensureDeterministicSort as couchEnsureDeterministicSort,
  getSortDirection,
  getSortFields,
  warnScanProneMangoOperators,
} from "@decaf-ts/for-couchdb";
import { Model, ValidationKeys } from "@decaf-ts/decorator-validation";
import { createHash } from "crypto";
import { FabricContractFlags } from "./types";
import { FabricContractContext } from "./ContractContext";
import {
  BadRequestError,
  BaseError,
  BulkCrudOperationKeys,
  ConflictError,
  InternalError,
  NotFoundError,
  onCreate,
  onCreateUpdate,
  OperationKeys,
  PrimaryKeyType,
  SerializationError,
} from "@decaf-ts/db-decorators";
import {
  Context as Ctx,
  Object as FabricObject,
  Property,
  Property as FabricProperty,
} from "fabric-contract-api";
import { Logger, Logging } from "@decaf-ts/logging";
import {
  PersistenceKeys,
  RelationsMetadata,
  Sequence,
  SequenceOptions,
  UnsupportedError,
  Adapter,
  AdapterFlags,
  PreparedModel,
  Repository,
  QueryError,
  PagingError,
  MigrationError,
  ObserverError,
  AuthorizationError,
  ForbiddenError,
  ConnectionError,
  ContextualizedArgs,
  Context,
  RawResult,
  Paginator,
  ContextualArgs,
  MaybeContextualArg,
  MethodOrOperation,
  AllOperationKeys,
  FlagsOf,
  ContextOf,
  OrderDirection,
  TransactionOperationKeys,
  EventIds,
  Dispatch,
  promiseSequence,
  resolveBulkSequenceResult,
} from "@decaf-ts/core";
import { FabricContractRepository } from "./FabricContractRepository";
import {
  ChaincodeStub,
  ClientIdentity,
  Iterators,
  StateQueryResponse,
} from "fabric-shim-api";
import { FabricStatement } from "./FabricContractStatement";
import { FabricContractSequence } from "./FabricContractSequence";
import { FabricFlavour, FabricModelKeys } from "../shared/constants";
import { SimpleDeterministicSerializer } from "../shared/SimpleDeterministicSerializer";
import {
  apply,
  Constructor,
  Decoration,
  Metadata,
  propMetadata,
} from "@decaf-ts/decoration";
import { FabricContractPaginator } from "./FabricContractPaginator";
import { MissingContextError } from "../shared/errors";
import { SegregatedModel } from "../shared/index";
import { FabricContractDispatch } from "./FabricContractDispatch";

export type FabricContextualizedArgs<
  ARGS extends any[] = any[],
  EXTEND extends boolean = false,
> = ContextualizedArgs<FabricContractContext, ARGS, EXTEND> & {
  stub: ChaincodeStub;
  identity: ClientIdentity;
};

type PrivateSyntheticBookmark = {
  sortField: string;
  direction: "asc" | "desc";
  idField: string;
  lastValue: any;
  lastId: string;
  queryHash: string;
};

/**
 * @description Sets the creator or updater field in a model based on the user in the context
 * @summary Callback function used in decorators to automatically set the created_by or updated_by fields
 * with the username from the context when a document is created or updated
 * @template M - Type extending Model
 * @template R - Type extending NanoRepository<M>
 * @template V - Type extending RelationsMetadata
 * @param {R} this - The repository instance
 * @param {FabricContractContext} context - The operation context containing user information
 * @param {V} data - The relation metadata
 * @param {string} key - The property key to set with the username
 * @param {M} model - The model instance being created or updated
 * @return {Promise<void>} A promise that resolves when the operation is complete
 * @function createdByOnFabricCreateUpdate
 * @memberOf module:fabric.contracts
 * @mermaid
 * sequenceDiagram
 *   participant F as createdByOnNanoCreateUpdate
 *   participant C as Context
 *   participant M as Model
 *   F->>C: get("user")
 *   C-->>F: user object
 *   F->>M: set key to user.name
 *   Note over F: If no user in context
 *   F-->>F: throw UnsupportedError
 */
export async function createdByOnFabricCreateUpdate<
  M extends Model,
  R extends FabricContractRepository<M>,
  V extends RelationsMetadata,
>(
  this: R,
  context: ContextOf<R>,
  data: V,
  key: keyof M,
  model: M
): Promise<void> {
  try {
    const user = context.get("identity") as ClientIdentity;
    model[key] = user.getID() as M[typeof key];
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
  } catch (e: unknown) {
    throw new UnsupportedError(
      "No User found in context. Please provide a user in the context"
    );
  }
}

/**
 * @description Adapter for Hyperledger Fabric chaincode state database operations
 * @summary Provides a CouchDB-like interface for interacting with the Fabric state database from within a chaincode contract
 * @template void - No configuration needed for contract adapter
 * @template FabricContractFlags - Flags specific to Fabric contract operations
 * @template FabricContractContext - Context type for Fabric contract operations
 * @class FabricContractAdapter
 * @example
 * ```typescript
 * // In a Fabric chaincode contract class
 * import { FabricContractAdapter } from '@decaf-ts/for-fabric';
 *
 * export class MyContract extends Contract {
 *   private adapter = new FabricContractAdapter();
 *
 *   @Transaction()
 *   async createAsset(ctx: Context, id: string, data: string): Promise<void> {
 *     const model = { id, data, timestamp: Date.now() };
 *     await this.adapter.create('assets', id, model, {}, { stub: ctx.stub });
 *   }
 * }
 * ```
 * @mermaid
 * sequenceDiagram
 *   participant Contract
 *   participant FabricContractAdapter
 *   participant Stub
 *   participant StateDB
 *
 *   Contract->>FabricContractAdapter: create(tableName, id, model, transient, ctx)
 *   FabricContractAdapter->>FabricContractAdapter: Serialize model to JSON
 *   FabricContractAdapter->>Stub: putState(id, serializedData)
 *   Stub->>StateDB: Write data
 *   StateDB-->>Stub: Success
 *   Stub-->>FabricContractAdapter: Success
 *   FabricContractAdapter-->>Contract: model
 */
export class FabricContractAdapter extends CouchDBAdapter<
  any,
  void,
  FabricContractContext
> {
  private static readonly PRIVATE_BOOKMARK_PREFIX = "__dcf_pvtbm__";

  private static stableStringify(value: unknown): string {
    if (value === null || typeof value !== "object") {
      return JSON.stringify(value);
    }

    if (Array.isArray(value)) {
      return `[${value
        .map((v) => FabricContractAdapter.stableStringify(v))
        .join(",")}]`;
    }

    const obj = value as Record<string, unknown>;
    return `{${Object.keys(obj)
      .sort()
      .map(
        (key) =>
          `${JSON.stringify(key)}:${FabricContractAdapter.stableStringify(obj[key])}`
      )
      .join(",")}}`;
  }

  private static toBuffer(value: unknown): Buffer {
    if (Buffer.isBuffer(value)) {
      return value;
    }

    if (value instanceof Uint8Array) {
      return Buffer.from(value);
    }

    if (typeof value === "string") {
      return Buffer.from(value, "utf8");
    }

    if (value instanceof ArrayBuffer) {
      return Buffer.from(value);
    }

    if (
      value &&
      typeof value === "object" &&
      "buffer" in value &&
      (value as any).buffer instanceof ArrayBuffer
    ) {
      const typed = value as {
        buffer: ArrayBuffer;
        byteOffset?: number;
        byteLength?: number;
      };

      return Buffer.from(typed.buffer, typed.byteOffset || 0, typed.byteLength);
    }

    throw new SerializationError(
      `Cannot convert private query value to Buffer: ${Object.prototype.toString.call(
        value
      )}`
    );
  }

  private static privateQueryHash(query: Record<string, any>): string {
    const relevant = {
      selector: query.selector || {},
      sort: query.sort || [],
      fields: query.fields || undefined,
      use_index: query.use_index || undefined,
    };

    return createHash("sha256")
      .update(FabricContractAdapter.stableStringify(relevant))
      .digest("base64url");
  }

  private static attachGeneratedPrivateUseIndex(
    query: MangoQuery,
    log?: Logger
  ): void {
    const selector = query.selector as Record<string, any> | undefined;
    const tableName = selector?.[CouchDBKeys.TABLE];
    if (typeof tableName !== "string" || !tableName.length) {
      return;
    }

    let clazz: Constructor<Model> | undefined;
    try {
      clazz = Model.fromTable(tableName) as Constructor<Model>;
    } catch {
      return;
    }

    requireGeneratedUseIndex(clazz, query, log, {
      requireSortCoverage: true,
    });
  }

  private static countSelectorFieldOccurrences(
    selector: unknown,
    counts: Map<string, number> = new Map<string, number>()
  ): Map<string, number> {
    if (!selector || typeof selector !== "object") {
      return counts;
    }

    if (Array.isArray(selector)) {
      for (const entry of selector) {
        FabricContractAdapter.countSelectorFieldOccurrences(entry, counts);
      }
      return counts;
    }

    for (const [key, value] of Object.entries(
      selector as Record<string, any>
    )) {
      if (!["$and", "$or", "$not"].includes(key)) {
        counts.set(key, (counts.get(key) || 0) + 1);
      }

      if (value && typeof value === "object") {
        FabricContractAdapter.countSelectorFieldOccurrences(value, counts);
      }
    }

    return counts;
  }

  private static isNullLowerBoundMarker(value: unknown): boolean {
    if (!value || typeof value !== "object" || Array.isArray(value)) {
      return false;
    }

    const entries = Object.entries(value as Record<string, any>);
    if (entries.length !== 1) {
      return false;
    }

    const [operator, bound] = entries[0];
    return (operator === "$gt" || operator === "$gte") && bound === null;
  }

  private static normalizeMangoQueryForExecution(
    query: MangoQuery
  ): MangoQuery {
    const normalized: MangoQuery = {
      ...query,
      selector:
        query.selector && typeof query.selector === "object"
          ? { ...(query.selector as Record<string, any>) }
          : query.selector,
    };

    if (!normalized.selector || typeof normalized.selector !== "object") {
      return normalized;
    }

    const counts = FabricContractAdapter.countSelectorFieldOccurrences(
      normalized.selector
    );

    for (const [field, value] of Object.entries(
      normalized.selector as Record<string, any>
    )) {
      if (
        !FabricContractAdapter.isNullLowerBoundMarker(value) ||
        (counts.get(field) || 0) <= 1
      ) {
        continue;
      }

      delete (normalized.selector as Record<string, any>)[field];
    }

    return normalized;
  }

  private static buildPrivateContinuationSelector(
    selector: Record<string, any>,
    sortField: string,
    direction: "asc" | "desc",
    idField: string,
    lastValue: any,
    lastId: string,
    variant: "same" | "next"
  ): Record<string, any> {
    const cmp = direction === "desc" ? "$lt" : "$gt";
    const idCmp = direction === "desc" ? "$lt" : "$gt";

    if (idField === sortField) {
      const continuation = {
        [sortField]: {
          [cmp]: lastValue,
        },
      };

      if (!selector || !Object.keys(selector).length) {
        return continuation;
      }

      return {
        $and: [selector, continuation],
      };
    }

    const clauses =
      variant === "same"
        ? [
            {
              [sortField]: {
                $eq: lastValue,
              },
            },
            {
              [idField]: {
                [idCmp]: lastId,
              },
            },
          ]
        : [
            {
              [sortField]: {
                [cmp]: lastValue,
              },
            },
          ];

    if (!selector || !Object.keys(selector).length) {
      return clauses.length === 1
        ? clauses[0]
        : {
            $and: clauses,
          };
    }

    return {
      $and: [selector, ...clauses],
    };
  }

  private static buildOrlessKeysetContinuationQueries(
    query: MangoQuery,
    cursor: PrivateSyntheticBookmark,
    pageSize: number
  ): MangoQuery[] {
    const baseQuery: Record<string, any> = { ...query };
    delete baseQuery.skip;
    delete baseQuery.bookmark;
    delete baseQuery.limit;

    if (cursor.idField === cursor.sortField) {
      const single: MangoQuery = {
        ...baseQuery,
        selector: FabricContractAdapter.buildPrivateContinuationSelector(
          (baseQuery.selector || {}) as Record<string, any>,
          cursor.sortField,
          cursor.direction,
          cursor.idField,
          cursor.lastValue,
          cursor.lastId,
          "same"
        ) as any,
        limit: pageSize + 1,
      };

      return [single];
    }

    const first: MangoQuery = {
      ...baseQuery,
      selector: FabricContractAdapter.buildPrivateContinuationSelector(
        (baseQuery.selector || {}) as Record<string, any>,
        cursor.sortField,
        cursor.direction,
        cursor.idField,
        cursor.lastValue,
        cursor.lastId,
        "same"
      ) as any,
      limit: pageSize + 1,
    };

    const second: MangoQuery = {
      ...baseQuery,
      selector: FabricContractAdapter.buildPrivateContinuationSelector(
        (baseQuery.selector || {}) as Record<string, any>,
        cursor.sortField,
        cursor.direction,
        cursor.idField,
        cursor.lastValue,
        cursor.lastId,
        "next"
      ) as any,
      limit: pageSize + 1,
    };

    return [first, second];
  }

  private static ensureDeterministicPrivateSort(
    query: MangoQuery,
    tieBreaker: string
  ): {
    sortField: string;
    direction: "asc" | "desc";
    idField: string;
  } {
    if (!Array.isArray(query.sort) || !query.sort.length) {
      throw new PagingError(
        "Private Mango pagination requires an explicit sort. Add orderBy(...) so a stable generated index can be selected."
      );
    }

    const direction = (getSortDirection(query) ||
      OrderDirection.ASC) as OrderDirection;
    couchEnsureDeterministicSort(query, tieBreaker, direction);

    const sortFields = getSortFields(query);
    const sortField = sortFields[0];
    if (!sortField) {
      throw new PagingError(
        "Private Mango pagination requires a valid first sort field."
      );
    }

    if (!sortFields.includes(tieBreaker)) {
      throw new PagingError(
        `Private Mango pagination requires tie-breaker sort field "${tieBreaker}".`
      );
    }

    return {
      sortField,
      direction: direction as "asc" | "desc",
      idField: tieBreaker,
    };
  }

  private static parsePrivateResultValue(value: Buffer): Record<string, any> {
    try {
      const parsed = JSON.parse(value.toString("utf8"));

      if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
        throw new Error("Private query result is not a JSON object");
      }

      return parsed as Record<string, any>;
    } catch (e) {
      throw new SerializationError(
        `Failed to parse private query result while building bookmark: ${e}`
      );
    }
  }

  private static async executePrivateMangoPageQueries(
    stub: ChaincodeStub,
    collection: string,
    queries: MangoQuery[],
    pageSize: number,
    log: Logger
  ): Promise<Array<{ key: string; value: Buffer }>> {
    const paged: Array<{ key: string; value: Buffer }> = [];

    for (const q of queries) {
      if (paged.length >= pageSize + 1) {
        break;
      }

      const remaining = pageSize + 1 - paged.length;

      const queryForExecution: MangoQuery = {
        ...q,
        limit: remaining,
      };

      delete queryForExecution.skip;
      delete queryForExecution.bookmark;

      const queryJson = JSON.stringify(queryForExecution);

      log.debug(
        `Querying private collection ${collection} with Mango query: ${queryJson}`
      );

      let response: unknown;

      try {
        response = await stub.getPrivateDataQueryResult(collection, queryJson);
      } catch (e: unknown) {
        log.error(
          [
            `Private Mango paginated query failed`,
            `collection=${collection}`,
            `query=${queryJson}`,
            `error=${e instanceof Error ? e.stack || e.message : String(e)}`,
          ].join("\n")
        );

        throw e;
      }

      const iterator = ((response as any).iterator ||
        response) as Iterators.StateQueryIterator;

      if (!iterator || typeof iterator.next !== "function") {
        throw new QueryError(
          `Private paginated query on collection ${collection} did not return a valid iterator`
        );
      }

      try {
        while (paged.length < pageSize + 1) {
          const res = await iterator.next();

          if (res.done) {
            break;
          }

          if (!res.value || !res.value.value) {
            continue;
          }

          paged.push({
            key: res.value.key as string,
            value: FabricContractAdapter.toBuffer(res.value.value),
          });
        }
      } catch (e: unknown) {
        log.error(
          [
            `Failed while reading private Mango iterator`,
            `collection=${collection}`,
            `query=${queryJson}`,
            `error=${e instanceof Error ? e.stack || e.message : String(e)}`,
          ].join("\n")
        );

        throw e;
      } finally {
        try {
          await iterator.close();
        } catch (e: unknown) {
          log.error(
            [
              `Failed to close private Mango iterator`,
              `collection=${collection}`,
              `query=${queryJson}`,
              `error=${e instanceof Error ? e.stack || e.message : String(e)}`,
            ].join("\n")
          );

          // eslint-disable-next-line no-unsafe-finally
          throw e;
        }
      }
    }

    return paged;
  }

  private static toFabricPaginationResponse(
    docs: Array<{ key: string; value: Buffer }>,
    bookmark: string
  ): {
    iterator: Iterators.StateQueryIterator;
    metadata: {
      fetchedRecordsCount: number;
      bookmark: string;
    };
  } {
    let idx = 0;

    const iterator = {
      async next() {
        if (idx < docs.length) {
          return {
            value: docs[idx++],
            done: false,
          };
        }

        return {
          value: undefined as any,
          done: true,
        };
      },
      async close() {},
    };

    return {
      iterator: iterator as unknown as Iterators.StateQueryIterator,
      metadata: {
        fetchedRecordsCount: docs.length,
        bookmark,
      },
    };
  }

  private static parseSyntheticPrivateBookmark(
    bookmark: unknown
  ): PrivateSyntheticBookmark | undefined {
    if (typeof bookmark !== "string") return undefined;
    if (!bookmark.startsWith(FabricContractAdapter.PRIVATE_BOOKMARK_PREFIX))
      return undefined;
    const raw = bookmark.slice(
      FabricContractAdapter.PRIVATE_BOOKMARK_PREFIX.length
    );
    try {
      const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
      if (
        !parsed ||
        typeof parsed !== "object" ||
        typeof parsed.sortField !== "string" ||
        typeof parsed.direction !== "string" ||
        typeof parsed.idField !== "string" ||
        typeof parsed.lastId !== "string" ||
        typeof parsed.queryHash !== "string"
      ) {
        return undefined;
      }
      return {
        sortField: parsed.sortField,
        direction: parsed.direction === "desc" ? "desc" : "asc",
        idField: parsed.idField,
        lastValue: (parsed as any).lastValue,
        lastId: parsed.lastId,
        queryHash: parsed.queryHash,
      };
    } catch {
      return undefined;
    }
  }

  private static buildSyntheticPrivateBookmark(cursor: {
    sortField: string;
    direction: "asc" | "desc";
    idField: string;
    lastValue: any;
    lastId: string;
    queryHash: string;
  }): string {
    return `${FabricContractAdapter.PRIVATE_BOOKMARK_PREFIX}${Buffer.from(
      JSON.stringify(cursor)
    ).toString("base64url")}`;
  }

  protected override getClient(): void {
    throw new UnsupportedError("Client is not supported in Fabric contracts");
  }
  /**
   * @description Text decoder for converting binary data to strings
   */
  private static textDecoder = new TextDecoder("utf8");

  protected static readonly serializer = new SimpleDeterministicSerializer();

  /**
   * @description Context constructor for this adapter
   * @summary Overrides the base Context constructor with FabricContractContext
   */
  protected override get Context(): Constructor<FabricContractContext> {
    return FabricContractContext;
  }
  /**
   * @description Gets the repository constructor for this adapter
   * @summary Returns the FabricContractRepository constructor for creating repositories
   * @template M - Type extending Model
   * @return {Constructor<Repository<M, MangoQuery, FabricContractAdapter, FabricContractFlags, FabricContractContext>>} The repository constructor
   */
  override repository<
    R extends Repository<
      any,
      Adapter<any, void, MangoQuery, Context<FabricContractFlags>>
    >,
  >(): Constructor<R> {
    return FabricContractRepository as unknown as Constructor<R>;
  }

  override Paginator<M extends Model>(
    query: MangoQuery,
    size: number,
    clazz: Constructor<M>
  ): Paginator<M, any, MangoQuery> {
    return new FabricContractPaginator(this, query, size, clazz);
  }

  override async Sequence(options: SequenceOptions): Promise<Sequence> {
    return new FabricContractSequence(options, this as any);
  }

  protected override Dispatch(): Dispatch<FabricContractAdapter> {
    return new FabricContractDispatch();
  }

  /**
   * @description Creates a new FabricContractAdapter instance
   * @summary Initializes an adapter for interacting with the Fabric state database
   * @param {void} scope - Not used in this adapter
   * @param {string} [alias] - Optional alias for the adapter instance
   */
  constructor(scope: void, alias?: string) {
    super(scope, FabricFlavour, alias);
  }

  override for(config: Partial<any>, ...args: any): typeof this {
    return super.for(config, ...args);
  }

  protected getModelDefaults<M extends Model>(clazz: Constructor<M>) {
    const m = new clazz();
    return (Metadata.properties(clazz) || []).reduce(
      (acc, p) => {
        if (typeof m[p as keyof M] !== "undefined")
          acc[p as keyof M] = m[p as keyof M];
        return acc;
      },
      {} as Record<keyof M, any>
    );
  }

  /**
   * @description Creates a record in the state database
   * @summary Serializes a model and stores it in the Fabric state database
   * @param {string} tableName - The name of the table/collection
   * @param {string | number} id - The record identifier
   * @param {Record<string, any>} model - The record data
   * @param {Record<string, any>} transient - Transient data (not used in this implementation)
   * @param {...any[]} args - Additional arguments, including the chaincode stub and logger
   * @return {Promise<Record<string, any>>} Promise resolving to the created record
   */
  override async create<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: ContextualArgs<Context<FabricContractFlags>>
  ): Promise<Record<string, any>> {
    const { ctx, log } = this.logCtx(args, this.create);
    this.enforceMirrorAuthorization(clazz, ctx);
    log.info(`in ADAPTER create with args ${args}`);
    const tableName = Model.tableName(clazz);
    const composedKey = ctx.stub.createCompositeKey(tableName, [String(id)]);
    const mirrorCollection = ctx.getOrUndefined("mirrorCollection") as
      | string
      | undefined;
    const fullySegregated = ctx.isFullySegregated && !mirrorCollection;

    if (!mirrorCollection) {
      let existing: any;
      try {
        existing = await this.readState(composedKey, ctx);
      } catch (e: unknown) {
        // eslint-disable-next-line no-ex-assign
        e = this.parseError(e as Error);
        if (!(e instanceof NotFoundError)) throw e;
      }
      if (existing)
        throw new ConflictError(
          `record with id ${id} in table ${tableName} already exists`
        );
    }

    try {
      log.info(`adding entry to ${tableName} table with pk ${id}`);

      if (mirrorCollection) {
        model = await this.forPrivate(mirrorCollection).putState(
          composedKey,
          model,
          ctx
        );
      } else {
        const defaults = this.getModelDefaults(clazz);
        // handle public data if not fully segregated
        if (!fullySegregated) {
          if (
            Object.keys(model).filter((k) => {
              if (k === CouchDBKeys.TABLE) return false;
              return !(
                defaults &&
                k in defaults &&
                defaults[k as keyof M] === model[k]
              );
            }).length
          )
            model = await this.putState(composedKey, model, ctx);
        }

        // handle segregated writes
        const data = ctx.getFromChildren("segregatedData");
        if (data) {
          for (const collection in data) {
            Object.assign(
              model,
              await this.forPrivate(collection).putState(
                composedKey,
                data[collection][id as any],
                ctx
              )
            );
          }
        }
      }
    } catch (e: unknown) {
      throw this.parseError(e as Error);
    }

    return model;
  }

  override async createAll<M extends Model>(
    tableName: Constructor<M>,
    id: PrimaryKeyType[],
    model: Record<string, any>[],
    ...args: ContextualArgs<FabricContractContext>
  ): 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(tableName);
    log.debug(`Creating ${id.length} entries ${tableLabel} table`);
    return Promise.all(
      id.map((i, count) => this.create(tableName, i, model[count], ...ctxArgs))
    );
  }

  /**
   * @description Reads a record from the state database
   * @summary Retrieves and deserializes a record from the Fabric state database
   * @param {string} tableName - The name of the table/collection
   * @param {string | number} id - The record identifier
   * @param {...any[]} args - Additional arguments, including the chaincode stub and logger
   * @return {Promise<Record<string, any>>} Promise resolving to the retrieved record
   */
  override async read<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    ...args: ContextualArgs<Context<FabricContractFlags>>
  ): Promise<Record<string, any>> {
    const { ctx, log } = this.logCtx(args, this.read);
    log.info(`in ADAPTER read with args ${args}`);
    const tableName = Model.tableName(clazz);

    const composedKey = ctx.stub.createCompositeKey(tableName, [String(id)]);
    const mirrorCollection = ctx.getOrUndefined("mirrorCollection") as
      | string
      | undefined;
    const isMirror = ctx.getOrUndefined("mirror") as boolean | undefined;
    if (isMirror && mirrorCollection) {
      try {
        return await this.forPrivate(mirrorCollection).readState(
          composedKey,
          ctx
        );
      } catch (e: unknown) {
        throw this.parseError(e as Error);
      } finally {
        ctx.put("mirror" as any, undefined);
        ctx.put("mirrorCollection" as any, undefined);
      }
    }

    let model: Record<string, any>;

    try {
      model = ctx.isFullySegregated
        ? {}
        : await this.readState(composedKey, ctx);
    } catch (e: unknown) {
      throw this.parseError(e as Error);
    }

    const readCollections = new Set<string>([
      ...(ctx.getReadCollections() || []),
      ...(ctx.consumeReadCollections() || []),
    ]);
    for (const col of readCollections) {
      try {
        Object.assign(
          model,
          await this.forPrivate(col).readState(composedKey, ctx)
        );
      } catch (e: unknown) {
        const parsed = this.parseError(e as Error);
        if (parsed instanceof NotFoundError) continue;
        throw parsed;
      }
    }
    if (!Object.keys(model).length) {
      throw new NotFoundError(
        `record with id ${id} in table ${tableName} does not exist`
      );
    }
    return model;
  }

  /**
   * @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
   */
  override async readAll<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType[],
    ...args: ContextualArgs<Context<FabricContractFlags>>
  ): Promise<Record<string, any>[]> {
    const { log, ctx } = this.logCtx(args, this.readAll);
    const tableName = Model.tableName(clazz);
    log.debug(`Reading ${id.length} entries ${tableName} table`);
    const breakOnSingleFailure = ctx.get("breakOnSingleFailureInBulk") ?? true;
    const continueOnError = !breakOnSingleFailure;

    const mirrorCollection = ctx.getOrUndefined("mirrorCollection") as
      | string
      | undefined;
    const isMirror = ctx.getOrUndefined("mirror") as boolean | undefined;

    const readMirror = async <M extends Model>(
      clazz: Constructor<M>,
      id: PrimaryKeyType,
      ...args: ContextualArgs<Context<FabricContractFlags>>
    ) => {
      if (!mirrorCollection)
        throw new BadRequestError("Missing mirror collection for mirror read");
      try {
        const { ctx, log } = this.logCtx(args, readMirror);
        log.info(`in ADAPTER read with args ${args}`);
        const tableName = Model.tableName(clazz);

        const composedKey = ctx.stub.createCompositeKey(tableName, [
          String(id),
        ]);

        return await this.forPrivate(mirrorCollection).readState(
          composedKey,
          ctx
        );
      } catch (e: unknown) {
        throw this.parseError(e as Error);
      }
    };

    try {
      const tasks = id.map(
        (i) => () =>
          isMirror && mirrorCollection
            ? readMirror(
                clazz,
                i,
                ...args,
                (ctx as any).override({ noEmitSingle: true })
              )
            : this.read(
                clazz,
                i,
                ...args,
                (ctx as any).override({ noEmitSingle: true })
              )
      );

      const rawResult = continueOnError
        ? await promiseSequence(tasks, true)
        : await promiseSequence(tasks);
      return resolveBulkSequenceResult(
        rawResult,
        continueOnError,
        log,
        BulkCrudOperationKeys.READ_ALL
      );
    } catch (e) {
      throw this.parseError(e as Error);
    } finally {
      ctx.put("mirror" as any, undefined);
      ctx.put("mirrorCollection" as any, undefined);
    }
  }

  /**
   * @description Updates a record in the state database
   * @summary Serializes a model and updates it in the Fabric state database
   * @param {string} tableName - The name of the table/collection
   * @param {string | number} id - The record identifier
   * @param {Record<string, any>} model - The updated record data
   * @param {Record<string, any>} transient - Transient data (not used in this implementation)
   * @param {...any[]} args - Additional arguments, including the chaincode stub and logger
   * @return {Promise<Record<string, any>>} Promise resolving to the updated record
   */
  override async update<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: ContextualArgs<Context<FabricContractFlags>>
  ): Promise<Record<string, any>> {
    const { ctx, log } = this.logCtx(args, this.update);

    this.enforceMirrorAuthorization(clazz, ctx);
    log.info(`in ADAPTER update with args ${args}`);
    const tableName = Model.tableName(clazz);
    const composedKey = ctx.stub.createCompositeKey(tableName, [String(id)]);
    const mirrorCollection = ctx.getOrUndefined("mirrorCollection") as
      | string
      | undefined;

    try {
      log.info(`updating entry in ${tableName} table with pk ${id}`);

      if (mirrorCollection) {
        model = await this.forPrivate(mirrorCollection).putState(
          composedKey,
          model,
          ctx
        );
      } else {
        const defaults = this.getModelDefaults(clazz);
        // handle public data
        if (
          Object.keys(model).filter((k) => {
            if (k === CouchDBKeys.TABLE) return false;
            return !(
              defaults &&
              k in defaults &&
              defaults[k as keyof M] === model[k]
            );
          }).length
        )
          model = await this.putState(composedKey, model, ctx);

        // handle segregated writes
        const data = ctx.getFromChildren("segregatedData");
        if (data) {
          for (const collection in data) {
            Object.assign(
              model,
              await this.forPrivate(collection).putState(
                composedKey,
                data[collection][id as any],
                ctx
              )
            );
          }
        }
      }
    } catch (e: unknown) {
      throw this.parseError(e as Error);
    }

    return model;
  }

  /**
   * @description Deletes a record from the state database
   * @summary Retrieves a record and then removes it from the Fabric state database
   * @param {string} tableName - The name of the table/collection
   * @param {string | number} id - The record identifier to delete
   * @param {...any[]} args - Additional arguments, including the chaincode stub and logger
   * @return {Promise<Record<string, any>>} Promise resolving to the deleted record
   */
  async delete<M extends Model>(
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    ...args: ContextualArgs<Context<FabricContractFlags>>
  ): Promise<Record<string, any>> {
    const { ctx, log } = this.logCtx(args, this.delete);

    this.enforceMirrorAuthorization(clazz, ctx);
    const tableName = Model.tableName(clazz);

    const composedKey = ctx.stub.createCompositeKey(tableName, [String(id)]);
    const mirrorCollection = ctx.getOrUndefined("mirrorCollection") as
      | string
      | undefined;
    let model: Record<string, any>;

    if (mirrorCollection) {
      try {
        model = await this.forPrivate(mirrorCollection).readState(
          composedKey,
          ctx
        );
        await this.forPrivate(mirrorCollection).deleteState(composedKey, ctx);
      } catch (e: unknown) {
        throw this.parseError(e as Error);
      }
    } else {
      try {
        model = ctx.isFullySegregated
          ? {}
          : await this.readState(composedKey, ctx);
        if (!ctx.isFullySegregated) {
          log.debug(`Deleting entry ${composedKey} from public ledger`);
          await this.deleteState(composedKey, ctx);
        }
      } catch (e: unknown) {
        throw this.parseError(e as Error);
      }

      const collections = ctx.getReadCollections();
      if (collections && collections.length) {
        for (const col of collections) {
          log.debug(
            `ensuring collection ${col} has entry for id ${composedKey} before deleting`
          );
          Object.assign(
            model,
            await this.forPrivate(col).readState(composedKey, ctx)
          );
          log.debug(
            `Deleting private collection ${col} entry for id ${composedKey}`
          );
          await this.forPrivate(col).deleteState(composedKey, ctx);
        }
      }
    }
    return model;
  }

  protected async deleteState(id: string, context: FabricContractContext) {
    await context.stub.deleteState(id);
  }

  forPrivate(collection: string): FabricContractAdapter {
    const toOverride = [
      this.putState,
      this.readState,
      this.deleteState,
      this.queryResult,
      this.queryResultPaginated,
    ].map((fn) => fn.name);
    return new Proxy(this, {
      get(target, prop, receiver) {
        if (!toOverride.includes(prop as string))
          return Reflect.get(target, prop, receiver);
        return new Proxy((target as any)[prop], {
          async apply(fn, thisArg, argsList) {
            switch (prop) {
              case "putState": {
                // putState signature: (id: string, model: Record<string, any>, ctx: FabricContractContext)
                const [id, model, ctx] = argsList;
                const log = ctx.logger.for(prop);
                log.debug(
                  `Private write to ${collection} for ${id.toString()}`
                );
                const data = Buffer.from(
                  FabricContractAdapter.serializer.serialize(
                    model as Model,
                    false
                  )
                );
                await ctx.stub.putPrivateData(collection, id.toString(), data);
                return model;
              }
              case "deleteState": {
                // deleteState signature: (id: string, context: FabricContractContext)
                const [id, ctx] = argsList;
                const log = ctx.logger.for(prop);
                log.debug(
                  `Private delete on ${collection} for ${id.toString()}`
                );
                await ctx.stub.deletePrivateData(collection, id.toString());
                return;
              }
              case "readState": {
                // readState signature: (id: string, ctx: FabricContractContext)
                const [id, ctx] = argsList;
                const log = ctx.logger.for(prop);
                log.debug(
                  `Private read from ${collection} for ${id.toString()}`
                );
                const data = await ctx.stub.getPrivateData(collection, id);
                if (!data || !data.toString().length)
                  throw new NotFoundError(`Record with id ${id} not found`);
                try {
                  return FabricContractAdapter.serializer.deserialize(
                    data.toString("utf8")
                  );
                } catch {
                  return data.toString("utf8");
                }
              }
              case "queryResult": {
                const [stub, rawInput, ...args] = argsList;
                const { log } = thisArg["logCtx"](args, prop);

                const normalizedInput =
                  FabricContractAdapter.normalizeMangoQueryForExecution(
                    rawInput
                  );

                const queryJson = JSON.stringify(normalizedInput);

                log.debug(
                  `Querying private collection ${collection} with Mango query: ${queryJson}`
                );

                try {
                  const res = await (
                    stub as ChaincodeStub
                  ).getPrivateDataQueryResult(collection, queryJson);

                  const iterator = (res as any).iterator || res;

                  if (!iterator || typeof iterator.next !== "function") {
                    throw new QueryError(
                      `Private query on collection ${collection} did not return a valid iterator`
                    );
                  }

                  return iterator;
                } catch (e: unknown) {
                  log.error(
                    [
                      `Private Mango query failed`,
                      `collection=${collection}`,
                      `query=${queryJson}`,
                      `error=${e instanceof Error ? e.stack || e.message : String(e)}`,
                    ].join("\n")
                  );

                  throw thisArg.parseError ? thisArg.parseError(e as Error) : e;
                }
              }
              case "queryResultPaginated": {
                const [stub, rawInput, limit, skip, bookmark, ...args] =
                  argsList;
                const { log, ctx } = thisArg["logCtx"](args, prop);
                if (skip !== undefined && skip !== null && Number(skip) > 0) {
                  throw new PagingError(
                    "Private Mango pagination does not support skip/offset pagination. Use the returned synthetic bookmark instead."
                  );
                }

                const pageSize = Math.max(1, Number(limit) || 250);
                const query = { ...rawInput } as MangoQuery;
                const queryInput = query as Record<string, any>;
                const queryPkField =
                  typeof queryInput["__pkField"] === "string" &&
                  queryInput["__pkField"].trim().length
                    ? String(queryInput["__pkField"])
                    : String(
                        ctx?.getOrUndefined("privatePaginationTieBreaker") ||
                          "id"
                      );
                delete queryInput["__pkField"];

                const syntheticCursor =
                  FabricContractAdapter.parseSyntheticPrivateBookmark(bookmark);

                if (
                  bookmark !== undefined &&
                  bookmark !== null &&
                  bookmark !== "" &&
                  !syntheticCursor
                ) {
                  throw new PagingError(
                    "Private Mango pagination only supports adapter-generated synthetic bookmarks."
                  );
                }

                if (!Array.isArray(query.sort) || !query.sort.length) {
                  throw new PagingError(
                    "Private Mango pagination requires an explicit sort. Add orderBy(...) so a stable generated index can be selected."
                  );
                }

                const {
                  sortField,
                  direction: sortDirection,
                  idField,
                } = FabricContractAdapter.ensureDeterministicPrivateSort(
                  query,
                  queryPkField
                );

                warnScanProneMangoOperators(query.selector || {}, log);

                const normalizedQuery =
                  FabricContractAdapter.normalizeMangoQueryForExecution(query);
                FabricContractAdapter.attachGeneratedPrivateUseIndex(
                  normalizedQuery,
                  log
                );

                log.debug(
                  `Private paginated query input collection=${collection} limit=${limit} skip=${skip} bookmark=${bookmark} sortField=${sortField} direction=${sortDirection} synthetic=${Boolean(
                    syntheticCursor
                  )}`
                );

                normalizedQuery.limit = pageSize + 1;

                const queryHash =
                  FabricContractAdapter.privateQueryHash(normalizedQuery);
                if (syntheticCursor) {
                  if (syntheticCursor.queryHash !== queryHash) {
                    throw new PagingError(
                      "Private Mango bookmark does not match the current query."
                    );
                  }
                }

                const queries = syntheticCursor
                  ? FabricContractAdapter.buildOrlessKeysetContinuationQueries(
                      normalizedQuery,
                      syntheticCursor,
                      pageSize
                    )
                  : [{ ...normalizedQuery, limit: pageSize + 1 } as MangoQuery];

                const paged =
                  await FabricContractAdapter.executePrivateMangoPageQueries(
                    stub as ChaincodeStub,
                    collection,
                    queries,
                    pageSize,
                    log
                  );

                const hasMore = paged.length > pageSize;
                const docs = hasMore ? paged.slice(0, pageSize) : paged;

                let nextBookmark = "";
                if (hasMore && docs.length) {
                  const last = docs[docs.length - 1];
                  const lastDoc = FabricContractAdapter.parsePrivateResultValue(
                    last.value
                  );

                  if (!(sortField in lastDoc)) {
                    throw new PagingError(
                      `Cannot build private pagination bookmark: sorted field "${sortField}" is missing from the last result`
                    );
                  }

                  const lastIdValue = lastDoc[idField];
                  if (lastIdValue === undefined || lastIdValue === null) {
                    throw new PagingError(
                      `Cannot build private pagination bookmark: id field "${idField}" is missing from the last result`
                    );
                  }

                  nextBookmark =
                    FabricContractAdapter.buildSyntheticPrivateBookmark({
                      sortField,
                      direction: sortDirection,
                      idField,
                      lastValue: lastDoc[sortField],
                      lastId: String(lastIdValue),
                      queryHash,
                    });
                }

                return FabricContractAdapter.toFabricPaginationResponse(
                  docs,
                  nextBookmark
                );
              }
              default:
                throw new InternalError(
                  `Unsupported method override ${String(prop)}`
                );
            }
          },
        });
      },
    });
  }

  protected async putState(
    id: string,
    model: Record<string, any>,
    ctx: FabricContractContext
  ) {
    let data: Buffer;

    try {
      data = Buffer.from(
        FabricContractAdapter.serializer.serialize(model as Model, false)
      );
    } catch (e: unknown) {
      throw new SerializationError(
        `Failed to serialize record with id ${id}: ${e}`
      );
    }
    await ctx.stub.putState(id.toString(), data);

    return model;
  }

  protected async readState(id: string, ctx: FabricContractContext) {
    let result: any;

    const { log } = this.logCtx([ctx], this.readState);
    const res = (await ctx.stub.getState(id.toString())).toString();
    if (!res) throw new NotFoundError(`Record with id ${id} not found`);
    log.silly(`state retrieved under id ${id}`);
    try {
      result = FabricContractAdapter.serializer.deserialize(res.toString());
    } catch (e: unknown) {
      throw new SerializationError(`Failed to parse record: ${e}`);
    }

    return result;
  }

  protected async queryResult(
    stub: ChaincodeStub,
    rawInput: any,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    ...args: ContextualArgs<FabricContractContext>
  ): Promise<Iterators.StateQueryIterator> {
    const normalizedInput =
      FabricContractAdapter.normalizeMangoQueryForExecution(rawInput);
    FabricContractAdapter.attachGeneratedPrivateUseIndex(normalizedInput);
    return stub.getQueryResult(JSON.stringify(normalizedInput));
  }

  protected async queryResultPaginated(
    stub: ChaincodeStub,
    rawInput: any,
    limit: number = 250,
    page?: number,
    bookmark?: string | number,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    ...args: any[]
  ): Promise<StateQueryResponse<Iterators.StateQueryIterator>> {
    const normalizedInput =
      FabricContractAdapter.normalizeMangoQueryForExecution(rawInput);
    FabricContractAdapter.attachGeneratedPrivateUseIndex(normalizedInput);
    return stub.getQueryResultWithPagination(
      JSON.stringify(normalizedInput),
      limit,
      bookmark?.toString()
    );
  }

  /**
   * @description Decodes binary data to string
   * @summary Converts a Uint8Array to a string using UTF-8 encoding
   * @param {Uint8Array} buffer - The binary data to decode
   * @return {string} The decoded string
   */
  protected decode(buffer: Uint8Array) {
    return FabricContractAdapter.textDecoder.decode(buffer);
  }

  /**
   * @description Creates operation flags for Fabric contract operations
   * @summary Merges default flags with Fabric-specific context information
   * @template M - Type extending Model
   * @param {OperationKeys} operation - The operation being performed
   * @param {Constructor<M>} model - The model constructor
   * @param {Partial<FabricContractFlags>} flags - Partial flags to merge with defaults
   * @param {Ctx} ctx - The Fabric chaincode context
   * @return {FabricContractFlags} The merged flags
   */
  protected override async flags<M extends Model>(
    operation: AllOperationKeys,
    model: Constructor<M> | undefined,
    flags: Partial<FabricContractFlags> | FabricContractContext | Ctx | any
  ): Promise<FabricContractFlags> {
    let baseFlags = {
      segregated: false,
      rebuildWithTransient: false,
      fullySegregated: false,
      strictPrivateMangoPagination: true,
      privatePaginationTieBreaker: "id",
    };
    if (flags instanceof FabricContractContext || flags instanceof Context) {
      flags = flags.toOverrides();
    }

    baseFlags = Object.assign(baseFlags, flags);
    const stubFromFlags =
      (flags as FabricContractContext).stub || (flags as Ctx).stub;
    const identityFromFlags =
      (flags as FabricContractContext).identity ||
      (flags as Ctx).clientIdentity;
    if (stubFromFlags && identityFromFlags) {
      const txId = stubFromFlags.getTxID();
      Object.assign(baseFlags, {
        stub: stubFromFlags,
        identity: identityFromFlags,
        cert: identityFromFlags.getIDBytes().toString(),
        roles: identityFromFlags.getAttributeValue("roles"),
        logger: Logging.for(
          operation,
          {
            logLevel: false,
            timestamp: false,
            correlationId: txId,
          },
          flags
        ),
        correlationId: txId,
      });
    } else {
      baseFlags = Object.assign(baseFlags, flags || {});
    }

    return (await super.flags(
      operation,
      model,
      baseFlags as any
    )) as FabricContractFlags;
  }

  /**
   * @description Creates an index for a model
   * @summary This method is not implemented for Fabric contracts and returns a resolved promise
   * @template M - Type extending Model
   * @param {Constructor<M>} models - The model constructor
   * @return {Promise<void>} Promise that resolves immediately
   */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  protected index<M>(models: Constructor<M>): Promise<void> {
    return Promise.resolve(undefined);
  }

  /**
   * @description Processes results from a state query iterator
   * @summary Iterates through query results and converts them to a structured format
   * @param {Logger} log - Logger instance for debugging
   * @param {Iterators.StateQueryIterator} iterator - The state query iterator
   * @param {boolean} [isHistory=false] - Whether this is a history query
   * @return {Promise<any[]>} Promise resolving to an array of processed results
   * @mermaid
   * sequenceDiagram
   *   participant Caller
   *   participant ResultIterator
   *   participant Iterator
   *
   *   Caller->>ResultIterator: resultIterator(log, iterator, isHistory)
   *   loop Until done
   *     ResultIterator->>Iterator: next()
   *     Iterator-->>ResultIterator: { value, done }
   *     alt Has value
   *       ResultIterator->>ResultIterator: Process value based on isHistory
   *       ResultIterator->>ResultIterator: Add to results array
   *     end
   *   end
   *   ResultIterator->>Iterator: close()
   *   ResultIterator-->>Caller: allResults
   */
  protected async resultIterator(
    log: Logger,
    iterator: Iterators.StateQueryIterator,
    isHistory = false
  ) {
    const allResults = [];
    let res: { value: any; done: boolean } = await iterator.next();
    while (!res.done) {
      if (res.value && res.value.value.toString()) {
        let jsonRes: any = {};
        log.debug(res.value.value.toString("utf8"));
        if (isHistory /* && isHistory === true*/) {
          jsonRes.TxId = res.value.txId;
          jsonRes.Timestamp = res.value.timestamp;
          try {
            jsonRes.Value = JSON.parse(res.value.value.toString("utf8"));
          } catch (err: any) {
            log.error(err);
            jsonRes.Value = res.value.value.toString("utf8");
          }
        } else {
          try {
            jsonRes = JSON.parse(res.value.value.toString("utf8"));
          } catch (err: any) {
            log.error(err);
            jsonRes = res.value.value.toString("utf8");
          }
        }
        allResults.push(jsonRes);
      }
      res = await iterator.next();
    }
    log.debug(`Closing iterator after ${allResults.length} results`);
    await iterator.close();
    return allResults;
  }

  /**
   * @description Executes a raw query against the state database
   * @summary Performs a rich query using CouchDB syntax against the Fabric state database
   * @template R - The return type
   * @param {MangoQuery} rawInput - The Mango Query to execute
   * @param {boolean} docsOnly - Whether to return only documents (not used in this implementation)
   * @param {...any[]} args - Additional arguments, including the chaincode stub and logger
   * @return {Promise<R>} Promise resolving to the query results
   * @mermaid
   * sequenceDiagram
   *   participant Caller
   *   participant FabricContractAdapter
   *   participant Stub
   *   participant StateDB
   *
   *   Caller->>FabricContractAdapter: raw(rawInput, docsOnly, ctx)
   *   FabricContractAdapter->>FabricContractAdapter: Extract limit and skip
   *   alt With pagination
   *     FabricContractAdapter->>Stub: getQueryResultWithPagination(query, limit, skip)
   *   else Without pagination
   *     FabricContractAdapter->>Stub: getQueryResult(query)
   *   end
   *   Stub->>StateDB: Execute query
   *   StateDB-->>Stub: Iterator
   *   Stub-->>FabricContractAdapter: Iterator
   *   FabricContractAdapter->>FabricContractAdapter: resultIterator(log, iterator)
   *   FabricContractAdapter-->>Caller: results
   */
  async raw<R, D extends boolean>(
    rawInput: MangoQuery,
    docsOnly: D = true as D,
    ...args: ContextualArgs<FabricContractContext>
  ): Promise<RawResult<R, D>> {
    const { log, ctx, ctxArgs } = this.logCtx(args, this.raw);

    const enableSegregates = !args.length || args[0] !== true;
    const fullySegregated = enableSegregates && ctx.isFullySegregated;

    const originalInput = { ...(rawInput as Record<string, any>) };
    const { skip, limit } = originalInput;
    const bookmark = originalInput["bookmark"];
    const hasSort =
      Array.isArray((originalInput as Record<string, any>).sort) &&
      (originalInput as Record<string, any>).sort.length > 0;
    const pkField =
      typeof originalInput["__pkField"] === "string" &&
      originalInput["__pkField"].trim().length
        ? String(originalInput["__pkField"])
        : String(ctx?.getOrUndefined("privatePaginationTieBreaker") || "id");
    const hasSkip = skip !== undefined && skip !== null && Number(skip) > 0;
    const hasBookmark =
      bookmark !== undefined && bookmark !== null && bookmark !== "";
    const paginationActive = Boolean(limit || hasSkip || hasBookmark);
    const shouldPaginate = Boolean(
      paginationActive &&
        (enableSegregates || hasSkip || hasBookmark || hasSort)
    );
    const baseInput: Record<string, any> = { ...originalInput };
    delete baseInput["skip"];
    delete baseInput["bookmark"];
    delete baseInput["__pkField"];
    let resp = { docs: [], bookmark: undefined as string | undefined };
    log.debug(
      `raw query start fullySegregated=${fullySegregated} enableSegregates=${enableSegregates} paginationActive=${paginationActive} limit=${limit} skip=${skip} bookmark=${bookmark} pkField=${pkField} query=${JSON.stringify(
        originalInput
      )}`
    );

    // Query public state only when the model is NOT fully segregated
    if (!fullySegregated) {
      let iterator: Iterators.StateQueryIterator;
      if (shouldPaginate) {
        const paginatedInput: Record<string, any> = { ...baseInput };
        delete paginatedInput["limit"];
        log.debug(
          `Retrieving public paginated iterator: limit: ${limit}/ skip: ${skip} bookmark=${bookmark}`
        );
        const response: StateQueryResponse<Iterators.StateQueryIterator> =
          (await this.queryResultPaginated(
            ctx.stub,
            paginatedInput,
            limit || 250,
            (skip as any)?.toString(),
            bookmark,
            ...[ctx as FabricContractContext]
          )) as StateQueryResponse<Iterators.StateQueryIterator>;
        resp.bookmark = response.metadata.bookmark;
        iterator = response.iterator;
        log.debug(`Retrieved public paging iterator`);
        log.debug(
          `public paginated response bookmark=${resp.bookmark} query=${JSON.stringify(
            paginatedInput
          )}`
        );
      } else {
        log.debug("Retrieving listing public iterator");
        iterator = (await this.queryResult(
          ctx.stub,
          { ...baseInput },
          ctx
        )) as Iterators.StateQueryIterator;
      }
      log.debug(`Retrieved public listing iterator`);

      resp.docs = (await this.resultIterator(log, iterator)) as any;
      log.debug(
        `returning ${Array.isArray(resp.docs) ? resp.docs.length : 1} results`
      );
    } else {
      log.debug("Skipping public state query (fully segregated model)");
    }

    const collections = enableSegregates ? ctx.getReadCollections() : undefined;
    log.debug(
      `read collections for raw query: ${JSON.stringify(collections || [])}`
    );

    if (collections && collections.length) {
      // Build a fresh input with limit/skip/bookmark restored
      const segregatedInput = { ...baseInput };
      if (limit) segregatedInput.limit = limit;
      if (skip) segregatedInput.skip = skip;
      if (bookmark) segregatedInput["bookmark"] = bookmark;
      log.debug(
        `segregated input prepared: ${JSON.stringify(segregatedInput)}`
      );

      const segregated: any[] = [];
      for (const collection of collections) {
        log.debug(`Querying from ${collection}`);
        const fromCols = await this.forPrivate(collection).raw(
          { ...segregatedInput } as MangoQuery,
          false,
          true,
          ...ctxArgs
        );
        log.verbose(
          `received ${(fromCols as unknown as any[]).length} from ${collection}`
        );
        segregated.push(fromCols);
      }
      // Prefer the response with the most results so mixed and fully segregated
      // reads return the collection that actually matched the query.
      resp = segregated.reduce((acc, curr) => {
        if (!acc) return curr;
        if (curr.docs && curr.docs.length >= acc?.docs.length) return curr;
        return acc;
      }, resp);
      log.debug(
        `segregated query resolved docs=${Array.isArray(resp.docs) ? resp.docs.length : 0} bookmark=${resp.bookmark}`
      );
    }

    if (docsOnly) {
      log.debug(
        `raw query returning docsOnly=${docsOnly} docs=${Array.isArray(resp.docs) ? resp.docs.length : 0}`
      );
      return resp.docs as any;
    }
    log.debug(
      `raw query returning docs/bookmark docs=${Array.isArray(resp.docs) ? resp.docs.length : 0} bookmark=${resp.bookmark}`
    );
    return resp as any;
  }

  /**
   * @description Lists all records of a model using range queries (deterministic, write-safe)
   * @summary Iterates records via `getStateByPartialCompositeKey` (public) and
   * `getPrivateDataByPartialCompositeKey` (private/shared), following the same collection-routing
   * logic as `raw()` but using Fabric range queries instead of Mango selectors.
   * Unlike `raw()`, this method is safe to use in the same transaction as writes because
   * range queries are deterministic and do not block subsequent `putState`/`putPrivateData` calls.
   * @template M - Type extending Model
   * @param {Constructor<M>} clazz - The model constructor
   * @param {string[]} [attributes=[]] - Partial composite key attributes to filter by (empty = all records)
   * @param {...ContextualArgs<FabricContractContext>} args - Contextual arguments including the chaincode context
   * @return {Promise<M[]>} Promise resolving to an array of reconstructed model instances
   */
  async rangeList<M extends Model>(
    clazz: Constructor<M>,
    attributes: string[] = [],
    ...args: ContextualArgs<FabricContractContext>
  ): Promise<M[]> {
    const { ctx, log } = this.logCtx(args, this.rangeList);
    const tableName = Model.tableName(clazz);
    const fullySegregated = ctx.isFullySegregated;

    const deserializeEntry = (raw: Uint8Array): Record<string, any> | undefined => {
      const str = Buffer.from(raw).toString("utf8");
      if (!str) return undefined;
      try {
        return FabricContractAdapter.serializer.deserialize(str) as Record<string, any>;
      } catch {
        try {
          return JSON.parse(str) as Record<string, any>;
        } catch {
          return undefined;
        }
      }
    };

    // Collect raw data keyed by composite key, merging public + private in order (same as read())
    const rawByKey = new Map<string, Record<string, any>>();

    if (!fullySegregated) {
      log.debug(`rangeList public: objectType=${tableName} attributes=${JSON.stringify(attributes)}`);
      const iterator = await ctx.stub.getStateByPartialCompositeKey(tableName, attributes);
      let next = await iterator.next();
      while (!next.done) {
        if (next.value?.value) {
          const data = deserializeEntry(next.value.value);
          if (data) rawByKey.set(next.value.key, data);
        }
        next = await iterator.next();
      }
      await iterator.close();
      log.debug(`rangeList public: ${rawByKey.size} entries`);
    }

    const collections = ctx.getReadCollections();
    if (collections?.length) {
      for (const collection of collections) {
        log.debug(`rangeList private: collection=${collection} objectType=${tableName} attributes=${JSON.stringify(attributes)}`);
        const iterator = await ctx.stub.getPrivateDataByPartialCompositeKey(collection, tableName, attributes);
        let count = 0;
        let next = await iterator.next();
        while (!next.done) {
          if (next.value?.value) {
            const data = deserializeEntry(next.value.value);
            if (data) {
              const existing = rawByKey.get(next.value.key);
              if (existing) {
                Object.assign(existing, data);
              } else {
                rawByKey.set(next.value.key, data);
              }
              count++;
            }
          }
          next = await iterator.next();
        }
        await iterator.close();
        log.debug(`rangeList private: collection=${collection} ${count} entries`);
      }
    }

    const results: M[] = [];
    for (const [compositeKey, data] of rawByKey) {
      try {
        const { attributes: keyParts } = ctx.stub.splitCompositeKey(compositeKey);
        const id = keyParts[0];
        if (!id) continue;
        results.push(this.revert(data, clazz, id, undefined, ctx));
      } catch (e: unknown) {
        log.warn(`rangeList: skipping entry ${compositeKey}: ${e instanceof Error ? e.message : e}`);
      }
    }

    log.debug(`rangeList: returning ${results.length} ${clazz.name} instances`);
    return results;
  }

  async view<R>(
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    ddoc: string,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    viewName: string,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    options: Record<string, any>,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    ..._args: ContextualArgs<FabricContractContext>
  ): Promise<ViewResponse<R>> {
    throw new UnsupportedError(
      "Fabric contracts do not support CouchDB views."
    );
  }

  override Statement<M extends Model>(
    overrides?: Partial<AdapterFlags>
  ): FabricStatement<M, any> {
    return new FabricStatement(this as any, overrides);
  }

  override async updateAll<M extends Model>(
    tableName: Constructor<M>,
    id: PrimaryKeyType[],
    model: Record<string, any>[],
    ...args: ContextualArgs<FabricContractContext>
  ): 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(tableName);
    log.debug(`Updating ${id.length} entries ${tableLabel} table`);
    return Promise.all(
      id.map((i, count) => this.update(tableName, i, model[count], ...ctxArgs))
    );
  }

  /**
   *
   * @param model
   * @param {string} pk
   * @param args
   */
  override prepare<M extends Model>(
    model: M,
    ...args: ContextualArgs<FabricContractContext>
  ): PreparedModel & {
    segregated?: Record<string, Record<string, any>>;
  } {
    const { log, ctx } = this.logCtx(args, this.prepare);
    const split: SegregatedModel<M> = Model.segregate(model);
    const tableName = Model.tableName(model.constructor as any);
    const pk = Model.pk(model.constructor as any);
    const id = model[pk as keyof M];

    const isMirror = ctx.getOrUndefined("mirror") as boolean | undefined;

    const mapToRecord = function (
      this: FabricContractAdapter,
      obj: Record<string, any>,
      keysOverride?: string[]
    ) {
      if (keysOverride)
        keysOverride = [...new Set([...keysOverride, pk as string])];
      const result = Object.entries(obj).reduce(
        (accum: Record<string, any>, [key, val]) => {
          if (typeof val === "undefined") return accum;
          if (keysOverride && !keysOverride.includes(key)) return accum;
          const mappedProp = Model.columnName(model, key as any);
          if (this.isReserved(mappedProp))
            throw new InternalError(`Property name ${mappedProp} is reserved`);
          val = val instanceof Date ? new Date(val) : val;
          accum[mappedProp] = val;
          return accum;
        },
        {}
      );
      if (Object.keys(result).filter((k) => Boolean(result[k])).length) {
        // Add table identifier
        result[CouchDBKeys.TABLE] = tableName;
      }
      return result;
    }.bind(this);

    log.silly(
      `Preparing record for ${tableName} table with pk ${(model as any)[pk]}`
    );

    const segregatedWriteKeys = ctx.getSegregatedWrites();
    const segregatedWrites: Record<string, any> = {};
    // Only apply segregated writes when the current model actually has a transient split.
    // The same FabricContractContext can be reused across nested operations (for example
    // sequence upserts during persistent version generation). In those cases, we must not
    // attempt to apply parent-model segregated write keys to unrelated models.
    if (
      segregatedWriteKeys &&
      split.transient &&
      typeof split.transient === "object"
    ) {
      for (const collection in segregatedWriteKeys) {
        segregatedWrites[collection] = segregatedWrites[collection] || {};
        segregatedWrites[collection][id as any] = mapToRecord(
          ctx.getOrUndefined("forceSegregateWrite")
            ? split.model
            : (split.transient as any) || {},
          segregatedWriteKeys[collection]
        );
      }
    }

    // In mirror mode, the record should contain ALL model properties (full copy)
    const record = isMirror ? mapToRecord(model) : mapToRecord(split.model);

    return {
      record,
      id: (model as any)[pk] as string,
      transient:
        !isMirror && split.transient && Object.keys(split.transient).length
          ? mapToRecord(split.transient)
          : undefined,
      segregated: isMirror ? undefined : segregatedWrites,
    };
  }

  override revert<M extends Model>(
    obj: Record<string, any>,
    clazz: Constructor<M>,
    id: PrimaryKeyType,
    transient?: Record<string, any>,
    ...args: ContextualArgs<FabricContractContext>
  ): M {
    const { log, ctx } = this.logCtx(args, this.revert);
    const ob: Record<string, any> = {};
    const pk = Model.pk(clazz);
    const pkProps = Model.pkProps(clazz);
    if (pkProps?.type === Number && typeof id === "string") {
      id = Number(id);
    }
    ob[pk as string] = id;
    log.silly(`Rebuilding model ${clazz.name} id ${id}`);

    function mapToModel(r: Record<any, any>) {
      const m = (
        typeof clazz === "string" ? Model.build(ob, clazz) : new clazz(ob)
      ) as M;
      const attributes = Model.getAttributes(clazz);
      const keys = attributes.length ? attributes : Object.keys(m);
      return keys
        .filter((k) => k !== (pk as string))
        .reduce((accum: M, key) => {
          (accum as Record<string, any>)[key] =
            r[Model.columnName(accum, key as any)];
          return accum;
        }, m);
    }

    let result = mapToModel(obj);

    if (transient && !this.shouldRebuildWithTransient(ctx)) {
      log.debug(
        `filtering transient properties: ${Object.keys(transient).join(", ")}`
      );
      result = Object.entries(result).reduce((acc, [key, v]) => {
        if (key === pk || !(key in transient)) {
          acc[key as keyof M] = v;
        }
        return acc;
      }, new clazz());
    }

    return result;
  }

  private shouldRebuildWithTransient(ctx: FabricContractContext): boolean {
    if (!ctx) return false;
    if (ctx.getOrUndefined("rebuildWithTransient")) return true;
    const operation = ctx.getOrUndefined("operation") as string | undefined;
    if (!operation) return true;
    const op = operation.toString().toLowerCase();
    return !TransactionOperationKeys.map((k) => k.toLowerCase()).includes(op);
  }

  private getContextMsp(context: FabricContractContext): string | undefined {
    const identity = context.get("identity") as
      | string
      | ClientIdentity
      | undefined;
    if (!identity) return undefined;
    if (typeof identity === "string") return identity;
    try {
      return identity.getMSPID();
    } catch {
      return undefined;
    }
  }

  private enforceMirrorAuthorization<M extends Model>(
    clazz: Constructor<M>,
    ctx: FabricContractContext
  ): void {
    const mirrorMeta = Model.mirroredAt(clazz);
    if (!mirrorMeta) return;
    const msp = this.getContextMsp(ctx);
    if (!msp) return;
    if (
      msp === mirrorMeta.mspId ||
      (mirrorMeta.condition && mirrorMeta.condition(msp))
    ) {
      throw new AuthorizationError(
        `Organization ${msp} is not authorized to modify mirrored data`
      );
    }
  }

  override createPrefix<M extends Model>(
    tableName: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: MaybeContextualArg<FabricContractContext>
  ) {
    const { ctxArgs } = this.logCtx(args, this.createPrefix);
    const record: Record<string, any> = {};
    record[CouchDBKeys.TABLE] = Model.tableName(tableName);
    Object.assign(record, model);

    return [tableName, id, record, ...ctxArgs] as [
      Constructor<M>,
      PrimaryKeyType,
      Record<string, any>,
      ...any[],
      FabricContractContext,
    ];
  }

  override updatePrefix<M extends Model>(
    tableName: Constructor<M>,
    id: PrimaryKeyType,
    model: Record<string, any>,
    ...args: MaybeContextualArg<FabricContractContext>
  ): any[] {
    const { ctxArgs } = this.logCtx(args, this.updatePrefix);
    const record: Record<string, any> = {};
    record[CouchDBKeys.TABLE] = Model.tableName(tableName);
    Object.assign(record, model);

    return [tableName, id, record, ...ctxArgs] as [
      Constructor<M>,
      PrimaryKeyType,
      Record<string, any>,
      ...any[],
      FabricContractContext,
    ];
  }

  protected override createAllPrefix<M extends Model>(
    tableName: Constructor<M>,
    ids: PrimaryKeyType[],
    models: Record<string, any>[],
    ...args: [...any, FabricContractContext]
  ): (string | string[] | number[] | Record<string, any>[])[] {
    if (ids.length !== models.length)
      throw new InternalError("Ids and models must have the same length");

    const ctx: FabricContractContext = args.pop();

    const records = ids.map((id, count) => {
      const record: Record<string, any> = {};
      record[CouchDBKeys.TABLE] = Model.tableName(tableName);
      Object.assign(record, models[count]);
      return record;
    });
    return [tableName, ids, records, ctx as any];
  }

  protected override updateAllPrefix<M extends Model>(
    tableName: Constructor<M>,
    ids: PrimaryKeyType[],
    models: Record<string, any>[],
    ...args: [...any, FabricContractContext]
  ) {
    if (ids.length !== models.length)
      throw new InternalError("Ids and models must have the same length");

    const ctx: FabricContractContext = args.pop();

    const records = ids.map((id, count) => {
      const record: Record<string, any> = {};
      record[CouchDBKeys.TABLE] = Model.tableName(tableName);
      Object.assign(record, models[count]);
      return record;
    });
    return [tableName, ids, records, ctx as any];
  }

  override parseError<E extends BaseError>(
    err: Error | string,
    reason?: string
  ): E {
    return FabricContractAdapter.parseError(reason || err);
  }

  protected override logCtx<
    ARGS extends any[] = any[],
    METHOD extends MethodOrOperation = MethodOrOperation,
  >(
    args: MaybeContextualArg<FabricContractContext, ARGS>,
    operation: METHOD
  ): FabricContextualizedArgs<ARGS, METHOD extends string ? true : false>;
  protected override logCtx<
    ARGS extends any[] = any[],
    METHOD extends MethodOrOperation = MethodOrOperation,
  >(
    args: MaybeContextualArg<FabricContractContext, ARGS>,
    operation: METHOD,
    allowCreate: false
  ): FabricContextualizedArgs<ARGS, METHOD extends string ? true : false>;
  protected override logCtx<
    ARGS extends any[] = any[],
    METHOD extends MethodOrOperation = MethodOrOperation,
  >(
    args: MaybeContextualArg<FabricContractContext, ARGS>,
    operation: METHOD,
    allowCreate: true,
    overrides?: Partial<FlagsOf<FabricContractContext>>
  ): Promise<
    FabricContextualizedArgs<ARGS, METHOD extends string ? true : false>
  >;
  protected override logCtx<
    ARGS extends any[] = any[],
    METHOD extends MethodOrOperation = MethodOrOperation,
  >(
    args: MaybeContextualArg<FabricContractContext, ARGS>,
    operation: METHOD,
    allowCreate: boolean = false,
    overrides?: Partial<FlagsOf<FabricContractContext>> | Ctx
  ):
    | Promise<
        FabricContextualizedArgs<ARGS, METHOD extends string ? true : false>
      >
    | FabricContextualizedArgs<ARGS, METHOD extends string ? true : false> {
    const ctx = args[args.length - 1];
    let mergedOverrides = overrides as
      | Partial<FlagsOf<FabricContractContext>>
      | Ctx
      | undefined;

    if (
      ctx instanceof FabricContractContext &&
      ctx.getOrUndefined("allowContextTransientMap") &&
      ctx.getOrUndefined("stub")
    ) {
      const transientOverrides = this.readTransientOverrides(ctx);
      if (transientOverrides) {
        mergedOverrides = Object.assign(
          {},
          overrides || {},
          transientOverrides
        );
      }
    }

    if (!allowCreate)
      return super.logCtx<ARGS, METHOD>(
        args,
        operation as any,
        allowCreate as any,
        mergedOverrides as any
      ) as any;

    return super.logCtx
      .call(this, args, operation as any, allowCreate, mergedOverrides as any)
      .then((res) => {
        if (!(res.ctx instanceof FabricContractContext))
          throw new InternalError(`Invalid context binding`);
        return Object.assign(res, {
          ...(res.ctx.getOrUndefined("stub")
            ? { stub: res.ctx.getOrUndefined("stub") }
            : {}),
          ...(res.ctx.getOrUndefined("identity")
            ? { identity: res.ctx.getOrUndefined("identity") }
            : {}),
        });
      }) as any;
  }

  private readTransientOverrides(
    ctx: FabricContractContext
  ): Partial<FlagsOf<FabricContractContext>> | undefined {
    if (!ctx.getOrUndefined("allowContextTransientMap")) return;
    const stub = ctx.getOrUndefined("stub") as ChaincodeStub | undefined;
    if (!stub) return undefined;
    const transientMap = stub.getTransient();
    if (!transientMap.has(FabricModelKeys.OVERRIDES)) return undefined;
    const raw = transientMap.get(FabricModelKeys.OVERRIDES);
    if (!raw) return undefined;
    return JSON.parse(Buffer.from(raw).toString("utf8"));
  }

  override async updateObservers(
    table: Constructor<any> | string,
    event: OperationKeys | BulkCrudOperationKeys | string,
    id: EventIds,
    ...args: ContextualArgs<FabricContractContext>
  ): Promise<void> {
    if (!this.observerHandler)
      throw new InternalError(
        "ObserverHandler not initialized. Did you register any observables?"
      );
    const { ctx, ctxArgs } = this.logCtx(args, this.updateObservers);
    if (ctx.isFullySegregated) return;
    if (ctx.getOrUndefined("noEmit")) return;
    if (!ctx.stub) return;
    const isBulk = Array.isArray(id);
    const emitSingle = !ctx.getOrUndefined("noEmitSingle");
    const emitBulk = !ctx.getOrUndefined("noEmitBulk");
    if ((isBulk && emitBulk) || (!isBulk && emitSingle)) {
      // Pass the MSP owner from stub and the result payload (if any) in the
      // positions the handler expects: (table, event, id, owner?, payload?, ctx)
      const mspId = ctx.stub.getMspID ? ctx.stub.getMspID() : undefined;
      const nonCtxArgs = ctxArgs.slice(0, -1); // everything before ctx
      const payload = nonCtxArgs.length > 0 ? nonCtxArgs[0] : undefined;
      await this.observerHandler.updateObservers(
        table,
        event,
        id,
        mspId,
        payload,
        ctx
      );
    }
  }

  static override parseError<E extends BaseError>(err: Error | string): E {
    // if (
    //   MISSING_PRIVATE_DATA_REGEX.test(
    //     typeof err === "string" ? err : err.message
    //   )
    // )
    //   return new UnauthorizedPrivateDataAccess(err) as E;
    const msg = typeof err === "string" ? err : err.message;
    if (msg.includes(NotFoundError.name)) return new NotFoundError(err) as E;
    if (msg.includes(ConflictError.name)) return new ConflictError(err) as E;
    if (msg.includes(BadRequestError.name))
      return new BadRequestError(err) as E;
    if (msg.includes(QueryError.name)) return new QueryError(err) as E;
    if (msg.includes(PagingError.name)) return new PagingError(err) as E;
    if (msg.includes(UnsupportedError.name))
      return new UnsupportedError(err) as E;
    if (msg.includes(MigrationError.name)) return new MigrationError(err) as E;
    if (msg.includes(ObserverError.name)) return new ObserverError(err) as E;
    if (msg.includes(AuthorizationError.name))
      return new AuthorizationError(err) as E;
    if (msg.includes(ForbiddenError.name)) return new ForbiddenError(err) as E;
    if (msg.includes(ConnectionError.name))
      return new ConnectionError(err) as E;
    if (msg.includes(SerializationError.name))
      return new SerializationError(err) as E;
    if (msg.includes("no ledger context"))
      return new MissingContextError(
        `No context found. this can be caused by debugging: ${msg}`
      ) as E;

    return new InternalError(err) as E;
  }

  /**
   * @description Static method for decoration overrides
   * @summary Overrides/extends decaf decoration with Fabric-specific functionality
   * @static
   * @override
   * @return {void}
   */
  static override decoration(): void {
    super.decoration();
    Decoration.flavouredAs(FabricFlavour)
      .for(PersistenceKeys.CREATED_BY)
      .define({
        decorator: function createdBy() {
          return apply(
            onCreate(createdByOnFabricCreateUpdate),
            propMetadata(PersistenceKeys.CREATED_BY, {})
          );
        },
      } as any)
      .apply();

    Decoration.flavouredAs(FabricFlavour)
      .for(PersistenceKeys.UPDATED_BY)
      .define({
        decorator: function createdBy() {
          return apply(
            onCreateUpdate(createdByOnFabricCreateUpdate),
            propMetadata(PersistenceKeys.UPDATED_BY, {})
          );
        },
      } as any)
      .apply();

    Decoration.flavouredAs(FabricFlavour)
      .for(PersistenceKeys.COLUMN)
      .extend(FabricProperty())
      .apply();

    Decoration.flavouredAs(FabricFlavour)
      .for(ValidationKeys.DATE)
      .extend(function fabricProperty() {
        return (target: any, prop?: any) => {
          Property(prop, "string:date")(target, prop);
        };
      });

    Decoration.flavouredAs(FabricFlavour)
      .for(PersistenceKeys.TABLE)
      .extend(function table(obj: any) {
        const chain: any[] = [];
        let current =
          typeof obj === "function"
            ? Metadata.constr(obj)
            : Metadata.constr(obj.constructor);

        while (current && current !== Object && current.prototype) {
          chain.push(current);
          current = Object.getPrototypeOf(current);
        }

        console.log(chain.map((c) => c.name || c));

        // Apply from the base class down to the decorated class
        while (chain.length > 0) {
          const constructor = chain.pop();
          console.log(`Calling on ${constructor.name}`);
          FabricObject()(constructor);
        }

        return FabricObject()(obj);
      })
      .apply();
  }
}

FabricContractAdapter.decoration();
Adapter.setCurrent(FabricFlavour);