Problem with _internal/capabilities, can't add new collection

I’m suddenly getting this error message on ForestAdmin locally, after I added a new collection. For a while it worked, and suddenly it did not anymore, whenever I open up this new collection in the interface, this error appears.

As soon as I do this

export const companyDiffsFlattenOptions = {
  asModels: ['sharesDiffs'],
};

The system just breaks. Here is the original model.

import Mongoose from 'mongoose';
import { mongooseLeanVirtuals } from 'mongoose-lean-virtuals';
import {
  Address,
  AddressWithCompanyName,
  Consent,
  ConsentEnum,
  CompanyModel,
  Domicile,
  DomicileHolderType,
} from './company';
import { File } from './files';
import { Individual } from './individuals';

// Custom error class to replace the imported DataCorruptionException
class DataCorruptionException extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'DataCorruptionException';
  }
}

export enum DiffType {
  Incorporation = 'incorporation',
  Diff = 'diff',
  CommercialRegistry = 'commercial-registry',
}

export enum DiffStatus {
  Draft = 'draft',
  Valid = 'valid',
}

export enum LegalEntityType {
  LLC = 'llc',
  StockCorporation = 'stock-corporation',
  SoleProprietorship = 'sole-proprietorship',
}

export enum CompanyPersonRole {
  // Accountant = 'accountant',
  // Auditor = 'auditor',
  Chairman = 'chairman',
  Director = 'director',
  // Employee = 'employee',
  ManagingDirector = 'managing-director',
  // Proprietor = 'proprietor',
  Shareholder = 'shareholder',
  // SingleSignatoryAuthority = 'single-signatory-authority',
  // JointSignatureAuthority = 'joint-signature-authority',
}

export enum PersonDiffType {
  AddRoles = 'add-roles',
  RemoveRoles = 'remove-roles',
  RemovePerson = 'remove-person',
  AddPerson = 'add-person',
  UpdateSignature = 'update-signature',
}

const isPopulatedIndividual = (obj: any): obj is Individual => {
  return obj && typeof obj === 'object' && obj._id;
};
const isPopulatedCompany = (obj: any): obj is CompanyModel => {
  return obj && typeof obj === 'object' && obj._id;
};

export enum PersonRelationEnum {
  Individual = 'individual',
  Company = 'company',
}

// Type aliases for consistent ObjectId usage
type MongooseObjectId = Mongoose.Types.ObjectId;

// Interfaces
interface PersonRelationInterface {
  individual?: MongooseObjectId;
  company?: MongooseObjectId;
}

interface MonetaryAmountInterface {
  amount: number;
  currency: string;
}

export type Person = {
  _id: MongooseObjectId;
  createdAt: Date;
  updatedAt: Date;
  birthdate?: string | null;
  client?: MongooseObjectId;
  companyFoundingDate?: string | null;
  companyUid?: string | null;
  consents: Consent[];
  domicile?: Domicile;
  gender?: string | null;
  hometown?: string | null;
  name: string;
  nationality?: string | null;
  type: PersonRelationEnum;
};

interface PersonDiffInterface {
  person: PersonRelationInterface;
  roles: CompanyPersonRole[];
  type: PersonDiffType;
  newEntry?: boolean;
  removal?: boolean;
  signatureAuthority?: 'single' | 'joint';
}

interface CapitalContributionInterface {
  bank?: MongooseObjectId | CompanyModel;
  paymentConfirmationDate?: string;
  totalAmount: MonetaryAmountInterface;
  totalContributedAmount: MonetaryAmountInterface;
  type: 'cash' | 'in-kind' | 'debt-to-equity';
}

interface ConsentDiffInterface {
  createdAt: Date;
  ipAddress?: string;
  target: 'mobiliar' | 'bexio' | 'zkb' | 'konsento';
  description: string;
  type: 'give' | 'revoke';
}

interface IncorporationInterface {
  requiresLexFriedrich: boolean;
  incorporationDate?: Date;
  documents?: MongooseObjectId[] | File[];
  requestVATNumber?: boolean;
  takeover?: MongooseObjectId | CompanyModel;
}

