メインコンテンツまでスキップ
バージョン: 0.13.0 (Previous)

💾 カスタムDataStoreの使用

NodeblocksはデフォルトでMongoDB例を含んでいますが、任意のストレージエンジンは、それがハンドラが期待するコレクションのようなインターフェースを公開する場合、対応するフィーチャーに使用できます。この柔軟性により、SQLデータベース、Redis、フラットファイル、またはテスト用のインメモリストレージを使用できます。

📦 必要なパッケージ: core-profileの例はExpress、SDK、およびRamdaをインポートします:

npm install express @nodeblocks/backend-sdk ramda
npm install --save-dev @types/node @types/express @types/ramda

📋 必須インターフェース

カスタムデータストアは、ほとんどのCRUDハンドラで使用される以下の主要メソッドを実装する必要があります:

  • insertOne(doc){ insertedId, acknowledged }
  • findOne(filter) → ドキュメントまたは null
  • find(filter, { skip?, limit? }?)toArray(): Promise<Record[]> を持つカーソルのようなオブジェクト
  • countDocuments(filter)Promise<number>
  • updateOne(filter, update){ matchedCount, modifiedCount }
  • deleteOne(filter){ deletedCount }

いくつかのSDKフィーチャーは updateManydeleteManyfindOneAndUpdate、またはMongoDB change-streamの watch などの追加メソッドも呼び出します。フィーチャーは $push$pull などのMongoDB演算子も使用する場合があります — これらのルートを公開する場合、それらの同等の動作を実装してください。

以下では、コア契約を示すJSONファイルアダプターを実装し、プロフィールフィーチャーの合成されたサブセットと統合する方法を示します。


1️⃣ アダプターの実装

datastore/jsonFileDataStore.ts
import {promises as fs} from 'fs';

interface DbRecord {
id: string;
[key: string]: unknown;
}
interface QueryFilter {
id?: string;
[key: string]: unknown;
}
interface FindOptions {
limit?: number;
skip?: number;
}
interface UpdateOperation {
$set?: Record<string, unknown>;
}

export function createJsonFileDataStore(dbFile: string) {
return {
/* 作成 */
async insertOne(doc: DbRecord) {
const records = await readAll();
records.push(doc);
await writeAll(records);
return {insertedId: doc.id, acknowledged: true};
},

/* 単一読み取り */
async findOne(query: QueryFilter) {
const records = await readAll();
return records.find(r => match(r, query)) ?? null;
},

/* 複数読み取り */
find(query: QueryFilter = {}, {skip = 0, limit}: FindOptions = {}) {
return {
async toArray() {
const records = await readAll();
const matches = records.filter(r => match(r, query));
return limit === undefined ? matches.slice(skip) : matches.slice(skip, skip + limit);
},
};
},

/* カウント */
async countDocuments(query: QueryFilter = {}) {
const records = await readAll();
return records.filter(r => match(r, query)).length;
},

/* 更新 */
async updateOne(query: QueryFilter, update: UpdateOperation) {
const records = await readAll();
const idx = records.findIndex(r => match(r, query));
if (idx === -1) {
return {matchedCount: 0, modifiedCount: 0, acknowledged: true};
}
const nextRecord = {...records[idx], ...update.$set};
const modifiedCount = JSON.stringify(records[idx]) === JSON.stringify(nextRecord) ? 0 : 1;
records[idx] = nextRecord;
await writeAll(records);
return {matchedCount: 1, modifiedCount, acknowledged: true};
},

/* 削除 */
async deleteOne(query: QueryFilter) {
const records = await readAll();
const idx = records.findIndex(r => match(r, query));
if (idx === -1) {
return {deletedCount: 0, acknowledged: true};
}
records.splice(idx, 1);
await writeAll(records);
return {deletedCount: 1, acknowledged: true};
},

/* オプションのバルクメソッド */
async updateMany(query: QueryFilter, update: UpdateOperation) {
const records = await readAll();
let matchedCount = 0;
let modifiedCount = 0;
const nextRecords = records.map(record => {
if (!match(record, query)) return record;
matchedCount += 1;
const nextRecord = {...record, ...update.$set};
if (JSON.stringify(record) !== JSON.stringify(nextRecord)) modifiedCount += 1;
return nextRecord;
});
await writeAll(nextRecords);
return {matchedCount, modifiedCount, acknowledged: true};
},

async deleteMany(query: QueryFilter) {
const records = await readAll();
const nextRecords = records.filter(r => !match(r, query));
const deletedCount = records.length - nextRecords.length;
await writeAll(nextRecords);
return {deletedCount, acknowledged: true};
},
};

/* ------------------------------------- */

function match(record: DbRecord, query: QueryFilter) {
return Object.entries(query).every(([k, v]) => record[k] === v);
}

async function readAll(): Promise<DbRecord[]> {
try {
const raw = await fs.readFile(dbFile, 'utf8');
return JSON.parse(raw);
} catch {
return [];
}
}

async function writeAll(records: DbRecord[]) {
await fs.writeFile(dbFile, JSON.stringify(records, null, 2));
}
}

なぜこれらのメソッド?

Nodeblocksハンドラはコレクションのようなメソッドを呼び出します。ストレージ技術がハンドラが使用するメソッド名、戻り形状、クエリ演算子、およびカーソル動作を実装できる場合、サービスコードを変更せずに使用できます。


2️⃣ カスタムDataStoreの使用

サービスは依存性注入を使用するため、カスタムデータストアをサービスに渡します。profileService はMongoDB Collection 値用に型指定され、プロフィールのフォロー/.likeルートを登録します。代わりに以下のコアプロフィールフィーチャーのみを合成:

