メインコンテンツまでスキップ

アダプターのカスタマイズ

ビジネスロジックに合わせてアダプターをさまざまな方法でカスタマイズを入れる必要があることがよくあります。 Nodeblocksは、初期設定ですぐに便利な機能を提供しますが、以下のようなカスタマイズが必要になる場合があります。

  • ブロックに新しいエンドポイントを追加
  • 新しいカスタムフィールドを追加
  • メールプロバイダーや他の第三者サービスを置き換え
  • 既存のエンドポイントのハンドラやバリデータをカスタマイズ

現時点では、アダプターのカスタマイズはコードを通じて行われます。ビジネス要件に合わせてアダプタのロジックをカスタマイズするための関数を、@basaldev/blocks-backend-sdkで提供しています。

新しいAPIエンドポイントの追加

各サービスには、デフォルトのエンドポイントが提供されています。しかし、新しい機能や仕様を実装するために新しいエンドポイントを追加する場合があります。これは、各nodeblocksサービスでstartServiceを実行する際にcustomRoutesの配列を提供することで行えます。

一般的な使用例:

  • Nodeblocksによって提供されていない新しい機能を実装
  • 第三者サービスとの統合
  • 既存の機能への代替手段の提供(例:データを異なる形式に変換する、または他のユーザーに限定的なアクセスを提供するなど)

各ルートには、以下を定義する必要があります:

 import {
createNodeblocksCatalogApp,
defaultAdapter,
} from '@basaldev/blocks-catalog-service';
import { adapter as sdkAdapter } from '@basaldev/blocks-backend-sdk';

// Nodeblocksのカタログのサーバーのインスタンスを作成
const server = createNodeblocksCatalogApp();

// デフォルトアダプターとしてアダプターを作成
const adapter = await defaultAdapter.createCatalogDefaultAdapter({ ... }, { ... });

// カスタムルートを定義
const customRoutes = [
{
handler: async (logger: Logger, context: adapter.AdapterHandlerContext) => {
logger.info('Getting coupons for user...', context.params.userId);

return {
data: ...,
status: 200
}
},
method: 'post' as const,
path: '/coupons/use',
validators: {
foo: (logger: Logger, context: sdkAdapter.AdapterHandlerContext) => {
if (!context.params?.userId) {
throw new NBError({
code: 'invalid_request',
httpCode: 400,
message: 'userId is required',
});
}

// TODO ここでバリデーションを行う
return { status: 200 };
},
}
},
];

// サービスを開始
await server.startService({
...
customRoutes,
});

