コンテンツにスキップ
HaneDOCSEN

アプリを作る

SDK(TypeScript)

@hane/sdk のクライアントの作り方、認証、よく使う呼出し、エラー、Idempotency-Key、If-Match、ページ送りを説明します。

このページでは、TypeScript の SDK @hane/sdk の使い方を説明します。SDK は公開 API の OpenAPI から生成した型と、openapi-fetch の上の薄いクライアントでできています。ブラウザと Node.js の両方で動きます。

名前 中身
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/...") のように、パスを書いて呼びます。パス・引数・本文・応答はすべて型が付きます。

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 を渡すと、要求の送り方を差し替えられます。

// 一覧(型と 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" } } } }),
);
// 自分か自分のグループの、開いている Task
const tasks = await unwrap(hane.GET("/v1/approvals/tasks", { params: { query: { mine: true, status: "open" } } }));

失敗の応答は 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 のエラーは、時間をおいて同じ要求を送り直せます。

副作用のある 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 には要りません。

レコードの 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 リファレンス にあります。