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.