Tec‧士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 2

Andrew Shek

Andrew Shek

2021-06-22

Tec‧士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 2

Part 1 講完理論部分。Part 2就開始實作了。

實作GraphQL Server


Tec。士多開發日記系列:第一篇:GraphQL簡介既example示範GraphQL Server使用Schema同Resolver。

import {ApolloServer,gql} from 'apollo-server-express';
import Express from 'express';

class Person{
    public name:string;
    public age: number;
    public email: string;
    constructor(name:string,age:number,email:string){
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

const personList:Person[] = [];
personList.push(new Person("Tom",34,"tom@gmail.com"));
personList.push(new Person("Peter",30,"peter@gmail.com"));
personList.push(new Person("Ken",25,"ken@gmail.com"));

const app = Express();
const typeDefs = gql`
    type Person{
        name:String,
        age:Int,
        email:String
    }
    type Query{
        person:[Person]
    }
`;
const resolvers= {
    Query:{person:()=>personList},
    Person:{
        name:(parent:Person)=>parent.name,
        age:(parent:Person)=>parent.age,
        email:(parent:Person)=>parent.email
    }
};

const server = new ApolloServer({typeDefs,resolvers});
server.applyMiddleware({app,path:'/graphql'});
app.listen(4000,()=>{
    console.log("GraphQL Server is started at http://localhost:4000/graphql");
});

typeDefs變數就是Schema。resolvers變數就是Resolver。呢個變數會被抽起變成獨立模組(Module), 所以backend會被簡化。

import {ApolloServer,gql} from 'apollo-server-express';
import {typeDefs} from './schema';
import {resolvers} from './resolvers';
import Express from 'express';

const app = Express();
const server = new ApolloServer({typeDefs,resolvers});
server.applyMiddleware({app,path:'/graphql'});
app.listen(4000,()=>{
    console.log("GraphQL Server is started at http://localhost:4000/graphql");
});

題目是Tec。士多開發日記,所以Schema同Resolver不再用Person做例子。改用零食飲品做例子。就由零食飲品物件開始。往後日子慢慢加入新成員。

//schema.ts
export const typeDefs = gql`
    type Commodity{
				id:ID!
        name:String!
				
    }
    type Query{
        commodities:[Commodity]
    }
`;

是Schema當中,定義一個物件是用type關鍵字開頭。例如,定義一個Commodity Object,就用如下寫法:

type Commodity{
	id:ID!
  name:String!			
}

定義Query物件是必需。因為Frontend需要透過Query物件進行查詢物件,Mutation如是,所以所有物件(例如,Commodity物件)必需放入Query物件。

每一個物件有不同特性/屬性,例如,Commodity物件。id及name就是特性/屬性名。ID及String就是資料類型。!是代表不可以NULL,就是必需要有值。

id:ID!
name:String!

再望一望Query物件,其實大同小異。不過 是咩意思呢? 是指特性/屬性(Field)的資料類型Commodity物件Array,即是Commodity[]。

 commodities:[Commodity]

講完Schema就到Resolver。要幫Schema入面已定義的物件、特性/屬性(Field)定義Resolver。

//resolvers.ts
import {Commodity} from './commodity';

const commodityList:Commodity[] = [];
commodityList.push(new Commodity(1,"240ml經典橙汁"));
commodityList.push(new Commodity(2,"朱古力夾心餅"));
commodityList.push(new Commodity(3,"XX杯麵。五香牛肉"));

export const resolvers = {
    Query:{
        Commodities:()=>commodityList
    },
    Commodity:{
        id:(commodity:Commodity)=>commodity.getID(),
        name:(commodity:Commodity)=>commodity.getName()
    }
}

首先為Query物件入面既Field定義Resolver,通常都是使用Closure寫法。每一個Closure最後會返回一個物件/值。例如,Commodities,會返回一個Commodity物件Array。

 Query:{
        Commodities:()=>commodityList
 }

做完?id及name唔駛定義返回值咩?答案是。。。已經做完。

GraphQL可以根據物件key名map返去GraphQL Field名。

Commodity:{
	id:(commodity:Commodity)=>commodity.getID(),
  name:(commodity:Commodity)=>commodity.getName()
}

不過,都可以針對每一個Field定義(或者稱Override#)一個Resolver。目的是可以做資料處理,因為Closure都是一個Function。Closure是返回數值前可以做編程。以下例子純粹示範,實際情況是無需要。

#因為GraphQL會為每一個Field自動加一個Default Resolver。

Commodity:{
	id:(commodity:Commodity)=>{
    const id = commodity.getID();
    if (id >= 0){
      return id;
    }else{
      return 0;
    }
  },
  name:(commodity:Commodity)=>{
    let name = commodity.getName();
    //For example, test -> Test
    name = name[0].toUpperCase() + name.substring(1);
    return name;
  }
}

以下是Commodity物件既源碼參考。因為typescript要俾type,所以俾大家參考用。不作詳細介紹。

//commodity.ts
export class Commodity{
    private id:number;
    private name:string;
    constructor(id:number,name:string){
        this.id = id;
        this.name = name;
    }
    getID = ()=>this.id;
    getName= ()=>this.name;
}

Tec。記士多有多款零食供大家選擇。如果學生只是想買飲品,但是每一次開站網都要問Backend攞一條完整零食清單。咁是好浪費頻寬,造成資訊過度獲取(REST是唔是一種完美既溝通形式? Point#3 )。其實可以透過使用參數(Parameters)解決問題。

//schema.ts
export const typeDefs = gql`
    type Commodity{
				id:ID!
        name:String!
    }
		enum CATEGORY{
			DRINK
      SNACK
      NOODLES
		}
    type Query{
        commodities:[Commodity],
				commodities(category:CATEGORY!):[Commodity]
    }
`;

為左令大家做Project唔會餓親,Tec。記士多會定時補貨。而且會不定是轉換零食飲品,迎合不同學生需要。例如,加入樽裝涼茶,幫做Project捱左幾晚通宵的學生們下火🤯。哈!所以Tec。記士多剛剛多左兩個Requirements:

  1. 補貨
  2. 新增、刪除、更新零食飲品

Query物件可以取得物件。如果要做新增、移除、更新物件得動作,例如,補貨、新增、刪除、更新零食飲品,就需要用Mutation物件幫手。根據以上既要求,schema.ts新增左Mutation物件:

//schema.ts
import {gql} from 'apollo-server';
export const typeDefs = gql`
type Commodity{
    id:ID!
    name:String!
    unit:UNIT!
    category:CATEGORY!
}

enum UNIT{
    CAN
    BOTTLE
    BOX
    PACK
    CUP
}

enum CATEGORY{
    DRINK,
    SNACK,
    NOODLES
}

type Query{
    Commodities:[Commodity],
    CommoditiesByCategory(category:CATEGORY):[Commodity]
}

type Mutation{
    addCommodity(name:String!,unit:UNIT!,category:CATEGORY!,stockQuantity:Int = 0):Commodity,
    updateCommodity(id:ID!,name:String!,unit:UNIT!,category:CATEGORY!,stockQuantity:Int = 0):Commodity,
    deleteCommodity(id:ID!):Commodity
}
`;

因應以上要求,commodity.ts做左相應更新。

//commodity.ts
export enum CATEGORY{
    DRINK = 0,
    SNACK,
    NOODLES
}
export enum UNIT{
    CAN = 0,
    BOTTLE,
    BOX,
    PACK,
    CUP
}
export class Commodity{
    private id:number;
    private name:string;
    private unit:UNIT;
    private category:CATEGORY;
    private stockQuantity:number;
    constructor(id:number,name:string,unit:UNIT,category:CATEGORY,stockQuantity:number = 0){
        this.id = id;
        this.name = name;
        this.unit = unit;
        this.category = category;
        this.stockQuantity = stockQuantity;
    }
    getID = ()=>this.id;
    getName= ()=>this.name;
    getUnit= ()=> UNIT[this.unit as number];
    getCategory = ()=> CATEGORY[this.category as number];
    getStockQuantity = ()=> this.stockQuantity;

    setName= (name:string)=>this.name = name;
    setUnit= (unit:UNIT)=>this.unit =unit;
    setCategory= (category:CATEGORY)=>this.category = category;
    setStockQuantity= (stockQuantity:number)=>this.stockQuantity = stockQuantity;
}

但是大家有無覺得schema.ts好冗長嗎?Mutation物件中有好多參數是重複。其實有方法簡化。使用Input Object Type集合所有會重用的參數。而該Input Object Type只可在Mutation物件中當作物件類型(Object Type)使用。

//建議所有Input Type,Type Name用Input結尾,因為方便分辨「真。資料類型」及「Input Type」
input CommodityInput{
    name:String!
    unit:UNIT!
    category:CATEGORY!
    stockQuantity:Int = 0
}

更新了的schema.ts如下。

//schema.ts
import {gql} from 'apollo-server';
export const typeDefs = gql`
type Commodity{
    id:ID!
    name:String!
    unit:UNIT!
    category:CATEGORY!
    stockQuantity:Int
}

enum UNIT{
    CAN
    BOTTLE
    BOX
    PACK
    CUP
}

enum CATEGORY{
    DRINK,
    SNACK,
    NOODLES
}

type Query{
    Commodities:[Commodity],
    CommoditiesByCategory(category:CATEGORY):[Commodity]
}

input CommodityInput{
    name:String!
    unit:UNIT!
    category:CATEGORY!
    stockQuantity:Int = 0
}

type Mutation{
    addCommodity(newCommodity:CommodityInput!):Commodity,
    updateCommodity(id:ID!,updateCommodity:CommodityInput!):Commodity,
    deleteCommodity(id:ID!):Commodity
}
`;

更新了的resolvers.ts如下。

//resolvers.ts
import {Commodity,UNIT,CATEGORY} from './commodity';

const commodityList:Commodity[] = [];
commodityList.push(new Commodity(0,"240ml經典橙汁",UNIT.CAN,CATEGORY.DRINK));
commodityList.push(new Commodity(1,"朱古力夾心餅",UNIT.PACK,CATEGORY.SNACK));
commodityList.push(new Commodity(2,"XX杯麵。五香牛肉",UNIT.CUP,CATEGORY.NOODLES));

export const resolvers = {
    Query:{
        Commodities:()=>commodityList,
        CommoditiesByCategory:(root:any,parameters:any)=>commodityList.filter(
          commodity=>commodity.getCategory() === parameters.category)
    },
    Commodity:{
        id:(commodity:Commodity)=>commodity.getID(),
        name:(commodity:Commodity)=>commodity.getName(),
        unit:(commodity:Commodity)=>commodity.getUnit(),
        category:(commodity:Commodity)=>commodity.getCategory(),
    },
    Mutation:{
        addCommodity:(root:any,parameters:any)=>{
            const {newCommodity:{name,unit,category,stockQuantity}} = parameters;
            const commodity = new Commodity(commodityList.length,
                                            name,
                                            UNIT[unit as string],
                                            CATEGORY[category as string],
                                            stockQuantity);
            commodityList.push(commodity);
            console.log(commodityList);
            return commodity;
        },
        updateCommodity:(root:any,parameters:any)=>{
            const {id,updateCommodity:{name,unit,category,stockQuantity}} = parameters;
            const commodity = commodityList[id];
            commodity.setName(name);
            commodity.setUnit(UNIT[unit as string]);
            commodity.setCategory(CATEGORY[category as string]);
            commodity.setStockQuantity(stockQuantity);
            return commodity;
        },
        deleteCommodity:(root:any,parameters:any)=>{
            const {id} = parameters;
            const delCommodities = commodityList.splice(id,1);
            return delCommodities[0];
        }
    }
}

測試GraphQL Server


要啟動GraphQL Server, 在VS Code Terminal執行以下Command。

node .

是瀏覽器輸入以下網址打開GraphQL Playground:

http://localhost:4000/graphql

取得所有飲品零食。

2.png

取得所有飲品。

1.png

新增一款新零食。

3.png

一款零食補貨。

4.png

刪除一款零食。

5.png

是GraphQL Playground見到一堆Queries,看唔明?下一編Blog詳細介紹。

Tec。記士 Demo Source

References


Comments

Read More

Tec ‧ 士多開發日記系列:第二篇:實作GraphQL Server(基礎入門)- Part 1

Tec ‧ 士多開發日記系列:第一篇:GraphQL簡介

分析「保就業計劃」數據,超方便隨時網上更新分享分析結果 (Microsoft Power BI 版本)

0成本!分析「保就業計劃」數據,超方便隨時網上更新分享分析結果

零基礎.10分鐘輕鬆製作STEM教材