各ルートには、以下を定義する必要があります:

  • path:エンドポイントのパス。パスパラメーターを定義するには:paramを使用します(例:/users/:userId
  • method:エンドポイントのHTTPメソッド
  • handler:エンドポイントが叩いた時に呼び出される関数。
    • この関数は、loggerとcontextを受け取ります。contextの内部には任意のパラメーターやボディデータ、およびリクエストとレスポンスオブジェクトが含まれています。
    • データベース関数を呼び出すには、adapter.dataServices.<service>.<function>にアクセスします。
  • validators: リクエストを検証するために呼び出される関数のオブジェクト。これらはハンドラーの前に実行され、いずれかがエラーをスローすると、   ハンドラ関数を呼び出さずにリクエストが拒否されます。
    • バリデータは上から下へと実行され、最初にエラーをスローしたものがクライアントに返されます。
    • バリデータには、任意の名前を持つキーがあります。
    • これらの関数は、リクエストが無効な場合に適切なhttpステータスでNBErrorをスローする必要があります。

便利な検証メソッド

@basaldev/blocks-backend-sdkは、カスタムルートで使用できる便利な検証メソッドを多く提供しています:

  • security.createIsAuthenticatedValidator - リクエストが正しく認証されているかどうかをチェックします
  • security.createIsAdminValidator - リクエストが認証され、ユーザーが管理者であるかどうかをチェックします
  • security.some - 検証関数の配列から、少なくとも一つが通るかどうかをチェックします

同様に、他のマイクロサービスにアクセスする必要がある時には、バリデータやハンドラから外部のユーザや組織API (adapter.dependencies.<service>Api.<function>) を使用することが出来ます。

新しいカスタムフィールドの追加

エンドポイントの機能を完全に変更するのではなく、既存のオブジェクトスキーマに新しいフィールドを追加したい場合がよくあります。 これは、アダプターを作成する際に customFields オブジェクトを提供することで可能。

一般的な使用例:

  • nodeblocksによって提供されるスキーマの変更
  • あるサービスのデータと別のサービスのデータとの間のリンクの追加
 import {
createNodeblocksCatalogApp,
defaultAdapter,
} from '@basaldev/blocks-catalog-service';

const adapter = await defaultAdapter.createCatalogDefaultAdapter({
customFields: {
// Attributeのデータ型にカスタムフィールドを追加
attribute: [
{
name: 'attribute_internal_id',
type: 'string' as const,
},
],
// Productのデータ型にカスタムフィールドを追加
product: [
{
name: 'is_promoted',
type: 'boolean' as const,
},
{
name: 'tags',
type: 'array' as const,
},
{
name: 'view_count',
type: 'number' as const,
},
{
name: 'metadata',
type: 'object' as const,
},
{
name: 'last_viewed',
type: 'date-time' as const,
},
],
},
}, { ... });

対応しているカスタムフィールド型は以下の通りです:

  • string
  • number
  • boolean
  • array
  • object
  • date-time
  • date

カスタムフィールドはAPIの通常のフィールドと同じように使用することができ、レスポンスでは customFields.<name> として返されます。 他のどのフィールドと同様に、フィルタリングや並べ替えが可能です。

例:エクスパンダー

カスタムフィールド設定では、expanderのプロパティもサポートされています。これは、カスタムフィールドが要求された際に呼び出される関数で、 他のサービスからデータを取得したり、複雑な計算を実行するために利用できます。 例えば、カタログAPIが ?expand=endorsed_by_user_id で要求された場合、expanderの関数にユーザーIDのリストが渡され、 各idに対して拡張されたユーザーを返すことが可能です。

const adapter = await defaultAdapter.createCatalogDefaultAdapter({
customFields: {
product: [
{
name: 'endorsed_by_user_id',
type: 'string' as const,
expander: async (ids: string[]) => {
// 他のサービスからデータを取得
const result = await userApi.getUsers(ids);
return result;
},
},
],
},
}, { ... });

第三者サービスの置き換え

Nodeblocksサービスは、マイクロサービス間の内部通信に加えて、多数の第三者APIをそのまま使用しています。これらの依存関係は、アダプタを作成する際 に二つ目のパラメーターとして注入され、同じインターフェースを実装する任意のクラスを使用することで置き換えることができます。

一般的な使用例:

  • メールプロバイダーを別のサービスに置き換える
  • Nodeblocksのマイクロサービスを独自のカスタムAPIと統合する

例:メールプロバイダーのカスタマイズ


import {
createNodeblocksUserApp,
defaultAdapter,
} from '@basaldev/blocks-user-service';
import { external } from '@basaldev/blocks-backend-sdk';

class CustomMailProvider implements external.mail.MailService {
sendMail(mailData: external.mail.MailData, opts?: external.mail.MailOptions): Promise<boolean> {
// メールプロバイダーのカスタムロジック

return true;
}
}

const adapter = await defaultAdapter.createUserDefaultAdapter({ ... }, {
...
mailService: new CustomMailProvider(),
});

...

例:カスタムユーザAPI

 import {
createNodeblocksCatalogApp,
defaultAdapter,
} from '@basaldev/blocks-catalog-service';
import { UserDefaultAdapterApi, RequestContext, UserResponse} from '@basaldev/blocks-default-adapter-api';

class CustomUserApi implements UserDefaultAdapterApi {
async getUserById(
userId: string,
requestContext?: RequestContext
): Promise<UserResponse | undefined> {
// ユーザ取得のカスタムロジック

}

async createUser(
user: CreateUserDto,
requestContext?: RequestContext
): Promise<UserResponse> {
// ユーザ作成のカスタムロジック
}

...
}

const adapter = await defaultAdapter.createCatalogDefaultAdapter({ ... }, {
...
userApi: new CustomUserApi(),
});

...

既存のエンドポイントのハンドラとバリデータをカスタマイズする

多くの場合、既存のエンドポイントを使用したいが、呼び出されるハンドラー(関数)や、リクエストが有効かどうかをチェックするために呼び出されるバリデーター(関数)の仕様を、ビジネスロジックに合わせて調整したいと思うでしょう。

一般的な使用例:

  • 既存のエンドポイントにカスタムバリデーションを追加(例:フィールドが一意かどうかをチェック)
  • エンドポイントから既存のバリデーションを削除
  • 既存のエンドポイントの振る舞いを変更
  • エンドポイントがヒットしたときに通知を送る

例:エンドポイントに新しいバリデーションルールを追加

この例では、createUser エンドポイントに nameUnique という新しいバリデータが追加されます:

import {
createNodeblocksUserApp,
defaultAdapter,
} from '@basaldev/blocks-user-service';
import { adapter as sdkAdapter } from '@basaldev/blocks-backend-sdk';

let adapter = await defaultAdapter.createUserDefaultAdapter({ ... }, { ... });

adapter = sdkAdapter.setValidator(adapter, 'createUser', 'nameUnique', async (logger, context) => {
const existingUsers = await adapter.dataServices.user.findUsers(logger, { name: context.body.name });
if (existingUsers.length > 0) {
throw new sdkAdapter.NBError({
code: 'invalid_request',
httpCode: 400,
message: '名前はすでに使用されています',
});
}
return { status: 200 };
});

...

例:既存のバリデーションルールをカスタムロジックで置き換える

setValidatorはバリデーターを置き換えるためにも使用出来ます。 この例では、updateOrder エンドポイントの isValidOrderStatus バリデータをカスタムバリデータで置き換えます:

import {
createNodeblocksOrderApp,
defaultAdapter,
} from '@basaldev/blocks-order-service';
import { adapter as sdkAdapter } from '@basaldev/blocks-backend-sdk';

let adapter = await defaultAdapter.createOrderDefaultAdapter({ ... }, { ... });

const validStatuses = [
'ACCEPTED',
'CANCELLED',
'CLOSED',
'PENDING',
'PROCESSING',
'YOUR_NEW_STATUS',
];

adapter = sdkAdapter.setValidator(adapter, 'updateOrder', 'isValidOrderStatus', async (logger, context) => {
if (!validStatuses.includes(context.body.status)) {
throw new sdkAdapter.NBError({
code: 'invalid_request',
httpCode: 400,
message: 'orderのステータスは許可されているステータスの一つではありません',
});
}
return { status: 200 };
});

...

例:エンドポイントからバリデーションを削除

この例では、listUsers エンドポイントの authorization バリデータ要件を削除します:

import {
createNodeblocksUserApp,
defaultAdapter,
} from '@basaldev/blocks-user-service';
import { adapter as sdkAdapter } from '@basaldev/blocks-backend-sdk';

let adapter = await defaultAdapter.createUserDefaultAdapter({ ... }, { ... });

adapter = sdkAdapter.removeValidator(adapter, 'listUsers', 'authorization');

...

例:エンドポイントのスキーマバリデーションを変更

Nodeblocksでは、リクエストボディとクエリパラメーターを検証するためにajvを使用しています。modifySchemaValidator 関数を使用して、エンドポイントのスキーマ検証を変更することができます。これにより、単純なカスタムフィールドを超えた、スキーマ検証に対する強力な変更が可能になります。

この例では、createUser エンドポイントが更新され、新しいフィールド age をサポートするようにスキーマ検証を変更します:

import {
createNodeblocksUserApp,
defaultAdapter,
} from '@basaldev/blocks-user-service';
import { adapter as sdkAdapter } from '@basaldev/blocks-backend-sdk';
import { merge } from 'lodash';

let adapter = await defaultAdapter.createUserDefaultAdapter({ ... }, { ... });

adapter = sdkAdapter.modifySchemaValidator(adapter, 'createUser', 'validBody', (schema) => ({
...schema,
properties: {
...schema.properties,
age: { type: 'number' },
},
required: [...schema.required, 'age'],
}));

...

例:既存のエンドポイントの振る舞いを変更

この例では、createUser エンドポイントが更新され、新しいユーザが作成されたときに、他のサービスからのデータも返すようになります:

import {
createNodeblocksUserApp,
defaultAdapter,
} from '@basaldev/blocks-user-service';
import { adapter as sdkAdapter } from '@basaldev/blocks-backend-sdk';

let adapter = await defaultAdapter.createUserDefaultAdapter({ ... }, { ... });

adapter = sdkAdapter.modifyHandler(adapter, 'createUser', async (oldHandler) => {
return (logger: Logger, context: adapter.AdapterHandlerContext) => {
const user = await oldHandler(logger, context);
const otherData = await adapter.dependencies.otherServiceApi.getOtherData(user.id);
return {
...user,
otherData,
};
};
});

...

例:エンドポイントがヒットしたときに通知を送るや、その他の副作用を実行

この例では、createUser エンドポイントが更新され、新しいユーザが作成されたときに、メールを送信するようになります:

import {
createNodeblocksUserApp,
defaultAdapter,
} from '@basaldev/blocks-user-service';
import { adapter as sdkAdapter } from '@basaldev/blocks-backend-sdk';

let adapter = await defaultAdapter.createUserDefaultAdapter({ ... }, { ... });

adapter = sdkAdapter.addHandlerSideEffect(adapter, 'createUser', async (logger, context, response) => {
await adapter.dependencies.mailService.sendMail({
from: 'no-reply@company.com',
to: 'admin@company.com',
subject: '新しいユーザが作成されました',
text: `新しいユーザが作成されました。名前は ${response.data.name} です`,
});
});

...

サイドエフェクト(副作用)は、ハンドラ関数の実行後に行われます。時には、サイドエフェクトを実行する前に、ある情報を先に読み込む必要があります。 例えば、注文のステータスの変更についての通知を送信する場合、ステータスが更新される前の注文データを先に読み込んでおきたいです。 そうすることで、ステータスが変更された時にのみ通知を行うことが可能になるます。 これは、addHandlerSideEffect の第4パラメータを渡すことで実現できます:

adapter = sdkAdapter.addHandlerSideEffect(
adapter,
'updateOrder',
async (logger, context, response, preloadedData) => {
if (response.data.status !== preloadedData.status) {
// 通知を送信
}

return { status: 200 };
},
// これは、ハンドラの前に実行されるプリロード関数です。ここでの返却データは、サイドエフェクトに `preloadedData` として渡されます
async (logger, context) => {
return adapter.dataServices.order.getOrder(logger, context.params.id);
}
);

例:エンドポイントを無効にする

この例では、エンドポイントを無効にして404エラーを返すようにします:

import {
createNodeblocksUserApp,
defaultAdapter,
} from '@basaldev/blocks-user-service';
import { adapter as sdkAdapter } from '@basaldev/blocks-backend-sdk';

let adapter = await defaultAdapter.createUserDefaultAdapter({ ... }, { ... });

adapter = sdkAdapter.disableAdapterMethod(adapter, 'createUser');

...

例:有効なエンドポイントのリストを設定する

この例では、有効なエンドポイントのリストを設定して、createUsergetUser エンドポイントのみを含めるようにします。他のエンドポイントは404エラーを返します:

import {
createNodeblocksUserApp,
defaultAdapter,
} from '@basaldev/blocks-user-service';
import { adapter as sdkAdapter } from '@basaldev/blocks-backend-sdk';

let adapter = await defaultAdapter.createUserDefaultAdapter({ ... }, { ... });

adapter = sdkAdapter.setEnabledAdapterMethods(adapter, ['createUser', 'getUser']);

...