Yu Lin Chen (霖LiN)
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 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` ![](https://i.imgur.com/9ajoHE5.png) [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 會怎麼實作間的關聯? ![](https://i.imgur.com/YNLZHl2.png) :::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 } ``` ![](https://i.imgur.com/32zx3NN.png) 寫法二: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 } ``` ![](https://i.imgur.com/GoUQKya.png) 補充: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 ![](https://i.imgur.com/S7lL2ID.png =500x) :::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 方式 ![](https://i.imgur.com/Nf4iCRE.png) ```jsonld= # 省略 @relation model Post { id Int @id @default(autoincrement()) categories Category[] } model Category { id Int @id @default(autoincrement()) posts Post[] } ``` ![](https://i.imgur.com/2fSTQl9.png) ::: 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]) } ``` ![](https://i.imgur.com/AdoDfYh.png) 如果想自己定義關聯的 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") } ``` ![](https://i.imgur.com/rikFBeb.png) * 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]) } ``` ![](https://i.imgur.com/LTAUsRm.png) ![](https://i.imgur.com/V6Ier26.png) * [如果是 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 怎麼操作 ![](https://i.imgur.com/PCt8w0Y.png) ### 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) ![](https://i.imgur.com/GiMkJcJ.png) ```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 的值) ![](https://i.imgur.com/tVLKzmy.png) ```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 ``` ![](https://i.imgur.com/UISj2lZ.png) * 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) ![](https://i.imgur.com/2DaTk5y.png) * 補充一個在 Prisma 1 正式、但 2 還在實驗性的 Admin GUI 功能 `npx prisma studio --experimental` ![](https://i.imgur.com/yIjEn67.png) 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. ![](https://i.imgur.com/0GLfX8B.png) 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)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully