Source

shared/decorators.ts

import {
  AuthorizationError,
  Repo,
  Context,
  UnsupportedError,
  Repository,
  ContextOf,
} from "@decaf-ts/core";
import {
  generated,
  InternalError,
  NotFoundError,
  onCreate,
  onDelete,
  onRead,
  onUpdate,
  readonly,
  transient,
  ValidationError,
  DBKeys,
  DBOperations,
  on,
} from "@decaf-ts/db-decorators";
import { Model, required } from "@decaf-ts/decorator-validation";
import { FabricModelKeys } from "./constants";
import type { Context as HLContext } from "fabric-contract-api";
import { ClientIdentity } from "fabric-shim-api";
import { FabricERC20Contract } from "../contracts/erc20/erc20contract";
import {
  apply,
  Constructor,
  Decoration,
  metadata,
  Metadata,
  prop,
  propMetadata,
} from "@decaf-ts/decoration";
import { FabricFlags } from "./types";
import { toPascalCase } from "@decaf-ts/logging";
import { FabricContractFlags } from "../contracts/types";
import "../shared/overrides";
import { type FabricContractContext } from "../contracts/ContractContext";

/**
 * @description Extracts the MSP ID from either a string or ClientIdentity object
 * @param identity - The identity value which can be a string MSP ID or ClientIdentity object
 * @returns The MSP ID as a string, or undefined if not available
 */
export function extractMspId(
  identity: string | ClientIdentity | undefined
): string | undefined {
  if (!identity) return undefined;
  if (typeof identity === "string") return identity;
  return identity.getMSPID();
}

/**
 * Decorator for marking methods that require ownership authorization.
 * Checks the owner of the token before allowing the method to be executed.
 *
 * @example
 * ```typescript
 * class TokenContract extends Contract {
 *   @Owner()
 *   async Mint(ctx: Context, amount: number) {
 *     // Mint token logic
 *   }
 * }
 * ```
 *
 * @returns {MethodDecorator} A method decorator that checks ownership authorization.
 */
