Source

shared/decorators.ts

import {
  AuthorizationError,
  Repo,
  Context,
  UnsupportedError,
  Repository,
  ContextOf,
} from "@decaf-ts/core";
import {
  afterCreate,
  afterDelete,
  afterUpdate,
  generated,
  InternalError,
  NotFoundError,
  onCreate,
  onDelete,
  onRead,
  onUpdate,
  readonly,
  transient,
  ValidationError,
} 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 { 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";

/**
 * 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;
};

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 repo = this.override(
    Object.assign({}, this._overrides, {
      segregate: collection,
      ignoreValidation: true,
      ignoreHandlers: true,
    } as any)
  );

  const mirror = await repo.create(model, context);
  context.logger.info(
    `Mirror for ${Model.tableName(this.class)} created with ${Model.pk(model) as string}: ${mirror[Model.pk(model)]}`
  );
}

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 repo = this.override(
    Object.assign({}, this._overrides, {
      segregate: collection,
      ignoreValidation: true,
      ignoreHandlers: true,
    } as any)
  );

  const mirror = await repo.update(model, context);
  context.logger.info(
    `Mirror for ${Model.tableName(this.class)} updated with ${Model.pk(model) as string}: ${mirror[Model.pk(model)]}`
  );
}

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 repo = this.override(
    Object.assign({}, this._overrides, {
      segregate: collection,
      ignoreValidation: true,
      ignoreHandlers: true,
    } as any)
  );

  const mirror = await repo.delete(Model.pk(model) as string, context);
  context.logger.info(
    `Mirror for ${Model.tableName(this.class)} deleted with ${Model.pk(model) as string}: ${mirror[Model.pk(model)]}`
  );
}

export function mirror(
  collection: CollectionResolver | string,
  condition?: MirrorCondition
) {
  function mirror(
    resolver: CollectionResolver | string,
    condition: MirrorCondition
  ) {
    const meta: MirrorMetadata = {
      condition: condition,
      resolver: resolver,
    };
    return apply(
      metadata(
        Metadata.key(FabricModelKeys.FABRIC, FabricModelKeys.MIRROR),
        meta
      ),
      privateData(collection),
      afterCreate(createMirrorHandler as any, meta, { priority: 95 }),
      afterUpdate(updateMirrorHandler as any, meta, { priority: 95 }),
      afterDelete(deleteMirrorHandler as any, meta, { priority: 95 })
    );
  }

  return Decoration.for(FabricModelKeys.MIRROR)
    .define({
      decorator: mirror,
      args: [collection, condition],
    })
    .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)}${mspId ? toPascalCase(mspId) : ""}`;
};

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;
};

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

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

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

  const rebuilt = keys.reduce(
    (acc: Record<keyof M, any>, k, i) => {
      const c =
        typeof data[i].collections === "string"
          ? data[i].collections
          : data[i].collections(model, msp, context);
      if (c !== collection)
        throw new UnsupportedError(
          `Segregated data collection mismatch: ${c} vs ${collection}`
        );
      acc[k] = model[k];
      return acc;
    },
    {} as Record<keyof M, any>
  );

  const toCreate = new this.class(rebuilt);

  // const segregated = Model.segregate(model);

  const created = await this.override({
    segregated: collection,
    mergeModel: false,
    ignoreHandlers: true,
    ignoreValidation: true,
  } as any).create(toCreate, context);
  Object.assign(model, created);
}

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

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

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

  const rebuilt = keys.reduce(
    (acc: Record<keyof M, any>, k, i) => {
      const c =
        typeof data[i].collections === "string"
          ? data[i].collections
          : data[i].collections(model, msp, context);
      if (c !== collection) return acc;
      acc[k] = model[k];
      return acc;
    },
    {} as Record<keyof M, any>
  );

  // const segregated = Model.segregate(model);
  //
  // const created = await this.override({ segregated: collection } as any).readAll(
  //   toCreate,
  //   context
  // );
  // Object.assign(model, created);
}

export async function segregatedDataOnUpdate<M extends Model>(
  this: Repository<M, any>,
  context: ContextOf<typeof this>,
  data: SegregatedDataMetadata[],
  key: keyof M[],
  model: M,
  oldModel: M
): Promise<void> {}

export async function segregatedDataOnDelete<
  M extends Model,
  R extends Repository<M, any>,
  V extends SegregatedDataMetadata,
>(
  this: R,
  context: ContextOf<R>,
  data: V[],
  key: keyof M[],
  model: M
): Promise<void> {}

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);
      meta.collections = [...collections];
      Metadata.set(constr as Constructor, type, meta);
    }

    const decs: any[] = [];
    if (!propertyKey) {
      // decorated at the class level
      Metadata.properties(target as Constructor)?.forEach((p) => {
        if (!filter || filter(p)) {
          segregated(collection, type)((target as any).prototype, p);
        }
      });
    } else {
      decs.push(
        prop(),
        transient(),
        segregatedDec,
        onCreate(
          segregatedDataOnCreate,
          { collections: collection },
          {
            priority: 95,
            group:
              typeof collection === "string"
                ? collection
                : collection.toString(),
          }
        ),
        onRead(
          segregatedDataOnRead as any,
          { collections: collection },
          {
            priority: 95,
            group:
              typeof collection === "string"
                ? collection
                : collection.toString(),
          }
        ),
        onUpdate(
          segregatedDataOnUpdate as any,
          { collections: collection },
          {
            priority: 95,
            group:
              typeof collection === "string"
                ? collection
                : collection.toString(),
          }
        ),
        onDelete(
          segregatedDataOnDelete as any,
          { collections: collection },
          {
            priority: 95,
            group:
              typeof collection === "string"
                ? collection
                : collection.toString(),
          }
        )
      );
    }
    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();
}