Source

contract/models/decorators.ts

import { Model } from "@decaf-ts/decorator-validation";
import { metadata, apply, Constructor } from "@decaf-ts/decoration";
import {
  afterCreate,
  afterDelete,
  afterUpdate,
  InternalError,
  onCreate,
  OperationKeys,
} from "@decaf-ts/db-decorators";
import { Audit } from "./Audit";
import {
  Repo,
  Repository,
  UnsupportedError,
  RelationsMetadata,
} from "@decaf-ts/core";
import { FabricContractContext } from "../../contracts/ContractContext";
import { CollectionResolver } from "../../shared/decorators";
import { GtinOwner } from "./GtinOwner";

export async function rebuildForMatchingCollection<M extends Model>(
  model: M,
  context: any,
  collections: {
    privateCols: (string | CollectionResolver)[];
    sharedCols: (string | CollectionResolver)[];
  }
) {
  const mapToCollections =
    collections && (collections.privateCols || collections.sharedCols);
  let cols: string[] | undefined = undefined;
  if (mapToCollections) {
    const msp = Model.ownerOf(model) || (await context.stub.getCreator()).mspid;
    cols = [
      ...new Set(
        [...collections.privateCols, ...collections.sharedCols].map((col) => {
          return typeof col === "string"
            ? col
            : (col as CollectionResolver)(model, msp);
        })
      ),
    ];

    const pk = Model.pk(model);

    cols.forEach((col) => {
      let segData: any;
      try {
        segData = context.get("segregatedData");
      } catch {
        return; // segregatedData not present in this context (e.g. cascade child)
      }
      if (!segData || !segData[col] || !((model[pk] as any) in segData[col]))
        return;
      Object.assign(model, segData[col][model[pk]]);
    });
  }
  return model;
}

/**
 * Returns a shallow-cloned copy of the model with relation properties resolved
 * to their full instances. The original model is NOT mutated.
 */
export async function populateRelations<M extends Model>(
  model: M,
  context: FabricContractContext,
  overrides?: Record<string, any>
): Promise<M> {
  // Create a shallow copy preserving the prototype so compare() etc. still work
  const copy = Object.assign(
    Object.create(Object.getPrototypeOf(model)),
    model
  ) as M;
  const relProps = Model.relations(model) as string[];
  if (!relProps || !relProps.length) return copy;
  for (const propKey of relProps) {
    const meta = Model.relations(
      model,
      propKey as keyof M
    ) as RelationsMetadata;
    const clazzOrFn = meta.class;
    const clazz: Constructor<Model> =
      typeof clazzOrFn === "function" && (clazzOrFn as any).name
        ? (clazzOrFn as Constructor<Model>)
        : (clazzOrFn as () => Constructor<Model>)();
    const value = (copy as any)[propKey];
    if (value === null || value === undefined) continue;
    const repo = Repository.forModel(clazz).override(overrides || {});
    if (Array.isArray(value)) {
      const resolved: any[] = [];
      for (const item of value) {
        if (typeof item === "string" || typeof item === "number") {
          try {
            resolved.push(await repo.read(String(item), context));
          } catch {
            resolved.push(item);
          }
        } else {
          resolved.push(item);
        }
      }
      (copy as any)[propKey] = resolved;
    } else if (typeof value === "string" || typeof value === "number") {
      try {
        (copy as any)[propKey] = await repo.read(String(value), context);
      } catch {
        // keep original value if read fails
      }
    }
  }
  return copy;
}

export async function createAuditHandler<
  M extends Model,
  R extends Repository<M, any>,
>(
  this: R,
  context: FabricContractContext,
  data: AuditMetadata,
  key: keyof M,
  model: M
): Promise<void> {
  const repo = Repository.forModel(Audit);

  if (!context.identity || !context.identity.getID)
    throw new InternalError(`Lost context apparently for audit`);

  model = await populateRelations(model, context, this._overrides);

  const toCreate = new Audit({
    userGroup: context.identity.getID(),
    userId: context.identity.getID(),
    model: Model.tableName(data.class),
    transaction: context.stub.getTxID(),
    action: OperationKeys.CREATE,
    diffs: new this.class().compare(model),
  });

  const audit = await repo.override(this._overrides).create(toCreate, context);
  context.logger.info(
    `Audit log for ${OperationKeys.CREATE} of ${Model.tableName(this.class)} created: ${audit.id}: ${JSON.stringify(audit, undefined, 2)}`
  );
}