export function Owner() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (
      this: FabricERC20Contract,
      ...args: any[]
    ) {
      const ctx: HLContext = args[0];
      const acountId = ctx.clientIdentity.getID();

      const select = await (this as FabricERC20Contract)[
        "tokenRepository"
      ].select();

      const tokens = await select.execute(ctx);

      if (tokens.length == 0) {
        throw new NotFoundError("No tokens avaialble");
      }

      if (tokens.length > 1) {
        throw new NotFoundError(`To many token available : ${tokens.length}`);
      }

      if (tokens[0].owner != acountId) {
        throw new AuthorizationError(
          `User not authorized to run ${propertyKey} on the token`
        );
      }

      return await originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

export async function ownedByOnCreate<
  M extends Model<boolean>,
  R extends Repo<M>,
  V,
>(
  this: R,
  context: ContextOf<R>,
  data: V,
  key: keyof M,
  model: M
): Promise<void> {
  const { stub } = context as any;

  const creator = await stub.getCreator();
  const owner = creator.mspid;

  const setOwnedByKeyValue = function <M extends Model>(
    target: M,
    propertyKey: string,
    value: string | number | bigint
  ) {
    Object.defineProperty(target, propertyKey, {
      enumerable: true,
      writable: false,
      configurable: true,
      value: value,
    });
  };

  setOwnedByKeyValue(model, key as string, owner);
}

export function ownedBy() {
  function ownedBy() {
    return function (obj: any, attribute?: any) {
      return apply(
        required(),
        generated(),
        readonly(),
        onCreate(ownedByOnCreate),
        propMetadata(
          Metadata.key(FabricModelKeys.FABRIC, FabricModelKeys.OWNED_BY),
          attribute
        )
      )(obj, attribute);
    };
  }

  return Decoration.for(FabricModelKeys.OWNED_BY)
    .define({
      decorator: ownedBy,
      args: [],
    })
    .apply();
}

export async function transactionIdOnCreate<
  M extends Model<boolean>,
  R extends Repo<M>,
  V,
>(
  this: R,
  context: ContextOf<R>,
  data: V,
  key: keyof M,
  model: M
): Promise<void> {
  const { stub } = context as any;
  model[key] = stub.getTxID();
}

export function transactionId() {
  function transactionId() {
    return function (obj: any, attribute?: any) {
      return apply(
        required(),
        readonly(),
        onCreate(transactionIdOnCreate),
        onUpdate(transactionIdOnCreate),
        propMetadata(
          Metadata.key(
            FabricModelKeys.FABRIC,
            attribute,
            FabricModelKeys.TRANSACTION_ID
          ),
          attribute
        )
      )(obj, attribute);
    };
  }

  return Decoration.for(FabricModelKeys.TRANSACTION_ID)
    .define({
      decorator: transactionId,
      args: [],
    })
    .apply();
}

export type MirrorCondition = (msp: string) => boolean;

export type MirrorMetadata = {
  condition?: MirrorCondition;
  resolver: CollectionResolver | string;
  mspId: string;
};

export async function evalMirrorMetadata<M extends Model>(
  model: M,
  resolver: undefined | string | CollectionResolver,
  ctx: Context<FabricContractFlags>
) {
  let collection: CollectionResolver | string | undefined = resolver;
  if (typeof collection !== "string") {
    try {
      const owner =
        Model.ownerOf(model) || ctx.get("stub").getCreator().toString();
      if (resolver && typeof resolver === "function")
        collection = await resolver(model, owner, ctx);
    } catch (e: unknown) {
      throw new InternalError(`Failed to resolve collection mirror name: ${e}`);
    }
  }

  if (!collection || typeof collection !== "string")
    throw new InternalError(
      `No collection found model ${model.constructor.name}`
    );
  return collection;
}

export async function createMirrorHandler<
  M extends Model,
  R extends Repository<M, any>,
>(
  this: R,
  context: Context<FabricContractFlags>,
  data: MirrorMetadata,
  key: keyof M,
  model: M
): Promise<void> {
  const collection = await evalMirrorMetadata(model, data.resolver, context);
  const fabricCtx = context as FabricContractContext;
  const sourceModel = model;
  // Put mirror flags directly on context so adapter.prepare() and adapter.create() can see them
  fabricCtx.put("mirror" as any, true);
  fabricCtx.put("mirrorCollection" as any, collection);
  try {
    const repo = this.override(
      Object.assign({}, (this as any)._overrides, {
        mirror: true,
        mirrorCollection: collection,
        ignoreValidation: true,
        ignoreHandlers: true,
      } as any)
    );
    const mirror = await repo.create(sourceModel, context);
    context.logger.info(
      `Mirror for ${Model.tableName(this.class)} created with ${Model.pk(model) as string}: ${mirror[Model.pk(model)]}`
    );
  } finally {
    // Clean up mirror flags from context
    fabricCtx.put("mirror" as any, undefined);
    fabricCtx.put("mirrorCollection" as any, undefined);
  }
}

export async function updateMirrorHandler<
  M extends Model,
  R extends Repository<M, any>,
>(
  this: R,
  context: Context<FabricContractFlags>,
  data: MirrorMetadata,
  key: keyof M,
  model: M
): Promise<void> {
  const collection = await evalMirrorMetadata(model, data.resolver, context);
  const fabricCtx = context as FabricContractContext;
  const sourceModel = model;
  fabricCtx.put("mirror" as any, true);
  fabricCtx.put("mirrorCollection" as any, collection);
  try {
    const repo = this.override(
      Object.assign({}, (this as any)._overrides, {
        mirror: true,
        mirrorCollection: collection,
        ignoreValidation: true,
        ignoreHandlers: true,
        applyUpdateValidation: false,
        mergeForUpdate: false,
      } as any)
    );
    await repo.update(sourceModel, context);
    context.logger.info(
      `Mirror for ${Model.tableName(this.class)} updated: ${(model as any)[Model.pk(model)]}`
    );
  } finally {
    fabricCtx.put("mirror" as any, undefined);
    fabricCtx.put("mirrorCollection" as any, undefined);
  }
}

export async function deleteMirrorHandler<
  M extends Model,
  R extends Repository<M, any>,
>(
  this: R,
  context: Context<FabricContractFlags>,
  data: MirrorMetadata,
  key: keyof M,
  model: M
): Promise<void> {
  const collection = await evalMirrorMetadata(model, data.resolver, context);
  const fabricCtx = context as FabricContractContext;
  fabricCtx.put("mirror" as any, true);
  fabricCtx.put("mirrorCollection" as any, collection);
  fabricCtx.put("segregated" as any, collection);
  try {
    const pkProp = Model.pk(model) as keyof M;
    const id = model[pkProp];
    const repo = this.override(
      Object.assign({}, (this as any)._overrides, {
        segregated: collection,
        mirror: true,
        ignoreValidation: true,
        ignoreHandlers: true,
      } as any)
    );
    try {
      await repo.delete(id as any, context);
    } catch {
      // May already be deleted by adapter.deleteSegregatedCollections
    }
    context.logger.info(
      `Mirror for ${Model.tableName(this.class)} deleted: ${String(id)}`
    );
  } finally {
    fabricCtx.put("mirror" as any, undefined);
    fabricCtx.put("mirrorCollection" as any, undefined);
    fabricCtx.put("segregated" as any, undefined);
  }
}

export async function mirrorWriteGuard<
  M extends Model,
  R extends Repository<M, any>,
>(
  this: R,
  context: Context<FabricContractFlags>,
  data: MirrorMetadata,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  key: keyof M,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  model: M
): Promise<void> {
  const msp = extractMspId(
    context.get("identity") as string | ClientIdentity | undefined
  );
  if (!msp) return;
  if (msp === data.mspId || (data.condition && data.condition(msp))) {
    throw new AuthorizationError(
      `Organization ${msp} is not authorized to modify mirrored data`
    );
  }
}

export async function readMirrorHandler<
  M extends Model,
  R extends Repository<M, any>,
>(
  this: R,
  context: Context<FabricContractFlags>,
  data: MirrorMetadata,
  key: keyof M,
  model: M
): Promise<void> {
  const msp = extractMspId(
    context.get("identity") as string | ClientIdentity | undefined
  );

  if (!msp) return;

  const collection = await evalMirrorMetadata(model, data.resolver, context);
  const fabricCtx = context as FabricContractContext;
  const matches = msp === data.mspId || (data.condition && data.condition(msp));

  if (matches) {
    context.logger.info(
      `Mirror read: MSP ${msp} matches, routing reads to mirror collection ${collection}`
    );
    // Route reads exclusively through the mirror collection
    fabricCtx.put("mirror" as any, true);
    fabricCtx.put("mirrorCollection" as any, collection);
    fabricCtx.put("fullySegregated", true);
    fabricCtx.readFrom(collection);
  }
  // When MSP does NOT match, normal read flow — all data is in public state
}

export function mirror(
  collection: CollectionResolver | string,
  mspIdOrCondition?: string | MirrorCondition,
  condition?: MirrorCondition
) {
  const isConditionOnly =
    typeof mspIdOrCondition !== "string" && Boolean(mspIdOrCondition);
  const mspId = isConditionOnly
    ? undefined
    : (mspIdOrCondition as string | undefined);
  const cond = isConditionOnly
    ? (mspIdOrCondition as MirrorCondition)
    : condition;

  function mirror(
    resolver: CollectionResolver | string,
    mspId: string,
    condition?: MirrorCondition
  ) {
    const meta: MirrorMetadata = {
      condition: condition,
      mspId: mspId,
      resolver: resolver,
    };
    return apply(
      metadata(
        Metadata.key(FabricModelKeys.FABRIC, FabricModelKeys.MIRROR),
        meta
      ),
      // Read handler runs early (priority 30) to set up context before any reads
      onRead(readMirrorHandler as any, meta, { priority: 30 }),
      // Write guards — reject matching MSPs before any processing
      onCreate(mirrorWriteGuard as any, meta, { priority: 20 }),
      onUpdate(mirrorWriteGuard as any, meta, { priority: 20 }),
      onDelete(mirrorWriteGuard as any, meta, { priority: 20 }),
      // Mirror sync handlers — write full model after other handlers (priority 100)
      onCreate(createMirrorHandler as any, meta, { priority: 100 }),
      onUpdate(updateMirrorHandler as any, meta, { priority: 100 }),
      onDelete(deleteMirrorHandler as any, meta, { priority: 100 })
    );
  }

  return Decoration.for(FabricModelKeys.MIRROR)
    .define({
      decorator: mirror,
      args: [collection, mspId, cond],
    })
    .apply();
}

export type CollectionResolver = <M extends Model>(
  model: M | Constructor<M>,
  msp?: string,
  ...args: any[]
) => string;

export const ModelCollection: CollectionResolver = <M extends Model>(
  model: M | Constructor<M>,
  mspId?: string
) => {
  const orgName =
    mspId || (typeof model !== "function" ? Model.ownerOf(model) : undefined);
  const constr = typeof model === "function" ? model : model.constructor;
  if (!orgName)
    throw new InternalError(
      `Model ${constr.name} is not owned by any organization. did you use @ownedBy() (or provide the name)?`
    );
  return `${toPascalCase(constr.name)}${orgName ? toPascalCase(orgName) : ""}`;
};

export function NamespaceCollection(namespace: string): CollectionResolver {
  return <M extends Model>(model: M | Constructor<M>, mspId?: string) => {
    const orgName =
      mspId || (typeof model !== "function" ? Model.ownerOf(model) : undefined);
    const constr = typeof model === "function" ? model : model.constructor;
    if (!orgName)
      throw new InternalError(
        `Model ${constr.name} is not owned by any organization. did you use @ownedBy() (or provide the name)?`
      );
    return `${namespace}${orgName ? toPascalCase(orgName) : ""}`;
  };
}

export const ImplicitPrivateCollection: CollectionResolver = <M extends Model>(
  model: M | Constructor<M>,
  mspId?: string
) => {
  const orgName =
    mspId || (typeof model !== "function" ? Model.ownerOf(model) : undefined);
  if (!orgName)
    throw new InternalError(
      `Model ${model.constructor.name} is not owned by any organization. did you use @ownedBy() (or provide the name)?`
    );
  return `__${toPascalCase(orgName)}PrivateCollection`;
};

export type SegregatedDataMetadata = {
  collections: string | CollectionResolver;
};

/**
 * @description Priority for early collection extraction (before pk generation at priority 60)
 * @summary This priority ensures collections are registered in context before any sequence
 * operations occur, allowing sequences to be replicated to private/shared collections.
 */
export const SEGREGATED_COLLECTION_EXTRACTION_PRIORITY = 35;

export function applySegregationFlags<M extends Model>(
  clazz: M,
  collections: string[],
  ctx: any
) {
  // Register collections early using readFrom - this allows sequence code
  // to know which collections to replicate to during pk generation
  if (collections.length > 0) {
    (ctx as FabricContractContext).readFrom(collections);
  }

  // Check if model is fully segregated (all non-pk properties are private/shared/transient).
  // Use Model.segregate() which is the canonical way to determine what's transient,
  // rather than reading DBKeys.TRANSIENT metadata which may not accumulate correctly
  // when class-level @privateData applies decorators iteratively.
  if (!ctx.isFullySegregated && collections.length) {
    if (!hasPublicProperties(clazz.constructor as Constructor<M>)) {
      ctx.markFullySegregated();
    }
  }
}

export async function applyMirrorFlags<M extends Model>(
  clazz: Constructor<M>,
  msp: string | undefined,
  ctx: FabricContractContext
) {
  if (!msp) return;
  const mirrorMeta = Model.mirroredAt(clazz);
  if (!mirrorMeta) return;
  const matches =
    msp === mirrorMeta.mspId ||
    (mirrorMeta.condition && mirrorMeta.condition(msp));
  if (!matches) return;
  const collection = await evalMirrorMetadata(
    new clazz(),
    mirrorMeta.resolver,
    ctx
  );
  // Mirror MSP matches — route reads exclusively through the mirror
  // collection. Clear any previously registered collections so queries
  // go ONLY to the mirror, not to the regular private/shared ones.
  ctx.put("segregateRead", undefined);
  ctx.put("segregateReadStack", undefined);
  ctx.put("fullySegregated", true);
  ctx.put("mirror" as any, true);
  ctx.put("mirrorCollection" as any, collection);
  ctx.readFrom(collection);
}

function hasPublicProperties<M extends Model>(clazz: Constructor<M>): boolean {
  const attributes = Metadata.getAttributes(clazz) || [];
  const pk = Model.pk(clazz);
  const transientMeta = Metadata.get(clazz as any, DBKeys.TRANSIENT) || {};
  const privateMeta =
    Metadata.get(clazz as any, Metadata.key(FabricModelKeys.PRIVATE)) || {};
  const sharedMeta =
    Metadata.get(clazz as any, Metadata.key(FabricModelKeys.SHARED)) || {};

  return attributes.some((attr) => {
    if (attr === pk) return false;
    if (attr in transientMeta) return false;
    if (attr in privateMeta) return false;
    if (attr in sharedMeta) return false;
    return true;
  });
}

/**
 * @description Early handler to extract and register collections in context
 * @summary Runs with priority < 40 to extract collection names before pk generation (priority 60).
 * This allows FabricContractSequence to know which collections to replicate to.
 * @template M - Type that extends Model
 * @param {ContextOf<Repository<M, any>>} context - The execution context
 * @param {SegregatedDataMetadata | SegregatedDataMetadata[]} data - The segregated data metadata
 * @param {string | string[]} keys - The property key(s) being segregated
 * @param {M} model - The model instance
 * @return {Promise<void>}
 */
export async function extractSegregatedCollections<M extends Model>(
  this: Repository<M, any>,
  context: ContextOf<typeof this>,
  data: SegregatedDataMetadata | SegregatedDataMetadata[],
  keys: keyof M | (keyof M)[],
  model: M
): Promise<void> {
  const dataArray = (
    Array.isArray(data) ? data : [data]
  ) as SegregatedDataMetadata[];

  const msp =
    Model.ownerOf(model) ||
    extractMspId(
      context.get("identity") as string | ClientIdentity | undefined
    );
  if (!msp) {
    // Can't extract collections without MSP, will be caught by later handlers
    return;
  }

  const collections: string[] = [];
  for (const metadata of dataArray) {
    const collectionResolver = metadata.collections;
    const collection =
      typeof collectionResolver === "string"
        ? collectionResolver
        : collectionResolver(model, msp, context);
    if (collection && !collections.includes(collection)) {
      collections.push(collection);
    }
  }
  applySegregationFlags(model, collections, context);

  // Store segregation metadata on the adapter (persists across context chains).
  // The Sequence creates its own context via logCtx, losing context-stored flags.
  const seqName = Model.sequenceName(model, "pk");
  (context as any).setSequenceSegregation(
    seqName,
    (context as any).isFullySegregated,
    collections
  );
}

export async function segregatedDataOnCreate<M extends Model>(
  this: Repository<M, any>,
  context: ContextOf<typeof this>,
  data: SegregatedDataMetadata | SegregatedDataMetadata[],
  keys: keyof M | (keyof M)[],
  model: M
): Promise<void> {
  const dataArray = (
    Array.isArray(data) ? data : [data]
  ) as SegregatedDataMetadata[];
  const keyArray = (Array.isArray(keys) ? keys : [keys]) as (keyof M)[];
  if (keyArray.length !== dataArray.length)
    throw new InternalError(
      `Segregated data keys and metadata length mismatch`
    );

  const msp =
    Model.ownerOf(model) ||
    extractMspId(
      context.get("identity") as string | ClientIdentity | undefined
    );
  if (!msp)
    throw new ValidationError(
      `There's no assigned organization for model ${model.constructor.name}`
    );

  const collectionResolver = dataArray[0].collections;
  const collection =
    typeof collectionResolver === "string"
      ? collectionResolver
      : collectionResolver(model, msp, context);

  // Validate all keys resolve to the same collection
  keyArray.forEach((_k, i) => {
    const c =
      typeof dataArray[i].collections === "string"
        ? dataArray[i].collections
        : dataArray[i].collections(model, msp, context);
    if (c !== collection)
      throw new UnsupportedError(
        `Segregated data collection mismatch: ${c} vs ${collection}`
      );
  });

  const keyStrings = keyArray.map((key) => String(key));
  // Store the segregated model — prepare() will filter to collection-specific fields
  (context as FabricContractContext).writeTo(collection, keyStrings);
}

export async function segregatedDataOnRead<M extends Model>(
  this: Repository<M, any>,
  context: Context<FabricFlags>,
  data: SegregatedDataMetadata | SegregatedDataMetadata[],
  keys: keyof M | (keyof M)[],
  model: M
): Promise<void> {
  const dataArray = (
    Array.isArray(data) ? data : [data]
  ) as SegregatedDataMetadata[];
  const keyArray = (Array.isArray(keys) ? keys : [keys]) as (keyof M)[];
  if (keyArray.length !== dataArray.length)
    throw new InternalError(
      `Segregated data keys and metadata length mismatch`
    );

  const msp = Model.ownerOf(model) || extractMspId(context.get("identity"));
  if (!msp)
    throw new ValidationError(
      `There's no assigned organization for model ${model.constructor.name}`
    );

  const collectionResolver = dataArray[0].collections;
  const collection =
    typeof collectionResolver === "string"
      ? collectionResolver
      : await collectionResolver(model, msp, context);

  (context as FabricContractContext).readFrom(collection);
}

export async function segregatedDataOnUpdate<M extends Model>(
  this: Repository<M, any>,
  context: ContextOf<typeof this>,
  data: SegregatedDataMetadata | SegregatedDataMetadata[],
  key: keyof M | (keyof M)[],
  model: M,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  oldModel: M
): Promise<void> {
  const dataArray = (
    Array.isArray(data) ? data : [data]
  ) as SegregatedDataMetadata[];
  const keyArray = (Array.isArray(key) ? key : [key]) as (keyof M)[];
  if (keyArray.length !== dataArray.length)
    throw new InternalError(
      `Segregated data keys and metadata length mismatch`
    );

  const msp =
    Model.ownerOf(model) ||
    extractMspId(
      context.get("identity") as string | ClientIdentity | undefined
    );
  if (!msp)
    throw new ValidationError(
      `There's no assigned organization for model ${model.constructor.name}`
    );

  const collectionResolver = dataArray[0].collections;
  const collection =
    typeof collectionResolver === "string"
      ? collectionResolver
      : collectionResolver(model, msp, context);

  keyArray.forEach((k, i) => {
    const c =
      typeof dataArray[i].collections === "string"
        ? dataArray[i].collections
        : dataArray[i].collections(model, msp, context);
    if (c !== collection)
      throw new UnsupportedError(
        `Segregated data collection mismatch: ${c} vs ${collection}`
      );
  });

  const keyStrings = (keyArray as (keyof M)[]).map((key) => String(key));
  // Store the original model — prepare() will filter to collection-specific fields
  (context as FabricContractContext).writeTo(collection, keyStrings);
}

export async function segregatedDataOnDelete<
  M extends Model,
  R extends Repository<M, any>,
  V extends SegregatedDataMetadata,
>(
  this: R,
  context: ContextOf<R>,
  data: V | V[],
  key: (keyof M)[],
  model: M
): Promise<void> {
  const dataArray = (Array.isArray(data) ? data : [data]) as V[];
  const keyArray = (Array.isArray(key) ? key : [key]) as (keyof M)[];
  if (keyArray.length !== dataArray.length)
    throw new InternalError(
      `Segregated data keys and metadata length mismatch`
    );

  const msp =
    Model.ownerOf(model) ||
    extractMspId(
      context.get("identity") as string | ClientIdentity | undefined
    );
  if (!msp)
    throw new ValidationError(
      `There's no assigned organization for model ${model.constructor.name}`
    );

  const collectionResolver = dataArray[0].collections;
  const collection =
    typeof collectionResolver === "string"
      ? collectionResolver
      : collectionResolver(model, msp, context);

  (context as FabricContractContext).readFrom(collection);
}

function segregated(
  collection: string | CollectionResolver,
  type: FabricModelKeys.PRIVATE | FabricModelKeys.SHARED,
  filter?: (propName: string) => boolean
) {
  return function innerSegregated(target: object, propertyKey?: any) {
    function segregatedDec(target: object, propertyKey?: any) {
      const key = Metadata.key(type, propertyKey);
      const constr: Constructor = target.constructor as Constructor;

      const meta = Metadata.get(constr as Constructor, key) || {};
      const collections = new Set(meta.collections || []);
      collections.add(collection);
      meta.collections = [...collections];
      Metadata.set(constr as Constructor, key, meta);

      const constrMeta = Metadata.get(constr as Constructor, type) || {};
      const constrCollections = new Set(constrMeta.collections || []);
      constrCollections.add(collection);
      constrMeta.collections = [...constrCollections];
      Metadata.set(constr as Constructor, type, constrMeta);

      const transientMeta =
        Metadata.get(constr as Constructor, DBKeys.TRANSIENT) || {};
      const updatedTransientMeta = {
        ...transientMeta,
        [propertyKey as any]: {},
      };
      Metadata.set(
        constr as Constructor,
        DBKeys.TRANSIENT,
        updatedTransientMeta
      );
    }

    const decs: any[] = [];
    if (!propertyKey) {
      // decorated at the class level
      const properties = Metadata.getAttributes(target as Constructor);
      properties?.forEach((p) => {
        if (!filter || filter(p)) {
          segregated(collection, type)((target as any).prototype, p);
        }
      });
      return target;
    } else {
      const groupName =
        typeof collection === "string" ? collection : collection.toString();
      // Use different group names for extraction vs data handlers to prevent merging
      const extractGroupName = `${groupName}:extract`;
      const dataGroupName = `${groupName}:data`;
      const earlyExtractionMeta = { collections: collection };
      const earlyExtractionGroupSort = {
        priority: SEGREGATED_COLLECTION_EXTRACTION_PRIORITY,
        group: extractGroupName,
      };
      decs.push(
        prop(),
        transient(),
        segregatedDec,
        // Early extraction handlers - run BEFORE pk generation (priority 60)
        // This ensures collections are registered in context for sequence replication
        // We register for each operation explicitly to ensure proper handler lookup
        on(
          DBOperations.ALL,
          extractSegregatedCollections as any,
          earlyExtractionMeta,
          earlyExtractionGroupSort
        ),
        // Main handlers for segregated data operations (priority 95)
        onCreate(
          segregatedDataOnCreate,
          { collections: collection },
          {
            priority: 95,
            group: dataGroupName,
          }
        ),
        onRead(
          segregatedDataOnRead as any,
          { collections: collection },
          {
            priority: 95,
            group: dataGroupName,
          }
        ),
        onUpdate(
          segregatedDataOnUpdate as any,
          { collections: collection },
          {
            priority: 95,
            group: dataGroupName,
          }
        ),
        onDelete(
          segregatedDataOnDelete as any,
          { collections: collection },
          {
            priority: 95,
            group: dataGroupName,
          }
        )
      );
    }
    return apply(...decs)(target, propertyKey);
  };
}

export function privateData(
  collection: string | CollectionResolver = ImplicitPrivateCollection
) {
  function privateData(collection: string | CollectionResolver) {
    return segregated(collection, FabricModelKeys.PRIVATE);
  }

  return Decoration.for(FabricModelKeys.PRIVATE)
    .define({
      decorator: privateData,
      args: [collection],
    })
    .apply();
}

export function sharedData(collection: string | CollectionResolver) {
  function sharedData(collection: string | CollectionResolver) {
    return segregated(collection, FabricModelKeys.SHARED);
  }

  return Decoration.for(FabricModelKeys.SHARED)
    .define({
      decorator: sharedData,
      args: [collection],
    })
    .apply();
}