🔧 コンポジションユーティリティ
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;
};
// コンポジションで動作するように関数を持ち上げる
const handler = compose(
getUserFromDb,
lift(normalizeUser) // これでペイロード結果で動作する
);
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
ペイロードから引数を抽出し、純粋関数に適用して、純粋なビジネスロジックをルートハンドラーにシームレスに統合できるようにします。
目的: 特定のパスを抽出して関数引数として適用することで、純粋関数とルートペイロードコンテキストを橋渡しします
ハンドラープロセス:
- 入力: コンテキスト、パラメータ、ボディデータを持つ
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')
// Result型を自動的に処理:
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つの関数のいずれかを選択する関数型条件。
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)
);
withSoftDelete
deletedAtタイムスタンプを自動的に管理することで、ハード削除をソフト削除に変換し、データの安全性と監査機能を提供します。
目的: レコードを削除するのではなく削除済みとしてマークすることで安全なデータ削除を可能にし、クエリからソフト削除されたレコードを自動的にフィルタリングします
データベース操作:
- 読み取り:
find、findOne、countDocumentsは自動的にソフト削除されたレコード(deletedAtが存在するもの)を除外 - 削除:
deleteOne、deleteManyはdeletedAt: new Date()を設定する更新に変換 - 更新:
updateOne、updateMany、findOneAndUpdateはソフト削除フィルターを尊重(既に削除されたレコードは更新されない) - 挿入: 影響を受けず、通常どおり動作
ハンドラープロセス:
- 入力: データベースコレクションを使用する任意の
AsyncRouteHandler - プロセス: 操作をインターセプトするソフト削除プロキシでデータベースコレクションをラップ
- 出力: 同じように動作するが、ソフト削除セマンティクスを持つハンドラー
- データ安全性: 削除されたレコードは監査目的で
deletedAtタイムスタンプとともにデータベースに残る
パラメータ:
handler: ソフト削除機能でラップする非同期ルートハンドラー
戻り値: すべてのデータベース操作にソフト削除セマンティクスが適用されたAsyncRouteHandler
使用例:
import { primitives } from '@nodeblocks/backend-sdk';
const { withSoftDelete } = primitives;
// 基本的な使用法 - ソフト削除機能のために任意のハンドラーをラップ
const softDeleteHandler = withSoftDelete(originalHandler);
// ルートコンポジションで
const deleteUserRoute = withRoute({
method: 'DELETE',
path: '/users/:userId',
handler: withSoftDelete(deleteUserHandler)
});
// すべてのデータベース操作で自動的に動作
const userOperations = compose(
withSoftDelete(createUserHandler), // 挿入は通常どおり動作
withSoftDelete(findUserHandler), // 検索はソフト削除されたユーザーを除外
withSoftDelete(updateUserHandler), // 更新はソフト削除フィルターを尊重
withSoftDelete(deleteUserHandler) // 削除はソフト削除になる
);
// 例: ソフト削除はレコードを削除するのではなくマークする
// 以前: DELETE FROM users WHERE id = '123' (永続的な削除)
// 以降: UPDATE users SET deletedAt = NOW() WHERE id = '123' AND deletedAt IS NULL
// 例: クエリは自動的にソフト削除されたレコードを除外
// 以前: SELECT * FROM users WHERE active = true
// 以降: SELECT * FROM users WHERE active = true AND deletedAt IS NULL
利点:
- データ安全性: 偶発的な永続的なデータ損失を防ぐ
- 監査証跡: コンプライアンスのために履歴レコードを維持
- 回復: 必要に応じてソフト削除されたレコードを復元可能
- API互換性: 既存のハンドラーは変更なしで動作
- パフォーマンス: 最小限のオーバーヘッド、
deletedAtフィルタリングのみを追加
データ回復例:
// ソフト削除されたレコードを復元するには(ソフト削除ラッパーをバイパス)
const restoreUser = async (payload: RouteHandlerPayload) => {
const { userId } = payload.params.requestParams;
await payload.context.db.users.updateOne(
{ id: userId },
{ $unset: { deletedAt: 1 } } // deletedAtフィールドを削除
);
return payload;
};
📝 実践的な例
マルチステップデータ処理
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) => /* バリデーションロジック */;
const hashPassword = (password: string) => /* ハッシュ化ロジック */;
const saveUser = (user: User) => /* データベースロジック */;
// ❌ 避ける: 大きく多目的な関数
const createUserMegaFunction = (data: any) => {
// 1つの関数にバリデーション、ハッシュ化、保存、メール送信がすべて含まれる
};
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
);
🔗 関連項目
- Handler Wrappers - ロギングやページネーションなどの横断的関心事
- Handler Component - 基本的なハンドラー概念
- Route Component - ルート定義
- Error Handling - Result型とエラーパターン
- Functional Programming - コアコンポジション概念