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

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

ハンドラーラッパーは、任意のハンドラー関数に適用できる横断的関心事を提供します。ハンドラーのコアビジネスロジックを変更せずに、ロギング、ページネーション、その他のミドルウェアに似た機能を追加できます。


🎯 概要

ハンドラーラッパーは 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_SANITIZATIONRouteHandlerPayload のインフラストラクチャーフィールド)と 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 から pagelimit を読み取り、下流のクエリフィルターから除外したうえで、MongoDB の find オプションに skiplimit を適用します。

ハンドラーが 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);

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

パラメーター既定値説明
pagenumber1ページ番号(1 始まり)
limitnumber101 ページあたりの項目数

pagelimit は内部ハンドラーへ渡す 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 から pagelimit を読み取り、内部で skip を算出します。


🔧 高度な使用方法

以下の例は説明用のパターンです。withRouteokmergeData などのシンボルは別の 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',
});

🔗 関連項目