From c91e2006b4cc8c0a401e8e7bb0bf8962c72489af Mon Sep 17 00:00:00 2001 From: "ry.yamafuji" Date: Sat, 22 Mar 2025 22:37:59 +0900 Subject: [PATCH] =?UTF-8?q?CouchDB=E3=82=92=E6=A7=8B=E7=AF=89=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examles/sampleCouchDB.js | 17 ++ package-lock.json | 174 +++++++++++++++++++- package.json | 3 +- src/classes/couchdb/CouchCollection.js | 97 ++++++++++++ src/classes/couchdb/CouchDatabaseClient.js | 40 +++++ src/classes/couchdb/CouchDocument.js | 34 ++++ src/script/couchDB/README.md | 175 +++++++++++++++++++++ src/script/couchDB/controlDB.js | 48 ++++++ src/script/couchDB/controlDoc.js | 85 ++++++++++ src/types/INoSQL.ts | 37 +++++ 10 files changed, 708 insertions(+), 2 deletions(-) create mode 100644 examles/sampleCouchDB.js create mode 100644 src/classes/couchdb/CouchCollection.js create mode 100644 src/classes/couchdb/CouchDatabaseClient.js create mode 100644 src/classes/couchdb/CouchDocument.js create mode 100644 src/script/couchDB/README.md create mode 100644 src/script/couchDB/controlDB.js create mode 100644 src/script/couchDB/controlDoc.js create mode 100644 src/types/INoSQL.ts diff --git a/examles/sampleCouchDB.js b/examles/sampleCouchDB.js new file mode 100644 index 0000000..c5fdd30 --- /dev/null +++ b/examles/sampleCouchDB.js @@ -0,0 +1,17 @@ +async function main() { + const {CouchDatabaseClient} = require('../src/classes/couchdb/CouchDatabaseClient'); + + const config = { + protocol: 'http', + host: 'localhost', + port: 5984, + username: 'admin', + password: 'secret' + } + const client = new CouchDatabaseClient(config); + const users = client.getCollection('user'); + + const user = await users.create({name: 'testUser1', email: 'testUser1@example.com' }); +} + +main(); diff --git a/package-lock.json b/package-lock.json index 4c9a4f1..97c2dad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "archiver": "^7.0.1", "csv-writer": "^1.6.0", "dotenv": "^16.4.7", - "minio": "^8.0.5" + "minio": "^8.0.5", + "nano": "^10.1.4" }, "devDependencies": { "jsdoc": "^4.0.4" @@ -373,6 +374,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -927,6 +954,26 @@ "node": ">=0.10.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -1711,6 +1758,26 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nano": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/nano/-/nano-10.1.4.tgz", + "integrity": "sha512-bJOFIPLExIbF6mljnfExXX9Cub4W0puhDjVMp+qV40xl/DBvgKao7St4+6/GB6EoHZap7eFnrnx4mnp5KYgwJA==", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.7.4", + "node-abort-controller": "^3.1.1", + "qs": "^6.13.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -1740,6 +1807,18 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1819,6 +1898,12 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -1829,6 +1914,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -1998,6 +2098,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", diff --git a/package.json b/package.json index b84cf63..b12981d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "archiver": "^7.0.1", "csv-writer": "^1.6.0", "dotenv": "^16.4.7", - "minio": "^8.0.5" + "minio": "^8.0.5", + "nano": "^10.1.4" }, "devDependencies": { "jsdoc": "^4.0.4" diff --git a/src/classes/couchdb/CouchCollection.js b/src/classes/couchdb/CouchCollection.js new file mode 100644 index 0000000..d19a557 --- /dev/null +++ b/src/classes/couchdb/CouchCollection.js @@ -0,0 +1,97 @@ +// couchdb/CouchCollection.js +const { CouchDocument } = require('./CouchDocument.js'); +/** + * @template T + */ + class CouchCollection { + /** + * @param {import('nano').DocumentScope} db + */ + constructor(db) { + this.db = db; + } + + /** + * @param {string} id + * @returns {Promise | null>} + */ + async get(id) { + try { + const doc = await this.db.get(id); + return new CouchDocument( + doc._id, + doc.data, + doc.createdAt ? new Date(doc.createdAt) : undefined, + doc.updatedAt ? new Date(doc.updatedAt) : undefined, + doc._rev + ); + } catch (err) { + if (err.statusCode === 404) return null; + throw err; + } + } + + /** + * @returns {Promise>>} + */ + async list() { + const result = await this.db.list({ include_docs: true }); + return result.rows + .filter(row => row.doc) + .map(row => + new CouchDocument( + row.doc._id, + row.doc.data, + row.doc.createdAt ? new Date(row.doc.createdAt) : undefined, + row.doc.updatedAt ? new Date(row.doc.updatedAt) : undefined, + row.doc._rev + ) + ); + } + + /** + * @param {T} data + * @returns {Promise>} + */ + async create(data) { + const now = new Date(); + const doc = { + data, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + }; + const response = await this.db.insert(doc); + return new CouchDocument(response.id, data, now, now, response.rev); + } + + /** + * @param {string} id + * @param {Partial} data + * @returns {Promise>} + */ + async update(id, data) { + const existing = await this.db.get(id); + const updated = { + ...existing, + data: { + ...existing.data, + ...data, + }, + updatedAt: new Date().toISOString(), + }; + const response = await this.db.insert(updated); + return new CouchDocument(response.id, updated.data, existing.createdAt, new Date(), response.rev); + } + + /** + * @param {string} id + * @returns {Promise} + */ + async delete(id) { + const doc = await this.db.get(id); + await this.db.destroy(id, doc._rev); + } +} + + +module.exports = { CouchCollection }; \ No newline at end of file diff --git a/src/classes/couchdb/CouchDatabaseClient.js b/src/classes/couchdb/CouchDatabaseClient.js new file mode 100644 index 0000000..0d1e516 --- /dev/null +++ b/src/classes/couchdb/CouchDatabaseClient.js @@ -0,0 +1,40 @@ +// couchdb/CouchDatabaseClient.js +const nano = require('nano'); +const { CouchCollection } = require('./CouchCollection.js'); + +/** + * @template T + * @typedef {import('../interfaces').ICollection} ICollection + */ + +/** + * @typedef {Object} CouchDBConfig + * @property {string} protocol + * @property {string} host + * @property {number} port + * @property {string} username + * @property {string} password + */ + +class CouchDatabaseClient { + /** + * @param {CouchDBConfig} config + */ + constructor(config) { + const { protocol, host, port, username, password } = config; + const url = `${protocol}://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`; + this.server = nano(url); + } + + /** + * @template T + * @param {string} name + * @returns {ICollection} + */ + getCollection(name) { + const db = this.server.db.use(name); + return new CouchCollection(db); + } +} + +module.exports = { CouchDatabaseClient }; \ No newline at end of file diff --git a/src/classes/couchdb/CouchDocument.js b/src/classes/couchdb/CouchDocument.js new file mode 100644 index 0000000..3ee7b44 --- /dev/null +++ b/src/classes/couchdb/CouchDocument.js @@ -0,0 +1,34 @@ +// couchdb/CouchDocument.js + +/** + * @template T + * @typedef {Object} IDocument + * @property {string} id + * @property {T} data + * @property {Date=} createdAt + * @property {Date=} updatedAt + * @property {string|number=} version + */ + +/** + * @template T + * @implements {IDocument} + */ +class CouchDocument { + /** + * @param {string} id + * @param {T} data + * @param {Date=} createdAt + * @param {Date=} updatedAt + * @param {string|number=} version + */ + constructor(id, data, createdAt, updatedAt, version) { + this.id = id; + this.data = data; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.version = version; + } +} + +module.exports = { CouchDocument }; \ No newline at end of file diff --git a/src/script/couchDB/README.md b/src/script/couchDB/README.md new file mode 100644 index 0000000..de0ab62 --- /dev/null +++ b/src/script/couchDB/README.md @@ -0,0 +1,175 @@ +# [NoSQL][CouchDB]Node.jsの利用ガイド(nanoライブラリ) + +CouchDBはドキュメント指向のNoSQLデータベースで、 +HTTPベースのRESTful APIで操作可能です。 + +Node.jsでは公式の軽量クライアントであるnanoを使うことで +簡単にCouchDBとやりとりできます。 + +[戻る](https://wiki.pglikers.com/e/en/private/db/nosql/couch-db) + + +## nanoのインストール + +```sh +npm install nano +``` + +nanoはNode.js向けに設計されているため +ブラウザ(フロントエンド)では使用できません。 + +## CouchDBとの接続 + +```sh +const nano = require('nano')('http://admin:secret@localhost:5984'); +``` + +**envなどを活用して設定する場合** + +```conf +COUCHDB_HOST=localhost +COUCHDB_PORT=5984 +COUCHDB_USER=admin +COUCHDB_PASSWORD=secret +``` + +サンプルコード + +```js +require('dotenv').config(); +const protocol = 'http'; // or https +const { + COUCHDB_HOST, + COUCHDB_PORT, + COUCHDB_USER, + COUCHDB_PASSWORD +} = process.env; +const url = `${protocol}://${COUCHDB_USER}:${COUCHDB_PASSWORD}@${COUCHDB_HOST}:${COUCHDB_PORT}`; +const nano = require('nano')(url); +``` + +## CouchDBの使い方 + +### データベースの操作 + +```js +// データベース作成 +await nano.db.create('user'); + +// データベース削除 +await nano.db.destroy('user'); + +// 一覧取得 +const dbs = await nano.db.list(); +console.log(dbs); +``` + +### ドキュメントの操作 + +```js +const db = nano.db.use('user'); + +// 作成 +await db.insert({ name: 'Alice', email: 'alice@example.com' }); + +// 取得 +const doc = await db.get('document_id'); + +// 更新 +doc.age = 30; +await db.insert(doc); + +// 削除 +await db.destroy(doc._id, doc._rev); +``` + + +### インデックス作成(パフォーマンス向上) + +Mangoクエリ(find)はインデックスがあると高速になります。 +例としてユーザーの年齢でよく検索するなら、 +事前に下記のようにインデックスを作ることを推奨します + +```js +await db.createIndex({ + index: { fields: ['age'] }, + name: 'age-index', + type: 'json' +}); +``` + +## Tips + +### nanoの初期化オプション + +```js +const nano = require('nano')({ + url: 'http://localhost:5984', + requestDefaults: { + headers: { + Authorization: 'Basic ...' // 任意のカスタムヘッダー + } + } +}); +``` + +### 検索のサンプル + +#### 名前が'A'から始まる人だけ取得 + +CouchDBのMangoクエリでは文字列の部分一致は +範囲指定($gte, $lt)を使う方法が一般的です +("startkey" などでは存在しない) + +```js +const result = await db.find({ + selector: { + name: { + "$gte": "A", + "$lt": "B" + } + }, + sort: [{ name: "asc" }] +}); +result.docs.forEach(doc => console.log(doc)); +``` + +#### OR条件を使いたい($or) + +```js +const result = await db.find({ + selector: { + "$or": [ + { age: { "$lt": 30 } }, + { + name: { + "$gte": "Z", + "$lt": "Z\ufff0" + } + } + ] + } +}); +``` + +複数条件やソートを行う場合は +インデックスも複数フィールドに対応しておくと効率が良くなります + +```js +await db.createIndex({ + index: { + fields: ['age', 'name'] + }, + name: 'age-name-index' +}); +``` + +### フロントエンドで使えるのか + +認証情報がURLに含まれるためセキュリティ上もフロントでの使用は不適切 + +* フロントエンドからCouchDBを扱いたい場合: + 1. **PouchDBを活用する** + * フロント向けCouchDB互換DB。リアルタイム同期・オフライン対応も可能 + 2. サーバー側でnanoを使いREST APIを作ってフロントから叩く + diff --git a/src/script/couchDB/controlDB.js b/src/script/couchDB/controlDB.js new file mode 100644 index 0000000..e7f4422 --- /dev/null +++ b/src/script/couchDB/controlDB.js @@ -0,0 +1,48 @@ +/** + * Create a new database + * nanoでデータベースを作成する + */ +const nano = require('nano')('http://admin:secret@localhost:5984'); + +// データベース作成 +const createDB = async (dbName) => { + nano.db.create(dbName, (err, body) => { + if (err) { + console.log(err); + return; + } + console.log(body); + }); +} + +// データベース削除 +const deleteDB = async (dbName) => { + nano.db.destroy(dbName, (err, body) => { + if (err) { + console.log(err); + return; + } + console.log(body); + }); +} + +// データベース一覧表示 +const getDbList = async () => { + nano.db.list((err, body) => { + if (err) { + console.log(err); + return; + } + console.log(body); + }); +} + + +async function main() { + // await deleteDB('user'); + // await createDB('product'); + // await createDB('item'); + // await getDbList(); +} + +main(); \ No newline at end of file diff --git a/src/script/couchDB/controlDoc.js b/src/script/couchDB/controlDoc.js new file mode 100644 index 0000000..7ffb666 --- /dev/null +++ b/src/script/couchDB/controlDoc.js @@ -0,0 +1,85 @@ +/** + * Create a new database + * nanoでデータベースを作成する + */ +const nano = require('nano')('http://admin:secret@localhost:5984'); + +// ドキュメント作成 +const createDocument = async (dbName,post) => { + const db = nano.use(dbName); + db.insert(post, (err, body) => { + if (err) { + console.log(err); + return; + } + console.log(body); + }); +} + +// ドキュメントを一覧を取得する +const getDocumentList = async (dbName) => { + const db = nano.use(dbName); + const result = await db.list({ include_docs: true }); + return result +} + + +// ドキュメントを一覧を取得する(抽出する) +const findDocument = async (dbName,selector) => { + const db = nano.use(dbName); + const result = await db.find({selector: selector}); + return result +} + + + + +// ドキュメントを取得する +const getDocument = async (dbName,docId) => { + const db = nano.use(dbName); + const doc = await db.get(docId); + return doc; +} + +// ドキュメントを更新する +const updateDocument = async (dbName,doc) => { + const db = nano.use(dbName); + return await db.insert(doc); +} + +// ドキュメントを削除する +const deleteDocument = async (dbName,doc) => { + const db = nano.use(dbName); + return await db.destroy(doc._id, doc._rev); +} + +async function main() { + // ドキュメントを作成する + await createDocument('user',{id:1,name:'Alice',email:'alice@example.com'}); + // IDを指定する + await createDocument('user',{_id:'user_1',id:2,name:'Bob',email:'bob@example.com'}); + + // ドキュメントを取得する + const doc = await getDocument('user','user_1'); + console.log(doc.name); + + // ドキュメントを更新する + doc.age = 20; + await updateDocument('user',doc); + + // ドキュメントを削除する + await deleteDocument('user',doc); + + // ドキュメントの一覧を取得する + const docs = await getDocumentList('user'); + console.log(docs); + + // ドキュメントの検索する + const selector = {name: 'Bob'}; + docs = await findDocument('user',selector); + console.log(docs); + + +} + +main(); \ No newline at end of file diff --git a/src/types/INoSQL.ts b/src/types/INoSQL.ts new file mode 100644 index 0000000..00597e8 --- /dev/null +++ b/src/types/INoSQL.ts @@ -0,0 +1,37 @@ +/** + * Interface for NoSQL document + * @param T - Type of data + * @param id - Document ID + */ +export interface IDocument { + id: string; + data: T; + createdAt?: Date; + updatedAt?: Date; + version?: string | number; +} + +/** + * Function to unsubscribe from updates * + * watch()を使わない・購読解除の必要もないならvoid戻り値でも問題なし + * 将来的には、unsubscribeするための関数を返す + */ +export type UnsubscribeFn = () => void; + +/** + * Interface for NoSQL collection + * @param T - Type of data + */ +export interface ICollection { + get(id: string): Promise | null>; + list(): Promise[]>; + create(data: T): Promise>; + update(id: string, data: Partial): Promise>; + delete(id: string): Promise; + // オプション: 更新監視など + watch?(onChange: (doc: IDocument) => void): UnsubscribeFn; +} + +export interface IDatabaseClient { + getCollection(name: string): ICollection; +} \ No newline at end of file