interface SharesDiffInterface {
  type: 'issuance' | 'cancellation';
  class?: string;
  start: number;
  end: number;
  nominalValue: MonetaryAmountInterface;
}

interface ShareHoldingDiffInterface {
  type: 'transfer' | 'capitalContribution';
  class?: string;
  start: number;
  end: number;
  holder: PersonRelationInterface;
  capitalContributions: CapitalContributionInterface[];
}

export interface CompanyDiffModelInterface {
  _id: MongooseObjectId;
  createdAt: Date;
  updatedAt: Date;
  auditor?: AddressWithCompanyName;
  businessYearStart?: Date;
  client?: MongooseObjectId;
  companyId: MongooseObjectId;
  consentDiffs: ConsentDiffInterface[];
  documents: MongooseObjectId[];
  fulfillment?: MongooseObjectId;
  incorporation: IncorporationInterface;
  industry?: string;
  legalEntityType: LegalEntityType;
  name?: string;
  personDiffs: PersonDiffInterface[];
  purpose?: string;
  seat?: Domicile;
  shareholdingDiffs: ShareHoldingDiffInterface[];
  sharesDiffs: SharesDiffInterface[];
  status: DiffStatus;
  type: DiffType;
  vatNumber?: string;
}

// Class for backward compatibility
export class PersonRelation implements PersonRelationInterface {
  individual?: MongooseObjectId;
  company?: MongooseObjectId;
}

export class PersonDiff implements PersonDiffInterface {
  person: PersonRelation;
  roles: CompanyPersonRole[] = [];
  type: PersonDiffType;
  newEntry?: boolean;
  removal?: boolean;
  signatureAuthority?: 'single' | 'joint';

  static normalizePerson(person: PersonRelation): Person {
    // the type returned by this func needs to be strictly defined (fx domicile exists in individual but not in company)
    if (isPopulatedIndividual(person.individual)) {
      return {
        _id: person.individual._id,
        createdAt: person.individual.createdAt,
        updatedAt: person.individual.updatedAt,
        birthdate: person.individual.birthdate,
        client: person.individual.client,
        companyFoundingDate: null,
        companyUid: null,
        consents: person.individual.consents,
        domicile: person.individual.domicile,
        gender: person.individual.gender,
        hometown:
          person.individual.nationality === 'Schweiz'
            ? `${person.individual.origin?.city} ${person.individual.origin?.canton}`
            : person.individual.nationality,
        name: [person.individual.firstName, person.individual.middleName, person.individual.lastName]
          .filter(Boolean)
          .join(' '),
        nationality: person.individual.nationality,
        type: PersonRelationEnum.Individual,
      };
    }
    if (isPopulatedCompany(person.company)) {
      let contactPerson: Individual | null = null;
      if (isPopulatedIndividual(person.company.contactPerson)) {
        contactPerson = person.company.contactPerson;
      }

      return {
        _id: person.company._id,
        createdAt: person.company.createdAt,
        updatedAt: person.company.updatedAt,
        birthdate: contactPerson?.birthdate,
        client: person.company.client,
        companyFoundingDate: person.company.foundingDate?.toString(),
        companyUid: person.company.uid,
        consents: person.company.consents,
        domicile: person.company.seat,
        gender: contactPerson?.gender, // TODO or null?
        hometown: null,
        name: person.company.name,
        nationality: contactPerson?.nationality,
        type: PersonRelationEnum.Company,
      };
    }

    throw new DataCorruptionException(`Invalid person relation: ${JSON.stringify(person)}`);
  }
}

// Schema definitions
const personRelation = new Mongoose.Schema(
  {
    individual: { type: Mongoose.Schema.Types.ObjectId, ref: 'Individual', required: false },
    company: { type: Mongoose.Schema.Types.ObjectId, ref: 'Company', required: false },
  },
  { timestamps: false, _id: false }
);

const monetaryAmount = new Mongoose.Schema(
  {
    amount: { type: Number, required: true },
    currency: { type: String, required: true },
  },
  { timestamps: false, _id: false }
);

