🔧 合成ユーティリティ
NodeBlocks SDKは、ハンドラーの合成、非同期操作の処理、複雑なビジネスロジックパイプラインの構築のための必須ユーティリティを提供します。これらのユーティリティにより、シンプルな関数を予測可能な方法で組み合わせ、複雑でエラーセーフなパイプラインを構築できます。
🎯 概要
合成ユーティリティは、堅牢で保守可能なビジネスロジックのビルディングブロックを提供します。関数合成とエラー伝播を処理し、安全かつ効率的に操作をチェーンできます。
主な機能
- 関数合成: 複数の関数を単一のパイプラインに組み合わせる
- エラーセーフ操作: 自動的なエラー伝播と処理
- 非同期サポート: 非同期操作のシームレスな処理
- 型安全性: 適切な型指定による完全なTypeScriptサポート
🔗 基本合成
compose
複数の関数を単一のパイプラインに組み合わせ、右から左へと実行します。
import { primitives } from '@nodeblocks/backend-sdk';
const { compose } = primitives;
const processUser = compose(
validateUser,
saveUser,
formatResponse
);
// formatResponse(saveUser(validateUser(input))) と同等
lift
通常の関数をペイロードコンテキストに持ち上げます。
import { primitives } from '@nodeblocks/backend-sdk';
const { lift } = primitives;
const normalizeUser = (user: User) => {
const { _id, ...normalized } = user;
return normalized;
};
// Lift the function to work in composition
const handler = compose(
getUserFromDb,
lift(normalizeUser) // Now works with payload results
);
mergeData
合成パイプラインの後続ハンドラーで使用するために、データをペイロードコンテキストにマージします。
import { handlers } from '@nodeblocks/backend-sdk';
const { mergeData } = handlers;
const enrichUserData = async (payload: RouteHandlerPayload) => {
const profile = await profileService.getProfile(payload.context.data.userId);
return mergeData(payload, { profile });
};
const getUserHandler = compose(
fetchUserFromDb,
flatMapAsync(enrichUserData), // profileはpayload.context.dataで利用可能
lift(formatUserResponse)
);
パラメータ:
payload
: 現在のペイロードオブジェクトdata
:payload.context.data
にマージするデータを含むオブジェクト
戻り値: マージされたデータを含む更新されたペイロードオブジェクト
使用例:
// 単一値をマージ
return mergeData(payload, { userId: '123' });
// 複数の値をマージ
return mergeData(payload, {
user: userData,
profile: profileData,
settings: settingsData
});
// 非同期ハンドラーでマージ
const createUser = async (payload: RouteHandlerPayload) => {
const user = await db.users.create(payload.params.requestBody);
return mergeData(payload, { user });
};
applyPayloadArgs
ペイロードから引数を抽出し、純粋関数に適用し、純粋ビジネスロジックをルートハンドラーにシームレスに統合できるようにします。
目的: 特定の経路を抽出し、それらを関数引数として適用することで、純粋関数をルートペイロードコンテキストに橋渡しします
ハンドラープロセス:
- 入力: context、params、本文データを持つ
RouteHandlerPayload
- 処理: ペイロードから指定された経路を抽出し、純粋関数(非同期または同期)に適用
- 出力: 関数結果がペイロードにマージされた
Result<RouteHandlerPayload, Error>
- エラー: 関数がResult.errを返す場合にエラーを伝播
パラメータ:
func
: 抽出された引数で実行する純粋関数(非同期と同期の両方をサポート)。Result型を使用可能。funcArgs
: ペイロードから抽出する経路の配列(直接キーまたはネストされた経路)key
: 関数結果をペイロードに保存するためのオプションのプロパティ名
戻り値: ペイロード抽出引数で純粋関数を適用するAsyncRouteHandler
使用例:
import { primitives } from '@nodeblocks/backend-sdk';
const { applyPayloadArgs } = primitives;
// 非同期関数でのルート合成で使用:
const updateRoute = withRoute({
handler: compose(
applyPayloadArgs(updateIdentityPure, [
['params', 'requestParams', 'identityId'],
['params', 'requestBody'],
['context', 'db', 'identities']
], 'identityId')
)
});
// 組み合わせる場合はflatMapAsyncを使用:
const updateRoute = withRoute({
handler: compose(
applyPayloadArgs(updateItem, [
['params', 'requestParams', 'itemId'],
['params', 'requestBody'],
['context', 'db', 'items']
], 'itemId'),
flatMapAsync(
applyPayloadArgs(
getItemById,
[
['context', 'data', 'itemId'],
['context', 'db', 'items'],
],
'item'
)
),
flatMapAsync(
applyPayloadArgs(
getOtherItemById,
[
['context', 'data', 'itemId'],
['context', 'db', 'otherItems'],
],
'item'
)
),
lift(normalizeItemTerminator)
)
});
// 同期関数でも動作:
applyPayloadArgs(someSyncFunction, [
['params', 'requestParams', 'id'],
['body']
], 'result')
// Handles Result types automatically:
applyPayloadArgs((x: number, y: number) => ok(x + y), [
['params', 'requestParams', 'x'],
['context', 'data', 'y']
], 'result')
orThrow
Result
の成功/エラーケースを処理し、カスタムエラーマッピングとオプションの成功データ抽出を行うターミネータ関数。
目的: 合成パイプラインのための制御されたエラーハンドリングとレスポンスフォーマットを提供
レスポンスフォーマット:
- 入力:
RouteHandlerPayload
またはError
を含むResult
- 処理: 特定のエラータイプをHTTPステータスコードまたはカスタムエラーにマッピングし、オプションで成功ペイロードからデータを抽出
- 出力: 成功データまたは適切なステータスコードでマップされたエラーをスロー
- エラー: ステータスコード付きの
NodeblocksError
またはカスタムエラーインスタンスをスロー
パラメータ:
errorMap
: エラーマッピングのための[CustomError, HttpStatusCode | Error]
タプルの配列successMap
: 成功データ抽出のためのオプションの[ObjectPath, HttpStatusCode]
タプル
戻り値: Result
を処理し、成功データを返すかマップされたエラーをスローする関数
🔎 ヘルパー
match
RamdaのpathSatisfies
を使用してネストされたパスをチェックする述語ユーティリティ。
import { primitives } from '@nodeblocks/backend-sdk';
const { match } = primitives;
// カリー化された使用法(2引数): 後でオブジェクトを期待する述語を返す
const isPositiveLimit = match(
(x: unknown) => Number(x) > 0,
['params', 'requestQuery', 'limit']
);
// 後で、オブジェクトを提供
const ok = isPositiveLimit(payload); // boolean
// 直接使用法(3引数): オブジェクトをすぐに渡す
const okImmediate = match(
(x: unknown) => Number(x) > 0,
['params', 'requestQuery', 'limit'],
payload
); // boolean
ifElse
述語に基づいて2つの関数のうち1つを選択する関数型条件分岐。
import { primitives } from '@nodeblocks/backend-sdk';
const { ifElse } = primitives;
const normalizeName = ifElse(
(x: any) => !!x.nickname,
(x: any) => x.nickname,
(x: any) => `${x.firstName} ${x.lastName}`
);
hasValue
値がnull以外かつ空でない場合にtrueを返す複合述語。
import { primitives } from '@nodeblocks/backend-sdk';
const { hasValue } = primitives;
hasValue('hello'); // true
hasValue(''); // false
hasValue([]); // false
hasValue({ a: 1 }); // true
使用例:
import { primitives } from '@nodeblocks/backend-sdk';
const { orThrow } = primitives;
// ルート合成でのエラーマッピング使用:
const createUserRoute = withRoute({
handler: compose(
createUserHandler,
lift(orThrow([
[ValidationError, 400],
[DuplicateError, 409],
[DatabaseError, 500]
]))
)
});
// 成功データ抽出付き:
const getUserRoute = withRoute({
handler: compose(
getUserHandler,
lift(orThrow(
[[UserNotFoundError, 404]],
[['user'], 200]
))
)
});
🔄 エラーセーフ合成
flatMap
Result
型を返す同期操作をチェーンし、エラーセーフな合成を可能にします。
import { primitives } from '@nodeblocks/backend-sdk';
const { flatMap } = primitives;
const processUserData = compose(
validateUserInput,
flatMap(enrichUserData), // 検証が成功した場合のみ実行
flatMap(formatUserData)
);
flatMapAsync
Result
型を返す非同期操作をチェーンし、エラーセーフな合成を可能にします。
import { primitives } from '@nodeblocks/backend-sdk';
const { flatMapAsync } = primitives;
const createAndFetchUser = compose(
createUserInDb,
flatMapAsync(fetchUserById), // createUserInDbが成功した場合のみ実行
lift(normalizeUserResponse)
);
📝 実践例
複数ステップのデータ処理
import { primitives } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync, lift } = primitives;
// ステップ1: ユーザーデータを強化
const enrichUserData = async (payload: RouteHandlerPayload) => {
const profile = await profileService.getProfile(payload.context.data.userId);
return mergeData(payload, { profile });
};
// ステップ2: レスポンス用にデータをフォーマット
const formatUserResponse = (payload: RouteHandlerPayload) => {
const { user, profile } = payload.context.data;
return { ...user, profile };
};
// パイプラインを合成
const getUserHandler = compose(
fetchUserFromDb,
flatMapAsync(enrichUserData),
lift(formatUserResponse)
);
データ変換パイプライン
const processOrder = compose(
validateOrderData,
lift(calculateTotals),
flatMapAsync(checkInventory),
flatMapAsync(createOrder),
lift(formatOrderResponse)
);
const createOrderRoute = withRoute({
method: 'POST',
path: '/orders',
handler: processOrder,
});
エラーセーフ検証チェーン
import { Result, ok, err } from 'neverthrow';
import { primitives } from '@nodeblocks/backend-sdk';
const { flatMap, flatMapAsync, lift } = primitives;
// ステップ1: 入力を検証
const validateUserInput = (data: any): Result<ValidatedUser, ValidationError> => {
if (!data.email || !data.name) {
return err(new ValidationError('Missing required fields'));
}
return ok(data);
};
// ステップ2: ユーザーが存在するかチェック
const checkUserExists = async (user: ValidatedUser): Promise<Result<User, DatabaseError>> => {
const existing = await db.users.findOne({ email: user.email });
if (existing) {
return err(new DatabaseError('User already exists'));
}
return ok(user);
};
// ステップ3: ユーザーを保存
const saveUser = async (user: ValidatedUser): Promise<Result<User, DatabaseError>> => {
try {
const saved = await db.users.create(user);
return ok(saved);
} catch (error) {
return err(new DatabaseError('Failed to save user'));
}
};
// エラーセーフパイプラインを合成
const createUserHandler = compose(
(payload) => validateUserInput(payload.params.requestBody),
flatMapAsync(checkUserExists),
flatMapAsync(saveUser),
lift(formatUserResponse)
);
📐️️ ベストプラクティス
1. 関数を小さく焦点化したものに保つ
// ✅ 良い例: 小さく焦点化した関数
const validateEmail = (email: string) => /* validation logic */;
const hashPassword = (password: string) => /* hashing logic */;
const saveUser = (user: User) => /* database logic */;
// ❌ 避けるべき: 大きく多目的の関数
const createUserMegaFunction = (data: any) => {
// validation, hashing, saving, emailing all in one function
};
2. 非同期操作を適切に処理
// ✅ 良い例: 非同期操作にflatMapAsyncを使用
const enrichDataPipeline = compose(
fetchUserData,
flatMapAsync(fetchUserProfile), // 非同期操作
flatMapAsync(fetchUserSettings), // 別の非同期操作
lift(formatResponse) // 同期変換
);
// ❌ 避けるべき: 適切なユーティリティなしに非同期/同期を混在
const badPipeline = compose(
fetchUserData,
fetchUserProfile, // 合成では適切に動作しない
formatResponse
);
3. 適切なレベルで合成
// ✅ 良い例: 関連する操作を合成
const userRegistrationFlow = compose(
validateRegistration,
createUser,
sendWelcomeEmail
);
// ✅ 良い例: 無関係な操作を分離
const userLoginFlow = compose(
validateCredentials,
authenticateUser,
generateToken
);
4. エラーハンドリングにResult型を使用
// ✅ 良い例: Result型による明示的なエラーハンドリング
const safeOperation = compose(
validateInput,
flatMapAsync(databaseOperation), // 検証が成功した場合のみ実行
lift(formatResponse)
);
// ❌ 避けるべき: 合成でスローされた例外に依存
const unsafeOperation = compose(
validateInput,
databaseOperation, // スローされる可能性があり、合成を破壊
formatResponse
);
🔗 関連項目
- ハンドラーラッパー - ロギングやページネーションなどの横断的関心事
- ハンドラーコンポーネント - 基本ハンドラー概念
- ルートコンポーネント - ルート定義
- エラーハンドリング - Result型とエラーパターン
- 関数型プログラミング - コア合成概念