メインコンテンツまでスキップ
バージョン: 0.5.0 (最新)

🎭 ハンドラーユーティリティ

ハンドラーラッパーは、任意のハンドラー関数に適用できる横断的関心事を提供します。これらのユーティリティは、コアビジネスロジックを変更することなく、ロギング、ページネーション、その他のミドルウェアのような機能を追加します。


🎯 概要

ハンドラーラッパーは、同じインターフェースを維持しながら、追加機能をハンドラーに付与するユーティリティです。デコレーターパターンに従い、複数の機能を追加するために一緒に構成できます。

主な機能

  • 非侵襲的: ハンドラー署名を変更せずに機能を追加
  • 合成可能: 同じハンドラーに複数のラッパーを組み合わせる
  • 設定可能: 各ラッパーの柔軟な設定オプション
  • 型安全: 適切な型指定による完全なTypeScriptサポート

📝 関数ロギング

withLogging

任意の関数をラップして、柔軟なパラメータ順序による包括的なロギング機能を提供します。

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging } = primitives;

// デフォルト設定でのシンプルなロギング
const loggedHandler = withLogging(createUserHandler);

// ログレベルを指定
const debugHandler = withLogging(createUserHandler, 'debug');

// カスタムロガーを指定
const customLoggedHandler = withLogging(createUserHandler, customLogger);

// ロガーとレベル両方を指定(任意の順序)
const fullLoggedHandler = withLogging(createUserHandler, customLogger, 'trace');

ログレベル

// 利用可能なログレベル
withLogging(handler, 'trace'); // 最も詳細
withLogging(handler, 'debug'); // 開発情報
withLogging(handler, 'info'); // 一般情報
withLogging(handler, 'warn'); // 警告
withLogging(handler, 'error'); // エラー
withLogging(handler, 'fatal'); // 致命的エラー

ログ出力

すべてのログレベルが構造化ログを生成し、以下の情報を含みます:

  • 関数名
  • サニタイズされた入力引数
  • サニタイズされた結果/戻り値
  • 非同期関数のPromise検出
{
"args": [{"params": {"requestBody": {"name": "John"}}}],
"functionName": "createUserHandler",
"msg": "Function: createUserHandler",
"result": {"user": {"id": "123", "name": "John"}}
}

スマートサニタイズ

withLogging関数は、機密オブジェクトを自動的にサニタイズします:

  • データベース接続 → 🗄️ [Database]
  • 設定オブジェクト → ⚙️ [Config]
  • メールサービス → 📧 [MailService]
  • リクエスト/レスポンス → 📤 [Request]/📥 [Response]
  • ロガーインスタンス → 📝 [Logger]

📄 自動ページネーション

withPaginationラッパーが実装されています。MongoDBスタイルのfind呼び出しを拡張し、requestQueryからpagelimitを適用し、下流のハンドラー/ターミネータのためにページネーションメタデータをペイロードに添付します。

withPagination

データベースバックエンドのルートハンドラーに自動ページネーションを追加し、特にMongoDBコレクションに有用です。

import { primitives } from '@nodeblocks/backend-sdk';

const { withPagination } = primitives;

// すべての製品を返す基本ハンドラー
const getAllProducts = async (payload: RouteHandlerPayload) => {
const products = await payload.context.db.products.find({}).toArray();
return ok(mergeData(payload, { products }));
};

// ハンドラーにページネーションを追加
const getPaginatedProducts = withPagination(getAllProducts);

ページネーションパラメータ

ラッパーはリクエストクエリから以下のパラメータを自動的に抽出します:

パラメータデフォルト説明
pagenumber1ページ番号(1ベースのインデックス)
limitnumber10ページあたりの項目数

レスポンス構造

withPaginationはページネーションメタデータをpayload.paginationResultとしてペイロードに保存します。必要に応じてターミネータを使用してHTTPレスポンスに含めてください。

// データ + ページネーションを返すターミネータの例
export const normalizeProductsListTerminator = (
result: Result<RouteHandlerPayload, NodeblocksError>
) => {
if (result.isErr()) throw result.error;
const payload = result.value;
const { data, pagination } = payload.paginationResult || {};
return { data: data ?? payload.context.data.products, pagination };
};

使用例

// リクエスト: GET /api/products?page=2&limit=20
// 現在のレスポンス(ページネーションメタデータなし):
[
{ "id": "prod-21", "name": "Product 21" },
{ "id": "prod-22", "name": "Product 22" }
]

// 将来のレスポンス(ページネーションメタデータ付き):
{
"data": [
{ "id": "prod-21", "name": "Product 21" },
{ "id": "prod-22", "name": "Product 22" }
],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}

🔧 高度な使用法

ラッパーの組み合わせ

同じハンドラーに複数のラッパーを組み合わせることができます:

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging, withPagination } = primitives;

