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.
The problem resurface and disappeared with the upgrade.