Chapter 5: Associations

Full Stack TypeScript

link Associations

In this section we will associate the User and Place models. TypeORM makes it easy to create associations. Let's get started!

By the end of this lesson, developers will be able to:

  • Create a One to Many relationship
  • Fetch related data from associated models

Introduction

Databases are responsible for storing records. In this section, we learn how our records can reference one another.

link Entity Relationship Diagram (ERD)

For any associated models, we would want to plot our Entity Relationship Diagram, or ERD. This allows us to visualize the relationships in our project.

Click here to see our project's Entity Relationship Diagram.

link User and Places

Every User will be allowed to 'own' multiple Place entities. Let's create a OneToMany association in entity/User.ts.

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  BaseEntity,
  OneToMany
} from 'typeorm';
import { Field, ObjectType } from 'type-graphql';
import { Place } from './Place';

{/* ... */}

@Field(() => [Place])
  @OneToMany(
    () => Place,
    places => places.user,
    {
      eager: true
    }
  )
  places: Place[];

We will also need to establish the inverse relationship. Let's add a ManyToOne association inside entity/Place.

import {
  Entity,
  BaseEntity,
  PrimaryGeneratedColumn,
  Column,
  ManyToOne
} from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';
import { User } from './User';

{/* ... */}

@Field({ nullable: true })
  @ManyToOne(
    () => User,
    user => user.places
  )
  user?: User;

link Modify PlaceResolver

Each time a user creates a new Place, we will pass their userId into the resolver and associate them with the new place.

At the top level imports of resolvers/PlaceResolver.ts, add the getUserId method and entity/User.ts.

import { getUserId } from '../utils';
import { User } from '../entity/User';

Then modify your resolvers with the following changes:

@Resolver(() => Place)
export class PlaceResolver {
  // @Query assume certain properties on a given class or function
  @Query(() => Place, { nullable: true })
  async place(@Arg('id') id: number): Promise<Place | undefined> {
    return await Place.findOne(id, { relations: ['user'] });
  }

  @Query(() => [Place], {
    description: 'Get all places from around the world'
  })
  async places(): Promise<Place[]> {
    const places = await Place.find({ relations: ['user'] });
    return places;
  }

  @Mutation(() => Place)
  async createPlace(
    @Arg('place') placeInput: PlaceInput,
    @Ctx() ctx: { req: Request }
  ): Promise<Place> {
    const userId = getUserId(ctx);
    if (userId) {
      const place = plainToClass(Place, {
        description: placeInput.description,
        title: placeInput.title,
        imageUrl: placeInput.imageUrl,
        creationDate: new Date()
      });
      const user = await User.findOne(userId);
      const newPlace = await Place.create({
        ...place,
        user
      }).save();
      return newPlace;
    }
    throw new Error('User not found');
  }

  @Mutation(() => Place)
  async updatePlace(
    @Arg('place') placeInput: PlaceInput,
    @Ctx() ctx: { req: Request }
  ): Promise<Place> {
    const userId = getUserId(ctx);
    if (userId) {
      const { id, title, description, imageUrl } = placeInput;
      const place = await Place.findOne({
        where: { id, user: { id: userId } },
        relations: ['user']
      });
      if (place) {
        place.title = title;
        place.description = description;
        place.imageUrl = imageUrl;
        place.save();
        return place;
      }
      throw new Error('Place not found');
    }
    throw new Error('User not found');
  }

  @Mutation(() => String)
  async deletePlace(
    @Arg('id') id: number,
    @Ctx() ctx: { req: Request }
  ): Promise<Number | undefined> {
    const userId = getUserId(ctx);
    if (userId) {
      const deleted = await Place.delete({ id, user: { id: userId } });
      if (deleted) {
        return id;
      }
      throw new Error('Place not deleted');
    }
    throw new Error('User not found');
  }
}

Given these changes, we should be able to include a user each time we create a new place. Let's revisit the GraphQL playground and try this out.

Drop Database

In case you were running into any non-null errors, be sure to delete your database.sqlite.

link Test Server

yarn start

Now let's try a new place mutation:

mutation CreatePlace($title: String!, $description: String!, $imageUrl: String!){
  createPlace(place: {
    title: $title
    description: $description,
    imageUrl: $imageUrl
  }) {
    title
    description
    imageUrl
    creationDate
    user {
      id
      username
    }
  }
}

Insert the input parameters as follows:

{
  "title":"Central Park",
  "description": "The Great Lawn",
  "imageUrl": "https://images.unsplash.com/photo-1568515387631-8b650bbcdb90"
}

Insert the request headers as follows:

{
    "Authorization": "Bearer <TOKEN>"
}

If this outputs a new place, success!

Now try to run the following:

query Places {
  places {
    id
    title
    description
    creationDate
    imageUrl
    user {
      id
      username
    }
  }
}

With this new list of places, we should also see each place's associated user.

WELL DONE!

link Wrapping Up

Today we created associative logic to create users and display places. Up next, we round off the full CRUD functionality from the client side.

format_list_bulleted
help_outline