# Prisma Schema
* 我們採用 `schema.prisma` 定義完 prisma schema 後,才建立、修改 MySQL 的 Tables 的方式,所以從這開始
* 想了解對應的 sql 語句請[自行參考 DATABASE WORKFLOWS 反推](https://www.prisma.io/docs/guides/database-workflows/unique-constraints-and-indexes)
以下都是在 `schema.prisma` 裡
## Generators
指定 `prisma generate` 產的 assets
```jsonld=
generator client {
provider = "prisma-client-js"
output = "node_modules/@prisma/client" # default
binaryTargets = ["native"] # default 指定為目前的 OS
}
```
* `provider` required:目前用 `prisma-client-js`
* `output` optinal:Prisma Client 位置(Default: node_modules/@prisma/client)
* [`binaryTargets` optional](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/generators/#binary-targets):指定 Prisma Client run 在哪個 OS
* 例:Mac OS `binaryTargets = ["darwin"]`
## Specify a MySQL data source
`schema.prisma` 最上方
MySQL connection URL:`mysql://USER:PASSWORD@HOST:PORT/DATABASE`

[Arguments 就有需求再來查,可以設定些連線相關](https://www.prisma.io/docs/reference/database-connectors/mysql#connection-details)
```jsonld=
datasource mysql {
provider = "mysql"
url = "mysql://johndoe:mypassword@localhost:5432/mydb"
}
```
http://localhost/phpmyadmin/sql.php
## Data Model
需要先知道:
### Model, Scalar Type, Enum
* Model 可以代表 db 的 table、prisma client queries 的基底
* Model 的 field 型別可以定義成
1. Scalar Type:String, Boolean, Int, Float, DateTime(Date), JSON(Object)、Enum
* Enum:固定 constant 可以宣告成 enum
```jsonld
enum Role {
USER
ADMIN
}
model User {
id Int @id @default(autoincrement())
role Role
}
```
2. Model(relation field)
* Type modifiers:`[]`, `?`
* List 的表示法 `MODELNAME[]`
Scalar list 只適用 data model(作為 relation field)
例如 `posts Post[]`
* 允許空值的 field 記得 TYPE 右邊加 `?`(optional),table 中會存為 NULL,例如 `headThumb String? @map(name: "head_thumb")`(List 例外它不需要加);一般是 required
* Models、Fields、Enums naming:`[A-Za-z][A-Za-z0-9_]*`
* Models、Enums 習慣 PascalCase
* Fields 習慣 camelCase
### Attributes, Functions
除了 List 以外,都可以配置 attribue 修飾符代表、約束,常用如
* attributes(幾乎都有可以刪除的 argument name,可以約定都簡寫)
`@` for field; `@@` for block
* `@id` 每個 Model 最多一個
* field id 通常會搭配預設值,例 `@default(autoincrement())` 預設+1
* 組合 id,例 `@@id([title, author])`、`@@id(fields: [title, author])`
* `@default(VALUE)` field 未給塞預設值
* 例:`@default(now())`、`@default(value: autoincrement())`
* `@unique` 限制 field 唯一
* 組合 fields `@@unique` 限制唯一,例 `@@unique([title, author])`、`@@unique(fields: [title, author])`
* `@@index` 建立索引,利於查詢效率(空間換速度)
* 通常會在 Primary Key、Foreign Key 或常放在查詢子句中的 field 建立索引,例 `@@index([title, author])`、`@@index(fields: [title, author])`
* `@updatedAt` 有更動該筆資料時自動更新更新時間
* `@map` 映射與 field 不同名的 table column;`@@map` 映射與 model 不同名的 table name
* 例:`@map("is_admin")`、`@map(name: "is_admin")`
*
```jsonld=
enum Role {
ADMIN @map("admin")
CUSTOMER @map("customer")
@@map("_Role")
}
```
* `@relation` 關聯其他 model (table),當中最複雜就它,後面再介紹。[想先了解熟悉可看這](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations#the-relation-attribute)
```jsonld=
@relation(fields: [userId], references: [id])
@relation(fields: [userFirstName, userLastName], references: [firstName, lastName])
@relation(name: "UserOnPost", references: [id])
@relation("UserOnPost", references: [id])
# 關聯的都是 id 時可以省略 references
@relation(name: "UserOnPost")
@relation("UserOnPost")
```
* functions (通常用在 default)
* `autoincrement()` 新增的資料 id 自動 +1(Int)
* `cuid()` 根據 cuid spec 產生 globally unique identifier
* `uuid()` 根據 UUID spec 產生 globally unique identifier
* `now()` 常用於新增資料的建立時間預設值(DateTime)
## Unique constraints `@unique`
MySQL 的 constraint / index,在 prisma model 是設 `@unique` 限定唯一,可以單獨 column 設、也可以多個 column 組合成唯一(再另外命名),一張表可以有多個 unique
可以在建立 table 時就先設好,或是建好的 table 在修改
id 有 `@id` 就也是唯一了不需要給 `@unique`
```jsonld=
model User {
firstName String?
id Int @default(autoincrement()) @id # 這邊 default 是指不需要給它會自動+1
lastName String?
account String @unique
@@unique([firstName, lastName], name: "firstName")
}
```
## [Connect Model `@relation` 🔆](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations)
* MySQL 表和表之間的關聯方式,建立表格時留個存關聯的表的 id 的 column,這個與外表關聯的 id 就是 foreign key(習慣叫 xxxId);在 prisma model 間的關聯則是設 `@relation`
* Relation fields:沒有 Scalar Type,其 Type 是其他 Model。每個表和表的關聯,兩方會各有一或多個 relation field
* `@relation(_ name: String?, fields: FieldReference[]?, references: FieldReference[]?)`
:::warning
Relation 有三種 `1-1`、`1-n`、`m-n`
:::
直接邊看範例邊了解:
Q:假設今天有 User、Profile、Category、Post 四個 Model,試著講出他們的關係:
> User ↔ Profile
User ↔ Post
Post ↔ Category
Q:會怎麼實作 Model 會怎麼實作間的關聯?

:::spoiler 看結果
```jsonld=
model User {
id Int @id @default(autoincrement())
posts Post[]
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [authorId], references: [id])
authorId Int
categories Category[] @relation(references: [id])
}
model Category {
id Int @id @default(autoincrement())
posts Post[] @relation(references: [id])
}
```
:::
### 一對一 1-1 relation
情境:每個 User 可能會有自己的 Profile
寫法一:forign key 在 Profile
```jsonld=
model User {
id Int @id @default(autoincrement())
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int // relation scalar field
}
```

寫法二:forign key 在 User
```jsonld=
model User {
id Int @id @default(autoincrement())
profile Profile? @relation(fields: [profileId], references: [id])
profileId Int? // relation scalar field
}
model Profile {
id Int @id @default(autoincrement())
user User
}
```

補充:multi-field IDs 時的 1-1
```jsonld=
model User {
firstName String
lastName String
profile Profile?
@@id([firstName, lastName])
}
model Profile {
id Int @id @default(autoincrement())
user User @relation(fields: [authorFirstName, authorLastName], references: [firstName, lastName])
authorFirstName String // relation scalar field
authorLastName String // relation scalar field
}
```
### 一對多 1-n relation
forign key 會在 non-list field
情境:每一 User 能有多篇 Post
```jsonld=
model User {
id Int @id @default(autoincrement())
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [authorId], references: [id])
authorId Int # relation scalar field 關聯 User 的 foreign key
}
```
上面的 `posts`、`author` 是 prisma 專屬產生關聯用法 Prisma Client 依據,database table 中**不存在**的 field;`authorId` 是**存在**於 table 的 forign key 得以關聯 User 和 Post

:::spoiler 建立 relation 後 generate 的 Prisma Client 應用例子
可以如下使用:
* 新增 User 同時新增他的 posts
(user id 不用給是因為有 deafult;post authorId 則自動存為 user id)
```javascript=
const userAndPosts = await prisma.user.create({
data: {
posts: {
create: [
{ title: 'Prisma Day 2020' },
{ title: 'How to write a Prisma schema' },
],
},
},
})
```
* 查詢 User 可同時取得與他關聯的 posts
```javascript=
const getAuthor = await prisma.user.findOne({
where: {
id: "20",
},
include: {
posts: true, // authorId == 20 的 post list
},
});
```
* 或是已存在沒有被關聯的 post,與已存在 User 做關聯
```javascript=
const updateAuthor = await prisma.user.update({
where: {
id: 20,
},
data: {
posts: {
connect: {
id: 4,
},
},
},
})
```
:::
::: warning
注意 1-1 和 1-n relation 中
* relations 中的 annotated relation field 和 relation scalar 必續同時 required 或是 optional,且 List Model 必為 required
```jsonld=
model User {
id Int @id @default(autoincrement())
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
```
* 1-1 forign key 關聯的必須是 unique;1-n 無限制
* `@relation` 只會在其中一邊
* 一個 Model 同時有對同 Model 的多種關聯會有多個 `@relation`(要disambiguate:命名區分)
* 或是 self-relation:可以是 1-1, 1-n, m-n
* Self-relations on the same model
```jsonld=
model User {
id Int @id @default(autoincrement())
name String?
husband User? @relation("MarriagePartners")
wife User @relation("MarriagePartners")
teacher User? @relation("TeacherStudents")
students User[] @relation("TeacherStudents")
followedBy User[] @relation("UserFollows")
following User[] @relation("UserFollows")
}
```
:::
### 多對多 m-n relation(Implicit、explicit)
#### Explicit 🔆
```jsonld=
model Post {
id Int @id @default(autoincrement())
title String
categories CategoriesOnPosts[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts CategoriesOnPosts[]
}
# 表示 MySQL 經過 JOIN, link or pivot 的 table
model CategoriesOnPosts {
post Post @relation(fields: [postId], references: [id])
postId Int
category Category @relation(fields: [categoryId], references: [id])
categoryId Int
# createdAt DateTime @default(now()) 可以加些 meta-information
@@id([postId, categoryId])
}
```
#### Implicit
* 不用給 relation scalar field,除非是同時多個 self-relations
* 只能在 Model 都是 single `@id` 的情況使用(不可以是組合 id 或是 `@unique`),不符合的話就得選用 explicit 方式

```jsonld=
# 省略 @relation
model Post {
id Int @id @default(autoincrement())
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
posts Post[]
}
```

::: warning
注意 m-n relation 中
* Implicit:兩邊都要 `@relation` 依據對方
```jsonld=
model Post {
id Int @id @default(autoincrement())
categories Category[] @relation(references: [id])
}
model Category {
id Int @id @default(autoincrement())
name String
position Int
posts Post[] @relation(references: [id])
}
```

如果想自己定義關聯的 table name:
```jsonld=
model Post {
id Int @id @default(autoincrement())
categories Category[] @relation("MyRelationTable")
}
model Category {
id Int @id @default(autoincrement())
posts Post[] @relation("MyRelationTable")
}
```

* Explicit:要定義額外的 Model 代表關聯,`@relation` 集中在這 Model,以雙表 id 組合在此 Model 的 id,並且可以額外加上資訊如建立關聯的時間
```jsonld=
model Post {
id Int @id @default(autoincrement())
title String
categories CategoriesOnPosts[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts CategoriesOnPosts[]
}
model CategoriesOnPosts {
post Post @relation(fields: [postId], references: [id])
postId Int
category Category @relation(fields: [categoryId], references: [id])
categoryId Int
createdAt DateTime @default(now())
@@id([postId, categoryId])
}
```


* [如果是 introspection generate prisma client,在那前 MySQL 寫表多對多的關聯可參考這](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations#conventions-for-relation-tables-in-implicit-m-n-relations)
:::
再一個範例:
```jsonld=
model AnotherPost {
authorFirstName String?
authorLastName String?
id Int @default(autoincrement()) @id
title String?
AnotherUser AnotherUser? @relation(fields: [authorFirstName, authorLastName], references: [firstName, lastName])
@@index([authorFirstName, authorLastName], name: "authorFirstName")
}
model AnotherUser {
firstName String?
id Int @default(autoincrement()) @id
lastName String?
AnotherPost AnotherPost[]
@@unique([firstName, lastName], name: "firstName")
}
model Post {
authorId Int?
id Int @default(autoincrement()) @id
title String?
User User? @relation(fields: [authorId], references: [id]) # 多方關聯父依據
@@index([authorId], name: "author")
}
model User {
id Int @default(autoincrement()) @id
name String?
Post Post[]
}
```
### Self-relations
#### [1-1 Self Relations](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations#one-to-one-self-relations)
```jsonld=
model User {
id Int @default(autoincrement()) @id
name String?
successorId Int?
successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id])
predecessor User? @relation("BlogOwnerHistory")
}
```
#### [1-n Self Relations](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations#one-to-many-self-relations)
```jsonld=
model User {
id Int @id @default(autoincrement())
name String?
teacherId Int?
teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
students User[] @relation("TeacherStudents")
}
```
#### [m-n Self Relations (Implicit)](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations#many-to-many-self-relations)
```jsonld=
model User {
id Int @id @default(autoincrement())
name String?
followedBy User[] @relation("UserFollows", references: [id])
following User[] @relation("UserFollows", references: [id])
}
```
##### Ex.
```jsonld=
model User {
id Int @id @default(autoincrement())
name String?
husband User? @relation("MarriagePartners")
wife User @relation("MarriagePartners")
teacher User? @relation("TeacherStudents")
students User[] @relation("TeacherStudents")
followedBy User[] @relation("UserFollows")
following User[] @relation("UserFollows")
}
```
### Disambiguating relations
```jsonld=
model User {
id Int @id @default(autoincrement())
name String?
writtenPosts Post[] @relation("WrittenPosts")
pinnedPost Post? @relation("PinnedPost")
}
model Post {
id Int @id @default(autoincrement())
title String?
author User @relation("WrittenPosts", fields: [authorId], references: [id])
authorId Int
pinnedBy User? @relation(name: "PinnedPost", fields: [pinnedById], references: [id])
pinnedById Int?
}
```
## Cascading deletes(待補)
得以定義當然除一筆 data 時,怎麼處理與它關聯的其他 table 的資料 ([MySQL](https://www.prisma.io/docs/guides/database-workflows/cascading-deletes/mysql#6-introspect-your-database-with-prisma))
但遺憾目前沒有方法能以 prisma schema 同概念的創建 table
https://github.com/prisma/prisma/discussions/2149
## 定義好後記得要 `npx prisma generate` 才會產生/更新 Prisma Client
# GraphQL Server
## Server 演變概要:[GraphQL Yoga](https://github.com/prisma-labs/graphql-yoga) 🔜 [Apollo Server](https://www.apollographql.com/docs/apollo-server/)
* 社群大小、活躍度、使用者多寡
* 考量未來可能會有使用 Federation 的場景 [Federation 可以參考這篇](https://chihching.net/intro-graphql-federation-zh-hant)
* 後端想要不同 microservice 處理他們各自的邏輯,各自維護負責範圍的 Schema
## GraphQL Schema
定義 GraphQL API 的輪廓及規範,當與 Resolver 處理出的資料不符合 Type 就會噴錯(但 Null 可以通過)
快速複習常用的定義 Schema 用法:
* Object type
* 包含 fields 和 field 的 type
```graphql=
#################### Type ###################
type User {
lastName: String
firstName: String
birthday: Date
createdAt: Date
updatedAt: Date
}
#################### Root ###################
type Query {
user(id: ID!): User
}
type Mutation {
createUser(data: UserCreateInput!): User
}
```
* Query、Mutation、Subscription 也是,但同時也是 Schema 的 entry point
* Input Object Type
跟 Object Type 幾乎一樣的還有 Input Object Type,但是一個是傳入 Argument 作為 Input(只會在左邊) ,一個是用於資料索取展示
* 推薦每支 mutation 都新增一支專屬的 input object type,習慣命名 xxxInput
* Scalar Type (Int, Float, String, Boolean, ID, Enum)
* Enum
```graphql
enum Sort {
asc
desc
}
```
* 其他例如 Date 都要另外自定義
* Non-null Syntax
* type 右邊加上 `!` 保證不為空
* 一旦修改 Not-Null field 就會是 Breaking Change,建議剛開始設計時,除了 ID 以外的欄位都不要加上
* Array Type Syntax
* 用 `[]` 包起來就是 array type
* 例如:`notIn: [Int]`、`teachers: [User]`、`courses: [Course]`
* 有無加上 Non-null Syntax 舉例:
* `teachers: [User]`
* `teachers: [User!]`
* `teachers: [User!]!`
* 不管是 Object Type 或 Scalar Type 都能使用 Argument
* 註解:
* 單行出現在文件 `"`
* 單行不出現在文件 `#`
* 多行出現在文件 `"""`
## 組成
### 1. Create an instance of ApolloServer
* `new ApolloServer({ typeDefs, resolvers }` 最基本要傳 typeDefs、 resolvers,但我們還需要透過 context 來放 prisma client:
```javascript=
// server.js
const { ApolloServer } = require('apollo-server');
const dotenv = require('dotenv/config');
const { createContext } = require('./context');
const { typeDefs } = require('./typeDefs');
const { resolvers } = require('./resolvers');
const server = new ApolloServer({ typeDefs, resolvers, context: createContext });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
```
* 起起來的預設 port 就是 GraphQL Playground,可以在這邊確認文件、試打 api 符不符合自己的預期
* `context` 主要都是放 request/response、ORM、處理認證等相關的東西,得以在每個 resolver 使用(又有點像 middleware)
```javascript=
// context.js
const { PrismaClient } = require('@prisma/client');
const jwt = require('jsonwebtoken');
const prisma = new PrismaClient();
const parseCookie = (str = '') =>
str
? str
.split(';')
.map(v => v.split('='))
.reduce((acc, v) => {
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
return acc;
}, {})
: {};
async function createContext ({ req, res }) {
const cookies = parseCookie(req.headers.cookie);
const { accessToken, refreshToken } = cookies;
const context = {
request: req,
response: res,
prisma,
};
let decoded = await jwt.decode(accessToken, process.env.ACCESS_TOKEN_SECRET);
if (!decoded) {
if (refreshToken) {
context.currentUser = decoded || null;
}
}
return context;
}
module.exports = {
createContext,
};
```
* `typeDefs` 定義 Schema (Query, Mutation, Type, Input object ...)
* Query, Mutation 其實就是定義 field 或 function name 被 call 時的 ==input, output 格式==,格式就是預設的 scalar type 或是自己定義的 type, input object, enum...等等,真正邏輯實作在與其 name 對應的 resolver
* `resolvers` 依據定義的 Query, Mutation Schema 的資料操作、邏輯等實作(名稱要對上)
* [typeDefs, resolvers 拆分參考](https://stackoverflow.com/a/60747952/7849574
)
typeDefs 可以給陣列
resolvers 可以在另外解構合併成一大包
### 2. typeDefs
* 透過 `apollo-server` 的 gql 寫 graph schema,一個 typeDefs 物件裡通常一定會有 type Query 和 type Mutation,裡面再定義要經過 resolver 處理的 field 或 function 的名稱、輸入型別、輸出型別(詳細寫法請參閱 [GraphQL 官方文件](https://graphql.org/learn/))
* 在定義 type 時,field name 要對照 DB 實際的 Table column name ,依我們 prisma 映射 DB 的專案可以直接參照檔案 `schema.prisma`(我們 MySQL DB 中的 Tables 就是下 `npx prisma migrate save --experimental`、`npx prisma migrate up --experimental`,然後依據這檔案建立/修改的)
舉例:
1. schema.prisma 有定義一個會產生 user table 的 data model
```json=
model User {
id Int @default(autoincrement()) @id
email String @unique
password String
lastName String @map(name: "last_name")
firstName String @map(name: "first_name")
nickName String @map(name: "nick_name")
birthday DateTime?
headThumb String? @map(name: "head_thumb")
desc String?
notified Boolean @default(false)
activated Boolean @default(false)
facebookId String? @map(name: "facebook_id")
googleId String? @map(name: "google_id")
createdAt DateTime @map(name: "created_at") @default(now())
updatedAt DateTime? @map(name: "updated_at") @updatedAt
courses Course[]
@@map(name: "user")
}
```
2. 然後在 typeDefs 定義一個 user 的 crud 輪廓,依據有無資料異動分到 type Query(無資料異動,通常 camelCase 名詞)/ type Mutation(有資料異動,通常 camelCase 動詞+名詞)底下。宣告 Type 類型的名稱習慣 PascalCase 名詞。
* 當要傳很多值的話,通常會宣告一個傳 input object type 的參數(負責新增的 input 傳的參數我們統一叫 `data` 好了),會依據不同操作定義不同的 `input XxxxInput`,當中字段可能就會包含 Create/Update/Update...。例如:`signUp(data: UserCreateInput!): User`
這些傳的參數很大包的話通常在 client query 就會宣告變數、input object 傳的整包另外被放在 Query Variables(playground範例)
* 另外提醒適當的註解有助於更完整 Schema 及文件的說明,可以多多利用
```javascript=
const { gql } = require('apollo-server');
const typeDefs = gql`
##################### Common 共用 ###################
scalar DateTime
scalar Date
enum Gender {
"Male"
M
"Female"
F
"TRANSGENDER"
T
}
enum Sort {
asc
desc
}
"""
共用表格filter保留字參數
"""
input TableStringFilterInput {
ne: String
eq: String
le: String
lt: String
ge: String
gt: String
contains: String
notContains: String
between: [String]
beginsWith: String
}
input TableIntFilterInput {
equals: Int
not: Int
in: Int
notIn: [Int]
lt: Int
lte: Int
ge: Int
gte: Int
}
##################### User 使用者 ###################
"""
建立使用者參數
"""
input UserCreateInput {
email: String!
password: String!
lastName: String!
firstName: String!
nickName: String!
birthday: DateTime!
headThumb: String
desc: String
notified: Boolean
activated: Boolean
facebookId: String
googleId: String
updatedAt: DateTime
courseIds: String
}
"""
可更新使用者參數
"""
input UserUpdateInput {
lastName: String
firstName: String
nickName: String
headThumb: String
desc: String
notified: Boolean
activated: Boolean
facebookId: String
googleId: String
}
"""
使用者欄位
"""
type User {
id: ID
email: String
lastName: String
firstName: String
nickName: String
birthday: DateTime
headThumb: String
desc: String
notified: Boolean
activated: Boolean
facebookId: String
googleId: String
createdAt: DateTime
updatedAt: DateTime
courseIds: String
}
"""
篩選User參數
"""
input UserFilterKey {
email: TableStringFilterInput
lastName: TableStringFilterInput
firstName: TableStringFilterInput
nickName: TableStringFilterInput
gender: TableStringFilterInput
}
"""
排序User參數
"""
input UserOrderByInput {
email: Sort
firstName: Sort
birthday: Sort
createdAt: Sort
updatedAt: Sort
}
##################### Root Object ###################
type Query {
currentUser: User
users(filter: UserFilterKey, skip: Int, take: Int, orderBy: UserOrderByInput): [User!]
signIn(email: String!, password: String!): User
}
type Mutation {
signUp(data: UserCreateInput!): User
updateUser(userId: Int!, data: UserUpdateInput!): User
deleteUser(userId: Int!): Boolean
}
`;
module.exports = {
typeDefs,
};
```
順便看一下在 playground 打 signUp 怎麼操作

### 3. resolvers
* 上次主要提到的,相當於 GraphQL query handler、controller 的概念,定義 GraphQL query response 的 functions 集合,這每個 function 都是 schema 的 type 或 field 的 resolver
* 看個範例
```javascript=
// resolvers.js
const path = require('path');
const { DateTimeResolver, DateResolver } = require('graphql-scalars');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const { AuthenticationError, ForbiddenError, UserInputError } = require('apollo-server-core');
const { combineResolvers, skip } = require('graphql-resolvers');
const createTokens = user => {
const refreshToken = jwt.sign(
{ userId: user.id, count: user.count },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' },
);
const accessToken = jwt.sign(
{ userId: user.id },
process.env.ACCESS_TOKEN_SECRET,
{
expiresIn: '15min',
},
);
return { refreshToken, accessToken };
};
const isAuthenticated = async (parent, args, { prisma, currentUser, request }) => {
if (currentUser) {
const user = await prisma.user.findOne({
where: {
id: Number(currentUser.userId),
},
});
if (user) {
return skip;
}
}
return new AuthenticationError('未認證無法存取,請先登入');
};
const resolvers = {
DateTime: DateTimeResolver,
Date: DateResolver,
Query: {
currentUser: async (parent, args, { prisma, currentUser }) => {
if (!currentUser || !currentUser.userId) {
throw new AuthenticationError('當前未登入!請重新登錄');
}
return await prisma.user.findOne({ where: { id: currentUser.userId } });
},
users: combineResolvers(
isAuthenticated,
async (parent, { filter, skip, take, orderBy }, { prisma }) =>
await prisma.user.findMany({ where: filter || {}, skip, take, orderBy }),
),
signIn: async (parent, { email, password }, { prisma, response, request }) => {
const user = await prisma.user.findOne({ where: { email } });
if (!user || !bcrypt.compareSync(password, user.password)) {
throw new AuthenticationError('登入失敗:帳號或密碼錯誤');
}
const { accessToken, refreshToken } = createTokens(user);
response.cookie('refreshToken', refreshToken, { httpOnly: true });
response.cookie('accessToken', accessToken, { httpOnly: true });
return user;
},
},
Mutation: {
signUp: async (parent, { data }, { prisma }) => {
if (!!(await prisma.user.findOne({ where: { email: data.email } }))) {
throw new UserInputError('已註冊,請直接登入');
}
data.password = bcrypt.hashSync(data.password, 12);
return await prisma.user.create({
data,
});
},
updateUser: async (parent, { userId, data }, { prisma }) => {
return await prisma.user.update({
where: { id: userId },
data,
});
},
deleteUser: async (parent, { userId }, { prisma }, info) => {
await prisma.user.delete({ where: { id: userId } });
return true;
},
},
};
module.exports = {
resolvers
};
```
* 之後想要做身份認證的話,可以使用 `graphql-resolvers`,用 combineResolvers 把多個 resolver 包起(應該是種 Higher order functions?)會由左至右順序執行(前面 resolver 要 return skip,最後一個仍要回傳 Schema 定義的回傳型別 data)
* Prisma Client 就是在 resolver 裡使用的操作資料管道(必須在前面定義好 Schema 下 `npx prisma generate` 才會更新得以正常使用)[詳細 CRUD 說明請參閱 Prisma 官方文件](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud)
# Prisma Client CRUD (Resolver 內)
🔆 重點:[Relation queries](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/relation-queries),nesting 的寫法
## ☼ Read
### findOne
* return object 或 null
* 要搭配 `where`,且搜尋條件要讓結果必成唯一,例如data model attribute 有 `@id`, `@unique` 修飾的 field
* [還有 `select`, `include` 可以使用](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud#type)(在其他 crud method 中也是相同用法),例如
* select: 指定要被 return 的 properties(但好像用不太到)
```jsonld=
# schema.prisma
model User {
id Int @id @default(autoincrement())
name String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [userId], references: [id])
authorId Int
}
```
```jsonld=
const result = await prisma.user.findMany({
select: {
id: true,
name: true,
posts: {
include: {
author: true,
},
},
},
})
```
* include: load relations 的部分,沒有給 true 的話會得到 null
* `@relation` 類似建立 foreign key 關聯別的 table,teacher 就像把關聯到的部分也綁在這張 table 可以隨時 include 載入得以取用
```jsonld=
# schema.prisma
model Course {
id Int @default(autoincrement()) @id
title String
desc String?
teacherId Int @map(name: "teacher_id")
teacher User @relation(fields: [teacherId], references: [id])
@@map(name: "course")
@@unique([title, teacherId])
}
```
```javascript=
const result = await prisma.course.findOne({
where: { id: courseId },
include: { teacher: true },
});
```
* 單一 id `where`
```javascript=
const result = await prisma.user.findOne({
where: {
id: 42,
},
})
```
* 組合 id `where`
```jsonld=
# schema.prisma
model User {
firstName String
lastName String
@@id([firstName, lastName])
}
```
```javascript=
const result = await prisma.user.findOne({
where: {
firstName_lastName: {
firstName: 'Alice',
lastName: 'Smith',
},
},
});
// 或是
const result = await prisma.user.findOne({
where: {
firstName: 'Alice',
lastName: 'Smith',
},
});
// 或是(這種寫法才有 filter 的最大彈性)
// 預設就是 AND 所以上兩例不用給 AND,其他還可以用 NOT, OR
const result = await prisma.user.findOne({
where: {
AND: [{ firstName: { equals: 'Alice' } }, { lastName: { equals: 'Smith' } }],
},
});
```
先介紹一下 `where` 的用法:作為 filter
### - where
* 依據 property 的型別可接受的 [filter types 不太一樣,請參考這裡](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/filtering),例如 `lt`, `lte`, `gt`, `gte`, `contains`, `startsWith`, `endsWith`, `equals` 等
用法簡言之就是:where 條件某個/list(AND/OR/NOT) property 然後給 filter type 和 filter 值,幾乎在任何你想得到的篩選場景都可以插入個 where 來實踐!
(但寫法真的滿多種,就得靠練習練出 sense 了(?)
請搭配參閱 [filtering documentation](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/filtering#filter-on-related-records)
* 來試著講看看下面這段是在篩什麼?
```javascript=
const result = await prisma.post.findMany({
where: {
OR: [
{
title: {
contains: 'Prisma',
},
},
{
title: {
contains: 'databases',
},
},
],
NOT: {
title: {
contains: 'SQL',
},
},
user: {
NOT: {
email: {
contains: 'meeting',
},
},
},
},
include: {
user: true,
},
})
```
上面代表:取得所有 title 包含 "Prisma" 或 "databases" 但不包含 "SQL"、且關聯的 user 的 email 不包含 "meeting" 的所有 post list
* 關聯 record 的 filter
```javascript=
const result = await prisma.post.findMany({
where: {
user: {
email: {
equals: 'sarah@prisma.io',
},
},
},
})
```
* `where` 除了 `AND`, `OR`, `NOT`,也可以搭配 `some`, `every`, `none` 來做條件
```javascript=
const result = await prisma.user.findMany({
where: {
post: {
every: {
published: true,
},
some: {
content: {
contains: 'Prisma',
},
},
},
},
});
```
上面代表:取得所有 post 都 published、且其中有內容含有 "Prisma" 的 user list
* `include` 中使用 `where`
```javascript=
const result = await prisma.user.findMany({
where: {
Post: {
some: {
published: false,
},
},
},
include: {
Post: {
where: {
published: false,
},
},
},
})
```
上面代表:取得所有至少一篇 post 沒有 publish 的,且 include 所有 publish 的 post list 的 user 的 list
* `select` 中使用
```javascript=
const result = await prisma.user.findMany({
where: {
email: {
contains: 'prisma.io',
},
},
select: {
posts: {
where: {
published: false,
},
select: {
title: true,
},
},
},
})
```
上面代表:取得所有 email 含有 "prisma.io" 的 user 的符合未 publish 的 post list
其實換個角度以 prisma.post.findMany 開頭也可以寫出同概念的 query
```javascript=
const result = await prisma.post.findMany({
where: {
published: false,
user: {
email: {
contains: 'prisma.io',
},
},
},
select: {
title: true,
},
})
```
### findMany
* return list
* [除了 `select`, `include` 還有得以 paginate, filter, 和 order 的用法](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud#type-1)
```javascript=
const result = await prisma.user.findMany({
where: {
email: {
endsWith: 'prisma.io',
},
},
})
```
```javascript=
const result = await prisma.post.findMany({
where: {
date_created: {
gte: new Date('2020-03-19T14:21:00+0200') /* Includes time offset for UTC */,
},
},
})
```
* 排序:`orderBy` field `asc`(小到大) 或 `desc`(大到小)
* `take` 取 list 前 n 筆或 `cursor` 後 n 筆
* `skip` 跳前過 n 筆
* `cursor` 指一個 list 的位置,通常是 id 或 unique value(補範例)
### - distinct
```jsonld
# schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["distinct"] # 要多這行設定才可使用
}
```
```javascript
const result = await prisma.user.findMany({
distinct: ['birthday'],
})
```
## ☼ Create
### create
* 必須給 `data`(通常就是定義的 input object 那包)
```javascript
const user = await prisma.user.create({
data: { email: 'alice@prisma.io' },
})
```
* 所以當 schema 定義傳入的是 data: XXXInput 時,resolver 第二個參數得的 data 可直接塞給 create
* return object,一樣可以 `select`、`include`
* 關聯的 type(別張table)可以同時一起新增
* 範例
* 一對多時的同時新增:假設新增 user 同時新增他關聯的多個 post
```javascript=
const user = await prisma.user.create({
data: {
email: 'alice@prisma.io',
posts: {
create: [{ title: 'This is my first post' }, { title: 'Here comes a second post' }],
},
},
})
```
* 一對多時的同時新增:新增已存在的 user post
```javascript=
const user = await prisma.post.create({
data: {
title: 'Hello World',
author: {
connect: { email: 'alice@prisma.io' },
},
},
})
```
`connect` 的 property 一樣必須是 id 或 unique,若找無 query 會失敗,想避免 connect 這種失敗可以使用 `connectOrCreate`(後面補充)
上面 code 也可以改以 user update 的角度寫:
```javascript=
const user = await prisma.user.update({
where: { email: 'alice@prisma.io' },
data: {
posts: {
create: { title: 'Hello World' },
},
},
})
```
### - connectOrCreate
它跟 distinct 一樣是 preview feature,必須先如下新增(但我們應該也不太需要使用到)
```jsonld
# schema.prisma
generator client {
provider = "prisma-client-js"
experimentalFeatures = ["connectOrCreate"] # 要多這行設定才可使用
}
```
```javascript=
const user = await prisma.post.create({
data: {
title: 'Hello World',
author: {
connectOrCreate: {
// connectOrCreate is a preview feature and must be enabled!
where: { email: 'alice@prisma.io' },
create: { email: 'alice@prisma.io' },
},
},
},
});
```
## ☼ Update
### update
大致和 create 用法相同。update 可同時新增/刪除/更新關聯
* 必須給 `data`(要更新的部分,也是通常 input object 的那包)
* 必須要給 `where` 篩 id 或 unique
```javascript=
const user = await prisma.user.update({
where: { id: 1 },
data: { email: 'alice@prisma.io' },
})
```
* return object 或 RecordNotFound failed,一樣可以 `select`、`include`
* 關聯的 type(別張table)可以同時一起更新
* 範例
```javascript=
const user = await prisma.user.update({
where: { email: 'alice@prisma.io' },
data: {
posts: {
update: [
{
data: { published: true },
where: { id: 32 },
},
{
data: { published: true },
where: { id: 23 },
},
],
},
},
})
```
也可以用 `upsert` 可以更新關聯或 create 新的(Insert 或 Update),return object
單筆
```javascript=
const user = await prisma.user.upsert({
where: { id: 1 },
update: { email: 'alice@prisma.io' },
create: { email: 'alice@prisma.io' },
})
```
關聯多筆
```javascript=
const user = await prisma.user.update({
where: { email: 'alice@prisma.io' },
data: {
posts: {
upsert: [
{
create: { title: 'This is my first post' },
update: { title: 'This is my first post' },
where: { id: 32 },
},
{
create: { title: 'This is mt second post' },
update: { title: 'This is mt second post' },
where: { id: 23 },
},
],
},
},
})
```
```javascript=
const user = await prisma.user.update({
where: { email: 'alice@prisma.io' },
data: {
posts: {
delete: [{ id: 34 }, { id: 36 }],
},
},
})
```
還有 disconnect 關聯的用法
```javascript=
const user = await prisma.user.update({
where: { email: 'alice@prisma.io' },
data: {
posts: {
disconnect: [{ id: 44 }, { id: 46 }],
},
},
})
```
重新指定關聯的 posts
```javascript=
const user = await prisma.user.update({
where: { email: 'alice@prisma.io' },
data: {
posts: {
set: [{ id: 32 }, { id: 42 }],
},
},
})
```
### updateMany
* 必須要給 `data`(要更新的部分,也是通常 input object 的那包)
* 批量 update 的部分 `where` 是 optional,沒給是指全部
* return 有 count 的 BatchPayload object
## ☼ Delete
### delete
* 必須要給 `where` 篩 id 或 unique
```javascript=
const user = await prisma.user.delete({
where: { id: 1 },
})
```
* return deleted object,一樣可以 `select`、`include`
### deleteMany
* `where` 是 optional,沒給是指全部
* return 有 count 的 BatchPayload object
## ☼ count
* return 有 count 的 BatchPayload object
* 可以給 [`where`, `orderBy`, `skip`, `after`, `before`, `first`, `last`](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/crud#reference-8)
```javascript=
const result = await prisma.user.count({
where: {
post: {
some: {
published: true,
},
},
},
})
```
# Prisma Client Pagination
簡介兩種分頁模式
## Offset pagination
利用 `skip`, `take` 迅速選取到指定的頁面資料範圍(MySQL 中利用 OFFSET)

```javascript=
// 假設一頁 20 筆,取第 11 頁(搭配 filter, sort)
const results = prisma.post.findMany({
skip: 200,
take: 20,
where: {
title: {
contains: 'Prisma',
},
},
orderBy: {
title: 'desc',
},
})
```
* Pros:且適用任意 sort (orderBy) 後的“不變”的結果(期間有增刪值可能會影響到)
* Cons:選越後面的筆數越耗能(假設 skip 200,000),因為它一樣會從起始往後掃到你要的那段
* 但一般來說像是個人部落這種的資料量不大的都還可以適用
## Cursor-based pagination
在經過 unique 且有序列的 column (EX. id 或 timestamp)排序過後的結果,取得指定點 `cursor` 前/後 limited 個數 `take` 的選取範圍結果
可以把 cursor 當書籤的概念
(MySQL not use OFFSET,而是查詢大於 cursor 的值)

```javascript=
// 上圖為例
const firstQueryResults = prisma.post.findMany({
take: 4,
where: {
title: {
contains: 'Prisma', // optional
},
},
orderBy: {
id: 'asc',
},
})
const lastPostInResults = firstQueryResults[3]
const myCursor = lastPostInResults.id // 29
```
呈上,指定從 cursor 位置開始取 4 個
```javascript=
const secondQueryResults = prisma.post.findMany({
take: 4, // 取後 4 筆,要改取前的話給負數即可
skip: 1, // 起始包含 cursor(上次選取的最後一個),會 skip 掉
cursor: {
id: myCursor, // 29
},
where: {
title: {
contains: 'Prisma'
},
},
orderBy: {
id: 'asc',
},
})
const lastPostInResults = secondQueryResults[3]
const myCursor = lastPostInResults.id // 52
```

* Pros:scales(增刪值不影響)
* Cons:必須要排序成唯一的序列(有點抽象,但記得要拿 unique 的值來 orderBy 過就是了)。且沒辦法直接跳到指定頁,只能靠 cursor ,而 cursor 是不可預測的。
* 適用場景:Infinite scroll、一次批次分頁完所有資料
## GraphQL Schema 的部分請自行參考
:::warning
Use Nodes when you have a finite list to use but don’t need the cursors for pagination. Use Edges when you have large list and you need to know the cursors to paginate
:::
* [GraphQL Pagination best practices: Using Edges vs Nodes in Connections](https://medium.com/javascript-in-plain-english/graphql-pagination-using-edges-vs-nodes-in-connections-f2ddb8edffa0)
* [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm)
* https://medium.com/@smallbee/super-fast-offset-pagination-with-prisma2-21db93e5cc90
# Documents / Resources
* [Prisma](https://www.prisma.io/docs/getting-started/quickstart-typescript) 🔆
* [GraphQL](https://graphql.org/learn/) 🔆
* [2019 iT 邦幫忙鐵人賽 - Think in GraphQL 系列](https://ithelp.ithome.com.tw/users/20111997/ironman/1878)
* [Code-first vs. schema-first development in GraphQL](https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/)
* [Shopify Tutorial: Designing a GraphQL API](https://github.com/Shopify/graphql-design-tutorial/blob/master/TUTORIAL.md)
* [N+1 Problem → GraphQL Design: 使用 DataLoader 提升效能 !](https://ithelp.ithome.com.tw/articles/10207606)
> [Github singple_backend](https://github.com/Tyler-ntut/singple_backend)
---
# Workshop
Clone → https://gitlab.baifu-tech.net/f2e_tw/serverworkshop
1. Custom Type
* GraphQL Scalar Type 之外的 Type
[`npm i graphql-scalars`](https://www.npmjs.com/package/graphql-scalars)
DateTime 格式:`1993-05-01T08:36:23.528Z`
[custom scalar type](https://www.graphql-tools.com/docs/scalars#custom-scalar-examples)
https://stackoverflow.com/a/41513681/7849574
* 自定義
```javascript=
// resolvers
const { GraphQLScalarType } = require('graphql');
const { formatDate } = require('./utils/formatDate');
const dateValue = value => formatDate(value);
const resolverMap = {
Date: new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
parseValue: dateValue, // value from the client 輸出到前端
serialize: dateValue, // value sent to the client 從前端 variables 進來的 input
parseLiteral (ast) {
// 從前端 inline variables 進來的 input
return new Date(ast.value);
},
}),
};
```
```graphql=
# typeDefs
scalar Date
```
2. 回憶稍早前的 relation queries 例子,來實作看看吧!
1. Prisma schema 定義 model 建 table
VScode install prisma extension
目前已經有 model User,幫我加進另外三個 model Profile、Category、Post,並且在 DB 根據此 schema 建立關聯的 Tables
* m-n 關聯的部分使用 [explicit 方式](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-schema/relations#implicit-vs-explicit-many-to-many-relations)(另外建關聯 model)

* 補充一個在 Prisma 1 正式、但 2 還在實驗性的 Admin GUI 功能
`npx prisma studio --experimental`

http://localhost:5555 開啟。在 API 還沒建好前,如果只是簡單無關聯的去 CRUD table 資料可以從這操作,應該會比 phpmyadmin 好用
2. GraphQL schema 修改 type User 並定義新 type(Profile, Post, Category)
* 修改 User type
* List 部分確保裡面的成員不為 null
* 可以直接對照參考 Prisma schema
* 最後先試著修改 resolver 裡的 createUser,得以同時新增 User 和其 Profile
* [自行查詢官方文件](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/relation-queries)
3. 完成 `deleteUser`, `deletePost`, `createPost`
* createPost input 定義一個 input object type,data 代入,回傳 Post
3. 
1. 現有這 9 個 Tables,請幫我 implement GraphQL Schema 的各個 type(你認為可以 response 的展示結果)
2. 實作 createUser (可同時新增 profile)
3. 新增 createCourse (connect user(teacher))
4. 新增課程的回覆/新增課程的公告
---
Some Examples.
* Add [Filter](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/filtering) input type
* Add Sort input type
* Self Relation examples (self-relations branch)
* Spilt TypeDefs & Resolvers (spilt-files-example)
*
---
* [SQL 語法教學、Constraint 限制可以參考這邊](https://www.fooish.com/sql/constraints.html)