Source

contract/models/decorators-private.ts

import { Model } from "@decaf-ts/decorator-validation";
import { metadata, apply, Constructor } from "@decaf-ts/decoration";
import {
  afterCreate,
  afterDelete,
  onUpdate,
  InternalError,
  OperationKeys,
} from "@decaf-ts/db-decorators";
import { OtherAudit } from "./OtherAudit";
import { Repository } from "@decaf-ts/core";
import { FabricContractContext } from "../../contracts/ContractContext";
import { CollectionResolver } from "../../shared/decorators";
import { populateRelations } from "./decorators";

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

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(OtherAudit);

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

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

  const toCreate = new OtherAudit({
    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)}`
  );
}

/**
 * Returns a shallow-cloned copy of the model where each relation array item
 * is normalised to its string ID. This ensures that `{ id: "x" }` and `"x"`
 * compare as equal so that unchanged relations produce no diff.
 */
function normalizeRelationsForAudit<M extends Model>(model: M): M {
  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 value = (copy as any)[propKey];
    if (Array.isArray(value)) {
      (copy as any)[propKey] = value.map((item: any) => {
        if (item == null) return item;
        if (typeof item === "string" || typeof item === "number")
          return String(item);
        if (item.id != null) return String(item.id);
        return item; // new object without id — keep as-is
      });
    } else if (value != null) {
      if (typeof value === "string" || typeof value === "number") {
        (copy as any)[propKey] = String(value);
      } else if ((value as any).id != null) {
        (copy as any)[propKey] = String((value as any).id);
      }
    }
  }
  return copy;
}

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(OtherAudit);

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

  // At onUpdate time `model` still has all private fields (not yet stripped by
  // adapter.revert). Normalise relation arrays to ID strings on both sides so
  // that { id: "x" } and "x" compare as equal and unchanged relations produce
  // no spurious diff.
  const normalizedModel = normalizeRelationsForAudit(model);
  const normalizedOldModel = normalizeRelationsForAudit(oldModel);

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

  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 OtherAudit({
    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(OtherAudit);
  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),
    onUpdate(updateAuditHandler as any, meta),
    afterDelete(deleteAuditHandler as any, meta),
    metadata("audit", true)
  );
}