About GraphQL
In recent years GraphQL has become very popular due to major advantages it offers against well known REST. Briefly,
- It eliminates over fetching & under fetching problems in REST
- With GraphQL, we don't need to version or api as in REST
- GraphQL imposes an opinionated structure which easily leads to standardization whithin teams
- Although this article is on creating an api, GraphQL is the best friend of frontend developers. The idea behind is to let the UI component decide its data reqirements and send a query to GraphQL to fetch exactly what it needs.
You can refer to graphql-at-paypal-an-adoption-story for a well detailed story
About NestJS
Javascript developers loved Nodejs. Number of projects with Nodejs is getting heigher every day. Using the same language in frontend and also in backend is really awesome. Today we can create our web projects in much more isomorphic manner. This really reduce impedance mismatch between these two worlds.
If we scaffold a barebone development environment, we need to solve many cross-cutting concerns by ourselves. So, using a framework saves us from these headaches.
NestJS is a popular NodeJS framework. It has received near 1 million weekly download on npm and is being used in many projects. So, we can easily say that it's battle tested. It uses typescript out of the box. To me, it's very important for a reliable development framework.
NestJS has good documentation NestJS. You can also read for more detail on NestJS's advantages why-choose-nest-js-over-other-node-frameworks
Let's start
You can find the completed project in Github in branch 07-legacydb
We'll create a countries GraphQL api. Our data model shown below
- One country has many cities
- One city belongs to only one country
- One country can join many treaties
- One treaty has many countries
As can be noted, while there is a one-to-many relation between country & city, there is a many-to-many relation between country & treaty
Project creation
NestJS has a very practical cli. For many tasks we'll be using it. Initially we need to install NestJS globally
npm install -g @nestjs/cli
after installation we can check the version nest -v
To create a new project nest new countries-graphql
. It'll ask your preferred package manager utility (npm or yarn) that's all.
Let's open countries-graphql directory (preferably in VSCode). As you see, a full featured project structure created. No need to bother with linting or typescript. Let's delete test directory, src\app.controller.spec.ts
, src\app.controller.ts
. Our initial project looks like below
We should be able to run with yarn start:dev
In package.json file's script part has all the necessary scripts ready for starting or building the project. Very nice, we don't need to bother about watching changes etc...
As seen, there are src\app.module.ts
and src\app.service.ts
. NestJS imposes a module bases structure. While our application itself is a module, all our business logics will be in their own module. This leads to clean domain structure and all business logic will be created in its own service.
GraphQL Code First
There are two approches for creating GraphQL schema;
- Schema / SDL (Schema Definition Language) first
- Code first
In the first approach, you define your GraphQL schema in SDL. Then you generate stub resolvers and add your code there.
The second approach is based on generating the schema from your code. i.e., you're not writing any SDL here. Code first is more developer friendly and we'll be following code first in this article.
Let's install some packages;
yarn add graphql @nestjs/graphql apollo-server-express
We'll add country, city and treaty modules. nest cli is very clever and lets you create various resource types. You see the list with nest --help
┌───────────────┬─────────────┬──────────────────────────────────────────────┐
│ name │ alias │ description │
│ application │ application │ Generate a new application workspace │
│ class │ cl │ Generate a new class │
│ configuration │ config │ Generate a CLI configuration file │
│ controller │ co │ Generate a controller declaration │
│ decorator │ d │ Generate a custom decorator │
│ filter │ f │ Generate a filter declaration │
│ gateway │ ga │ Generate a gateway declaration │
│ guard │ gu │ Generate a guard declaration │
│ interceptor │ in │ Generate an interceptor declaration │
│ interface │ interface │ Generate an interface │
│ middleware │ mi │ Generate a middleware declaration │
│ module │ mo │ Generate a module declaration │
│ pipe │ pi │ Generate a pipe declaration │
│ provider │ pr │ Generate a provider declaration │
│ resolver │ r │ Generate a GraphQL resolver declaration │
│ service │ s │ Generate a service declaration │
│ library │ lib │ Generate a new library within a monorepo │
│ sub-app │ app │ Generate a new application within a monorepo │
│ resource │ res │ Generate a new CRUD resource │
└───────────────┴─────────────┴──────────────────────────────────────────────┘
We'll create our modules as resource
. Use the command nest g resource country --no-spec
We're not creating tests in this article, so --no-spec
flag prohibits test files from being generated
nest cli asks what kind of resource to ceate. As you can see, NestJS offers wide variaty of options. Let's pick GraphQL code first
Then we'be asked wether to ceate CRUD end points. Select yes.
Would you like to generate CRUD entry points? (Y/n)
Eventually, our country module has beed created. It's a full fledged module with its service, resolver, entities, dto.
NestJS added CountryModule added to app module as import. Every modules should be imported by app module;
src/app.module.ts
import { Module } from '@nestjs/common';
import { AppService } from './app.service';
import { CountryModule } from './country/country.module';
@Module({
imports: [CountryModule],
providers: [AppService],
})
export class AppModule {}
We need to import GraphQLModule in app.module.ts and tell it we're using code first;
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppService } from './app.service';
import { CountryModule } from './country/country.module';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'schema.gql'),
}),
CountryModule,
],
providers: [AppService],
})
export class AppModule {}
Let's run with yarn start:dev
. If everything goes well, your app should be running and schema file should be generated;
src/schema.gql
# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type Country {
"""Example field (placeholder)"""
exampleField: Int!
}
type Query {
country(id: Int!): Country!
}
type Mutation {
createCountry(createCountryInput: CreateCountryInput!): Country!
updateCountry(updateCountryInput: UpdateCountryInput!): Country!
removeCountry(id: Int!): Country!
}
input CreateCountryInput {
"""Example field (placeholder)"""
exampleField: Int!
}
input UpdateCountryInput {
"""Example field (placeholder)"""
exampleField: Int
id: Int!
}
This is our schema file generated in SDL. As we proceed to created our resolvers, this file will be updated automatically.
Navigate to http://localhost:3000/graphql
. NestJS uses graphql playground by default. It's a lovely GraphQL IDE. We can check our schema here.
Complete GraphQL Schema
We have a basic GraphQL schema. Let's complete it with actual types. Initially, we'll create city and treaty modules as we did for country.
Run nest g resource city --no-spec
& nest g resource treaty --no-spec
. Note that, these two new modules are added to app.module.ts
. Let's update country, city & treaty entities as below;
src/country/entities/country.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { City } from './../../city/entities/city.entity';
import { Treaty } from './../../treaty/entities/treaty.entity';
@ObjectType()
export class Country {
@Field(() => Int)
id: number;
@Field()
name: string;
@Field(() => Int, { nullable: true })
population: number;
@Field(() => [City], { nullable: true })
cities: City[];
@Field(() => [Treaty], { nullable: true })
treaties: Treaty[];
@Field(() => City, { nullable: true })
capital: City;
}
src/city/entities/city.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class City {
@Field(() => Int)
id: number;
@Field()
name: string;
@Field({ nullable: true })
touristic: boolean;
@Field(() => Int, { nullable: true })
population: number;
}
src/treaty/entities/treaty.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Country } from './../../country/entities/country.entity';
@ObjectType()
export class Treaty {
@Field(() => Int)
id: number;
@Field({ nullable: false })
name: string;
@Field(() => [Country], { nullable: true })
countries: Country[];
}
NestJS uses decorators to includes a class and its properties in GraphQL schema. @ObjectType()
converts this class to schema type. @Field()
decorator adds the selected property of the class to its parent schema type. While class itself is in typescript syntax, decorators use SDL syntax. Let's examine Treaty
class;
@Field(() => Int)
id: number;
we use number
for numeric datatypes in typescript. However, in GraphQL schema we want to make it an integer. So, in decorator we let NestJS know this.
GraphQL SDL has these scalar types
Int
, Float
, String
, Boolean
, ID
For the name field, on the other hand, we don't need to explicitly indicate an SDL type. GraphQL converts string
to String
. We can set some properties to fields. Here we set { nullable: false }
@Field({ nullable: false })
name: string;
A treaty has joined countries. It's an array. Note that, we indicate an array of country differently in typescript & in SDL
@Field(() => [Country], { nullable: true })
countries: Country[];
Let's start the app and observe the schema update.
Adding TypeORM
Let's stop nestjs and install some packages;
yarn add typeorm @nestjs/typeorm pg
we're using Postgresql. So, need to install pg
. In the final part, we'll use mssql as well.
We can keep db connection params in .env
file. This way, we can have our prod db params as environment variables in deployment environment.
.env
DB_NAME=countrydb
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
Now add type orm config file;
ormconfig.js
module.exports = {
name: 'countrydb',
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
database: process.env.DB_NAME,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
entities: ['src/**/*entity.ts'],
migrations: ['src/migrations/**/*.ts'],
logging: false,
cli: {
entitiesDir: 'src',
migrationsDir: 'src/migrations',
},
options: { trustServerCertificate: true },
};
Nest is using naming conventions. If a file is an entity, nest name the file as *.entity.ts
. In all three of our modules you can see them. In this config file we simply give the database connection info and also where to find the entities and where to create the migrations.
Note that, we'll be using our entities as graphql schema models and also as our db models. It's very nice not to create different models for both purposes. To achieve this, we need to add typeorm decorators to our country, city and treaty entities;
todo/////*tALK ABOUT entities and type orm decorators
npm scripts for migrations
nestjs comes with ts-node installed. We'd want to create our migrations using our typescript entities. So, those three weird script should be added to package.json;
"migration:generate": "ts-node ./node_modules/typeorm/cli.js migration:generate -c countrydb -n ",
"migration:run": "ts-node ./node_modules/typeorm/cli.js migration:run -c countrydb",
"migration:revert": "ts-node ./node_modules/typeorm/cli.js migration:revert -c countrydb"
-c flag is for connection name. Since we'll add a second one, we had to name them.
We have three operations about migrations;
migration:generate, creates a new migration comparing the models current state and the databse schema
migration:run, executes the not-yet-executed migrations in the database. The ones already executed, have no effect.
migration:revert, reverts the final migration
Our entities decorated with typeorm decorators are as follows;
src/city/entities/city.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Country } from '../../country/entities/country.entity';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
@ObjectType()
export class City {
@PrimaryGeneratedColumn()
@Field(() => Int)
id: number;
@Column()
@Field()
name: string;
@Column({ nullable: true })
@Field({ nullable: true })
touristic: boolean;
@Column({ nullable: true })
@Field(() => Int, { nullable: true })
population: number;
@ManyToOne(() => Country, (country) => country.cities)
@JoinColumn({ name: 'country_id' })
country: Country;
@Column({ type: 'int', name: 'country_id' })
countryId: number;
}
}
src/country/entities/country.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import {
Column,
Entity,
JoinColumn,
JoinTable,
ManyToMany,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { City } from './../../city/entities/city.entity';
import { Treaty } from './../../treaty/entities/treaty.entity';
@Entity()
@ObjectType()
export class Country {
@PrimaryGeneratedColumn()
@Field(() => Int)
id: number;
@Column()
@Field()
name: string;
@Column({ type: 'int', nullable: true })
@Field(() => Int, { nullable: true })
population: number;
@OneToMany(() => City, (city) => city.country)
@Field(() => [City], { nullable: true })
cities: City[];
@ManyToMany(() => Treaty, (treaty) => treaty.countries, { cascade: true })
@Field(() => [Treaty], { nullable: true })
@JoinTable({
name: 'country_treaty', // table name for the junction table of this relation
joinColumn: {
name: 'country_id',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'treaty_id',
referencedColumnName: 'id',
},
})
treaties: Treaty[];
@OneToOne(() => City)
@Field(() => City, { nullable: true })
@JoinColumn({ name: 'capital_city_id' })
capital: City;
}
}
src/treaty/entities/treaty.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Country } from './../../country/entities/country.entity';
@Entity()
@ObjectType()
export class Treaty {
@PrimaryGeneratedColumn()
@Field(() => Int)
id: number;
@Column()
@Field({ nullable: false })
name: string;
@ManyToMany(() => Country, (country) => country.treaties)
@Field(() => [Country], { nullable: true })
countries: Country[];
}
}
Currently we have an empty countrydb. Let's create our initial migration using yarn migration:generate Init
. Now you must have a migration like below. First part is the timestamp and would change in each run.
src/migrations/1634739033521-Init.ts
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Init1634791876559 implements MigrationInterface {
name = 'Init1634791876559';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "treaty" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_7876e417863f6fa3c9a51d0d3eb" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "country" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "population" integer, "capital_city_id" integer, CONSTRAINT "REL_f3e41ef5df2a6a975986042fdf" UNIQUE ("capital_city_id"), CONSTRAINT "PK_bf6e37c231c4f4ea56dcd887269" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "city" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "touristic" boolean, "population" integer, "country_id" integer NOT NULL, CONSTRAINT "PK_b222f51ce26f7e5ca86944a6739" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "country_treaty" ("country_id" integer NOT NULL, "treaty_id" integer NOT NULL, CONSTRAINT "PK_3e59c9693b624da2b8779527a10" PRIMARY KEY ("country_id", "treaty_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_2010956a26a968fa554b6eb759" ON "country_treaty" ("country_id") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_0ff0d4a234014c46946032aa42" ON "country_treaty" ("treaty_id") `,
);
await queryRunner.query(
`ALTER TABLE "country" ADD CONSTRAINT "FK_f3e41ef5df2a6a975986042fdf9" FOREIGN KEY ("capital_city_id") REFERENCES "city"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "city" ADD CONSTRAINT "FK_08af2eeb576770524fa05e26f39" FOREIGN KEY ("country_id") REFERENCES "country"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "country_treaty" ADD CONSTRAINT "FK_2010956a26a968fa554b6eb7598" FOREIGN KEY ("country_id") REFERENCES "country"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "country_treaty" ADD CONSTRAINT "FK_0ff0d4a234014c46946032aa421" FOREIGN KEY ("treaty_id") REFERENCES "treaty"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "country_treaty" DROP CONSTRAINT "FK_0ff0d4a234014c46946032aa421"`,
);
await queryRunner.query(
`ALTER TABLE "country_treaty" DROP CONSTRAINT "FK_2010956a26a968fa554b6eb7598"`,
);
await queryRunner.query(
`ALTER TABLE "city" DROP CONSTRAINT "FK_08af2eeb576770524fa05e26f39"`,
);
await queryRunner.query(
`ALTER TABLE "country" DROP CONSTRAINT "FK_f3e41ef5df2a6a975986042fdf9"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_0ff0d4a234014c46946032aa42"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_2010956a26a968fa554b6eb759"`,
);
await queryRunner.query(`DROP TABLE "country_treaty"`);
await queryRunner.query(`DROP TABLE "city"`);
await queryRunner.query(`DROP TABLE "country"`);
await queryRunner.query(`DROP TABLE "treaty"`);
}
}
You can run it with yarn migration:run
. After this, our db is supposed to have country
, city
, treaty
, country-treaty
and migrations
tables. migrations table keeps track of your migrations. It now has one record. More to come soon!
id|timestamp |name |
--+-------------+-----------------+
2|1634739033521|Init1634739033521|
For our nest application to start, we need to supply TypeORM connection data from environment variables to app.module.js
. First install nest config module;
yarn add @nestjs/config
and update app.module.ts;
src/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppService } from './app.service';
import { CountryModule } from './country/country.module';
import { CityModule } from './city/city.module';
import { TreatyModule } from './treaty/treaty.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Country } from './country/entities/country.entity';
import { City } from './city/entities/city.entity';
import { Treaty } from './treaty/entities/treaty.entity';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'schema.gql'),
}),
TypeOrmModule.forRoot({
name: 'countrydb',
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
database: process.env.DB_NAME,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
entities: [Country, City, Treaty],
//logging: true,
}),
CountryModule,
CityModule,
TreatyModule,
],
providers: [AppService],
})
export class AppModule {}
logging
is very useful to see generated SQL commands.
Let's start the app. You should have all green;
Resolvers...
Query & mutation declarations reside in resolver files. They construct our GraphQL schema.
src/city/city.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { CityService } from './city.service';
import { City } from './entities/city.entity';
import { CreateCityInput } from './dto/create-city.input';
import { UpdateCityInput } from './dto/update-city.input';
@Resolver(() => City)
export class CityResolver {
constructor(private readonly cityService: CityService) {}
@Mutation(() => City)
createCity(@Args('input') input: CreateCityInput) {
return this.cityService.create(input);
}
@Query(() => [City], { name: 'cities' })
findAll() {
return this.cityService.findAll();
}
@Query(() => City, { name: 'city' })
findOne(@Args('id', { type: () => Int }) id: number) {
return this.cityService.findOne(id);
}
@Mutation(() => City)
updateCity(@Args('input') input: UpdateCityInput) {
return this.cityService.update(input);
}
@Mutation(() => Int, { nullable: true })
removeCity(@Args('id', { type: () => Int, nullable: false }) id: number) {
return this.cityService.remove(id);
}
}
src/country/country.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { CountryService } from './country.service';
import { Country } from './entities/country.entity';
import { CreateCountryInput } from './dto/create-country.input';
import { UpdateCountryInput } from './dto/update-country.input';
@Resolver(() => Country)
export class CountryResolver {
constructor(private readonly countryService: CountryService) {}
@Mutation(() => Country)
createCountry(
@Args('input', { nullable: false })
input: CreateCountryInput,
) {
return this.countryService.create(input);
}
@Query(() => [Country], { name: 'countries' })
findAll() {
return this.countryService.findAll();
}
@Query(() => Country, { name: 'country' })
findOne(@Args('id', { type: () => Int }) id: number) {
return this.countryService.findOne(id);
}
@Mutation(() => Country)
updateCountry(@Args('input') input: UpdateCountryInput) {
return this.countryService.update(input);
}
@Mutation(() => Int, { nullable: true })
removeCountry(@Args('id', { type: () => Int, nullable: false }) id: number) {
return this.countryService.remove(id);
}
@Mutation(() => Country, { name: 'addCountryToTreaty' })
addToTreaty(
@Args('countryId', { type: () => Int, nullable: false }) countryId: number,
@Args('treatyId', { type: () => Int, nullable: false }) treatyId: number,
) {
return this.countryService.addToTreaty(countryId, treatyId);
}
@Mutation(() => Country, { name: 'removeCountryFromTreaty' })
removeFromTreaty(
@Args('countryId', { type: () => Int, nullable: false }) countryId: number,
@Args('treatyId', { type: () => Int, nullable: false }) treatyId: number,
) {
return this.countryService.removeFromTreaty(countryId, treatyId);
}
}
src/treaty/treaty.resolver.ts
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { TreatyService } from './treaty.service';
import { Treaty } from './entities/treaty.entity';
import { CreateTreatyInput } from './dto/create-treaty.input';
import { UpdateTreatyInput } from './dto/update-treaty.input';
@Resolver(() => Treaty)
export class TreatyResolver {
constructor(private readonly treatyService: TreatyService) {}
@Mutation(() => Treaty)
createTreaty(@Args('input') input: CreateTreatyInput) {
return this.treatyService.create(input);
}
@Query(() => [Treaty], { name: 'treaties' })
findAll() {
return this.treatyService.findAll();
}
@Query(() => Treaty, { name: 'treaty' })
findOne(@Args('id', { type: () => Int }) id: number) {
return this.treatyService.findOne(id);
}
@Mutation(() => Treaty)
updateTreaty(@Args('input') input: UpdateTreatyInput) {
return this.treatyService.update(input);
}
@Mutation(() => Treaty)
removeTreaty(@Args('id', { type: () => Int }) id: number) {
return this.treatyService.remove(id);
}
}
Query & mutations are not supposed to contain business logic. Theye are rather like façades to services which contains actual logic. This pattern is very useful as you may need to trigger your logic for somewhere other than query or mutation resolvers, such as background jobs.
Please note that we're injecting services in resolvers constructor. Nestjs has dependency injection out of the box. Writing tests are quite easy this way.
Services
src/city/city.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateCityInput } from './dto/create-city.input';
import { UpdateCityInput } from './dto/update-city.input';
import { City } from './entities/city.entity';
@Injectable()
export class CityService {
constructor(
@InjectRepository(City, 'countrydb') private cityRepo: Repository<City>,
) {}
async create(input: CreateCityInput): Promise<City> {
return await this.cityRepo.save(input);
}
async findAll(): Promise<City[]> {
return await this.cityRepo.find();
}
async findOne(id: number): Promise<City> {
return await this.cityRepo.findOne(id);
}
async update(input: UpdateCityInput): Promise<City> {
let found = await this.cityRepo.findOne(input.id);
return await this.cityRepo.save({ ...found, ...input });
}
async remove(id: number) {
let found = await this.cityRepo.findOne(id);
if (found) {
await this.cityRepo.remove(found);
return id;
} else {
return null;
}
}
}
src/country/country.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Treaty } from 'src/treaty/entities/treaty.entity';
import { Repository } from 'typeorm';
import { CreateCountryInput } from './dto/create-country.input';
import { UpdateCountryInput } from './dto/update-country.input';
import { Country } from './entities/country.entity';
@Injectable()
export class CountryService {
constructor(
@InjectRepository(Country, 'countrydb')
private countryRepo: Repository<Country>,
@InjectRepository(Treaty, 'countrydb')
private treatyRepo: Repository<Treaty>,
) {}
async create(input: CreateCountryInput): Promise<Country> {
return await this.countryRepo.save(input);
}
async findAll(): Promise<Country[]> {
return await this.countryRepo.find({ relations: ['cities', 'treaties'] });
}
async findOne(id: number): Promise<Country> {
return await this.countryRepo.findOne(id);
}
async update(input: UpdateCountryInput): Promise<Country> {
let found = await this.countryRepo.findOne(input.id);
return await this.countryRepo.save({ ...found, ...input });
}
async remove(id: number) {
let found = await this.countryRepo.findOne(id);
if (found) {
await this.countryRepo.remove(found);
return id;
} else {
return null;
}
}
async addToTreaty(countryId: number, treatyId: number): Promise<Country> {
let foundCountry = await this.countryRepo.findOne(
{ id: countryId },
{ relations: ['treaties'] },
);
let foundTreaty = await this.treatyRepo.findOne({ id: treatyId });
if (foundCountry && foundTreaty) {
foundCountry.treaties = foundCountry.treaties
? [...foundCountry.treaties, foundTreaty]
: [foundTreaty];
return this.countryRepo.save(foundCountry);
} else {
throw new Error(`Founding country or treaty problem`);
}
}
async removeFromTreaty(
countryId: number,
treatyId: number,
): Promise<Country> {
let foundCountry = await this.countryRepo.findOne(
{ id: countryId },
{ relations: ['treaties'] },
);
let foundTreaty = await this.treatyRepo.findOne({ id: treatyId });
if (foundCountry && foundTreaty) {
foundCountry.treaties = foundCountry.treaties
? [...foundCountry.treaties.filter((f) => f.id != treatyId)]
: [];
return this.countryRepo.save(foundCountry);
} else {
throw new Error(`Founding country or treaty problem`);
}
}
}
src/treaty/treaty.service.ts
import { Treaty } from './entities/treaty.entity';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateTreatyInput } from './dto/create-treaty.input';
import { UpdateTreatyInput } from './dto/update-treaty.input';
@Injectable()
export class TreatyService {
constructor(
@InjectRepository(Treaty, 'countrydb')
private treatyRepo: Repository<Treaty>,
) {}
async create(input: CreateTreatyInput): Promise<Treaty> {
return await this.treatyRepo.save(input);
}
async findAll(): Promise<Treaty[]> {
return await this.treatyRepo.find({ relations: ['countries'] });
}
async findOne(id: number): Promise<Treaty> {
return await this.treatyRepo.findOne(id);
}
async update(input: UpdateTreatyInput): Promise<Treaty> {
let found = await this.treatyRepo.findOne(input.id);
return await this.treatyRepo.save({ ...found, ...input });
}
async remove(id: number) {
let found = await this.treatyRepo.findOne(id);
if (found) {
await this.treatyRepo.remove(found);
return id;
} else {
return null;
}
}
}
As seen above, all our logic is in services. Please note that we're injecting db entity repositories in service constructors. @nestjs/typeorm
wrapper makes it possible, wonderful!
Input types
Our last graphql schema element is input. We receive everything from GraphQL schema as strict types. In addition, what we supply to it is supposed to be schema type as well. To achive this, we're decorating our input classes with @InputType()
from @nestjs/graphql
wrapper. Nest automatically generated dto
directories in our modules. We decorate its fields similarly with @Field()
. Let's see city module inputs;
src/city/dto/create-city.input.ts
import { InputType, Int, Field } from '@nestjs/graphql';
@InputType()
export class CreateCityInput {
@Field({ nullable: false })
name: string;
@Field(() => Int, { nullable: true })
population: number;
@Field(() => Int, { nullable: false })
countryId: number;
}
src/city/dto/update-city.input.ts
import { CreateCityInput } from './create-city.input';
import { InputType, Field, Int, PartialType } from '@nestjs/graphql';
@InputType()
export class UpdateCityInput extends PartialType(CreateCityInput) {
@Field(() => Int, { nullable: false })
id: number;
}
update input extends create input and adds a mandatory id field.
Let's give it a try...
We'll use graphql playground in http://localhost:3000
. First, query countries;
query countries {
countries {
id
name
population
treaties {
id
name
}
}
}
And the reply from graphql is empty since we've not yet created any. Let's create three countries;
mutation createCountry {
createCountry(input: { name: "Japan", population: 35000 }) {
id
name
}
}
mutation createCountry {
createCountry(input: { name: "France", population: 25000 }) {
id
name
}
}
mutation createCountry {
createCountry(input: { name: "Germany", population: 55000 }) {
id
name
}
}
Now run the countries query again, you must have three now;
{
"data": {
"countries": [
{
"id": 2,
"name": "France",
"population": 25000,
"treaties": []
},
{
"id": 1,
"name": "Japan",
"population": 35000,
"treaties": []
},
{
"id": 3,
"name": "Germany",
"population": 55000,
"treaties": []
}
]
}
}
it's important to note that, the shape of the result determined by the query we sent. You can test all the mutations and queries your self. There're all functional!
Adding continent field to country
Let's say we need to add a continent field to country. Moreover, since continent data is not to subject to change, we want the continent to be enum. Let's create Continent
enum;
src/country/enums.ts
import { registerEnumType } from '@nestjs/graphql';
export enum Continent {
Asia = 'Asia',
Europe = 'Europe',
America = 'America',
Africa = 'Africa',
}
registerEnumType(Continent, { name: 'Continent' });
We need to register enums with registerEnumType
. Let's add below field to country entity;
@Column({ type: 'enum', enum: Continent, nullable: true })
@Field(() => Continent, { nullable: true })
continent: Continent;
We're using the same enum for our db, and also for our graphql schema, awesome! Finally we need to update CreateCountryInput
with below field;
@Field(() => Continent, { nullable: true })
continent: Continent;
Now we should create a new typeorm db migration to update countrydb accordingly; yarn migration:generate Continent
. Our second migration should be like this;
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Continent1634807399486 implements MigrationInterface {
name = 'Continent1634807399486';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "public"."country_continent_enum" AS ENUM('Asia', 'Europe', 'America', 'Africa')`,
);
await queryRunner.query(
`ALTER TABLE "country" ADD "continent" "public"."country_continent_enum"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "country" DROP COLUMN "continent"`);
await queryRunner.query(`DROP TYPE "public"."country_continent_enum"`);
}
}
Only necessary DDL commands are added to newly created migration. We're updating our db incrementally. We can have our migrations to execute manually added DML commands. Our treaty table is empty. Let's insert some;
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Continent1634807399486 implements MigrationInterface {
name = 'Continent1634807399486';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "public"."country_continent_enum" AS ENUM('Asia', 'Europe', 'America', 'Africa')`,
);
await queryRunner.query(
`ALTER TABLE "country" ADD "continent" "public"."country_continent_enum"`,
);
await queryRunner.query(
`INSERT INTO "treaty"(name) VALUES ('Kyoto Protocol'),('Paris Agreement'),('Vienna Convention');`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "country" DROP COLUMN "continent"`);
await queryRunner.query(`DROP TYPE "public"."country_continent_enum"`);
await queryRunner.query(`DELETE FROM "treaty"`);
}
}
We've added insert and delete DML commands to up
and down
functions of Continent migration respectively. Let's run it, yarn migration:run
Country table is supposed to have a continent
field of type country_continent_enum
. Besides, treaty
table must have three records inserted. We can see Continent enum in our graphql schema;
Let's check migrations
table. It now has a new record of Continent migration. This way we're versioning countrydb. When we deploy our code to prod, our prod countrydb migration level will be updated and we can trace it easily. No manuel DDL update would be necessary, wonderful!
id|timestamp |name |
--+-------------+----------------------+
1|1634791876559|Init1634791876559 |
2|1634807399486|Continent1634807399486|
We can update the countries with continent. Since it's enum type, no need to write, just hit ctrl+space and select from continents, nice!
mutation updateCountry {
updateCountry(input: { id: 1, continent: Asia }) {
id
name
continent
}
}
Query a legacy database
We may need to query an existing legacy database. It'll be a db first approach. So, we'll create its module, service, resolver one by one;
nest g module legacy --no-spec
nest g service legacy --no-spec
nest g resolver legacy --no-spec
Our legacydb is a mssql. Need to install mssql package
yarn add mssql
We need to add one more TypeOrmModule
among app module's imports array;
TypeOrmModule.forRoot({
name: 'legacydb',
type: 'mssql',
host: process.env.LEGACY_DB_HOST,
port: parseInt(process.env.LEGACY_DB_PORT),
username: process.env.LEGACY_DB_USERNAME,
password: process.env.LEGACY_DB_PASSWORD,
database: process.env.LEGACY_DB_NAME,
extra: {
trustServerCertificate: true,
},
}),
new .env
file should be as below;
DB_NAME=countrydb
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
LEGACY_DB_HOST=localhost\SQLEXPRESS
LEGACY_DB_PORT=1433
LEGACY_DB_USERNAME=dummy_user
LEGACY_DB_PASSWORD=dummy_password
LEGACY_DB_NAME=legacydb
Let's say legacydb has a company
table which we'll query from;
id|name |products |
--+---------+---------------------+
1|Apple |iphone, ipad, macbook|
2|Samsung |Android devices |
3|Del Monte|Food |
4|Tesla |Electric cars |
Let's create a dto object to be used as result type of our query;
src/legacy/dto/legacy.company.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Company {
@Field(() => Int, { nullable: true })
id: number;
@Field({ nullable: true })
name: string;
@Field({ nullable: true })
products: string;
}
src/legacy/legacy.service.ts
import { Injectable } from '@nestjs/common';
import { getManager } from 'typeorm';
import { Company } from './dto/legacy.company';
@Injectable()
export class LegacyService {
async findCompanies(companyName: string): Promise<Company[]> {
return new Promise(async (resolve, reject) => {
try {
const entityManager = getManager('legacydb');
let strQueryCompany = `SELECT id, name, products FROM legacydb.dbo.company WHERE name = @0;`;
let result = await entityManager.query(strQueryCompany, [companyName]);
resolve(result);
} catch (error) {
reject(error);
}
});
}
}
src/legacy/legacy.resolver.ts
import { LegacyService } from './legacy.service';
import { Args, Query, Resolver } from '@nestjs/graphql';
import { Company } from './dto/legacy.company';
@Resolver()
export class LegacyResolver {
constructor(private legacyService: LegacyService) {}
@Query(() => [Company])
async companies(
@Args('companyName', { nullable: true }) companyName: string,
) {
return await this.legacyService.findCompanies(companyName);
}
}
We can see our Company
type and companies
query in graphql schema;
Our query is supposed to run as below;
Finally, we're querying two databases from the same graphql api 👍
Top comments (0)