dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

HTTP/Tokyo #1に行ってきた

行ってきました。
中身の濃い勉強会で非常に勉強になった。

jxckさんの発表は圧巻でした。

Response Status codes 3xx @haormauyraa

資料: https://slides.araya.dev/http-tokyo-1/#slide=1 demo: https://playground.araya.dev/http-redirections/

  • 3xx系のステータスコードの話
    • 300〜308まで
  • RFC7231, 7232, 7538
  • 301, 302はHTTP/1.0
  • 300 Multiple Choices
    • 対象のリソースが複数の表現をもつ
      • サーバーはリダイレクト先として複数の選択肢を提示し、クライアント側はその中で優先するものを1つ選ぶ
    • サーバーが優先するべき選択を持っていたら、サーバーはその選択肢の URI 参照をLocaionヘッダーに含めるべき(SHOULD)。UA はその値を自動リダイレクトに利用してもよい(MAY)
  • 301 Moved Permanently
    • 対象のリソースに新しい恒久的なURIがあてられていて、このリソースへの今後の参照は同封されたURIのいずれかを利用するべきことを指示する
  • 302 Found
    • 302 Foundは301 Moved Permanentlyと異なりリダイレクト先となるURIは一時的という扱い。なので301はキャッシュ可能だが302はキャッシュすべきでない
  • 303 See Other
  • 304 Not Modified
    • "リクエストされたリソースを再送する必要がないことを示します。これはキャッシュされたリソースへの暗黙のリダイレクトです。" (MDN より)
    • リクエスト側が If-Modified-Since などを付けた条件つきのリクエストを送ったときに、「仮に条件がfalseでなければ200で応答することになる」ことを示す
    • 実際のリソースは返さない
    • 条件をつけたクライアント側が必要なリソースを(キャッシュなどで)持っている、ということをリクエストで示しているので、サーバーからはリソースそのものを返す必要がない
  • 305は非推奨、306はunused

「425 Too Early」@flano_yuki

資料: https://www.slideshare.net/yuki-f/status-425-httptokyo

f:id:dackdive:20200117221846p:plain

  • TLSハンドシェイクにはClientHello→ServerHelloを経てからHttpRequestを送るフルハンドシェイクと、1回目のClientHelloと一緒にHttpRequestを送る0-RTTハンドシェイクがあり、後者のClientHelloと一緒に送るHttpRequestがEarly Data
  • Early Dataの危険性
    • Early Dataは暗号化されているが第三者が複製することはできる
    • 冪等性のないリクエスト(POSTでデータを作成するとか)だとまずい
    • サーバーはHTTPリクエストを見て冪等でないリクエストは拒否することもできる

f:id:dackdive:20200117222159p:plain

  • HTTPでEarly Dataを安全に扱えない問題
    • 例:Proxyを介していると
      • Proxyはリクエストが冪等かわからない
      • originは元のリクエストがEarly Dataで送られてきたものかわからない
  • どうするか
    • Proxyは Early-Data: 1というヘッダーをつけてoriginに送る
    • originはリクエストが冪等でない場合は HTTPステータス425 Too Earlyをを返す
  • Firefoxでの実験
    • サーバーが425を返したとき、クライアントはTLSハンドシェイクを待ってからリクエストを送っている


Cookie @Jxck

資料はなし。ホワイトボードを使って説明。

  • RFC6265の話
  • Cookie はなぜ生まれたか
    • リクエスト・レスポンスが冪等な世界で、リクエストを送ってきたのが誰なのかを判断したい
  • Cookieとして送るのはIDであることがほとんどなので、ほとんどCredential
  • Cookieの欠点
    • SOP(Same Origin Policy)に則っていない
    • わかりやすい攻撃
      • 悪意のあるサイトが正規のサイトと同じリクエストを送る <form> を設置すると、Cookieを送ってリクエストが成立してしまう
      • ぼくはまちちゃん
  • 今どうやって対策しているか
    • csrf-tokenという一意の識別子を埋め込んでおき、リクエスト同時に送る
  • サーバーから Set-Cookie するときに、idの後ろに SameSite: というプロパティを付けることができるようになった
  • 3rd party cookie
    • サイトAにアクセス
    • サイトA内のiframeからADサーバーにアクセス -> cookie 456を返す
    • サイトBにアクセス
    • サイトB内のiframeから同じADサーバーにアクセス、このとき cookie 456を送る -> 同じ人だと特定できる
    • さらに、サイトAがECサイトだった場合、検索キーワードも一緒にADサーバーに送る -> サイトBを訪問したとき、サイトAの検索クエリの内容を元に適切な広告を送るといったことが可能
  • SafariのITP
  • ITPで困るのはADだけじゃなくAuthenticationサービス(SSO)
    • ITPはトラッキングをしてそうなサイトだけブロックする、と主張している
  • Cookieの原理は変えられないが、トラッキング目的のCookieは防ぎたい。Chromeはどうするか
  • Cookie
  • Cookie上書き問題への対策
    • id=xyz という Cookieに対して __Secure-id=xyzというように __Secure- prefix をつけると、httpsでの通信時のみしか送らなくなる
    • __Host-Secure; Path=/ じゃないとだめにするprefix
  • クライアント側でtokenを生成する Sec-Http-State: token=XXX
  • SameSite=Strict
    • Lax は別オリジンだったとしてもnavigation時にはCookieを付ける
    • Strict は別オリジンからは全てつけない
      • 例: connpassにログインした状態で、Google検索結果からconnpassへのリンクを開く
        • アドレスバーにconnpass.comと打ってアクセスしたときですらそうなる
    • Laxで何が問題か?
      • GET で何かするようなAPI(ログアウトリンクなど)
    • read cookieとwrite cookieを分ける
      • GitHubは実はやってるっぽい?
        • __Host-user_session_name_site
        • _gh_sess
        • user_sessionかも?