import express from 'express';
import {partial} from 'ramda';
import {features, middlewares, primitives} from '@nodeblocks/backend-sdk';
import {createJsonFileDataStore} from './datastore/jsonFileDataStore';

const {nodeBlocksErrorMiddleware} = middlewares;
const {compose, defService} = primitives;
const {createProfileFeature, getProfileFeature, findProfilesFeature, editProfileFeature, deleteProfileFeature} =
features;

const profilesDataStore = createJsonFileDataStore('./profiles.json');
const identitiesDataStore = createJsonFileDataStore('./identities.json');

const profileCoreService: primitives.Service = (dataStores, configuration) =>
defService(
partial(
compose(createProfileFeature, getProfileFeature, findProfilesFeature, editProfileFeature, deleteProfileFeature),
[{dataStores, configuration}],
),
);

express()
.use(
profileCoreService(
{
profiles: profilesDataStore,
identities: identitiesDataStore,
},
{
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
identity: {
typeIds: {
admin: '100',
guest: '000',
regular: '001',
},
},
},
),
)
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));

これだけです — コアプロフィールサービスはこれでMongoDBの代わりにアダプターを通じてデータを永続化します。

ノート: このアダプターはコアプロフィールルートとページネーションのみをサポートします。この実装でプロフィールフォロー/いいねフィーチャーを追加しないでください — これらはMongoDBの $push$pull 演算子を使用します。アバター付きプロフィールには、URL正規化に fileStorageDriver も必要です。他のサービスは追加メソッドまたはクエリセマンティックスを必要とする場合があります。選択したフィーチャーが使用するコレクション動作のみを実装してください。

単一コレクションの完全なカスタムドメインサービスの場合、カスタムサービスの作成 を参照し、{ reviews: jsonFileDataStore } (または類似)を独自のサービスファクトリに注入してください。


3️⃣ 実装チェックリスト

カスタムデータストアを実装する際、以下を確認:

  1. メソッドシグネチャ がハンドラが期待するもの(insertOnefindOne など)と一致している
  2. 戻り型 に必須フィールド(insertedIdmodifiedCount など)が含まれている
  3. 非同期サポート - I/Oを実行するメソッドはPromiseを返す — find() は同期的にPromiseを返すカーソルを返せる
  4. エラー処理 - クラッシュを防ぐために適切なエラー実装
  5. 追加メソッドと演算子 - 使用するフィーチャーが呼び出すもの(updateMany / deleteMany / findOneAndUpdate、MongoDB風更新演算子、または watch())を追加

これらの条件が満たされると、フラットファイル、SQLデータベース、クラウド関数、またはテスト用のインメモリモックなどのストレージを自由に選択できます。


🧪 モックDataStoreでのテスト

ユニットテストのために、同じインターフェースに従った简单なインメモリ実装を作成できます:

export const memoryDataStore = {
_data: [] as any[],
async insertOne(doc) {
this._data.push(doc);
return {insertedId: doc.id, acknowledged: true};
},
async findOne(q) {
return this._data.find(r => r.id === q.id) ?? null;
},
find(q = {}, {skip = 0, limit} = {}) {
return {
toArray: async () => {
const matches = this._data.filter(r => Object.entries(q).every(([k, v]) => r[k] === v));
return limit === undefined ? matches.slice(skip) : matches.slice(skip, skip + limit);
},
};
},
async countDocuments(q = {}) {
return this._data.filter(r => Object.entries(q).every(([k, v]) => r[k] === v)).length;
},
async updateOne(q, u) {
const idx = this._data.findIndex(r => Object.entries(q).every(([k, v]) => r[k] === v));
if (idx === -1) return {matchedCount: 0, modifiedCount: 0, acknowledged: true};
this._data[idx] = {...this._data[idx], ...u.$set};
return {matchedCount: 1, modifiedCount: 1, acknowledged: true};
},
async deleteOne(q) {
const before = this._data.length;
this._data = this._data.filter(r => !Object.entries(q).every(([k, v]) => r[k] === v));
return {deletedCount: before - this._data.length, acknowledged: true};
},
};

このモックデータストアはテストに最適です:

  • I/Oが不要 - すべての操作がインメモリで発生
  • 同じインターフェースに従う - いかなる実際のデータストアと交換可能
  • 高速テスト - データベースのセットアップまたはクリーンアップが不要

🔧 一般的なユースケース

開発とテスト

  • JSONファイル - simpleなセットアップ、人間が読み可能なデータ
  • インメモリストレージ - 高速テスト、永続化不要
  • SQLite - ファイルベースのSQLデータベース、サーバー不要

本番環境

  • PostgreSQL/MySQL - 堅牢でスケーラブルなリレーショナルデータベース
  • MongoDB - 豊富なクエリ機能を備えたドキュメントデータベース
  • Redis - 高パフォーマンスのキャッシングとセッションストレージ

クラウドとサーバーレス

  • DynamoDB - AWSマネージドNoSQLデータベース
  • Firestore - Google Cloudドキュメントデータベース
  • CosmosDB - Azureマルチモデルデータベース

✅ まとめ

  • インターフェース準拠 - 必要なメソッドを正しいシグネチャで実装
  • 依存性注入 - サービス作成時にデータストアを渡す
  • 正しいコレクションキー - 各サービスの期待されるデータストア形状(profilesidentities など)に一致
  • ストレージの柔軟性 - インターフェースを実装できる任意のストレージ技術を使用
  • テストサポート - 高速で信頼性の高いユニットテスト用のインメモリモックを作成
  • 本番対応 - コード変更なく開発と本番のデータストア間で切り替え

この抽象化は、Nodeblocksサービスとの互換性を維持しながら、ニーズに合った最適なストレージソリューションを選択する complete フリーダムを提供します。