Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
TypeScript による GraphQL バックエンド開発
Search
Naoya Ito
October 14, 2022
Technology
38k
29
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
TypeScript による GraphQL バックエンド開発
10/14 の Tech Play での発表資料です
https://techplay.jp/event/873259
Naoya Ito
October 14, 2022
More Decks by Naoya Ito
See All by Naoya Ito
アルゴリズムは何を圧縮しているのか ─ Haskell から育った「圧縮代数」というメンタルモデル
naoya
16
4.4k
Haskell を武器にして挑む競技プログラミング ─ 操作的思考から意味モデル思考へ
naoya
13
4.7k
Haskell でアルゴリズムを抽象化する / 関数型言語で競技プログラミング
naoya
21
8k
Functional TypeScript
naoya
19
6.9k
TypeScript 関数型スタイルでバックエンド開発のリアル
naoya
77
38k
シェルの履歴とイクンリメンタル検索を使う
naoya
16
6.8k
20230227-engineer-type-talk.pdf
naoya
91
87k
関数型プログラミングと型システムのメンタルモデル
naoya
63
110k
フロントエンドのパラダイムを参考にバックエンド開発を再考する / TypeScript による GraphQL バックエンド開発
naoya
67
25k
Other Decks in Technology
See All in Technology
AIに任せた品質は、誰が見立てるのか - AI時代のテストマネジメント
nakanao
3
2.6k
JSONataとAWS Step Functionsで目指すRuntimelessな世界
mu7889yoon
0
330
株式会社シーエーシー エンジニア向け会社紹介資料
cac
0
57k
AIエージェントの権限管理 3: Agentic RAG の Fine grained access control 編
ren8k
0
170
2026-09-18 gotanda.sre Terraformで複数環境作ったり、複数Stateに分割したりそれとTerragrunt / Terraform multi envs and multi states
masasuzu
3
660
アプリをもっと"iOSアプリっぽく"する小さな工夫 / Small Touches That Make Your App Feel More Like an iOS App
matsuji
2
980
アプリログインとWeb認証基盤をつなぐ ASWebAuthenticationSession 作法
shimastripe
1
350
GoのInterface内部構造から学ぶ!最高パフォーマンスを出すコード設計
yappli_developers
1
210
Issue 駆動でスペシャリストの意図を届ける、AI 実装のアクセシビリティ向上
thkt
0
120
作って終わりじゃないサーバーレス 〜9年運用する大規模EC物流API基盤の設計・運用のリアル〜
zozotech
PRO
0
360
積み重なった技術負債への挑戦 〜初手としての全社ゴト化〜
techtekt
PRO
0
1.4k
登壇の自信を奪う3匹のオバケ / 3 Ghosts That Rob You of Your Confidence in Public Speaking
pauli
9
1k
Featured
See All Featured
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
1
400
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
550
Building Flexible Design Systems
yeseniaperezcruz
330
41k
Lightning Talk: Beautiful Slides for Beginners
inesmontani
PRO
2
700
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
250
エンジニアに許された特別な時間の終わり
watany
109
250k
Making Projects Easy
brettharned
120
6.8k
Are puppies a ranking factor?
jonoalderson
2
3.9k
Why Our Code Smells
bkeepers
PRO
340
58k
What does AI have to do with Human Rights?
axbom
PRO
1
2.4k
Leadership Guide Workshop - DevTernity 2021
reverentgeek
1
370
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
420
Transcript
5ZQF4DSJQUʹΑΔ (SBQI2-όοΫΤϯυ։ൃ 5ZQF4DSJQUͷܕγεςϜͱσʔλϑϩʔʹணͨ͠એݴతϓϩάϥϛϯά גࣜձࣾ Ұٳ ҏ౻
Ϟνϕʔγϣϯ
ࡢࠓɺϑϩϯτΤϯυ όοΫΤϯυͷٕज़తؔ৺ࣄʹΪϟοϓ • ΞϓϦέʔγϣϯͷঢ়ଶཧϞσϧ • σβΠϯγεςϜ • ϓϦϨϯμϦϯά • ŋŋŋ
ϑϩϯτΤϯυ όοΫΤϯυ • υϝΠϯϞσϧ • ϨΠϠʔυɾΞʔΩςΫνϟ • $234 • ŋŋŋ ৫ͷٕज़࿅্͕͕Ε্͕Δ΄Ͳɺؔ৺ࣄͷΪϟοϓ͕͕͍ͬͯͬͨ
3FBDUͰϑϩϯτΤϯυΛ։ൃ͔ͯ͠Βɺ όοΫΤϯυΛॻ͘ͱŋŋŋ • 3FBDUŋŋŋ খ͞ͳؔΛΈ߹Θͤͯએݴతʹॻ͍͍ͯ͘ • όοΫΤϯυ ŋŋŋ ΫϥεΛͨ͘͞Μॻ͍ͯɺϨΠϠʔΛލ͙ͱ %50Ͱͷ٧Ίସ͑Λߦͬ
ͯɺJOUFSGBDFͰґଘੑͷٯసΛߦͬͯŋŋŋ – ʮŋŋŋϑϩϯτΤϯυͩͱ͜͏͍͏͜ͱɺ͋Μ·ΓΒͳ͍ΑͶʯ ։ൃ࣌ͷϝϯλϧϞσϧͷΪϟοϓ͕େ͖͍ ίϯςΩετεΠονͷෛ୲େ͖͍
όοΫΤϯυ։ൃͷΓํΛ࠶ߟͯ͠Έ͍ͨ • 3FBDUΛ͍ͬͯΔͱϑϩϯτΤϯυബ͘ॻ͘͜ͱ͕Ͱ͖Δ • ؔ৺ࣄ͕ҧ͏ͷવɻ͔ͱ͍ͬͯɺΓํ͕ҧ͏ͷΛશٙ͘Θͳ͍ͷͲ͏ͩΖ͏ ϑϩϯτΤϯυͷঢ়ଶཧෳࡶ ͦͷෳࡶͳͷΛͲ͏ѻ͏͔ɺݱ࣌Ͱ࠷ྑͷϞσϧͷͻͱ͕ͭ 3FBDUͷͣ ʮෳࡶͳঢ়ଶΛͲ͏ѻ͏͔ʯͱ͍͏؍ͰɺαʔόʔαΠυಉ͡Α͏ʹߟ͑ΒΕͳ͍ͷ͔ Α͠ɺ(SBQI2-όοΫΤϯ
υ 5ZQF4DSJQUͰॻ͍ͯΈ Α͏
ຊʹೖΔલʹɺ(SBQI2-όοΫΤϯυͱ $234
$234 • ίϚϯυΫΤϦݪଇ $PNNBOE2VFSZ3FTQPOTJCJMJUZ4FHSFHBUJPO$234 – ΞϓϦέʔγϣϯ࣮ͷจ຺Ͱ ࢀরܥ 2VFSZ ͱߋ৽ܥ $PNNBOE
ͰҟͳΔϞσϧΛ ͏Ξϓϩʔν
(SBQI2-2VFSZͱυϝΠϯϞσϧͷʮूʯɺטΈ߹Θ͕ͤѱ͍ • (SBQI2-2VFSZ ˞ .VUBUJPOͰͳ͍ – ΫϥΠΞϯτ͕ϢʔεέʔεΛߏ͢Δ – ू୯ҐͰͳ͘ɺϦιʔε୯ҐͰૢ࡞͞ΕΔ •
(SBQI2-2VFSZʹݶΒͣ ूΛશ෦Ҿ͘Θ͚ʹ͍͔ͳ͍໘ͰͲ͏͢Δ͔ŋŋŋ '"2 – ྫݕࡧҰཡϖʔδ – $234Λద༻ͯ͠ 2VFSZ4FSWJDFΛ࣮͢Δͱ͍͏ͷ͕ɺͻͱͭͷղܾࡦ
(SBQI2-2VFSZͰෳࡶͳۀϩδοΫ͕͋·Γඞཁͳ͍ ˞զʑͷ߹ • ܦݧతʹ͔͖ͬͯͨ – ଟ͘σʔλϕʔεͷΛͦͷ·· (SBQI2-0CKFDUʹ͢Εྑ͍͚ͩ – ϦονͳʮυϝΠϯϞσϧʯࢀরܥʹɺඞཁͳ͍͜ͱ͕ଟ͍ •
ͦͦ %%%ͷूࢀরͱ͍͏ΑΓɺूશମͷ߹ੑΛอͭ͜ͱʹॏ͖͕ஔ͔Ε͍ͯΔŋŋŋͣ
https://speakerdeck.com/qsona/architecture-decision-for-the-next-n-years-at-studysapuri?slide=35
1SJTNB
1SJTNB • ʮ03.ʯͱ͋Δ͕࣮ࡍΫΤϦϏϧμʔ ϓϨʔϯͳΦϒδΣΫτΛฦ͢σʔλΞΫηεϥΠϒϥϦ • ࠓͷͱ͋·Γؔͳ͍ ϓϩάϥϛϯάݴޠʹґଘ͠ͳ͍εΩʔϚఆٛɺϚΠάϨʔγϣϯػߏΛ͍࣋ͬͯ Δͷ͕ॴɻ։ൃऀମݧ͕ͱͯྑ͍
Domain Model GraphQL Mutation Repository ߋ৽ܥ GraphQL Query Prisma Prisma
࣌ʑ ബ͍ %PNBJO ࢀরܥ (SBQI2-1SJTNB$234
Domain Model GraphQL Mutation Repository ߋ৽ܥͷυϝΠϯϞσϧΛͲ͏ॻ͔͘ɻ͔͜͜Β͕ຊ ߋ৽ܥ GraphQL Query Prisma
Prisma 薄い Domain Layer ࢀরܥ
8FCΞϓϦέʔγϣϯόοΫΤϯυͷ࣮ํ๏Λ࠶ߟ͢Δ
ࡢࠓͷϑϩϯτΤϯυͷϓϩάϥϛϯάύϥμΠϜΛߟ͑ͯΈΔ https://zenn.dev/mizchi/articles/oop-think-modern
&MNΞʔΩςΫνϟ https://guide.elm-lang.jp/architecture/
update : Msg -> Model -> ( Model, Cmd Msg
) update msg model = case msg of ToggleLike -> ( { model | photo = Maybe.map toggleLike model.photo }, Cmd.none ) UpdateComment comment -> ( { model | photo = Maybe.map (updateComment comment) model.photo }, Cmd.none ) SaveComment -> ( { model | photo = Maybe.map saveNewComment model.photo }, Cmd.none ) LoadFeed (Ok photo) -> ( { model | photo = Just photo }, Cmd.none ) LoadFeed (Err _) -> ( model, Cmd.none ) viewLikeButton : Photo -> Html Msg viewLikeButton model = let buttonClass = if model.liked then ... div [ class "like-button" ] [ i [ class "fa fa-2x", class buttonClass, onClick ToggleLike ] [] ] &MN 7JFX .PEFMΛඳըɻ Ϣʔβʔૢ࡞ʹԠͯ͡Πϕ ϯτΛૹΔͱŋŋŋ &MNϥϯλΠϜ͕ VQEBUFؔ ΛݺͿɻؔʹΠϕϯτͷछ ྨʹԠͨ͡Ϟσϧͷঢ়ଶભҠΛ هड़͓ͯ͘͠
ঢ়ଶભҠͷؔ ֎քͱΓͱ Γ *0 イベント コマンド
model -> model' model -> model' model -> model' ΠϕϯτΛܖػʹঢ়ଶ͕ભҠ͢Δ
ŋŋŋ ࣌ܥྻʹج͍ͮͨঢ়ଶ
ঢ়ଶભҠͷؔ ϥϯλΠϜϑ ϨʔϜϫʔΫ イベント コマンド Πϕϯτʹ͍ঢ়ଶΛભҠͤͯ͞ɺ͋ͱϑϨʔϜϫʔΫϥϯλΠϜʹͤΔ
3FEVY"QQMJDBUJPO%BUB'MPX https://redux.js.org/tutorials/essentials/part-1-overview-concepts
None
όοΫΤϯυͰಉ͡Α͏ʹʮ࣌ܥྻʹجͮ͘ঢ়ଶભҠʯͷࢹͰߟ͑ΒΕͳ͍͔ • όοΫΤϯυͷੈքͷओͳʮঢ়ଶʯ ŋŋŋ υϝΠϯϞσϧͷঢ়ଶ • υϝΠϯϞσϧͷঢ়ଶΛભҠͤ͞ΔΠϕϯτ ŋŋŋ υϝΠϯΠϕϯτ
ͨͱ͑ʮ॓ധ༧ʯΛྫʹυϝΠϯϞσϧΛվΊͯߟ͑ͯΈΔ • ͲΜͳ؍ʹͯ͠ߟ͑ͯΈΔ͖͔ – σʔλߏ – &3ਤ – Ϋϥεͷ࣮ –
ը໘ • ͍ͣΕ੩తͳߏʹযΛ͍ͯͯΔɻࢹΛม͑ͯΈ͍ͨ – ಈతͳͷŋŋŋυϝΠϯΠϕϯτঢ়ଶʹযΛͯͯΈΔͱ
ʮ༧ʯͷঢ়ଶભҠʹணͯ͠ΈΔ ༧ྃ Χʔυܾࡁ ࡁΈ Ωϟϯηϧ ॓ധࡁΈ
৽ن༧͕ྃ͢Δલ͔ΒυϝΠϯϞσϧଘࡏ͍ͯ͠Δ ༧ྃ Ωϟϯηϧ ॓ധࡁΈ ೖྗ ݕূࡁΈ ೖྗະݕূ ࡏݿ֬อ ࡁΈ
ঢ়ଶԿ͔͠ΒͷΠϕϯτΛܖػʹભҠ͢Δ ༧ྃ Ωϟϯηϧ ॓ധࡁΈ ೖྗ ݕূࡁΈ ೖྗະݕূ ࡏݿ֬อ ࡁΈ ༧Λ։࢝ͨ͠
ݕূ͕ྃͨ͠ ࡏݿΛ֬อͨ͠ ༧Λߦͬͨ Ωϟϯηϧ͞Εͨ ॓ധͨ͠
model -> model' model -> model' model -> model' ͓
྆֎෦ͱͷΠϯλϑΣʔε model -> model model -> model &WFOU)BOEMFS 8FC"QQͳΒ SPVUFS
%#ʹอଘ͠ Ϩεϙϯε 6*
model -> model model -> model event &WFOU)BOEMFS 8FC"QQͳΒ SPVUFS
%#ʹอଘ͠ Ϩεϙϯε 6* event event ֎ͷੈք ֎ͷੈք ֎ͷੈք Πϕϯτ ˠϞσϧͷঢ়ଶભҠ 🤔 Ͳ͔͜Ͱݟͨͳŋŋŋ
ঢ়ଶભҠͷؔ ϥϯλΠϜϑ ϨʔϜϫʔΫ イベント コマンド ಉ͡
*0ঢ়ଶભҠ *0 Pure function Model -> Model *0 JOQVUMPBE *0
PVUQVU
None
https://www.slideshare.net/ScottWlaschin/reinventing-the-transaction-script-ndc-london-2020
ϑϩϯτΤϯυͱόοΫΤϯυͷঢ়ଶཧ • ϑϩϯτΤϯυͷঢ়ଶཧ ŋŋŋ ओͳؔ৺ࣄʮΞϓϦέʔγϣϯͷঢ়ଶʯ • όοΫΤϯυͷঢ়ଶཧ ŋŋŋ ओͳؔ৺ࣄʮυϝΠϯϞσϧɺυϝΠϯΦϒδΣΫτͷঢ় ଶʯ
ཧ͍ͯ͠Δঢ়ଶͷίϯςΩετҧ͏ͷͷ ঢ়ଶཧͷϞσϧࣅͨΑ͏ʹߟ͑ΒΕΔͷͰͳ͍͔
όοΫΤϯυ 5ZQF4DSJQUͰએݴతϓϩάϥϛϯά
ʮυϝΠϯΦϒδΣΫτͷঢ়ଶભҠΛએݴతʹهड़ͭͭ͠ *0͔Β͢Δʯ • ͜ͷίϯηϓτͰ࣮ • &MNΞʔΩςΫνϟॻ੶ %PNBJO.PEFM.BEF'VODUJPOBMΛࢀߟʹ
͜ͷελΠϧͰΑ͘͏ͷ • type / interface • λά͖ϢχΦϯ ܕ • 3FTVMUܕ
• ΧϦʔԽ • ܕͷϒϥϯυԽ ίϯύχΦϯΦϒδΣΫτ
͋·ΓΘͳ͍ͷ • class • ྫ֎ͷ throw – Error Ϋϥε͍·͢
؆୯ͳϢʔεέʔεྫ
5BHΤϯςΟςΟ ूϧʔτ ͷঢ়ଶભҠʹண͢Δ 7BMJEBUFE 6OWBMJEBUFE $SFBUFE ೖྗ͕͋ͬͨ ݕূͨ͠ ࡞ͨ͠ ˞͜͜Ͱͷʮ$SFBUFEʯ͋͘·ͰυϝΠϯΦϒδΣΫτ͕
l࡞ࡁΈzʹͳͬͨঢ়ଶͰ͋ͬͯɺσʔλϕʔεʹϨίʔυ ΛՃͨ͠ɺͱ͍͏ঢ়ଶͰͳ͍
5BHΫϥεΛ࡞Δ export class Tag { state: 'Unvalidated' | 'Validated' |
'Created', id: TagId | undefined, groupId: RestaurantGroupId, label: string, icon: TagIcon | undefined, sortOrder: number | undefined, builtin: boolean | undefined }
export class Tag { state: 'Unvalidated' | 'Validated' | 'Created',
id: TagId | undefined, groupId: RestaurantGroupId, label: string, icon: TagIcon | undefined, sortOrder: number | undefined, builtin: boolean | undefined } ঢ়ଶભҠલʹ֬ఆ͠ͳ͍ϓϩύςΟ͕ VOEFGJOFE ʹͳͬͯ͠·͏ŋŋŋ
interface UnvalidatedTag { kind: 'Unvalidated' groupId: string label: string icon?:
{ symbol: string; type: TagIconType; color?: string | null | undefined } | null | undefined } interface ValidatedTag { kind: 'Validated' groupId: RestaurantGroupId label: string icon: TagIcon } export interface CreatedTag { kind: 'Created' id: TagId groupId: RestaurantGroupId label: TagLabel icon: TagIcon sortOrder: number builtin: boolean } //※この型は実際には出番がないので使っていない export type Tag = UnvalidatedTag | ValidatedTag | CreatedTag ͦ͜Ͱঢ়ଶ͝ͱʹܕΛఆٛ͢Δ ঢ়ଶ͕ભҠ͢Δ υϝΠϯΠϕ ϯτ͕ൃੜ͢Δ͝ͱʹϞσϧͷ ͕֬ఆ͍ͯ͘͠ͷ͕એݴͰ͖͍ͯ Δ
l.BLF*MMFHBM4UBUFT6OSFQSFTFOUBCMFz interface User { memberId: MemberId | undefined guestId: GuestId
| undefined } interface Member { userId: MemberId } interface Guest { guestId: GuestId } type User = Member | Guest औΓಘΔͷछྨ֤ଐੑͷੵʹͳΔ ੵ Y ɾ྆ํ VOEFGJOFE ɾ྆ํͷ͕ຒ·Δ ͱ͍͏্༷͋Γಘͳ͍ঢ়ଶ͕ੜ·ΕΔ औΓಘΔछྨ֤ଐੑͷ ্༷͋Γಘͳ͍ঢ়ଶදݱ͠ͳ͍ Ϩίʔυʮ͔ͭ "/% ʯ ϢχΦϯʮ·ͨ 03 ʯ
ͪ͜ΒΑΓŋŋŋ export class Tag { state: 'Unvalidated' | 'Validated' |
'Created', id: TagId | undefined, groupId: RestaurantGroupId, label: string, icon: TagIcon | undefined, sortOrder: number | undefined, builtin: boolean | undefined }
interface UnvalidatedTag { kind: 'Unvalidated' groupId: string label: string icon?:
{ symbol: string; type: TagIconType; color?: string | null | undefined } | null | undefined } interface ValidatedTag { kind: 'Validated' groupId: RestaurantGroupId label: string icon: TagIcon } export interface CreatedTag { kind: 'Created' id: TagId groupId: RestaurantGroupId label: TagLabel icon: TagIcon sortOrder: number builtin: boolean } export type Tag = UnvalidatedTag | ValidatedTag | CreatedTag ͪ͜Βͷํ͕ɺͷΈ߹Θͤύλʔϯ͕গͳ͘ݫີ
type validateTag = (model: UnvalidatedTag) => ValidatedTag const validateTag: validateTag
= (model) => { // (省略: 値の validation ...) return { ...model, kind: 'Validated', groupId: RestaurantGroupId(model.groupId), icon: model.icon ? TagIcon(model.icon) : NoIcon(), } } ঢ়ଶΛભҠͤ͞Δεςοϓ ؔ
ঢ়ଶΛભҠͤ͞Δεςοϓ ؔ type createTag = (model: ValidatedTag) => CreatedTag
const createTag: CreatedTag = (model) => { return { ...model, kind: 'Created', id: generateTagId(), sortOrder: getTagSortOrder({ groupId: model.groupId }), builtin: false, } } ४උ͕ͬͯॳΊ͕ͯ֬ఆ ͢ΔͷΛࣗવʹهड़Ͱ͖Δ ͳ͓ getTagSortOrder *0͕͋Δ ͨΊ %*͢Δɻޙड़
ϞσϧͷܕɺؔͷܕʹΑͬͯঢ়ଶભҠΛએݴతʹهड़͢Δ 7BMJEBUFE 6OWBMJEBUFE $SFBUFE (model: UnvalidatedTag) => ValidatedTag (model: ValidatedTag)
=> CreatedTag
ݸผʹఆٛͨ͠ঢ়ଶભҠͷؔΛܨ͍͛ͨ • Ͱɺܭࢉʮ్தͰࣦഊ͢ΔʯՄೳੑ͕͋Δ – ͨͱ͑υϝΠϯϞσϧͷࣄલ݅Λຬͨ͞ͳ͍Τϥʔ – 7BMJEBUJPO&SSPSŋŋŋ – .BY5BH-JNJU&YDFFEFEʜ •
ʮ్தͰࣦഊ͢Δʯ͜ͱΛܕͰએݴͰ͖ͳ͍͔ ˠ 3FTVMUܕ
3FTVMUܕͰࣦഊͷՄೳੑͷ͋ΔܭࢉΛҰຊಓʹ߹͢Δ import { Result, ok, err } from 'neverthrow' function
itsUnder100(n: number): Result<number, Error> { return n <= 100 ? ok(n) : err(new Error('100より大きい数字です')) } function itsEven(n: number): Result<number, Error> { return n % 2 == 0 ? ok(n) : err(new Error('奇数です')) } function itsPositive(n: number): Result<number, Error> { return n > 0 ? ok(n) : err(new Error('負数です')) } const result = ok(96).andThen(itsUnder100).andThen(itsEven).andThen(itsPositive) result.match( (n) => console.log(n), (error) => { throw error } )
3FTVMUܕͰঢ়ଶભҠؔΛͭͳ͛ͯɺҰͭͷʮϫʔΫϑϩʔʯΛ࡞Δ 7BMJEBUFE5BH $SFBUFE5BH 6OWBMJEBUFE5BH 7BMJEBUFE5BH 8PSL'MPX
type validateTag = (model: UnvalidatedTag) => Result<ValidatedTag, ValidationError> const validateTag:
validateTag = (model) => { const groupId = RestaurantGroupId(model.groupId) const label = TagLabel(model.label) const icon = model.icon ? TagIcon(model.icon) : ok(NoIcon()) const values = Result.combine(tuple(groupId, label, icon)) return values.map(([groupId, label, icon]) => ({ ...model, kind: 'Validated', groupId, label, icon, })) } ঢ়ଶભҠͷޭ ࣦഊΛ 3FTVMUܕͰฦ͢Α͏ʹ͢Δ ͷੜʹࣦഊ͢ΔՄೳੑ͕͋ΔͷͰɺ ͜ΕΒ 3FTVMUܕΛฦ͢
3FTVMUܕͰঢ়ଶભҠؔΛܨ͛ͯɺϫʔΫϑϩʔ υϝΠϯϩδοΫ Λ࡞Δ type WorkFlow = (model: UnvalidatedTag) => Result<CreatedTag,
CreateTagError> export const createTagWorkFlow: WorkFlow = (model) => ok(model).andThen(validateTag).andThen(createTag)
ϫʔΫϑϩʔͷ࢝·ΓͱऴΘΓ͕ɺ֎քͱͷ JOPVU 7BMJEBUFE5BH $SFBUFE5BH 6OWBMJEBUFE5BH 7BMJEBUFE5BH 8PSL'MPX ೖྗͷ%50
ྫ(SBQI2-*OQVU5ZQF Λ 6OWBMJEBUFE5BHʹม UBH3FQPTJUPSZͰ $SFBUFE5BHΛอଘ
(SBQI2-SFTPMWFS3FQPTJUPSZ %# ͱϫʔΫϑϩʔΛଓ͢Δ import { saveCreatedTag } from '../../../customers/repos/tagRepository' export
const createTagMutation = mutationField('createTag', { ... resolve(_root, { input }, context) { const workflow = createTagWorkFlow() // GraphQL 入力をワークフローの入力に変換 const unvalidatedTag = toUnvalidatedTag({ ...input, groupId: context.operator.groupId, }) // ワークフローを実行し Repository パターンの関数で DB に保存 (ここも Result で繋ぐ) const result = ok(unvalidatedTag).andThen(workflow).andThen(saveCreatedTag(context)) return result.match( (tag) => ({ tag: { ...tag, id: toGlobalId('Tag', tag.id), }, }), (error) => { // ここで初めて例外をスロー (単に Nexus にエラーを伝える手段としてスローする) throw error } ) }, })
ok(model).andThen(workflow).andThen(saveCreatedTag(context)) (SBQI2-*OQVU σʔλϕʔε Pure function Model -> Model *0 JOQVUMPBE
*0 PVUQVU
ঢ়ଶભҠͷؔ ϥϯλΠϜϑ ϨʔϜϫʔΫ イベント コマンド
σʔλϑϩʔϓϩάϥϛϯά • 3FTVMUܕͰࣦഊͷ͋ΔܭࢉΛ߹͠ɺσʔλͷ௨Γಓͱͯ͠ͷܭࢉաఔΛ࡞Δ – ͦ͜ʹσʔλΛ์ΓࠐΉͱɺͦͷதΛ௨ͬͯঢ়ଶભҠͨ͠σʔλ͕ಘΒΕΔ – σʔλΛσʔλͷ··ɺͦͷՄൖੑΛԼ͛ͣʹѻ͍͍ͨɻ݁Ռ class ͷొػձ͕ͳ͍ •
ܭࢉΛҰຊಓʹ͢Δ – େҬग़͠ͳ͍ɻେҬग़͢Δͱܭࢉ͕ҰຊಓʹͳΒͳ͍ ˠྫ֎ΛΘͳ͍ – ࣦഊͷذ 3FTVMUͰ߹ ˞3FTVMUܕϞφυ – ܭࢉ͕ҰຊಓʹͳΔ σʔλෆมɻೝෛՙ͕͘ͳΔ
υϝΠϯϞσϧͷอଘैདྷ௨Γ 3FQPTJUPSZ export const saveCraetedTag = ({ prisma }: applicationContext)
=> (model: CreatedTag): ResultAsync<TagData, PrismaClientError> => { const { kind: _, ...tag } = model const icon = toIconData(tag.icon) return ResultAsync.fromPromise( prisma.tag.create({ data: { ...tag, ...icon, }, }), PrismaClientError ) } 3FTVMU"TZODGSPN1SPNJTFΛ͏͜ ͱͰ 1SPNJTFΛ 3FTVMUʹแΈɺଞͷ 3FTVMUܕͱ߹Ͱ͖Δ ྫ֎෧͡ࠐΊΔ͜ͱ͕Ͱ͖Δ
3FQPTJUPSZύλʔϯʹ͍ͭͯ • %PNBJO.PEFM.BEF'VODUJPOBMͰʮ͜ͷख๏ͳΒ 3FQPTJUPSZඞཁͳ͍ʯͱ͍͏هड़͕͋Δ – ᐌ͘ 3FQPTJUPSZ .VUBCMFͳυϝΠϯϞσϧʹجͮ͘ͷ͔ͩΒɺͱͷ͜ͱ – ղઆ͕͍͜ͱ͋Γจҙ͕Α͘௫Ίͳ͔ͬͨŋŋŋ
• ैདྷ௨Γ 3FQPTJUPSZΛར༻ – ͨͩ͠3FQPTJUPSZΫϥεͷΦϒδΣΫτͰͳ͘ɺؔ – ूηΦϦʔ௨Γʹߏங͍ͯ͠Δɻ͑ͯݸผͷߋ৽༻ؔʹׂ͢Δඞཁͳ͍ͱߟ͑ͨ
XPSLGMPXʹॾʑॻ͍͍ͯ͘ͱɺτϥϯβΫγϣϯεΫϦϓτʹͳΔ • %PNBJO.PEFMJOH.BEF'VODUJPOBMτϥϯβΫγϣϯεΫϦϓτ • ŋŋŋ͕ɺڽूੑʹ՝͕ग़ͦ͏ͳͷͰɺ߲࣍ͷͱ͓ΓϞδϡʔϧԽͨ͠
ΤϯςΟςΟͷܕΛجʹͨ͠ϞδϡʔϧΛ࡞ΓɺίΞυϝΠϯϩδοΫؔͦ͜ʹूΊΔ // customers/objects/tag.ts export interface Tag { id: TagId groupId:
RestaurantGroupId label: TagLabel icon: TagIcon sortOrder: number builtin: boolean } export const updateLabel = (label: TagLabel) => (tag: Tag) => ({ ...tag, label }) export const updateIcon = (icon: TagIcon) => (tag: Tag) => ({ ...tag, icon })
τϥϯβΫγϣϯεΫϦϓτͰͳ͘ͳΔ • ίΞυϝΠϯΦϒδΣΫτͷपΓʹɺίΞυϝΠϯϩδοΫ͕ؔू·Δ • XPSLGMPXͦͷίΞυϝΠϯϩδοΫؔΛݺͼग़ͯ͠ɺۀϑϩʔΛΈཱͯΔׂʹ – ΦχΦϯΞʔΩςΫνϟͳͲͷ 6TF$BTFͱಉׂ͡ ΞϓϦέʔγϣϯΞʔΩςΫνϟͷશ ମ૾ɺैདྷͷΞʔΩςΫνϟͱͦΕ
΄Ͳେ͖͘มΘ͍ͬͯͳ͍
έʔεͦͷطଘͷΤϯςΟςΟͷߋ৽ • ͖͞΄Ͳ·ͰͷΤϯςΟςΟͷ৽ن࡞ • طଘͷΤϯςΟςΟΛߋ৽͢ΔΑ͏ͳϢʔεέʔεͷ߹ŋŋŋ – σʔλϕʔε͔ΒΤϯςΟςΟΛ෮ݩ͢Δ – ͦͷΤϯςΟςΟͱผʹɺೖྗ͕͋Δ
7BMJEBUFE *OQVU 6OWBMJEBUFE *OQVU 6QEBUFE 5BH 5BH (SBQI2- %# 3FQPTJUPSZ
ͳΜ͔ͩͪ͝Όͭ͘ŋŋŋ
(SBQI2- %# 3FQPTJUPSZ *OQVU 5BH 6OWBMJEBUFE $PNNBOE 7BMJEBUFE $PNNBOE ŋŋŋ
6QEBUFE5BH 8PSL'MPX HFU5BH#Z*E ೖྗͱυϝΠϯΦϒδΣΫτΛͻͱͭʹ·ͱΊͨʮίϚϯυʯΛϫʔΫϑϩʔͷೖྗʹ͢Δ ͦͷ্Ͱɺઌ΄Ͳಉ༷ɺ୯ํσʔλ ϑϩʔͷதͰঢ়ଶભҠͤͯ͞తͷग़ ྗʹ͚͍ۙͮͯ͘
// customers/workflows/updateTag.ts interface UnvalidatedInput { kind: 'Unvalidated' label: string icon?:
{ symbol: string; type: TagIconType; color?: string | null | undefined } | null | undefined } interface UnvalidatedCommand { input: UnvalidatedInput tag: Tag } interface ValidatedInput { kind: 'Validated' label: TagLabel icon: TagIcon } interface ValidatedCommand { input: ValidatedInput tag: Tag } // Output type UpdatedTag = Tag & { kind: 'Updated' }
// customers/workflows/updateTag.ts // substep1: validateCommand type validateCommand = (command: UnvalidatedCommand)
=> Result<ValidatedCommand, ValidationError> // substep2: updateTag type updateTag = (command: ValidatedCommand) => Result<UpdatedTag, UpdateTagError> // workflow: validateCommand -> updateTag type WorkFlow = (command: UnvalidatedCommand) => Result<UpdatedTag, UpdateTagError> export const updateTagWorkFlow = (): WorkFlow => (command) => ok(command).andThen(validateCommand).andThen(updateTag)
(SBQI2- %# 3FQPTJUPSZ *OQVU 5BH 6OWBMJEBUFE $PNNBOE 7BMJEBUFE $PNNBOE ŋŋŋ
6QEBUFE5BH 8PSL'MPX HFU5BH#Z*E
// customers/repos/tagRepository.ts export const findTagById = ({ prisma }: applicationContext)
=> (id: TagId): ResultAsync<Tag | null, ValidationError | PrismaClientError> => ResultAsync.fromPromise( prisma.tag.findUnique({ where: { id } }), PrismaClientError ).andThen((tag) => (tag ? Tag(tag) : ok(null))) export const getTagById = (context: applicationContext) => (id: TagId): ResultAsync<Tag, EntityNotFound | ValidationError | PrismaClientError> => findTagById(context)(id).andThen((tag) => tag ? ok(tag) : err(new EntityNotFound(`タグがみつかりません: ${id}`)) )
// graphql/mutation/updateTag.ts export const updateTagMutation = mutationField('updateTag', { type: UpdateTagPayload,
args: { tagId: idArg({ description: 'タグID' }), input: TagInput, }, resolve(_root, { tagId, input }, context) { const workflow = updateTagWorkFlow() const preprocess = TagId(tagId) .asyncAndThen(getTagById(context)) .map((tag) => toUnvalidatedCommand({ input, tag })) const result = preprocess.andThen(workflow).andThen(saveTag(context)) return result.match( ... ) }, })
*0ঢ়ଶભҠ *0 Pure function Model -> Model *0 JOQVUMPBE *0
PVUQVU
ΑΓෳࡶͳϑϩʔ • جຊతʹߏಉ͡ͰɺߏࣗମෳࡶʹͳΒͳ͍ • ঢ়ଶભҠͷαϒεςοϓ͕૿͍͑ͯ͘
έʔεͦͷυϝΠϯϩδοΫͷ్தͰ *0͕ൃੜ͢Δ߹ • ඞͣ͠υϝΠϯϩδοΫ࣮ߦલʹಡΈࠐΈΛࡁ·ͤΒΕΔέʔε͔ΓͰͳ͍ • ͨͱ͑ŋŋŋ – ࡞ͨ͠ΤϯςΟςΟͷฒͼҐஔΛɺσʔλϕʔεʹ͍߹Θܾͤͯఆ͢Δ – υϝΠϯϞσϧͷߋ৽ʹɺԿΒ͔֎෦ͷ
8FC "1*Λίʔϧͯ͠औಘͨ͠Λར༻͢Δ – ŋŋŋ
%FQFOEFODZ*OKFDUJPOΛ͏ • *0ͦͷͷΛۀॲཧͷ్த͔ΒऔΓআ͚ͳ͍ͳΒɺ%*Ͱ XPSLGMPX͕ *0 ʹ·ͭΘΔܕ ྫ σʔλ ϕʔεଓΦϒδΣΫτ ʹґଘ͠ͳ͍Α͏ʹ͢Δ
– XPSLGMPX *0ʹґଘ͠ͳ͍ؔͰ͋Δ͜ͱΛҡ࣋͢Δ • %*ΧϦʔԽʹΑΔ෦ద༻Ͱ࣮ݱ͢Δ – %PNBJO.PEFM .BEF'VODUJPOBMͰఏҊ͞Ε͍ͯͨख๏
// customer/services/tag.ts export type getTagSortOrder = ({ groupId }: {
groupId: RestaurantGroupId }) => ResultAsync<number, PrismaClientError> export const getTagSortOrder = ({ prisma }: applicationContext): getTagSortOrder => ({ groupId }) => ResultAsync.fromPromise( prisma.tag .aggregate({ _max: { sortOrder: true, }, where: { groupId }, }) .then((x) => (x._max.sortOrder ? x._max.sortOrder + 1 : 1)), PrismaClientError ) ΧϦʔԽʹΑΓɺ෦ద༻Ͱ͖ΔΑ͏ ʹ͢Δ
// graphql/mutation/createTag.ts export const createTagMutation = mutationField('createTag', { type: CreateTagPayload,
args: { input: TagInput, }, resolve(_root, { input }, context) { const workflow = createTagWorkFlow(checkTagExists(context), getTagSortOrder(context)) // DI const unvalidatedTag = toUnvalidatedTag({ ...input, groupId: context.operator.groupId, }) const result = workflow(unvalidatedTag).andThen(saveCreatedTag(context)) return result.match(...) }, }) ϫʔΫϑϩʔʹίϯςΩετ ͕෦ద༻͞ΕͨؔΛ͢
// customer/workflows/createTag.ts type createTag = (model: ValidatedTag) => ResultAsync<CreatedTag, CreateTagError>
const createTag = (getTagSortOrder: getTagSortOrder): createTag => (model) => { const values = getTagSortOrder({ groupId: model.groupId }) return values.map((n) => ({ ...model, kind: 'Created', id: generateTagId(), sortOrder: n, builtin: false, })) } // workflow: validateTag -> createTag type WorkFlow = (model: UnvalidatedTag) => ResultAsync<CreatedTag, CreateTagError> export const createTagWorkFlow = ( checkTagExists: checkTagExists, // dependency getTagSortOrder: getTagSortOrder // dependency ): WorkFlow => (model) => okAsync(model).andThen(validateTag(checkTagExists)).andThen(createTag(getTagSortOrder)) ϫʔΫϑϩʔ͔Βσʔλϕʔείϯ ςΩετ͕ཁΒͳ͍७ਮؔʹݟ͑Δɻ ͭ·Γɺςετσόοά࣌ɺ७ਮؔ ʹࠩ͑͠Δ͜ͱͰ͖Δ %*͞ΕͨؔΛೖͭͭ͠ɺσʔλ ϑϩʔ͜Ε·Ͱ௨Γͷߏ HFU5BH4PSU0SEFS 3FTVMUΛฦ͢ ͷͰɺσʔλϑϩʔͷதͰ݁Ռ߹ ͞ΕΔ
%*Ͱղܾ͢ΔΑ͏ͳ *0Ͱͳ͍߹ϫʔΫϑϩʔͷαϯυΠονʹ͢Δͱྑ͍ɺͱͷ͜ͱ https://www.slideshare.net/ScottWlaschin/reinventing-the-transaction-script-ndc-london-2020
ߟ
ϑϩϯτΤϯυͱͷൺֱ • ࣌ܥྻʹجͮ͘ঢ়ଶભҠ σʔλϑϩʔ Λએݴతʹهड़͢Δɺͱ͍͏ߟ͑ํಉ͡ʹͳͬͨ – ܕͱখ͞ͳؔͷએݴతͳهड़ͰɺϑϩʔΛΈ্͛Δ • ҰํɺϫʔΫϑϩʔͷ࣮Λ͍ͯ͠Δͱ͖ͷײ֮ʹ·ͩڑ͕͋Δ –
υϝΠϯΠϕϯτͰঢ়ଶભҠɺͱ͍ͬͯଟ͘ͷ߹ʮ7BMJEBUFͯ͠ɺೖྗͰυϝΠϯϞσϧ Λߋ৽͢Δʯ͚ͩ • ݁ՌɺϫʔΫϑϩʔఆܕతͳهड़͕ଟ͘ͳΔ ŋŋŋ ϑϨʔϜϫʔΫԽͰ͖Δ͔ • ϑϩϯτΤϯυͦ͜Λ 3FBDU &MNͳͲͷϑϨʔϜϫʔΫ͕͍ͬͯΔ ͔ͩΒɺΠϕϯτʹର͢ΔϞ σϧͷঢ়ଶભҠͱɺͦͷঢ়ଶΛදݱ͢ΔϓϨθϯςʔγϣϯͷهड़ʹूதͰ͖Δ • ΠϕϯτͱΠϕϯτͷͭͳ͗߹Θͤ ྫ3FTVMUܕʹΑΔ߹ ΛࣗͰهड़͍ͯ͠Δͷ͕ݱঢ়
ैདྷͷΞʔΩςΫνϟͱͷࠩҟʹ͍ͭͯ • ్தͰ৮Εͨͱ͓ΓɺେͷΞʔΩςΫνϟ͋·ΓมΘ͍ͬͯͳ͍ – 6TF$BTF૬ͷ XPSLGMPX – 3FQPTJUPSZ – ίΞυϝΠϯϞσϧ
– ूɺΤϯςΟςΟ • ίϯϙʔωϯτͷதͷ࣮ͷύϥμΠϜ͕ҟͳΔ – ܕͰۀͷঢ়ଶɺϑϩʔΛએݴ͢Δ – σʔλϑϩʔϓϩάϥϛϯάʹΑΔɺ୯ํߴσʔλϑϩʔ
ैདྷख๏ʹൺֱ͠هड़ྔগͳ͍ • σʔλΛσʔλͷ··ӡΜͰ͍ΔͷͰʮ٧Ίସׂ͑ͯͷҟͳΔผछͷΦϒδΣΫτʯʹ͢ Δŋŋŋͱ͍͏ඞཁ͕ͳ͍ – ͷίϐʔ࣮ࡍʹͱ͜ΖͲ͜Ζ͍ͬͯΔ͕ ͨͩͷσʔλΛ ׂೖͰهड़Ͱ͖ΔͷͰɺ هड़ྔ࠷খ
ݱ࣌Ͱͷײ • ্༷ͳ͍ঢ়ଶΛ࡞ΒͣʹࡁΉͨΊɺݎ࿚ • ΑΓෳࡶͳϫʔΫϑϩʔΛ࣮ͨ͠߹ಉ͡ߏʹऩ·Δɻೝෛՙ͕͍ • 3FTVMUܕʹΑΓܭࢉΛܨ͛ΒΕΔΑ͏هड़͢Δྑ͍ڧ੍ྗ͕ಇ͘ – ͨͩ͠ andThen().andThen().asyncAndThen().map()
͕͢͞ʹಡΈͮΒ͍ – 3FTVMU͕ೖΕࢠʹͳͬͯ͘Δͱɺ3FTVMUܕύζϧʹΉ࣌ŋŋŋ • )BTLFMMͷ EPه๏ɺ'ͷίϯϐϡςʔγϣϯࣜʹ૬͢Δͷ͕ཉ͍͠ŋŋŋ • ͷ٧Ίସ͑ͷهड़͕ͳ͍ͷ ͱͯ خ͍͠
;Γ͔͑Γ https://zenn.dev/mizchi/articles/oop-think-modern
·ͱΊ • ࣌ܥྻʹجͮ͘ঢ়ଶભҠͷએݴͱϑϨʔϜϫʔΫଆʹΑΔঢ়ଶભҠɺௐఀ – ϑϩϯτΤϯυɺͦͷͨΊͷϑϨʔϜϫʔΫͷ࣮͕ॆ࣮͍ͯ͠Δ – ؔܕ͔ΒӨڹΛड͚ͨྑ͍࡞๏ • ʮએݴతϓϩάϥϛϯάʯͷϓϥΫςΟεɺݱ࣌Ͱܦݧతʹྑ͍ͷ –
όοΫΤϯυ։ൃ͜ͷߟ͑ํʹऩᏑ͍ͤͯ͘͞ͷѱ͘ͳ͍ͷͰ ˠͬͯΈͨΒײ৮ – ྲྀߦΔ͔Ͳ͏͔Θ͔Γ·ͤΜ • ϑϩϯτΤϯυ όοΫΤϯυͷύϥμΠϜΪϟοϓΛগͳ͍͖͍ͯͨ͘͠
ัଊ • 5ZQF4DSJQUʹΈࠐΈͷ 3FTVMUܕͳ͍ͷͰɺOFWFSUISPXΛͬͨɻଞʹ GQUTͳͲͷީิ͕͋Δ • 3FTVMUܕ XPSLGMPXͷߏ͚ͩͰͳ͋͘ΒΏΔॴͰ͏ • 1SPNJTF
3FTVMU"TZODʹΑͬͯ 3FTVMUԽͯ͠߹Ͱ͖Δ • UZQFͱ JOUFSGBDFͷ͍͚ ŋŋŋ GQUTʹ฿͍ͬͯΔɻಛʹͦ͏͠ͳ͚ΕͳΒ͍ཧ༝ͳ͍ͱࢥ͏ • ͕ؔσϑΥϧτͰΧϦʔԽ͞Εͳ͍ͷํ͕ͳ͍ɻ࣌ંΧϦʔԽͨ͠ͷΛΕͯ·Δ • SFBEPOMZԣணͯ͠ɺͬͯͳ͍ɻ ͪΌΜͱݕ౼͍ͯ͠ͳ͍
͓·͚ ŋŋŋ OFXUZQF શίϯετϥΫλ declare const __newtype: unique symbol export
type newtype<Constructor, Type> = Type & { readonly [__newtype]: Constructor } export type TagId = newtype<'TagId', string> export function TagId(value: string): Result<TagId, ValidationError> { return validate(value) ? ok(value as TagId) : err(new ValidationError('IDの形式が不正です')) } υϝΠϯϞσϧͷߏʹ /PNJOBMͳܕ͕ཉ͍͠ͱ͖ɺ͜͏͍ ͏࣮Ͱͬͯ·͢
ࢀߟจݙ • 4DPUU8MBTDIJO ʮ%PNBJO.PEFMJOH.BEF'VODUJPOBMᴷ5BDLMF4PGUXBSF$PNQMFYJUZXJUI%PNBJO%SJWFO %FTJHOBOE'ʯ • +FSFNZ'BJSCBOL ஶ ϠΪͷ͘͞ΒͪΌΜ
༁ ʮϓϩάϥϛϯά&MNᴷ҆શͰϝϯςφϯε͍͢͠ϑϩϯτΤϯυ ΞϓϦέʔγϣϯ։ൃೖʯ • ླ ྅ଠ ʮϓϩΛࢦ͢ਓͷͨΊͷ5ZQF4DSJQUೖ ᴷ ҆શͳίʔυͷॻ͖ํ͔Βߴͳܕͷ͍ํ·Ͱʯ • #PSJT$IFSOZ ஶ ࠓଜ ݠ࢜ म ݪ ོจ ༁ ʮϓϩάϥϛϯά5ZQF4DSJQUʕεέʔϧ͢Δ+BWB4DSJQUΞϓϦέʔ γϣϯ։ൃʯ • ੵܕͱܕʹ͍ͭͯ – ʮू߹ͱͯ͠ͷܕ u"O*OUSPEVDUJPOUP&MNʯ IUUQTHVJEFFMNMBOHKQBQQFOEJYUZQFT@BT@TFUTIUNM – ʮͳͥ࣍ʹֶͿݴޠؔܕͰ͋Δ͖͔ʯ IUUQTZNPUPOHQPPIBUFOBCMPHDPNFOUSZ