const personDiff = new Mongoose.Schema(
  {
    person: { type: personRelation, required: true },
    roles: {
      type: [String],
      enum: Object.values(CompanyPersonRole),
      required: false,
      default: [],
    },
    type: {
      type: String,
      enum: Object.values(PersonDiffType),
      required: true,
    },
    newEntry: { type: Boolean, required: false },
    removal: { type: Boolean, required: false },
    signatureAuthority: {
      type: String,
      enum: ['single', 'joint'],
      required: false,
    },
  },
  { timestamps: false, _id: false }
);

const capitalContribution = new Mongoose.Schema(
  {
    bank: {
      type: Mongoose.Schema.Types.ObjectId,
      ref: 'Company',
      required: false,
    },
    paymentConfirmationDate: { type: String, required: false },
    totalAmount: { type: monetaryAmount, required: true },
    totalContributedAmount: { type: monetaryAmount, required: true },
    type: {
      type: String,
      enum: ['cash', 'in-kind', 'debt-to-equity'],
      required: true,
    },
  },
  { timestamps: false, _id: false }
);

const consentDiff = new Mongoose.Schema(
  {
    createdAt: {
      type: Date,
      required: true,
      default: new Date(),
    },
    ipAddress: { type: String, required: false },
    target: {
      type: String,
      enum: ['mobiliar', 'bexio', 'zkb', 'konsento'],
      required: true,
    },
    description: { type: String, required: true },
    type: {
      type: String,
      enum: ['give', 'revoke'],
      required: true,
    },
  },
  { timestamps: false, _id: false }
);

const incorporation = new Mongoose.Schema(
  {
    requiresLexFriedrich: { type: Boolean, required: true },
    incorporationDate: { type: Date, required: false },
    documents: {
      type: [Mongoose.Schema.Types.ObjectId],
      ref: 'File',
      required: false,
    },
    requestVATNumber: { type: Boolean, required: false },
    takeover: {
      type: Mongoose.Schema.Types.ObjectId,
      ref: 'Company',
      required: false,
    },
  },
  { timestamps: false, _id: false }
);

const sharesDiff = new Mongoose.Schema(
  {
    type: {
      type: String,
      enum: ['issuance', 'cancellation'],
      required: true,
    },
    class: { type: String, required: false },
    start: { type: Number, required: true },
    end: { type: Number, required: true },
    nominalValue: { type: monetaryAmount, required: true },
  },
  { timestamps: false, _id: false }
);

const shareHoldingDiff = new Mongoose.Schema(
  {
    type: {
      type: String,
      enum: ['transfer', 'capitalContribution'],
      required: true,
    },
    class: { type: String, required: false },
    start: { type: Number, required: true },
    end: { type: Number, required: true },
    holder: { type: personRelation, required: true },
    capitalContributions: {
      type: [capitalContribution],
      required: false,
      default: [],
    },
  },
  { timestamps: false, _id: false }
);

const companyDiffSchema = new Mongoose.Schema(
  {
    auditor: { type: Mongoose.Schema.Types.Mixed, required: false },
    businessYearStart: { type: Date, required: false },
    client: {
      type: Mongoose.Schema.Types.ObjectId,
      ref: 'clients',
      required: false,
    },
    companyId: { type: Mongoose.Schema.Types.ObjectId, required: true },
    consentDiffs: {
      type: [consentDiff],
      required: false,
      default: [],
    },
    documents: {
      type: [Mongoose.Schema.Types.ObjectId],
      ref: 'files',
      required: false,
      default: [],
    },
    fulfillment: {
      type: Mongoose.Schema.Types.ObjectId,
      ref: 'fulfillments',
      required: false,
    },
    incorporation: { type: incorporation, required: true },
    industry: { type: String, required: false },
    legalEntityType: {
      type: String,
      enum: Object.values(LegalEntityType),
      required: true,
    },
    name: { type: String, required: false },
    personDiffs: {
      type: [personDiff],
      required: false,
      default: [],
    },
    purpose: { type: String, required: false },
    seat: { type: Mongoose.Schema.Types.Mixed, required: false },
    shareholdingDiffs: {
      type: [shareHoldingDiff],
      required: false,
      default: [],
    },
    sharesDiffs: {
      type: [sharesDiff],
      required: false,
      default: [],
    },
    status: {
      type: String,
      enum: Object.values(DiffStatus),
      required: true,
    },
    type: {
      type: String,
      enum: Object.values(DiffType),
      required: true,
    },
    vatNumber: { type: String, required: false },
  },
  {
    timestamps: true,
    collection: 'companyDiffs',
  }
);

