InteractiveFrameworks

Persistence

Move the cats into a database without changing a single response, then add adoptions: a second table related to the first, and an adoption that marks the cat and records itself in one transaction, rolled back whole when a rule refuses it.

What you'll learn

  • Replace an in-memory store with TypeORM repositories behind the same service, so the API does not change
  • Seed a fresh database when a module starts, with OnModuleInit
  • Relate two entities with @ManyToOne, typed with Relation<> so the files can import each other safely, and load the relation with every read
  • Make a multi-step change all or nothing with DataSource.transaction(), and prove the rollback by reading the data back

The skeleton keeps its cats in an array, which forgets everything when the process restarts and cannot answer two questions at once honestly. A shelter's records belong in a database. This lesson moves them there without changing a single response, which is the test of a good service layer: the controllers and every client keep working, and only the service learns about tables. Then it adds the shelter's first piece of real business: adoptions, where two writes must happen together or not at all.

Same API, new store

TypeORM over SQLite (the sqljs driver, in memory here, a file or Postgres in production) replaces the array. TypeOrmModule.forRoot({ type: 'sqljs', autoLoadEntities: true, synchronize: true }) opens the database; synchronize builds the tables from the entities, fine for a fresh database, never for one with data. Each module registers its entities with TypeOrmModule.forFeature([Cat]), which is also what autoLoadEntities collects, and its service receives @InjectRepository(Cat).

Three details keep the API identical. The table starts empty on every run, so the service seeds Tom and Luna in onModuleInit when count() is zero. Lists need an explicit order: { id: 'ASC' }: a database promises no order otherwise. And save() returns the entity with its columns in an order of its own, generated id last, so a create answered with save()'s result changes shape. Answer every write with a read of the row instead; it costs a query and gives every response the table's shape.

A second table

An adoption records who adopted which cat. It is its own entity with a many-to-one relation to the cat:

@ManyToOne(() => Cat, { eager: true })
cat: Relation<Cat>;

eager loads the cat with every adoption read. Relation<Cat> is TypeORM's wrapper for relation types: it erases to Object in the emitted metadata, so two entity files can import each other without one reading the other's class before it exists, the trap the Techniques course met. This relation is one-way, but the habit costs nothing.

All or nothing

Adopting a cat is two writes: mark the cat adopted, record the adoption. If the second fails after the first succeeded, the database says a cat is adopted by nobody. A transaction makes the writes one unit:

await this.dataSource.transaction(async (manager) => {
  const kennel = await manager.findOneByOrFail(Kennel, { id });
  kennel.occupied += 1;
  await manager.save(kennel);
  await manager.save(manager.create(Booking, { kennel, nights }));
  if (kennel.occupied > kennel.capacity) throw new ConflictException('Kennel is full');
});

Everything done through manager belongs to the transaction. If the callback throws, every write in it is rolled back and the exception continues to the caller, here as a 409; if it returns, they are committed together. The rule is checked after the writes on purpose: some rules can only be checked against the data as it would be, and the rollback makes that safe.

The only proof that a rollback happened is reading the data afterwards. A 409 says the request was refused; it does not say the cat is still available.

Your task

Everything from lesson 1 still holds, with adopted: false on every cat.

  1. Keep the cats in the database: a Cat entity with a generated id, the lesson 1 fields, a nullable breed, and adopted, false by default. Seed Tom and Luna when the table is empty. Every write answers with a read.
  2. An Adoption entity: a generated id, the adopter's name, and its cat, loaded with every adoption.
  3. POST /cats/:id/adopt with { adopter } adopts the cat in a transaction: 404 for a cat the shelter does not have, 409 Tom was already adopted, otherwise the cat marked adopted and the adoption recorded, answered with a read of the adoption.
  4. Nobody adopts more than two cats: a third is a 409 ana has adopted 2 cats already, and the cat stays available.
  5. GET /adoptions lists every adoption, oldest first.

When it fails

  • Nest can't resolve dependencies of the CatsService (?). Please make sure that the argument "CatRepository" at index [0]: the module does not register the entity with TypeOrmModule.forFeature().
  • body {"name":"Milo","age":5,"breed":null,"id":3,"adopted":false}, the right data in the wrong order: the create answers save()'s result instead of a read.
  • GET /cats/4 answers "adopted":true after the refused third adoption: the limit is checked after the transaction committed, so there is nothing left to roll back.
  • The list comes back in another order: find() without order.

Remember

  • The service changes store; the API does not. Seed in onModuleInit, order lists explicitly, answer writes with reads.
  • Type relations with Relation<>, and eager loads them with every read.
  • dataSource.transaction() commits everything done through its manager, or nothing if the callback throws.
  • Prove a rollback by reading the data back.
Stuck? Show a hint

app.module.ts: TypeOrmModule.forRoot({ type: 'sqljs', autoLoadEntities: true, synchronize: true }). Each module: TypeOrmModule.forFeature([Entity]). CatsService: @InjectRepository(Cat), seed in onModuleInit when count() is 0, find with order { id: 'ASC' }, answer writes with findOne(). Adoption: @ManyToOne(() => Cat, { eager: true }) cat: Relation<Cat>. adopt: this.dataSource.transaction(async (manager) => { findOneBy, 404, 409, save the cat, save the adoption, countBy adopter > 2 throws }), then read the adoption back.