🎭 ハンドラーユーティリティ
ハンドラーラッパーは、任意のハンドラー関数に適用できる横断的関心事を提供します。ハンドラーのコアビジネスロジックを変更せずに、ロギング、ページネーション、その他のミドルウェアに似た機能を追加できます。
🎯 概要
ハンドラーラッパーは primitives 名前空間にあります。同じインターフェースを保ったままハンドラーへ機能を追加し、デコレーター形式で compose により合成できます。
import { primitives } from '@nodeblocks/backend-sdk';
const {
withLogging,
withPagination,
withPaginatedProperty,
DEFAULT_REDACTION,
DEFAULT_SANITIZATION,
} = primitives;
主なエクスポート: withLogging, withPagination, withPaginatedProperty, WithLoggingOptions, RedactOption, DEFAULT_REDACTION, DEFAULT_SANITIZATION, PaginationParams, PaginationResult.
Top-level import { withLogging } from '@nodeblocks/backend-sdk' is not supported — use primitives.
主な機能
- 非侵襲的: ハンドラーシグネチャーを変更せずに機能を追加します。
- 合成可能: 同じハンドラーに複数のラッパーを組み合わせられます。
- 設定可能:
withLoggingはオプションを受け取り、ページネーションラッパーはクエリパラメーターを読み取ります。 - 型安全: エクスポートされたインターフェースによる完全な TypeScript サポートがあります。
📝 関数ロギング
withLogging
任意の関数をラップし、機密データの自動マスキングを備えた包括的なログ機能を提供します。
パラメーター:
fn: (...args: T[]) => R— ログ機能でラップする関数options?: WithLoggingOptions— ログ動作の設定オプション
WithLoggingOptions:
interface WithLoggingOptions {
logger?: Logger; // Custom logger instance (default: nodeblocksLogger)
level?: 'info' | 'debug' | 'warn' | 'error' | 'fatal' | 'trace'; // Log level
redact?: RegExp | RedactOption[]; // Sensitive data redaction rules
}
RedactOption:
interface RedactOption {
approach: 'fields' | 'patterns'; // Redaction method
pattern: RegExp; // Pattern to match
replacement?: string; // Replacement text
}
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
// Simple logging with default settings (level: 'info', logger: nodeblocksLogger)
const loggedHandler = withLogging(createUserHandler);
// Specify log level
const debugHandler = withLogging(createUserHandler, { level: 'debug' });
// Specify custom logger
const customLoggedHandler = withLogging(createUserHandler, { logger: customLogger });
// Specify both logger and level
const fullLoggedHandler = withLogging(createUserHandler, {
logger: customLogger,
level: 'trace',
});
// Advanced usage with redaction
const secureHandler = withLogging(createUserHandler, {
level: 'info',
redact: [
{ approach: 'fields', pattern: /^password$/i, replacement: '[REDACTED_PASSWORD]' },
{ approach: 'patterns', pattern: /secret/i, replacement: '[REDACTED]' },
],
});
既定値: level: 'info', logger: nodeblocksLogger (from utils.nodeblocksLogger).
ロガーの注入: ラップした関数が context を持つペイロードオブジェクトを受け取ると、withLogging は内部関数を呼び出す前に構成済みロガーを payload.logger に自動設定します。
ログレベル
レベルはオプションオブジェクトで指定します(文字列だけを渡す省略オーバーロードはありません)。
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
withLogging(handler, { level: 'trace' }); // Most detailed
withLogging(handler, { level: 'debug' }); // Development info
withLogging(handler, { level: 'info' }); // General information (default)
withLogging(handler, { level: 'warn' }); // Warnings
withLogging(handler, { level: 'error' }); // Errors
withLogging(handler, { level: 'fatal' }); // Critical errors
ログ出力
すべてのログレベルは、次の情報を含む構造化ログを出力します。
- 関数名
- 自動マスキング済みの入力引数
- 自動マスキング済みの結果/戻り値
- 非同期関数の 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 関数は、DEFAULT_SANITIZATION(RouteHandlerPayload のインフラストラクチャーフィールド)と DEFAULT_REDACTION(フィールド/パターンルール)により機密データを自動マスキングします。
インフラストラクチャーフィールド(DEFAULT_SANITIZATION — ペイロードオブジェクトへ適用):
- データベース接続 →
🗄️ [Database] - 構成オブジェクト →
⚙️ [Configuration] - ファイルストレージドライバー →
📂 [FileStorageDriver] - OAuth ドライバー →
🔐 [GoogleOAuthDriver]、🔐 [LineOAuthDriver]、🔐 [TwitterOAuthDriver] - メールサービス →
✉️ [MailService] - リクエスト/レスポンス →
📥 [Request]/📤 [Response] - ロガーインスタンス →
📝 [Logger]
機密データパターン(DEFAULT_REDACTION — フィールドおよびパターンルール):
- Passwords:
password,pass,pwd,pw→[REDACTED_PASSWORD] - Email addresses →
[REDACTED_EMAIL] - Credit cards →
[REDACTED_CREDIT_CARD] - Tokens and authorization →
[REDACTED_TOKEN],[REDACTED_AUTHORIZATION] - Secrets, credentials, API keys →
[REDACTED_SECRET],[REDACTED_CREDENTIAL],[REDACTED_API_KEY] - Phone numbers →
[REDACTED_PHONE] - Address, private, sensitive, signature, SSN fields
- Bearer tokens in string values →
[REDACTED_BEARER_TOKEN]
カスタムマスキング
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, DEFAULT_REDACTION } = primitives;
const secureHandler = withLogging(myFunction, {
redact: [
// Add custom redaction rules
{ approach: 'fields', pattern: /apiKey/i, replacement: '[REDACTED_API_KEY]' },
{ approach: 'patterns', pattern: /customSecret/i, replacement: '[HIDDEN]' },
// Include default rules (DEFAULT_REDACTION is a Record — use Object.values)
...Object.values(DEFAULT_REDACTION),
],
});
📄 自動ページネーション
withPagination
context.db.*.find() をプロキシしてページネーションを自動適用します。requestQuery から page と limit を読み取り、下流のクエリフィルターから除外したうえで、MongoDB の find オプションに skip/limit を適用します。
ハンドラーが cursor.toArray() を呼び出すと、戻り値は通常の配列ではなく次の形式です。
{ data: T[]; metadata: PaginationResult }
ハンドラーは通常これを context.data(例: { products: { data, metadata } })に保存します。ターミネーターは通常の配列とページネーション済みの形式の両方を処理する必要があります。
import { primitives } from '@nodeblocks/backend-sdk';
const { withPagination } = primitives;
const findProducts = async (payload: RouteHandlerPayload) => {
const products = await payload.context.db.products
.find(payload.params.requestQuery || {})
.toArray();
// When wrapped with withPagination, `products` is { data, metadata }
return ok(mergeData(payload, { products }));
};
const getPaginatedProducts = withPagination(findProducts);
ページネーションパラメーター
| パラメーター | 型 | 既定値 | 説明 |
|---|---|---|---|
page | number | 1 | ページ番号(1 始まり) |
limit | number | 10 | 1 ページあたりの項目数 |
page と limit は内部ハンドラーへ渡す params.requestQuery から除外されるため、フィルターフィールドとして扱われません。
レスポンス構造
ページネーションメタデータは、toArray() から data と並んで metadata として返されます。ターミネーターが HTTP レスポンスを整形します。SDK の normalizeProductsListTerminator(product ハンドラー内)は、通常の配列と { data, metadata } の両方を処理します。
// Simplified from SDK product list terminator
export const normalizeProductsListTerminator = (
result: Result<RouteHandlerPayload, Error>
) => {
if (result.isErr()) throw result.error;
const { context } = result.value;
const products = context.data.products;
if (Array.isArray(products)) {
return products.map(formatProduct);
}
return {
data: products.data.map(formatProduct),
metadata: { pagination: products.metadata },
};
};
使用例
// Request: GET /api/products?page=2&limit=20
// Response shape (after terminator):
{
"data": [
{ "id": "prod-21", "name": "Product 21" },
{ "id": "prod-22", "name": "Product 22" }
],
"metadata": {
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
}
withPaginatedProperty
findOne() が返すドキュメント内のネストされた配列(例: 組織メンバー)をページネーションします。context.db.*.findOne() をプロキシし、propertyPath の配列を { data, metadata } に置き換えます。
import { primitives } from '@nodeblocks/backend-sdk';
const { withPaginatedProperty, withLogging } = primitives;
// Paginate organization.members from a findOne result
const paginatedMembersHandler = withPaginatedProperty(
withLogging(findOrganizationMembers, { level: 'info' }),
['members']
);
GET /organizations/:organizationId/members などの SDK ルートで使用されます。
withPagination はオプションを受け取りません。requestQuery から page と limit を読み取り、内部で skip を算出します。
🔧 高度な使用方法
以下の例は説明用のパターンです。withRoute、ok、mergeData などのシンボルは別の SDK モジュールから提供されます。
ラッパーの組み合わせ
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, withPagination } = primitives;
// Add both logging and pagination
const enhancedHandler = withLogging(withPagination(getAllProducts), {
level: 'debug',
});
// Or compose them in any order
const alternativeHandler = withPagination(
withLogging(getAllProducts, { level: 'info' })
);
条件付きラップ
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, withPagination } = primitives;
const createHandler = (config: Config) => {
let handler = getAllProducts;
if (config.enablePagination) {
handler = withPagination(handler);
}
if (config.environment === 'development') {
handler = withLogging(handler, { level: 'debug' });
} else if (config.environment === 'production') {
handler = withLogging(handler, { level: 'info' });
}
return handler;
};
カスタムロガーの統合(例のパターン)
WithLoggingOptions.logger は Pino 互換の Logger を想定します。同じインターフェースを実装していないサードパーティーロガーは動作しない場合があります。
import { primitives, utils } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
const { nodeblocksLogger } = utils;
// Use SDK logger with a child binding
const customLogger = nodeblocksLogger.child({ service: 'user-service' });
const loggedHandler = withLogging(createUserHandler, {
logger: customLogger,
level: 'debug',
});
📝 実践例
ログ付きユーザー作成
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
const createUserHandler = async (payload: RouteHandlerPayload) => {
const { name, email } = payload.params.requestBody;
// Business logic here
const user = await userService.create({ name, email });
return ok(mergeData(payload, { user }));
};
// Add comprehensive logging
const loggedCreateUser = withLogging(createUserHandler, { level: 'debug' });
// Usage in route
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 }));
};
// Add pagination — toArray() returns { data, metadata } when wrapped
const getPaginatedProducts = withPagination(getAllProducts);
// Usage in route
const getProductsRoute = withRoute({
method: 'GET',
path: '/products',
handler: getPaginatedProducts,
});
ロギングとページネーションの組み合わせ
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, withPagination, compose, lift } = primitives;
const getProductsHandler = compose(
withPagination(
withLogging(findProducts, { level: 'debug' })
),
lift(normalizeProductsListTerminator)
);
// Pipeline:
// 1. withPagination proxies db.find and paginates toArray()
// 2. withLogging logs args/return with redaction
// 3. normalizeProductsListTerminator formats { data, metadata } for HTTP response
📐 ベストプラクティス
1. 適切なログレベルを使用する
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, compose } = primitives;
// ✅ Good: Use appropriate log levels for different operations
const userPipeline = compose(
withLogging(validateUser, { level: 'debug' }),
withLogging(saveUser, { level: 'info' }),
withLogging(formatResponse, { level: 'trace' })
);
// ❌ Avoid: Using the same level for everything
const badPipeline = compose(
withLogging(validateUser, { level: 'info' }),
withLogging(saveUser, { level: 'info' }),
withLogging(formatResponse, { level: 'info' })
);
2. 適切なレベルでラッパーを適用する
// ✅ Good: Apply pagination to database operations
const getProducts = withPagination(
async (payload) => {
const products = await payload.context.db.products.find({}).toArray();
return ok(mergeData(payload, { products }));
}
);
// ❌ Avoid: Applying pagination to already processed data
const badGetProducts = async (payload) => {
const products = await payload.context.db.products.find({}).toArray();
return withPagination(() => ok(mergeData(payload, { products })));
};
3. ロギングを戦略的に使用する
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, compose } = primitives;
// ✅ Good: Log at appropriate levels
const userPipeline = compose(
withLogging(validateUser, { level: 'debug' }),
withLogging(saveUser, { level: 'info' }),
withLogging(formatResponse, { level: 'trace' })
);
// ✅ Good: Conditional logging
const getLoggedHandler = (isDevelopment: boolean) => {
const level = isDevelopment ? 'debug' : 'info';
return withLogging(handler, { level });
};
4. ラッパーとビジネスロジックを合成する
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, withPagination, compose } = primitives;
// ✅ Good: Separate concerns clearly
const businessLogic = compose(
validateInput,
processData,
formatResponse
);
const enhancedHandler = withLogging(withPagination(businessLogic), {
level: 'debug',
});
🔗 関連項目
- ロギングユーティリティ -
nodeblocksLoggerと HTTP ロギング - 合成ユーティリティ - 関数合成とエラーハンドリング
- ハンドラーコンポーネント - 基本的なハンドラーの概念
- ルートコンポーネント - ルート定義
- エラーハンドリング - Result 型とエラーパターン