// Types for virtuals
export interface PersonNormalized {
  person: Person;
}

export interface SeatNormalized {
  holderName?: string;
  holderType?: DomicileHolderType;
}

export interface ShareHoldingDiffNormalized {
  holder: Person;
}

export interface CompanyDiffVirtuals {
  personDiffsNormalized: PersonNormalized[];
  seatNormalized: SeatNormalized;
  shareholdingDiffsNormalized: ShareHoldingDiffNormalized[];
}

// Virtual definitions
companyDiffSchema.virtual('personDiffsNormalized').get(function () {
  return this.personDiffs.map((personDiff) => ({
    ...personDiff,
    person: PersonDiff.normalizePerson(personDiff.person),
  }));
});

export const normalizeSeat = function (): SeatNormalized {
  if (isPopulatedIndividual(this.seat?.holder?.individual)) {
    return {
      ...this.seat.holder.individual.domicile.address,
      holderName: PersonDiff.normalizePerson(this.seat.holder).name,
      holderType: DomicileHolderType.Individual,
    };
  }

  if (isPopulatedCompany(this.seat?.holder?.company)) {
    return {
      ...this.seat?.address!,
      holderName: this.seat?.holder?.company.name,
      holderType: DomicileHolderType.Company,
    };
  }

  return {
    ...this.seat?.address!,
    holderName: undefined,
    holderType: undefined,
  };
};

companyDiffSchema.virtual('seatNormalized').get(normalizeSeat);

companyDiffSchema.virtual('shareholdingDiffsNormalized').get(function (): ShareHoldingDiffNormalized[] {
  return this.shareholdingDiffs.map((shareholdingDiff) => ({
    ...shareholdingDiff,
    holder: PersonDiff.normalizePerson(shareholdingDiff.holder),
  }));
});

companyDiffSchema.plugin(mongooseLeanVirtuals);

export type CompanyDiff = CompanyDiffModelInterface;

export interface PopulatedCompanyDiff extends CompanyDiff {
  fulfillment?: MongooseObjectId;
  client?: MongooseObjectId;
}

export { companyDiffSchema };

Setup:

  "dependencies": {
    "@forestadmin/agent": "1.60.0",
    "@forestadmin/datasource-mongoose": "1.11.1",
    "@forestadmin/datasource-replica": "latest",
    "@forestadmin/plugin-flattener": "latest",
    "@googlemaps/google-maps-services-js": "^3.4.0",
    "@notionhq/client": "^2.2.15",
    "axios": "^1.4.0",
    "core-js": "^3.37.0",
    "cron": "^3.1.7",
    "dotenv": "^16.0.1",
    "jsonwebtoken": "^9.0.0",
    "moment": "^2.29.4",
    "mongoose": "^8.7.0",
    "mongoose-lean-virtuals": "^1.1.0",
    "node-cache": "^5.1.2",
    "sqlite3": "^5.1.6",
    "stripe": "^12.10.0",
    "typescript": "^4.9.4"
  },

It seems to be working now, when I took out the virtualizers. Now the application does not crash anymore, but all the collection names I have used previously, now seem to crash, if reuse them with a correct setup. It seems to have contaminated the ForestAdmin api.

Hello @David_Roegiers,

Could you try to bump all @forestadmin packages to their latest versions to benefit from the latest bug fixes.

There was a recent patch on something similar where nestedFields from mongoose presented a similar issue. It might not be the same cause in your case but at least it will increase the chances that it is indeed caused by virtual fields.

1 Like

The problem resurface and disappeared with the upgrade.

1 Like