「みんなのデータ構造」学習メモ:2.3 ArrayQueue

引き続き、「みんなのデータ構造」という本を読む。
みんなのデータ構造(紙書籍+電子書籍) – 技術書出版と販売のラムダノート

amazon

みんなのデータ構造

みんなのデータ構造

  • 作者:Pat Morin
  • 出版社/メーカー: ラムダノート
  • 発売日: 2018/07/20
  • メディア: 単行本(ソフトカバー)

前回

今回は「P32. 2.3 ArrayQueue 配列を使ったキュー」。

TypeScript によるソースコード
https://github.com/zaki-yama/open-data-structures-typescript/blob/master/src/ArrayQueue.ts

メモ

ArrayQueue とは

f:id:dackdive:20200114042005p:plain:w320

  • 前回の ArrayStack と同じく、List インターフェースを実装したデータ構造の1つ
  • いわゆるキュー。入れた順に取り出す(FIFO
    • イメージは「コンビニのレジに並ぶ列」

ArrayQueue の実行時間

  • add(i, x) および remove(i) の実行時間は
    • resize() を考慮しなければ O(1)
    • resize() も m 回の操作で O(m) なので、resize() を考慮すると O(1 + 1) ?(特に明言なし)

FIFO Queue の実装に ArrayStack が適切でない理由

f:id:dackdive:20200114042827p:plain

ArrayStack は配列のどちらか一方からの要素の追加/削除を高速に行えることを考えればよかった。
これに対し、ArrayQueue はどちらか一方から要素の追加、もう一方から要素の削除となる。
ので、ArrayStack で実現すると要素の追加または削除の必ずどちらかで全要素の移動が発生する。

すなわち要素数 n に比例する実行時間がかかってしまい効率が悪い。

どうするか

f:id:dackdive:20200114043309p:plain

もし、無限長の配列があれば効率的なキューを簡単に実装できる。

  • j: 次に remove する要素のインデックス
  • n: 配列内の要素数

だけを持っていればいいはず。

実際には無限長というのは現実的でないため、これを 剰余算術 で模倣する。
剰余算術とはXをYで割った余り、mod のこと。

インデックス j を配列 a の長さで割れば、必ず余り j % a.length は配列のサイズ内 0, ..., a.length - 1 におさまることを利用する。

このような配列 a を循環配列を呼ぶ。

a, j, n に具体的な数字を用いた例:

f:id:dackdive:20200114043835p:plain

こうすれば、

  • 要素の追加(add(x)
    • 循環配列の末尾 a[(j+n) % a.length] に要素を追加
    • n++
  • 要素の削除(remove()
    • インデックス j の位置の要素 a[j] を削除
    • j を1つずらす。 (j + 1) % a.length となる
    • n--

というわけで、add/remove の実行時間は要素数によらず定数時間になる。

resize() については ArrayStack と同じ考え方。

疑問

ArrayStack のときのように任意のインデックスに対する add(i, x)/remove(i) とせず、add(x)/remove() 決め打ちだったのはなんでだろう。
たぶん次の 2.4 ArrayDeque のための前置きのような位置付けでさらっと終わってるんだと思うが、納得感がなかった。

「みんなのデータ構造」学習メモ:2.1 ArrayStack

「みんなのデータ構造」という本を読み始めたので、その読書メモを。
今回は 2.1 節の「ArrayStack」というデータ構造について。


前置き:「みんなのデータ構造」という書籍について

https://sites.google.com/view/open-data-structures-ja/home より引用。

Open Data StructuresPat Morin 氏が執筆した、データ構造の入門書です。本プロジェクトでは、この本の和訳を作成し、PDF ファイルおよびそのソースコードを公開しています。

データ構造やアルゴリズムについて学びたいと思っていたので、この本で勉強することにした。
PDF版であれば日本語でもフリーで手に入るが、私はラムダノート社のサイトから紙書籍+電子書籍版を購入した。

みんなのデータ構造(紙書籍+電子書籍) – 技術書出版と販売のラムダノート

また、書籍内のサンプルコードはC++で書かれているが、C++はわからないのでTypeScriptで写経することにした。

リポジトリ


本題:ArrayStack について

ArrayStack とは

f:id:dackdive:20200106001441j:plain:w320

  • List インターフェースを実装したデータ構造の1つ
  • List インターフェースとは、値の列  x_0, x_1, ... , x_{n-1} とその列に対する以下の操作からなる(1.2.2 より)
    • size(): リストの長さ n を返す
    • get(i):  x_i の値を返す
    • set(i, x):  x_i の値を  x にする
    • add(i, x):  x i 番めとして追加し、 x_i, ... , x_{n-1} を後ろにずらす
    • remove(i):  x_i を削除し、  x_{i+1}, ... , x_{n-1} を前にずらす
  • Stack は LIFO Queue とも呼ばれ、最後に追加された要素が次に削除される
    • イメージは「皿を積んだ状態」(積み上げた皿を取るとき、上から順に持っていく)

ArrayStack の実行時間

  • get(i) および set(i, x) の実行時間は O(1)
  • add(i, x) および remove(i) の実行時間は
    • resize() を考慮しなければ O(n - i)
    • resize() を考慮すると O(1 + n - i) ただしこれは償却実行時間

f:id:dackdive:20200106002955p:plain

「償却実行時間」という言葉が出てくるが、これは「1.5 正しさ、時間計算量、空間計算量」の節に定義がある。

償却実行時間が  f (n) であるとは、典型的な操作にかかるコストが  f (n) を超えないことを意味する。より正確には、 m 個の操作にかかる実行時間を合計しても、  mf (n) を超えないことを意味する。いくつかの操作には  f (n) より長い時間がかかるかもしれないが、操作の列全体として考えれば、1 つあたりの実行時間は  f (n) という意味だ。

要するに同じ操作を m 回やったときの実行時間から1回あたりの実行時間を考えるという話。


補題2.1 resize() の実行時間について

ここがしばらくわからなかった。

空のArrayStackが作られたあと、  m \geq 1 回の add(i, x) および remove(i) からなる操作の列が順に実行されるとき、 resize() の実行時間は O(m) である。

書籍では

  1. resize() が呼ばれるとき、その前の resize() から add/remove が実行された回数は n/2 -1 以上である
  2. 1を満たすとき、 resize() の実行時間の合計は O(m) である

という2段階で説明しており、また 2 → 1 の順に証明している。
ここでは普通に1から書く。

    1. resize() が呼ばれるとき、その前の resize() から add/remove が実行された回数は n/2 -1 以上である

resize()が呼ばれるのは、add(i, x) 内で呼ばれるケースと remove(i) 内で呼ばれるケースの2通りある。


ケース1: add(i, x) 内で呼ばれる場合

f:id:dackdive:20200106004339p:plain

※書籍に倣って、「◯回めの resize() か」を表す◯に  i を使っているが、これは add(i, x)i とは無関係。ややこしい

この場合は i 回めの時点では配列 a は要素で満たされた状態、 i - 1 回めの時点では配列 a の長さは同じで、要素数は半分。
なので空いている a.length /2 = n_i / 2 分を埋めるのに、少なくとも n_i / 2 回の add() は実行されているはずである。


ケース2: remove(i) 内で呼ばれる場合

f:id:dackdive:20200106004401p:plain

逆に remove(i) 内で呼ばれる場合、要素数 n_i が配列 a のサイズの 1/3 以下になる場合なので、 n_i <= a.length / 3
1つ前の i - 1 回めの resize() が実行された直後の状況を考えると、このときは resize() によって配列のサイズの半分の要素数になっているはず。
n_(i-1) = a.length / 2 でもいいが、n = 0 && a.length = 1 という特殊ケースを考えると n_(i-1) >= a.length / 2 - 1 となる。
今度は要素数n_(i-1) から n_i になるまで remove() が実行されているはずなので、差をとって↑のように式変形すると n_i / 2 - 1 以上であることが示せる。


  • 2.1を満たすとき、 resize() の実行時間の合計は O(m) である

f:id:dackdive:20200106010721p:plain

↑では、add(i, x)Aremove(i)R と表現している。
いま、この ARをランダムに実行した一連の操作があり、その操作の合計が m 回である。
またこの間に不定期に resize() が実行されており、合計で r 回実行された。

1でわかったのは色をつけた部分で、
i - 1回めの resize() から i 回めの resize() の間に呼ばれた add/remove の数は >= n_i - 1 である」
ということ。

また、

  (1回めのresize()から2回めのresize()までのadd/removeの数)
+ (2回めのresize()から2回めのresize()までのadd/removeの数)
+ ...
+ (i - 1 回めのresize()からi回めのresize()までのadd/removeの数)
+ ...
+ (r - 1 回めのresize()からr回めのresize()までのadd/removeの数)

<= m

r 回めの resize() の後にも何回か add/remove が呼ばれている可能性があるため)

で、かつ左辺のそれぞれの項は n_i /2 - 1 よりも大きいと言っているんだから、結局

 \displaystyle
\sum_{i=1}^r ( \rm {n}_{i} / 2 - 1) \geq m

が示せたことになる。

2019年のふりかえりと2020年の抱負

初ふりかえりです。

主なできごと

現職のサイボウズに入社したのが2018年12月なので、ほぼほぼサイボウズでの最初の1年という感じだった。

ブログは22件。
https://dackdive.hateblo.jp/archive/2019

登壇は(わずか)3件。


2019年の目標のふりかえり

昨年はきちんと目標を立てていなかったが、なんとなく頭の中にあった「チャレンジしたいこと」が実際どうだったか。


2019年のKeep

TypeScript

現在、複業で react-lightning-design-system というReact製UIライブラリのTypeScript移行のお手伝いをさせてもらっており、その結果TypeScriptのお作法だったりESLint, Prettier, Jestなど周辺パッケージとの併用方法を学ぶことができた。

この記事は比較的多くの反応があったのでうれしい。


モブプログラミング

サイボウズは多くのチームでモブプログラミングという開発スタイルを採用している。
自分は現在kintoneというプロダクトチームと フロントエンドエキスパートチーム というチームを兼務しているが、kintoneチームに関しては100%モブプロ、フロントエンドエキスパートチームは個人作業とモブプロを両方取り入れた開発を行っている。

前職でもペアプロ・モブプロを取り入れてみたいなーという思いがあったのだが、実際に業務で体験してみて良い面も悪い面も知ることができた、というのが率直な感想。

入社してすぐに 社外のモブプロ体験会に参加した のは非常に良い経験だったし、モブプロに対する悩みMAXだった夏頃は自分でMeetupを主催するなどした。
またこちらの本も読んだが、これからモブプロを始めようと思っている人には有用な本だと思う。


OSSにコントリビュートできた

これまで、OSSにIssueやPRを出すというのは自分にとって非常にハードルが高かった。
ところが、今所属しているチームのメンバーは日常的にOSSにIssue・PRを出しており、その影響から自分も一歩足を踏み出すことができた。

といってもバグ報告やドキュメントなどの簡単なtypo修正だけです。こういうの。


英語のドキュメント・ブログ記事を以前よりも読むようになった

これもチームメンバーに刺激を受けて。
今までも読んでいたが、より日常的に読むようになった。
ブラウザのアップデート情報やカンファレンス動画を観るというのは、1年前はやっていなかったと思う。

個人的なおすすめは、Mozillaが昨年ぐらいから始めたこのYouTubeチャンネル。

トピックがCSSアクセシビリティというのはあるが、尺としても10分前後のものが多く英語学習も兼ねて観るのに丁度いい。


Salesforceのキャッチアップを継続することができた

転職して界隈を離れてしまったが、1年を通してSalesforceウォッチャーとしての活動は継続することができた。
さすがにリリースノートは読まないが、twitterから面白そうなアップデートを見たり、Dreamforceの発表内容を軽くチェックしたり、Meetupに顔を出すなどした。

Salesforceの動向をキャッチアップしている理由として「業務に無関係ではないこと」「今のところ開発者としての希少性を高められるのがSalesforceぐらいなこと」「キャッチアップをやめるとこれまでの経験が無になるという恐怖感」などいろいろあるが、今のところ無理なく続けられているので今年も継続したい。


2019年のProblem

フロントエンドの分野で強みといえる専門領域がない

これは転職して強く実感したことだが、ありがたいことに今同じチームにフロントエンドつよつよエンジニアしかいないので、今まで以上に自分の技術力の無さを思い知らされる。
どこかの領域では負けている、ではなく、知識・アウトプット力などさまざまな面で完全に劣っていると感じることが多々あり、恵まれた環境に感謝しつつこのままじゃやばいという危機感がある。

どこかの分野ではチームに貢献できるような強みを作りたい。今自分が興味あるのは

  • Webパフォーマンス(というか、ブラウザがどういうしくみで動いているのかとかをちゃんと理解したいみたいな欲求)
  • Web Components
  • Rust、WebAssembly(そろそろやらないとまずいという危機感に近い)

の3点だろうか。


行動を習慣化するのが苦手

昨年はいろんなことにチャレンジできて良かったと思う反面、腰を据えてやると決めたことが継続できなかったり、他の面白そうな話題に目移りしてしまうといったことが多々あったように思う。
これは昨年に限らず自分の弱みというか性格として認識しておかなければいけない。

この問題に対するTryとしては、継続できていることをちゃんと記録して可視化することだと思う。
GitHubみたく草が生えればもうちょっとモチベーションが続く気がする。Studyplusってそんな感じのアプリでしたっけ。


2020年のTry

  • 英語ブログ、今年こそ書く!目標は3本
  • 今年もどっかのカンファレンスにCfP出す
  • 「みんなのデータ構造」とあともう1冊、コンピュータサイエンスに関する本を読む
  • 本は年間で6冊読む

コンピュータサイエンス系の本については、気になっているのはこちらの本。

なっとく!アルゴリズム

なっとく!アルゴリズム

このブログを読んで↓

Chrome拡張をChromium版Microsoft Edgeに対応する

メモ。
Microsoftのこちらのブログを読んで。

In most cases, existing extensions built for Chromium will work without any modifications in the new Microsoft Edge

というのを見て、半信半疑で試してみた。

実際やってみると、必要な作業としてはMicrosoft Edge Addon Store に公開する作業だけで、ほんとに既存のChrome拡張のソースコードをそのまま使うことができた。
chrome.tabschrome.storage などのAPIがEdgeでもそのまま動く。

なお、記事執筆時点で新版 Microsoft Edge および Addon Store はベータ版という位置付け。
ベータ版 Edge はここからダウンロードできる→https://www.microsoftedgeinsider.com/ja-jp/download/

できたもの

こちらが以前作ったChrome拡張:

同じものを今回 store に公開したもの:

https://microsoftedge.microsoft.com/addons/detail/cepmaeppcipafbfjonahpohfmolliblp

f:id:dackdive:20191226162903p:plain

(なぜかアイコンの背景が青い)

store への公開手順

Microsoft Developer Account を登録する

Partner Center Developer Dashboard にアクセスするとアカウント登録を求められる。
GitHub アカウントでも登録できる。

f:id:dackdive:20191226163615p:plain

もろもろ必要事項を入力する。

Extension を登録する

「Create new extension」からパッケージ(.zip)をアップロードし、Chrome ウェブストアのように必要事項を記入していく。
記載する内容はだいたいこんな感じ。

  • Availability
    • パッケージをストアに公開(Public)するか、URLを知っている人だけ利用可能(Hidden)にするか
  • Properties
    • 拡張のカテゴリーや、個人情報を収集するかどうか。収集する場合はプライバシーポリシーが記載されたサイトへのURLが必要
  • Store listings
    • store に公開したときのロゴや説明文を載せる

Chrome拡張として公開済みの場合、Edge用に新たに必要になるのはおそらく以下の2つの画像だけで済むはず。

  • 300 x 300 のロゴ
  • 440 x 280 のプロモーション画像(Small promotional tile)

f:id:dackdive:20191226164124p:plain

後者については、今のところstoreのどこにも表示されないので謎。


補足

以下、作りながら調べたこととかハマったこと。

注意:個人アカウントの場合、「Publisher display name (Company Name)」は個人名を入れた方が良い

ここの項目は、storeで公開したときに拡張名のすぐ下に表示される。

f:id:dackdive:20191226170859p:plain

そのため、(Company Name)とあるが Account type が Individual であれば自身の名前やニックネームを付けた方が良い。
また、フォームにも書いてあるが、この項目を更新した際はそのままだとstoreには反映されず、拡張側の再Publishも必要。無意味にバージョンを上げる羽目になる。

storeへの公開には審査が必要で、2~3日待たされる

初回だけでなく、パッケージのアップデート時も同じぐらいの日数を要した。

開発者向けのドキュメントはどこ?

https://docs.microsoft.com/en-us/microsoft-edge/extensions-chromium/

このページから始まる、 Extensions(Chromium) というセクションにまとまっているよう。

本当にすべてのChrome拡張が動くの?

Port Chrome Extension To Microsoft (Chromium)Edge - Microsoft Edge Development | Microsoft Docs
によると、

The Extension APIs and manifest keys supported by Chrome are code-compatible with Microsoft Edge. However, Microsoft Edge does not support the following Extension APIs:

  • chrome.gcm
  • chrome.identity.getAccounts
  • chrome.identity.getAuthToken
  • chrome.identity.getProfileUserInfo
  • chrome.instanceID

ということで、manifest.jsonなども互換性があるが、一部のAPIは非対応とのこと。

Salesforce: Customer 360とは何だったのか、そしてDeveloper 360

Salesforce Platform Advent Calendar 2019 11日目の記事です。
遅れに遅れてすみません。OB枠*1からの参加です。

はじめに

界隈を離れて1年経ちますが、なんだかんだSalesforceウォッチャーを続けています。
今年のDreamforceもDev向けの内容ぐらいは追っておこう...と思ってKeynote観ました(まとめ)。

そんな中で、いろんなKeynoteやリリース記事でCustomer 360という言葉を目にするものの、何を指してるのかあまりピンときてなかったのでここらで一度情報を整理しとこうという趣旨の記事になります。

間違ってたらご指摘ください。

※なお、Twitter上や先日の Tokyo Dreamforce Global Gatherings でも何人かからこのあたりの情報を教えていただきました。ありがとうございます


Customer 360とは

Customer 360という言葉自体が登場したのは、昨年のDreamforce 2018です。

f:id:dackdive:20191216010408p:plain

動画(42:31あたり):Dreamforce: A Celebration of Trailblazers - YouTube

そして、おそらく同じタイミングでの記事がこちら。

ざっくりと、Sales Cloud、Service Cloudなどのデータと、Marketing CloudやCommerce CloudなどのB2C向けCloudで扱うデータにつながりがなかったのを連携できるようにします、ということのようですね。

また、GAが翌年の2019年であることも明記されています。
というわけで、昨年時点ではコンセプトレベルの紹介だったものを1年かけてエンハンスし、今年のDreamforceに持ってきたということかな。


Customer 360 Truth

日本でDreamforceの発表をウォッチしていて、気になったのはCustomer 360 Truthという新しいキーワードが登場していたことです。

後からKeynoteも探してみましたが、たしかにOpening Keynoteでも登場しています。

f:id:dackdive:20191216012010p:plain

動画(1:17:46あたり) Dreamforce Opening Keynote: Trailblazers, Together - YouTube

マーク・ベニオフ曰く "new version of customer 360" だそう。

↑の記事を読む限り、Customer 360 Truthは具体的には

  • Customer 360 Data Manager
  • Salesforce Identity for Customers
  • Customer 360 Audiences
  • Privacy and Data Governance

というサービスから構成されるもののようです。

https://www.salesforce.com/products/platform/features/customer-360-truth/
というプロダクトページを見てもそのように読み取れますね。


Customer 360 Data Manager

サービス間をつなぐためのおそらくキモとなる機能です。
こちらの紹介動画がわかりやすいです。

Salesforceの各Cloudや外部のデータシステムを連携し、顧客ごとにグローバルなIDで管理できるようになる、という機能ですね。

また、実際にデータソースを先に挙げたOpening Keynoteでもベニオフの発表の後にデモがあって、そこでもData Managerによって各Cloudをつなげる様子が見れます。

Trailheadモジュールも登場しています。

気になるんだけど、Developer Editionで使えるものなんですかね?


Customer 360 Audiences

Customer 360 Truthを構成するもう1つの機能ですが、こちらについては情報がほとんど見つかりませんでした。

Marketing Keynote: The New Decade of Data, Trust and Engagement - YouTube

f:id:dackdive:20191216015137p:plain

マーケティング系のCloudでもデータ統合が進むのかな...という雰囲気ですが
そもそも日頃からこっちのサービスは情報追ってないんでよくわかりません。

また、GAが2020年ということなので、こちらはまだコンセプトレベルなんでしょう。


Customer 360 Platform

これは単にPlatformの呼び名が変わっただけと考えていいと思います。(M年ぶりN度目)
Keynoteのタイトルでも積極的に使われていますね。


話は変わって、Developer 360

別の話として、Platform Keynoteでは "Developer 360" というワードも登場しています。

動画 13:10

f:id:dackdive:20191211162138p:plain

f:id:dackdive:20191211162219p:plain

f:id:dackdive:20191216021305p:plain こちらもまだほとんど情報が出ておらず、Keynoteで5分ほどデモで紹介されたのみです。
観たところ

  • ノーコードも含めた開発者向け機能を一箇所にまとめたポータル的なもの
  • チームで共同作業できそうなWorkspaceという概念と、Flow的なGUIの何か

が特徴かな。

スピーカーは "one developer 360 for all of you to work together seamlessly as teams" と表現していました。


おわりに

なんか頑張って調べたわりに完全に理解したとは言えない状況ですが、とりあえずData Managerだけがまともな「機能」としてリリースされたことはなんとなくわかりました。

いまCustomer 360とは何を指すのか?と聞かれると返答に困りますが、Customer 360 Platform 上にあるアプリケーション群っていうことなのかな。

*1:そんなものはない

kintoneのAPIスキーマ情報からREST APIドキュメントを自動生成する

この記事は kintone Advent Calendar 2019 6日目の記事です。
kintone のアドベントカレンダーは初参加です、よろしくお願いします。


はじめに

kintone にはさまざまなAPIがありますが、その中でもちょっと特殊なのがAPIスキーマ情報を取得するためのAPIです。

これは、各APIのリクエスト・レスポンスがどういったパラメータで構成されているかを JSON Schema というフォーマットで返してくれるAPIです。

このリクエスト・レスポンスのデータ構造の情報を応用するといろんなツールに使えそうだと思い、今回はここからOpenAPI(旧Swagger)という規格のファイルを生成することで、REST APIドキュメントをいい感じに作成できないかな〜というのを試してみた話です。


JSON SchemaとOpenAPIとは何か

JSON Schema とは、ざっくりいうとJSONのデータ構造をJSONで定義するための言語です。

たとえば、firstName, lastName, age というプロパティを持つ Person というJSONデータをJSON Schemaで表現すると以下のようになります。

{
  "$id": "https://example.com/person.schema.json",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Person",
  "type": "object",
  "properties": {
    "firstName": {
      "type": "string",
      "description": "The person's first name."
    },
    "lastName": {
      "type": "string",
      "description": "The person's last name."
    },
    "age": {
      "description": "Age in years which must be equal to or greater than zero.",
      "type": "integer",
      "minimum": 0
    }
  }
}

https://json-schema.org/learn/miscellaneous-examples.html より引用)

一方のOpenAPIですが、こちらはREST APIの仕様を記述するためのフォーマットです。
Swagger という言葉をご存知の方は多いかもしれません。2015年にOpenAPI Initiativeという団体が発足したのに伴い、Swaggerは現在OpenAPIに名前が変わっているようです。

OpenAPIやSwaggerでREST APIの仕様を記述するメリットとして、その仕様を利用した便利なツールが充実していることが挙げられます。
今回使用するAPIドキュメント生成のためのツールもその1つです。

Swagger ◯◯と名のつくツール群については、以下の記事がわかりやすいです。
【連載】Swagger入門 - 初めてのAPI仕様管理講座 [1] Swaggerとは|開発ソフトウェア|IT製品の事例・解説記事

また、OpenAPIがどのようなフォーマットなのか、またそこからどのようなAPIドキュメントができるのかについては、Swagger Editorというオンラインエディタのデモサイト https://editor.swagger.io/ を見てみるのが良いかと思います。

f:id:dackdive:20191209021924p:plain

成果物

やってみる

以下、記事中のコードはすべてJavaScriptです。(↑のリポジトリではTypeScriptで書いています)


1. APIスキーマ情報を取得する

はじめに、APIスキーマ情報を取得するAPIを実行し、結果をJSONで保存します。
流れとしては

  1. API 一覧の取得API (/k/v1/apis.json) を叩き、全APIスキーマ情報取得用のlinkを得る
  2. 取得したlinkからスキーマ情報の取得API (/k/v1/apis/*.json) を順番に実行し、各APIスキーマ情報を得る

という2ステップです。

import fetch from "node-fetch";
import fs from "fs";
import path from "path";
import prettier from "prettier";

const subdomain = process.env.KINTONE_SUBDOMAIN;

export async function fetchKintoneAPISchemas() {
  const baseUrl = `https://${subdomain}.cybozu.com/k/v1`;

  // TODO: fetch all of kintone REST apis
  // const resp = await fetch(`${baseUrl}/apis.json`);
  // const apis: Apis = (await resp.json()).apis;

  // const fetchSchemasPromises = Object.values(apis).map(async api => {
  //   const resp: any = await fetch(`${baseUrl}/${api.link}`);
  //   return resp.json();
  // });
  //
  // const schemas = await Promise.all(fetchSchemasPromises);

  const schemas = [await (await fetch(`${baseUrl}/apis/app/acl/get.json`)).json()]; // (*)
  fs.writeFileSync(
    path.resolve(__dirname, "generated", "kintone-api-schemas.json"),
    prettier.format(JSON.stringify(schemas), { parser: "json" })
  );
}

ただし、今回はとりあえず適当なAPI1つを例にうまくいくか試したかったので、アプリのアクセス権の取得API (/k/v1/app/acl.json)だけにしています。
APIスキーマ情報を取得したい場合は、(*) 行のかわりにコメントアウトしている部分を使うとうまくいきます。

生成した generated/kintone-api-schemas.json はこんな中身になっています。

[
  {
    "id": "app/acl/get",
    "baseUrl": "https://zaki-yama.cybozu.com/k/v1/",
    "path": "app/acl.json",
    "httpMethod": "GET",
    "request": {
      "properties": { "app": { "format": "long", "type": "string" } },
      "required": ["app"],
      "type": "object"
    },
    "response": {
      "properties": {
        "rights": { "items": { "$ref": "Right" }, "type": "array" },
        "revision": { "format": "long", "type": "string" }
      },
      "type": "object"
    },
    "schemas": {
      "Right": {
        "properties": {
          "recordImportable": { "type": "boolean" },
          "appEditable": { "type": "boolean" },
          "recordExportable": { "type": "boolean" },
          "recordAddable": { "type": "boolean" },
          "recordViewable": { "type": "boolean" },
          "recordEditable": { "type": "boolean" },
          "includeSubs": { "type": "boolean" },
          "recordDeletable": { "type": "boolean" },
          "entity": {
            "properties": {
              "code": { "type": "string" },
              "type": {
                "enum": ["USER", "ORGANIZATION", "GROUP", "CREATOR"],
                "type": "string"
              }
            },
            "type": "object"
          }
        },
        "type": "object"
      }
    }
  }
]


2. APIスキーマ情報をOpenAPI Specificationのファイルに変換する

続いて、先ほど保存したJSONファイルからOpenAPI Specificationという仕様に準拠したファイル(以下、OpenAPI Specファイルと呼びます)に変換します。
OpenAPIのドキュメントは、以下のようなフォーマットで記述します。
(YAMLでもJSONでもOKですが、読みやすさからYAMLで記述しています)

openapi: 3.0.1
info:
  description: Kintone REST API
  version: 1.0.0
  title: Kintone REST API
paths:
  /app/acl.json:
    get:
      parameters:
        - in: query
          name: app
          schema:
            type: string
          required: true
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                properties:
                  rights:
                    items:
                      $ref: '#/components/schemas/Right'
                    type: array
                  revision:
                    format: long
                    type: string
                type: object
components:
  schemas:
    Right:
      properties:
        recordImportable:
          type: boolean
        appEditable:
          type: boolean
        recordExportable:
          type: boolean
          ...(略)...

paths の下に各APIエンドポイントのパスを書き、リクエストを parameters に、レスポンスを responses に記述します。

一方、kintoneのAPIスキーマ情報のレスポンスとしては、request, responseにそれぞれリクエスト、レスポンスのパラメータ情報が含まれているので、これらをうまくマッピングしてあげれば良さそうです。

import fs from "fs";
import path from "path";
import yaml from "js-yaml";
import { convertRequestToParameters } from "./request-converter";

export function generateOpenAPISchema() {
  const kintoneAPISchemas = JSON.parse(
    fs.readFileSync(
      path.resolve(__dirname, "generated", "kintone-api-schemas.json"),
      "utf8"
    ),
    (key, value) => {
      if (key === "$ref") {
        return `#/components/schemas/${value}`;
      }
      return value;
    }
  );

  const json = {
    openapi: "3.0.1",
    info: {
      description: "Kintone REST API",
      version: "1.0.0",
      title: "Kintone REST API"
    }
  };
  const paths = generatePaths(kintoneAPISchemas);
  const components = generateComponents(kintoneAPISchemas);
  json.paths = paths;
  json.components = components;

  fs.writeFileSync(
    path.resolve(__dirname, "generated", "openapi.yaml"),
    yaml.safeDump(json)
  );
}

function generatePaths(kintoneAPISchemas) {
  const paths = {};
  kintoneAPISchemas.forEach(schema => {
    const key = `/${schema.path}`;
    paths[key] = {
      [schema.httpMethod.toLowerCase()]: {
        parameters: convertRequestToParameters(schema.request),
        responses: {
          "200": {
            description: "OK",
            content: {
              "application/json": {
                schema: schema.response
              }
            }
          }
        }
      }
    };
  });
  return paths;
}

function generateComponents(kintoneAPISchemas) {
  let schemas = {};
  kintoneAPISchemas.forEach((schema) => {
    schemas = {
      ...schema.schemas,
      ...schemas
    };
  });
  return { schemas };
}
// request-converter.js
export function convertRequestToParameters(request) {
  const parameters = [];
  Object.keys(request.properties).forEach(fieldCode => {
    const required = request.required.includes(fieldCode);
    parameters.push({
      in: "query",
      name: fieldCode,
      schema: {
        type: request.properties[fieldCode].type
      },
      required
    });
  });
  return parameters;
}

https://github.com/zaki-yama/kintone-openapi-generator/blob/master/src/generate-openapi-schema.ts


3. OpenAPI SpecファイルからREST APIドキュメントを生成する

スキーマ情報からOpenAPI Specのファイルが生成できたら、ここから周辺ツールを使ってREST APIドキュメントを生成します。
Swaggerのツール群でいうと Swagger UI がこれに該当します。

関連しそうなライブラリがいくつもあって迷ったんですが、今回は swagger-ui-express を使いました。

先ほど生成した OpenAPI Spec ファイルを読み込んで、以下のように渡してあげます。

// doc-server.js
import fs from "fs";
import path from "path";
import jsyaml from "js-yaml";
import swaggerUi from "swagger-ui-express";
import express from "express";

const spec = fs.readFileSync(
  path.resolve(__dirname, "generated", "openapi.yaml"),
  "utf8"
);
const doc = jsyaml.safeLoad(spec);
const app = express();

app.use("/", swaggerUi.serve, swaggerUi.setup(doc));

app.listen(3000, () => {
  console.log("Listen on port 3000");
});

あとは、ターミナルで

$ node doc-server.js

でローカルサーバーを起動し、 http://localhost:3000 にアクセスすると...

f:id:dackdive:20191206220248p:plain

無事APIドキュメントが表示できました。

なお余談ですが、VSCode というエディタを使っている場合は Swagger Viewer というextensionを入れると、エディタ上でこのようなAPIドキュメントを表示できます。

OpenAPI Specファイルを開いているときにコマンドパレットで

> Preview Swagger

というコマンドを実行してみてください。

f:id:dackdive:20191206220513p:plain

ドキュメントをどこかのサイトにホスティングする必要がないのであれば、これでも十分ですね。


4. [未完成😤] 認証機能を追加し、ドキュメントからAPIを実行できるようにする

先ほど生成したAPIドキュメントには「Try it out」というボタンが表示されています。
ここから自社のkintone環境にリクエストを送り、実際のデータのレスポンスを確認できたら便利ですね。

というわけでトライしてみます。

実際のkintone環境にアクセスするには認証が必要ですが、調べてみるとOpenAPI Specファイルにいくつかの項目を追加すればいいことがわかります。

これによると、必要なのは以下の2点です。

  1. servers > url にkintone環境のURLを指定する (例: https://hoge.cybozu.com)
  2. components > securitySchemes を指定する

後者について、OpenAPIではベーシック認証やAPIキー(トークン)による認証、OAuth2といった認証認可のスキームをサポートしています。
一方kintone REST APIがサポートしている認証方式は kintone REST APIの共通仕様 > ユーザー認証 の項に記載があります。
残念ながらユーザー名・パスワードによる認証は一般的なベーシック認証とは異なるため、ここではAPIトークン認証を使います。

APIトークン認証はトークンを X-Cybozu-Authorization というヘッダーに乗せて送ります。これはOpenAPIでは以下のように書きます。

components:
  securitySchemes:
    ApiTokenAuth:
      type: apiKey
      in: header
      name: X-Cybozu-API-Token
...
# 最後にこれも必要っぽい
security:
  - ApiTokenAuth: []

servers > url と合わせて、ステップ2で作成したスクリプトに処理を追加しておきます。

詳しくはリポジトリgenerate-openapi-schema.ts を参照ください。

結果

さて、この状態で再度APIドキュメントを立ち上げると、期待通りAuthorizeというボタンが追加されています。

f:id:dackdive:20191206233220p:plain:w320

kintoneで発行したAPIトークンを入力してAPIを実行してみましたが、エラーになってしまいました。

f:id:dackdive:20191206233232p:plain

localhostだからいけない(httpsならいける?)のか、それとも結局同一ドメインじゃないとうまくいかない...?
というのを調査しようと思いましたが、残念ながら時間切れです。


余談: モックサーバーを立てる

OpenAPI を利用したツールは今回紹介したAPIドキュメント生成以外にもさまざまなものがあります。
別の活用例として、たとえば prism というツールを使うと、モックサーバーを生成することもできます。

$ npm install @stoplight/prism-cli

でインストールし、

$ ./node_modules/.bin/prism mock <OpenAPI Specファイルのパス>

というコマンドを実行するだけです。

# モックサーバー起動
$ ./node_modules/.bin/prism mock <OpenAPI Specファイルのパス>
[01:13:41] › [CLI] …  awaiting  Starting Prism…
[01:13:41] › [HTTP SERVER] ℹ  info      Server listening at http://127.0.0.1:4010
[01:13:41] › [CLI] ℹ  info      GET        http://127.0.0.1:4010/app/acl.json?app=magni
[01:13:41] › [CLI] ▶  start     Prism is listening on http://127.0.0.1:4010

# 別ウィンドウで
$ curl "http://localhost:4010/app/acl.json?app=1"
{"rights":[{"recordImportable":true,"appEditable":true,"recordExportable":true,"recordAddable":true,"recordViewable":true,"recordEditable":true,"includeSubs":true,"recordDeletable":true,"entity":{"code":"string","type":"USER"}}],"revision":"string"}% 


TODO

ここまでやって、一通り完成したように見えましたが、このまま全APIスキーマをOpenAPI Specファイルに変換しようとしたところ、盛大にエラーが出ます。

f:id:dackdive:20191209013154p:plain
生成したSpecファイルを https://editor.swagger.io/ に貼り付けたら大量にエラーが出た様子

エラーメッセージを少し見た様子だと、OpenAPIとして期待している型が違う(integerを期待しているところが文字列)だったり、patternPropertiesなどOpenAPIがサポートしていないJSON Schemaのキーワードを使用しているのが問題のようです。

前者はまだいいとして、後者にちゃんと対応するのはなかなか大変そうですね。。。(そもそも回避できるのかもわかってない)


所感

JSON SchemaもOpenAPIも今回初めてまともに使用したのでうまくいくか全くわかりませんでしたが、それっぽいものを作ることはできました。
とはいえ、TODOに書いたように完全なREST APIドキュメントを生成できるようにするのはだいぶハードルが高そうです。

また、調べていて苦労した点は、どのツールがデファクトスタンダードなのかあまりよくわからなかったというところです。
今回はGitHubのStar数やコミットの最終更新日時などを判断材料に選定しましたが、適切な選択になっていたかはわかりません。

また、そもそもツールを探すのも一苦労でした。
一応、今回の知見としてOpenAPI.Toolsというツール一覧サイトがあるようです。

OpenAPI関連でやりたいことがあれば、まずはこのサイトを見るのがいいのかなーと思いました。
このサイトの信頼性もよくわかりませんが。。。