InteractiveFrameworks

Entities and Repositories

Move the cats into a database: connect with TypeOrmModule, turn the Cat class into an entity, register it so a repository can be injected, and write the service against find, save, delete and the query builder.

What you'll learn

  • Connect an application with TypeOrmModule.forRoot() and explain synchronize and autoLoadEntities
  • Define an entity with @Entity, @PrimaryGeneratedColumn and @Column, and register it with forFeature so its repository is injectable
  • Use a repository's find, findOneBy, save and delete, decide where a 404 comes from, and write an aggregate with the query builder

Every cat so far has lived in an array inside CatsService, which means every cat dies with the process. A real service keeps its data in a database, and the question this lesson answers is how Nest talks to one without the database leaking into every controller and service. The docs' answer is TypeORM: an object-relational mapper that turns a class into a table, and gives each entity a repository with find, save and delete so the service works with objects and the mapper writes the SQL.

Connecting

@nestjs/typeorm wraps TypeORM in a module. The root module opens the connection:

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

The type selects the driver; the docs use MySQL, and this runtime uses 'sqljs', SQLite compiled to WebAssembly, which needs no host, port or credentials and keeps the database in memory for the length of a run. Everything else in this chapter is identical whichever driver you pick, which is the point of an ORM. synchronize: true creates or alters tables to match the entities at every boot: perfect for development and a lesson, dangerous in production, where a renamed property would drop a column; there, migrations replace it. autoLoadEntities: true spares you the entities list: every entity a feature module registers is included.

Two options you will meet in the wild: retryAttempts and retryDelay retry a failing connection before giving up, and forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config) => ({ ... }) }) builds the same options from configuration, as lesson 1 taught.

Entities

An entity is a class whose instances are rows:

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column({ default: true })
  isActive: boolean;
}

@Entity() names the table after the class. @PrimaryGeneratedColumn() is an auto-incremented primary key; @Column() maps a property to a column whose type is inferred from the TypeScript type, with options for default, unique, nullable, length and the rest. What is not decorated is not a column: a plain property stays in memory only.

Repositories

A feature module registers the entities it works with, and that registration is what makes their repositories injectable:

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
})
export class UsersModule {}

@Injectable()
export class UsersService {
  constructor(@InjectRepository(User) private readonly users: Repository<User>) {}

  findAll(): Promise<User[]> {
    return this.users.find();
  }

  findOne(id: number): Promise<User | null> {
    return this.users.findOneBy({ id });
  }

  async remove(id: number): Promise<void> {
    await this.users.delete(id);
  }
}

@InjectRepository(User) names the token, because Repository<User> is generic and the type alone does not say which entity. Then: create() builds an instance without saving it, save() inserts or updates and returns the entity with its generated id, find() takes where, order, relations and take, findOneBy() returns the row or null, delete() returns a result whose affected says how many rows went. Nothing is a 404 until your service decides it is.

For a query that is not a lookup, the query builder writes SQL by method calls and returns raw rows:

this.users.createQueryBuilder('user').select('COUNT(*)', 'count').getRawOne();

Your task

The cats move into the database.

  1. In AppModule, connect with TypeOrmModule.forRoot(): type sqljs, entities loaded automatically, schema synchronised.
  2. cats/cat.entity.ts becomes an entity: a generated primary key, name, age and adoptable columns, the last defaulting to true.
  3. CatsModule registers the entity, so that the Repository<Cat> the service already asks for exists.
  4. In CatsService, findOne looks a cat up and turns null into a 404; remove turns "nothing deleted" into the same 404; stats counts the cats and averages their ages in one query with the query builder.

Run before touching anything: the service asks for a repository nobody provides, and Nest tells you so, with the token's name.

When it fails

  • Nest can't resolve dependencies of the CatsService (?), argument CatRepository: forFeature([Cat]) is missing from CatsModule, or forRoot is missing altogether.
  • Entity metadata for Cat was not found or No metadata for "Cat" was found: the class has no @Entity(), or it was not registered and autoLoadEntities is off.
  • Entity "Cat" does not have a primary column: @PrimaryGeneratedColumn() is missing; every entity needs a primary key.
  • adoptable is missing from every cat: the property has no @Column(), so TypeORM does not know it is a column.
  • DELETE /cats/9 answers 204: delete() does not throw for a missing row; check result.affected.

Remember

  • TypeOrmModule.forRoot() connects once; type: 'sqljs' here, the same code with any driver.
  • @Entity(), @PrimaryGeneratedColumn() and @Column() turn a class into a table; synchronize builds it, in development only.
  • forFeature([Entity]) in a module makes @InjectRepository(Entity) work in that module.
  • A repository never throws for a missing row: findOneBy returns null, delete returns affected.
Stuck? Show a hint

app.module.ts: TypeOrmModule.forRoot({ type: 'sqljs', autoLoadEntities: true, synchronize: true }). cat.entity.ts: @Entity() on the class, @PrimaryGeneratedColumn() on id, @Column() on name and age, @Column({ default: true }) on adoptable. cats.module.ts: imports: [TypeOrmModule.forFeature([Cat])]. Service: findOneBy({ id }) and throw when null; delete(id) and throw when result.affected is 0; createQueryBuilder('cat').select('COUNT(*)', 'count').addSelect('AVG(cat.age)', 'averageAge').getRawOne().