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:
- No metadata for “User” was found using TypeOrm
- Entity metadata for Role#users was not found
- No metadata for “User” was found using TypeOrm
- https://github.com/typeorm/typeorm/issues/8920
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
When comment is removed
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;