“EntityMetadataNotFoundError: No metadata for ‘Card’ was found” with Express TypeScript TypeORM app

EntityMetadataNotFoundError: No metadata for “Card” was found.

When I un-comment await cardController.create(newCard); in my User class my TypeORM set up stops working.

Before I explicitly mapped the foreign key column in my card class entity using the @JoinColumn decorator, I was getting this error:

TypeORMError: Entity metadata for Card#owner was not found. Check if you specified a correct entity object and if it’s connected in the connection options.

I am using the following dependencies on my package.json.

"express": "^4.17.1",
"@types/express": "^4.17.17",
"typescript": "^4.4.4",
"typeorm": "^0.3.20"

Links I already consulted:

My guess is that there should be an error with circular dependencies affecting metadata resolution, but I am not sure how to resolve it, I thought I did by using aliases in the path and defining an index for all entities, this way when another entity imports from the same index, the resolution path should avoid circular imports ?

Outputs

When line is commented

DB Connection Stablished Correctly, Entities and Metada Columns Loaded Ok

When comment is removed

Connection error, no metadata for Card entity

DB TypeORM Settings

I already made sure that the values obtained from the .env file are ok. Also, the import paths resolve correctly, and every entity file on my app use the same @entities shortcut path.

Also, I already tried declaring a cardStore that is the returned object from getting the Card repository associated to the DBContextSource object, and adding this line doesn’t throw an error.

const cardStore = DBContextSource.getRepository(Card);
export default DBContextSource;

I can guarantee all of that because the connection to the DB is done correctly when I don’t use the cardController.create() method.

import { ServerError } from "@errors";
import { DataSource, DataSourceOptions } from "typeorm";

const type          = "postgres";
const username = process.env.DB_USER;
const password = process.env.DB_PASSWORD;
const host         = process.env.DB_HOST;
const port         = parseInt(process.env.DB_PORT ?? "0");
const database = process.env.DB_NAME;

if(!username) throw new ServerError("Must provide a username environment variable");
if(!password) throw new ServerError("Must provide a password environment variable");
if(!host)     throw new ServerError("Must provide a host environment variable");
if(port <= 0) throw new ServerError("Must provide a port environment variable");
if(!database) throw new ServerError("Must provide a database environment variable");

import { User, Card } from "@entities";

const TypeORMConfig: DataSourceOptions = {
    type,
    host,
    port,
    username,
    password,
    database,
    synchronize: true,
    logging: true,
    entities: [ User, Card ],
    subscribers: [],
    migrations: []
};

const DBContextSource = new DataSource(TypeORMConfig);

(async () => {
    await DBContextSource.initialize();
    console.log("DB connection successfully established!");
    console.log(`Entities loaded: ${DBContextSource.entityMetadatas.map((e) => e.name)}`);
    console.log(DBContextSource.getMetadata(Card).columns.map((col) => col.propertyName));
})().catch((error) => console.log(error));

export default DBContextSource;

Endpoint

I already made sure the attribute values in newCard are set correctly, so assume newCard has all the required properties when await user.addCard(newCard) line is executed.

import { Router } from "express";
import { User, Card } from "@entities";
import { CreateCardPayload } from "@common/types/cards";

const router = Router();

router.post("/", async (req, res, next) => {
    try {
        const user: User = req.userData;
        const options = req.body as CreateCardPayload;
        const newCard: Card = new Card(options, parsedType);
        
        // add card to user array of cards
        await user.addCard(newCard);

        return res.status(200).json(newCard);
    } catch(error) { return next(error); }
});

Entities

User Class

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Card } from "@backend/lib/entities";
import cardController   from "@entities/cards/cardController";

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    public readonly id!: number; // Assertion added since TypeORM will generate the value hence TypeScript does eliminates compile-time null and undefined checks
    @Column()
    public readonly email!: string;
    @Column()
    public readonly password!: string;

    // One-to-Many relationship: A user can have many cards
    // eager: when i fetch the user, typeorm will automatically fetch all cards
    @OneToMany(() => Card, (card) => card.owner, {
        eager: true
    })
    public cards!: Card[];

    // TypeORM requires that entities have parameterless constructors (or constructors that can handle being called with no arguments).
    constructor(email?: string, password?: string) {
        if(email && password) {
            this.email    = email;
            this.password = password;
            this.cards    = [];
        }
    }

    public async addCard(newCard: Card) {
        newCard.owner = this;
        // save card in db
        await cardController.create(newCard);

        // save card locally to persist data and avoid re fetch
        this.cards.push(newCard);
    }
}

Card Class

import { Entity, PrimaryColumn, Column, ManyToOne, JoinColumn, Index } from "typeorm";
import { CreateCardPayload } from "@common/types/cards";
import { User } from "@entities";

@Entity()
export class Card {
    // Assertion! added since TypeORM will generate the value hence TypeScript does eliminates compile-time null and undefined checks
    @PrimaryColumn()
    public cardNumber!: string; // id

    // Many-to-One relationship: A card belongs to one user, but a user can have many cards
    // Since querying by owner is frequent, adding database indexes to improve performance
    @ManyToOne(() => User, (user) => user.cards, { nullable: false })
    @JoinColumn({ name: "ownerId" }) // Explicitly map the foreign key column
    @Index()
    public owner!: User;
    @Column()
    public ownerId!: number; // Explicitly define the foreign key column

    // TypeORM requires that entities have parameterless constructors (or constructors that can handle being called with no arguments).
    public constructor(options?: CreateCardPayload) {
        if(options) {
            this.cardNumber = options.cardNumber;
            this.issuer     = options.issuer;
        }
    }
}

Card Controller (Data Mapper)

import { BadRequestError } from "@backend/lib/errors";
import { Card }  from "@entities";
import DBContextSource from "@db";

class CardController {
    protected cardStore = DBContextSource.getRepository(Card);

    // WHEN THIS METHOD IS CALLED THE ERROR IS THROWN.
    public async create(newCard: Card) {
        console.log(newCard);
        const foundCard = await this.getByCardNumber(newCard.cardNumber);
        if(foundCard) {
            throw new BadRequestError(`A card with the same "${newCard.cardNumber}" card number already exists.`);
        }
        await this.cardStore.save(newCard);
    }

    public async getByCardNumber(cardNumber: string) {
        return await this.cardStore.findOne({
            where: {
                cardNumber: cardNumber
            }
        });
    }

    public async getUserCards(userId: number) {
        return await this.cardStore.find({
            where: {
                owner: {
                    id: userId
                }
            },
            relations: [ "owner" ] // Explicitly load the `owner` relation
        });
    }
}

const cardController = new CardController();

export default cardController;