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

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

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


🎯 概要

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

主な機能

  • 非侵入的: ハンドラーのシグネチャを変更せずに機能を追加
  • コンポーズ可能: 同じハンドラーに複数のラッパーを組み合わせ
  • 設定可能: 各ラッパーに柔軟な設定オプション
  • 型安全: 適切な型付けによる完全なTypeScriptサポート

📝 関数ロギング

withLogging

機密データの自動編集機能を備えた包括的なロギング機能を提供するために、任意の関数をラップします。

パラメータ:

  • fn: (...args: T[]) => R — ロギングでラップする関数
  • options?: WithLoggingOptions — ロギング動作の設定オプション

WithLoggingOptions:

interface WithLoggingOptions {
logger?: Logger; // カスタムロガーインスタンス(デフォルト: nodeblocksLogger)
level?: 'info' | 'debug' | 'warn' | 'error' | 'fatal' | 'trace'; // ログレベル
redact?: RegExp | RedactOption[]; // 機密データ編集ルール
}

RedactOption:

interface RedactOption {
approach: 'fields' | 'patterns'; // 編集方法
pattern: RegExp; // マッチするパターン
replacement?: string; // 置換テキスト
}
import { withLogging } from '@nodeblocks/backend-sdk';

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

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

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

// ロガーとレベルを両方指定
const fullLoggedHandler = withLogging(createUserHandler, {
logger: customLogger,
level: 'trace'
});

// 編集機能付きの高度な使用法
const secureHandler = withLogging(createUserHandler, {
level: 'info',
redact: [
{ approach: 'fields', pattern: /^password$/i, replacement: '[REDACTED_PASSWORD]' },
{ approach: 'patterns', pattern: /secret/i, replacement: '[REDACTED]' }
]
});

ログレベル

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

ログ出力

すべてのログレベルは、以下の構造化ログを生成します:

  • 関数名
  • 自動編集機能付きのサニタイズされた入力引数
  • 自動編集機能付きのサニタイズされた結果/戻り値
  • 非同期関数のPromise検出
  • neverthrow ResultオブジェクトのResult型検出
{
"args": [
{
"params": {
"requestBody": {
"name": "John",
"email": "[REDACTED_EMAIL]",
"password": "[REDACTED_PASSWORD]"
}
}
}
],
"functionName": "createUserHandler",
"return": {
"promise": false,
"result": { "ok": true },
"value": {
"user": {
"id": "123",
"name": "John",
"email": "[REDACTED_EMAIL]"
}
}
}
}

自動編集

withLogging関数は、機密データを自動的に編集します:

インフラストラクチャフィールド(常に編集):

  • データベース接続 → 🗄️ [Database]
  • 設定オブジェクト → ⚙️ [Configuration]
  • ファイルストレージドライバー → 📂 [FileStorageDriver]
  • OAuthドライバー → 🔐 [GoogleOAuthDriver]など
  • メールサービス → ✉️ [MailService]
  • リクエスト/レスポンス → 📥 [Request]/📤 [Response]
  • ロガーインスタンス → 📝 [Logger]

機密データパターン(自動的に編集):

  • パスワード: passwordpasspwdpw[REDACTED_PASSWORD]
  • メールアドレス → [REDACTED_EMAIL]
  • クレジットカード → [REDACTED_CREDIT_CARD]
  • トークンと認証 → [REDACTED_TOKEN][REDACTED_AUTHORIZATION]
  • シークレット → [REDACTED_SECRET]
  • 電話番号 → [REDACTED_PHONE]

カスタム編集

import { withLogging, DEFAULT_REDACTION } from '@nodeblocks/backend-sdk';

const secureHandler = withLogging(myFunction, {
redact: [
// カスタム編集ルールを追加
{ approach: 'fields', pattern: /apiKey/i, replacement: '[REDACTED_API_KEY]' },
{ approach: 'patterns', pattern: /customSecret/i, replacement: '[HIDDEN]' },
// デフォルトルールを含める
...DEFAULT_REDACTION
]
});

📄 自動ページネーション

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

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;
};

🔗 関連項目