アプリを作る
SDK(TypeScript)
@hane/sdk のクライアントの作り方、認証、よく使う呼出し、エラー、Idempotency-Key、If-Match、ページ送りを説明します。
このページでは、TypeScript の SDK @hane/sdk の使い方を説明します。SDK は公開 API の OpenAPI から生成した型と、openapi-fetch の上の薄いクライアントでできています。ブラウザと Node.js の両方で動きます。
SDK が出すもの
Section titled “SDK が出すもの”| 名前 | 中身 |
|---|---|
createHaneClient(options) |
クライアントを作る。Authorization と Idempotency-Key を付ける |
unwrap(promise) |
応答の data を返し、失敗なら HaneError を投げる |
HaneError |
失敗の応答。status・code・problem・retryAfterSeconds を持つ |
ifMatch(version) |
If-Match に入れる値("<version>")を作る |
paths・components・Schemas |
OpenAPI から生成した型 |
@hane/sdk/openapi.json |
公開 API をまとめた OpenAPI |
エンドポイントごとの関数はありません。client.GET("/v1/...") のように、パスを書いて呼びます。パス・引数・本文・応答はすべて型が付きます。
クライアントを作る
Section titled “クライアントを作る”import { createHaneClient, unwrap } from "@hane/sdk";
const hane = createHaneClient({ baseUrl: process.env.HANE_API!, // API キーかアクセストークン。要求のたびに呼ぶので、期限が来たら差し替えられる token: async () => session.accessToken, // すべての要求に付けるヘッダー(任意) headers: { "x-hane-client": "my-app" },});token() は要求のたびに呼ばれます。アクセストークンを交換し直したら、同じクライアントのまま新しい値を返せます。fetch を渡すと、要求の送り方を差し替えられます。
よく使う呼出し
Section titled “よく使う呼出し”// 一覧(型と CEL の絞込み)const page = await unwrap( hane.GET("/v1/data/records", { params: { query: { type: "LeaseContract", where: 'record.status == "active"', limit: 100 } }, }),);
// 1 件と、その関係と履歴const { entity, relations } = await unwrap(hane.GET("/v1/data/records/{id}", { params: { path: { id } } }));
// 作るconst created = await unwrap( hane.POST("/v1/data/records", { body: { type: "Vehicle", values: { registration_no: { value: "架空 300 あ 10-01" } } } }),);
// 自分か自分のグループの、開いている Taskconst tasks = await unwrap(hane.GET("/v1/approvals/tasks", { params: { query: { mine: true, status: "open" } } }));エラーを扱う
Section titled “エラーを扱う”失敗の応答は RFC 9457 の Problem Details です。unwrap は HaneError を投げます。
import { HaneError } from "@hane/sdk";
try { await unwrap(hane.POST("/v1/data/records", { body }));} catch (e) { if (e instanceof HaneError) { console.error(e.status, e.code, e.message); for (const x of e.problem?.errors ?? []) console.error(x.path, x.message); // 項目ごとの誤り if (e.code === "RATE_LIMITED") await sleep((e.retryAfterSeconds ?? 1) * 1000); }}| code | HTTP | 意味 |
|---|---|---|
VALIDATION_FAILED |
400 | 本文や引数が契約に合わない。problem.errors に項目ごとの誤り |
UNAUTHENTICATED |
401 | 資格情報がない・正しくない |
POLICY_EPOCH_STALE |
401 | 権限が変わった後の古いトークン。交換し直す |
FORBIDDEN |
403 | スコープや権限が足りない |
NOT_FOUND |
404 | ない、または読めない |
CONFLICT |
409 | 今の状態ではできない |
IDEMPOTENCY_CONFLICT |
409 | 同じ Idempotency-Key で違う要求を送った |
PRECONDITION_FAILED |
412 | If-Match のバージョンが今と違う |
PRECONDITION_REQUIRED |
428 | If-Match がない |
RATE_LIMITED・BUDGET_EXCEEDED |
429 | 呼びすぎ、または予算を超えた |
TENANT_SUSPENDED |
423 | 組織が緊急停止している |
problem.retryable が true のエラーは、時間をおいて同じ要求を送り直せます。
Idempotency-Key
Section titled “Idempotency-Key”副作用のある POST には Idempotency-Key が要ります。SDK は、呼ぶ側が付けていなければ乱数のキーを付けます。同じキーの要求には、保存した最初の応答が返ります(7 日保存します)。
通信の失敗で送り直すときに二重に作らないよう、送り直しでも同じキーを使うには、呼ぶ側でキーを決めて渡します。
const key = crypto.randomUUID(); // 送り直しでも同じ値を使うawait unwrap( hane.POST("/v1/data/records", { params: { header: { "Idempotency-Key": key } }, body: { type: "Vehicle", values }, }),);同じキーで中身の違う要求を送ると IDEMPOTENCY_CONFLICT になります。トークン交換や定義の差分のような副作用のない POST には要りません。
If-Match(楽観ロック)
Section titled “If-Match(楽観ロック)”レコードの PATCH と、Task の引き受け・手放す・完了には、読んだときのバージョンを If-Match に付けます。ほかの人が先に変えていれば PRECONDITION_FAILED になるので、読み直してから決め直します。
import { ifMatch } from "@hane/sdk";
const { entity } = await unwrap(hane.GET("/v1/data/records/{id}", { params: { path: { id } } }));await unwrap( hane.PATCH("/v1/data/records/{id}", { params: { path: { id }, header: { "If-Match": ifMatch(entity.version) } }, body: { values: { odometer_km: { value: 41200 } }, reason: "点検の記録で確かめた" }, }),);一覧の多くは limit と cursor を受け、next_cursor を返します。next_cursor が null になるまで続けて読みます。where で絞り込むと、途中のページが空でも続きがあることがあるので、件数ではなく next_cursor で判断します。
import type { Schemas } from "@hane/sdk";
type Page = { items: Schemas["Record"][]; next_cursor: string | null };const all: Schemas["Record"][] = [];let cursor: string | null = null;do { const page: Page = await unwrap( hane.GET("/v1/data/records", { params: { query: { type: "Vehicle", limit: 500, ...(cursor ? { cursor } : {}) } } }), ); all.push(...page.items); cursor = page.next_cursor;} while (cursor);一覧ごとの引数と上限は API リファレンス にあります。