export async function updateAuditHandler<
  M extends Model,
  R extends Repository<M, any>,
>(
  this: R,
  context: FabricContractContext,
  data: AuditMetadata,
  key: keyof M,
  model: M,
  oldModel: M
): Promise<void> {
  const repo = Repository.forModel(Audit);

  const collections = Model.collectionsFor(Audit);
  //
  // model = await rebuildForMatchingCollection(model, context, collections);

  if (!context.identity || !context.identity.getID)
    throw new InternalError(`Lost context apparently for audit`);

  const populatedModel = await populateRelations(
    model,
    context,
    this._overrides
  );
  const populatedOldModel = await populateRelations(
    oldModel,
    context,
    this._overrides
  );

  const toCreate = new Audit({
    userGroup: context.identity.getID(),
    userId: context.identity.getID(),
    model: Model.tableName(data.class),
    transaction: context.stub.getTxID(),
    action: OperationKeys.UPDATE,
    diffs: populatedModel.compare(populatedOldModel),
  });

  const audit = await repo.override(this._overrides).create(toCreate, context);
  context.logger.info(
    `Audit log for ${OperationKeys.UPDATE} of ${Model.tableName(this.class)} created: ${JSON.stringify(audit, undefined, 2)}`
  );
}

export async function deleteAuditHandler<
  M extends Model,
  R extends Repository<M, any>,
>(
  this: R,
  context: FabricContractContext,
  data: AuditMetadata,
  key: keyof M,
  model: M
): Promise<void> {
  if (!context.identity || !context.identity.getID)
    throw new InternalError(`Lost context apprently. no getId in identity`);

  model = await populateRelations(model, context, this._overrides);

  const toCreate = new Audit({
    userGroup: context.identity.getID(),
    userId: context.identity.getID(),
    model: Model.tableName(data.class),
    transaction: context.stub.getTxID(),
    action: OperationKeys.DELETE,
    diffs: model.compare(new this.class()),
  });

  const repo = Repository.forModel(Audit);
  const audit = await repo.override(this._overrides).create(toCreate, context);
  context.logger.info(
    `Audit log for ${OperationKeys.DELETE} of ${Model.tableName(this.class)} created: ${JSON.stringify(audit, undefined, 2)}`
  );
}

export type AuditMetadata = {
  class: Constructor<Model>;
};

export function audit(model: Constructor<Model<boolean>>) {
  const meta: AuditMetadata = {
    class: model,
  };
  return apply(
    afterCreate(createAuditHandler as any, meta),
    afterUpdate(updateAuditHandler as any, meta),
    afterDelete(deleteAuditHandler as any, meta),
    metadata("audit", true)
  );
}

export async function createAssignGtinOwnerHandler<
  M extends Model,
  R extends Repo<M>,
  V,
>(
  this: R,
  context: FabricContractContext,
  data: V,
  key: keyof M,
  model: { productCode: string }
): Promise<void> {
  if (!model.productCode)
    throw new UnsupportedError(`Gtin owner can only be assigned to products`);
  const repo = Repository.forModel(GtinOwner);
  const toCreate = new GtinOwner({
    productCode: model.productCode,
  });
  const owner = await repo.create(toCreate, publicContext(context));
  context.logger.info(
    `GTIN owner assigned for product ${model.productCode}: ${owner.ownedBy}`
  );
}

export async function deleteAssignGtinOwnerHandler<
  M extends Model,
  R extends Repo<M>,
  V,
>(
  this: R,
  context: FabricContractContext,
  data: V,
  key: keyof M,
  model: { productCode: string }
): Promise<void> {
  if (!model.productCode)
    throw new UnsupportedError(`Gtin owner can only be assigned to products`);
  const repo = Repository.forModel(GtinOwner);
  const owner = await repo.delete(model.productCode, publicContext(context));
  context.logger.info(
    `GTIN owner assigned for product ${model.productCode}: ${owner.ownedBy}`
  );
}

export function assignProductOwner() {
  return apply(
    onCreate(createAssignGtinOwnerHandler as any, {}),
    afterDelete(deleteAssignGtinOwnerHandler as any, {})
  );
}

function publicContext(ctx: FabricContractContext) {
  return new (ctx.constructor as any)(ctx).accumulate({
    fullySegregated: false,
    segregated: undefined,
    segregatedData: undefined,
    segregateRead: undefined,
    segregateWrite: undefined,
    segregateReadStack: undefined,
  });
}