// ロギングとページネーションの両方を追加
const enhancedHandler = withLogging(
withPagination(getAllProducts),
'debug'
);

// または任意の順序で構成
const alternativeHandler = withPagination(
withLogging(getAllProducts, 'info')
);

条件付きラッピング

環境または設定に基づいて条件付きでラッパーを適用:

const createHandler = (config: Config) => {
let handler = getAllProducts;

// 有効な場合はページネーションを追加
if (config.enablePagination) {
handler = withPagination(handler);
}

// 環境に基づいてロギングを追加
if (config.environment === 'development') {
handler = withLogging(handler, 'debug');
} else if (config.environment === 'production') {
handler = withLogging(handler, 'info');
}

return handler;
};

カスタムロガー統合

import { createLogger } from 'winston';
import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging } = primitives;

// カスタムロガーを作成
const customLogger = createLogger({
level: 'debug',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'app.log' }),
new winston.transports.Console()
]
});

// 特定のレベルでカスタムロガーを使用
const loggedHandler = withLogging(
createUserHandler,
customLogger,
'debug'
);

設定

withPaginationは現在オプションを受け付けません。requestQueryからpagelimitを読み取り、skipを内部的に計算します。


📝 実践例

ログ付きユーザー作成

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging } = primitives;

const createUserHandler = async (payload: RouteHandlerPayload) => {
const { name, email } = payload.params.requestBody;

// ビジネスロジックはここ
const user = await userService.create({ name, email });

return ok(mergeData(payload, { user }));
};

// 包括的なロギングを追加
const loggedCreateUser = withLogging(createUserHandler, 'debug');

// ルートでの使用
const createUserRoute = withRoute({
method: 'POST',
path: '/users',
handler: loggedCreateUser,
});

ページネーション付き製品リスト

import { primitives } from '@nodeblocks/backend-sdk';

const { withPagination } = primitives;

const getAllProducts = async (payload: RouteHandlerPayload) => {
const products = await payload.context.db.products
.find({})
.sort({ createdAt: -1 })
.toArray();

return ok(mergeData(payload, { products }));
};

// ページネーションを追加(現在は配列を直接返す)
const getPaginatedProducts = withPagination(getAllProducts);

// ルートでの使用
const getProductsRoute = withRoute({
method: 'GET',
path: '/products',
handler: getPaginatedProducts,
});

ロギングとページネーションの組み合わせ

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging, withPagination } = primitives;

const getProductsHandler = compose(
withLogging(withPagination(findProducts), 'debug'),
lift(normalizeProductsList)
);

// これは以下の処理を行うパイプラインを作成します:
// 1. データベースクエリにページネーションを適用
// 2. ページネーションされた結果をログ
// 3. 製品リスト形式を正規化

📐 ベストプラクティス

1. 適切なログレベルを使用

// ✅ 良い例: 異なる操作に適切なログレベルを使用
const userPipeline = compose(
withLogging(validateUser, 'debug'), // 検証用デバッグ
withLogging(saveUser, 'info'), // データベース操作用情報
withLogging(formatResponse, 'trace') // フォーマット用トレース
);

// ❌ 避けるべき: すべてに同じレベルを使用
const badPipeline = compose(
withLogging(validateUser, 'info'),
withLogging(saveUser, 'info'),
withLogging(formatResponse, 'info')
);

2. 適切なレベルでラッパーを適用

// ✅ 良い例: データベース操作にページネーションを適用
const getProducts = withPagination(
async (payload) => {
const products = await payload.context.db.products.find({}).toArray();
return ok(mergeData(payload, { products }));
}
);

// ❌ 避けるべき: すでに処理されたデータにページネーションを適用
const badGetProducts = async (payload) => {
const products = await payload.context.db.products.find({}).toArray();
return withPagination(() => ok(mergeData(payload, { products })));
};

3. 戦略的にロギングを使用

// ✅ 良い例: 適切なレベルでログ
const userPipeline = compose(
withLogging(validateUser, 'debug'), // 検証用デバッグ
withLogging(saveUser, 'info'), // データベース操作用情報
withLogging(formatResponse, 'trace') // フォーマット用トレース
);

// ✅ 良い例: 条件付きロギング
const getLoggedHandler = (isDevelopment: boolean) => {
const logLevel = isDevelopment ? 'debug' : 'info';
return withLogging(handler, logLevel);
};

4. ビジネスロジックとラッパーを合成

// ✅ 良い例: 関心事を明確に分離
const businessLogic = compose(
validateInput,
processData,
formatResponse
);

const enhancedHandler = withLogging(
withPagination(businessLogic),
'debug'
);

// ❌ 避けるべき: ラッパーロジックとビジネスロジックを混在
const mixedHandler = async (payload) => {
// ビジネスロジックにロギングロジックが混在
console.log('Starting handler');
const result = await businessLogic(payload);
console.log('Handler completed');
return result;
};

🔗 関連項目