Many-to-one / One-to-many ๊ด๊ณ๋ A๊ฐ B์ ์ฌ๋ฌ ์ธ์คํด์ค๋ฅผ ํฌํจํ์ง๋ง B๋ A์ ์ธ์คํด์ค๋ฅผ ํ๋๋ง ํฌํจํ๋ ๊ด๊ณ์ด๋ค.
// Category Entity
@Field((type) => [Restaurant])
@OneToMany((type) => Restaurant, (restaurant) => restaurant.category)
restaurants: Restaurant[];
// Restaurant Entity
@Field((type) => Category)
@ManyToOne((type) => Category, (category) => category.restaurants)
category: Category;
์คํค๋ง์ ์ด๋ฆ์ ๊ณ ์ ํด์ผํ๋ค. ํ์ง๋ง ๊ฐ Entity์์ @ObjectType ๊ณผ @InputType ๊ฐ ๊ฐ์ ์ด๋ฆ์ผ๋ก ์คํค๋ง๊ฐ ๋๋ค. ์๋์ ์๋ฌ ํด๊ฒฐ์ ์ํด์๋ @InputType ์ ๋ฐ๋ก ์ด๋ฆ์ ๋ถ์ฌํด์ผํ๋ค.
Error: Schema must contain uniquely named types but contains multiple types named "Category".
@InputType('CategoryInputType', { isAbstract: true })
@ObjectType()
@Entity()
export class Category extends CoreEntity {
//.....
}
2๊ฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ ์ด๋ธ์ ์ฐ๊ฒฐํ์ ๋, ํ ์ชฝ์ ํ ์ด๋ธ ์์๋ฅผ ์ญ์ ํ์ ๊ฒฝ์ฐ ์ฐ๊ฒฐ๋ ์ชฝ์ ํ ์ด๋ธ์ ์ํ๋ฅผ ๊ฒฐ์ ํ ์ ์๋ค.
https://orkhan.gitbook.io/typeorm/docs/relations#relation-options
@Field((type) => Category, { nullable: true })
@ManyToOne((type) => Category, (category) => category.restaurants, {
nullable: true,
onDelete: 'SET NULL',
})
category: Category;
Resolver๋ ๊ฐ๊ฐ ๋ค๋ฅธ ๊ถํ ์ฒด๊ณ๋ฅผ ๊ฐ์ง ์ ์๋ค. ์ ์ฐํ๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ฐฉ์์ผ๋ก ๊ถํ ์ฒด๊ณ๋ฅผ ๊ตฌํํ๊ธฐ ์ํด์ metadata๊ฐ ์ฌ์ฉ๋๋ค. Nest๋ @SetMetadata() ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ํตํด Resolver์ ์ปค์คํ
๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ถ์ด๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.
https://docs.nestjs.com/guards#setting-roles-per-handler
route์์ ์ง์ @SetMetadata()๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ์ข์ ์ต๊ด์ด ์๋๋ค.
๋์ ์๋์ ๊ฐ์ด ์ง์ ์ปค์คํ
๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ง๋ค ์ ์๋ค.
//role decorator
import { SetMetadata } from '@nestjs/common';
import { UserRole } from 'src/users/entities/user.entity';
type AllowedRoles = keyof typeof UserRole | 'Any';
export const Role = (roles: AllowedRoles) => SetMetadata('roles', roles);
๋ค์๊ณผ ๊ฐ์ด APP_GUARD๋ฅผ ์ฌ์ฉํ ์ ๋ชจ๋ resolver์ ๋ํด Guard๊ฐ ์ ์ฉ๋ฉ๋๋ค.
//app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth.guard';
@Module({
providers: [
{
provide: APP_GUARD,
useValue: AuthGuard,
},
],
})
export class AuthModule {}
https://docs.nestjs.com/guards#binding-guards
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext) {
const roles = this.reflector.get<AllowedRoles>(
'roles',
context.getHandler(),
);
if (!roles) {
return true;
}
const gqlContext = GqlExecutionContext.create(context).getContext();
const user: User = gqlContext['user'];
if (!user) {
return false;
}
if (roles.includes('Any')) {
return true;
}
return roles.includes(user.role);
}
}
Field์ ํน์ relation์ id๋ฅผ ๋ก๋ํ๋ค. ์๋ฅผ ๋ค์ด Restuarant ์ํฐํฐ์ Many-to-one์ด ์๋ ๊ฒฝ์ฐ ์ ์์ฑ์ @RelationId๋ก ํ์ํ์ฌ ์ Relation ID๋ฅผ ๊ฐ์ง ์ ์๋ค. ์ด ๊ธฐ๋ฅ์ many-to-many๋ฅผ ํฌํจํ ๋ชจ๋ ์ข ๋ฅ์ ๊ด๊ณ์์ ์๋ํ๋ค. Relation ID๋ ์ถ๊ฐ/์ ๊ฑฐ/๋ณ๊ฒฝ๋์ง ์๋๋ค.
//Restaurant Entity
@Field((type) => User)
@ManyToOne((type) => User, (user) => user.restaurants, {
onDelete: 'CASCADE',
})
owner: User;
@RelationId((restaurant: Restaurant) => restaurant.owner)
ownerId: number;
๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ์ํ ๋ฉ์๋๋ฅผ ํฌํจํด์ผ ํ๋ ์ฌ์ฉ์ ์ ์ ๋ ํฌ์งํ ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๋ค.
์ผ๋ฐ์ ์ผ๋ก ์ฌ์ฉ์ ์ง์ ๋ฆฌํฌ์งํ ๋ฆฌ๋ ๋จ์ผ ์ํฐํฐ์ ๋ํด ์์ฑ๋๊ณ ํน์ ์ฟผ๋ฆฌ๋ฅผ ํฌํจํฉ๋๋ค.
์๋ฅผ ๋ค์ด, ์๋์ ๊ฐ์ด getOrCreate(name: string)์ด๋ผ๋ ๋ฉ์๋๊ฐ ์๋ค๊ณ ๊ฐ์ ํ๋ฉด this.categories.getOrCreate(...)์ ๊ฐ์ด ํธ์ถํ ์ ์๋ค.
// @EntityRepository(Category)
@CustomRepository(Category)
export class CategoryRepository extends Repository<Category> {
async getOrCreate(name: string): Promise<Category> {
const categoryName = name.trim().toLowerCase();
const categorySlug = categoryName.replace(/ /g, '-');
let category = await this.findOne({ where: { slug: categorySlug } });
if (!category) {
category = await this.save(
this.create({ slug: categorySlug, name: categoryName }),
);
}
return category;
}
}
TypeOrm 0.2.45๊น์ง๋ @EntityRepository๋ฅผ ์ฌ์ฉํ์ง๋ง deprecated ๋๋ฉด์ ์ง์ decorator๋ฅผ ์์ฑํด์ผํ๋ค.
https://stackoverflow.com/questions/71557301/how-to-workraound-this-typeorm-error-entityrepository-is-deprecated-use-repo
๋ค์ด๊ทธ๋ ์ด๋ํด๋ ์๋ฌ stackoverflow๋๋ก ํด๋ ์๋ฌ 8์๊ฐ ์ก์๋จน์...
์ธ์์ด ๋ ์ต๊นํ๋คใ ใ ใ ์ผ๋จ ์ง๋ ๋๊ฐ๊ณ ๋ค์์ ๊ณ ์น์..
๋งค request๋ง๋ค ๊ณ์ฐ๋ field๊ฐ์ ๊ฐ์ ธ์จ๋ค.
(DB์๋ ์กด์ฌํ์ง ์๊ณ GraphQL ์คํค๋ง์๋ง ์กด์ฌ)
@Resolver((of) => Category)
export class CategoryResolver {
constructor(private readonly restaurantService: RestaurantService) {}
@ResolveField((type) => Int)
restaurantCount(): number {
return 80;
}
@Query((returns) => AllCategoriesOutput)
allCategories(): Promise<AllCategoriesOutput> {
return this.restaurantService.allCategories();
}
}
const [restaurants, totalResults] = await this.restaurants.findAndCount({
where: {
name: Like(`%${query}%`),
},
});
์์ ์ฝ๋๋ ์๋ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ค.
SELECT * FROM "post" WHERE "title" LIKE '%{query}%'
https://orkhan.gitbook.io/typeorm/docs/find-options#advanced-options
https://github.com/typeorm/typeorm/blob/master/docs/find-options.md#advanced-options
Like๋ ๋์๋ฌธ์๋ฅผ ๊ตฌ๋ถํ์ง๋ง ILike๋ ๋์๋ฌธ์๋ฅผ ๊ตฌ๋ถํ์ง ์๋๋ค.
const [restaurants, totalResults] = await this.restaurants.findAndCount({
where: {
name: Raw(name => `${name} ILIKE '%${query}%'`),
},
SQL LIKE ์ ์ wildcard ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ์ ์ ์ฌํ ๊ฐ๊ณผ ๋น๊ตํ๋ ๋ฐ ์ฌ์ฉ๋๋ค. ํผ์ผํธ ๊ธฐํธ(%)๋ 0, ํ๋ ๋๋ ์ฌ๋ฌ ๋ฌธ์๋ฅผ ๋ํ๋ธ๋ค. ๋ฐ์ค(_)์ ๋จ์ผ ์ซ์ ๋๋ ๋ฌธ์๋ฅผ ๋ํ๋ธ๋ค. ์ด๋ฌํ ๊ธฐํธ๋ ์กฐํฉํ์ฌ ์ฌ์ฉํ ์ ์๋ค.
EX )
WHERE SALARY LIKE '200%' : 200์ผ๋ก ์์ํ๋ ๋ชจ๋ ๊ฐ์ ์ฐพ์ต๋๋ค.
WHERE SALARY LIKE '%200%': ์ด๋ ์์น๋ 200์ด ์๋ ๊ฐ์ ์ฐพ์ต๋๋ค.
WHERE SALARY LIKE '_00%': ๋ ๋ฒ์งธ ๋ฐ ์ธ ๋ฒ์งธ ์์น์ 00์ด ์๋ ๊ฐ์ ์ฐพ์ต๋๋ค.
WHERE SALARY LIKE '%2': 2๋ก ๋๋๋ ๊ฐ์ ์ฐพ์ต๋๋ค.
https://www.tutorialspoint.com/sql/sql-like-clause.htm