From 2ae11ca3e40df64e3575e38caf051fd53c625d20 Mon Sep 17 00:00:00 2001 From: Hayden Young Date: Tue, 7 Feb 2023 02:31:58 +0000 Subject: [PATCH] feat: A basic document store. --- src/documents.js | 90 ++++++++++++++++++++++++++ test/documents.spec.js | 140 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 src/documents.js create mode 100644 test/documents.spec.js diff --git a/src/documents.js b/src/documents.js new file mode 100644 index 0000000..e79506f --- /dev/null +++ b/src/documents.js @@ -0,0 +1,90 @@ +const DocumentStore = async ({ OpLog, Database, ipfs, identity, databaseId, accessController, storage, indexBy = '_id' }) => { + const database = await Database({ OpLog, ipfs, identity, databaseId, accessController, storage }) + + const { addOperation, log } = database + + /** + * Stores a document to the store. + * + * @param {Object} doc An object representing a key/value list of fields. + * @returns {string} The hash of the new oplog entry. + */ + const put = async (doc) => { + if (!doc[indexBy]) { throw new Error(`The provided document doesn't contain field '${indexBy}'`) } + + return addOperation({ op: 'PUT', key: doc[indexBy], value: doc }) + } + + /** + * Deletes a document from the store. + * + * @param {string} key The key of the doc to delete. + * @returns {string} The hash of the new oplog entry. + */ + const del = async (key) => { + if (!get(key)) { throw new Error(`No entry with key '${key}' in the database`) } + + return addOperation({ op: 'DEL', key, value: null }) + } + + /** + * Gets a document from the store by key. + * + * @param {string} key The key of the doc to get. + * @returns {Object} The doc corresponding to key or null. + */ + const get = async (key) => { + for await (const entry of iterator()) { + const { key: k, value } = entry + + if (key === k) { + return value + } + } + + return null + } + + /** + * Queries the document store for documents matching mapper filters. + * + * @param {function(Object)} mapper A function for querying for specific results. + * @returns {Array} Found documents. + */ + const query = async (mapper) => { + const results = [] + + for await (const entry of iterator()) { + if (Object.values(entry).find(mapper)) { + results.push(entry.value) + } + } + + return results + } + + const iterator = async function * () { + const keys = {} + for await (const entry of log.traverse()) { + const { op, key, value } = entry.payload + if (op === 'PUT' && !keys[key]) { + keys[key] = true + yield { key, value } + } else if (op === 'DEL' && !keys[key]) { + keys[key] = true + } + } + } + + return { + ...database, + type: 'documents', + put, + del, + get, + iterator, + query + } +} + +export default DocumentStore diff --git a/test/documents.spec.js b/test/documents.spec.js new file mode 100644 index 0000000..4f56000 --- /dev/null +++ b/test/documents.spec.js @@ -0,0 +1,140 @@ +import { deepStrictEqual, strictEqual } from 'assert' +import rimraf from 'rimraf' +import * as Log from '../src/log.js' +import IdentityProvider from 'orbit-db-identity-provider' +import Keystore from '../src/Keystore.js' + +import Documents from '../src/documents.js' +import Database from '../src/database.js' + +// Test utils +import { config, testAPIs, getIpfsPeerId, waitForPeers, startIpfs, stopIpfs } from 'orbit-db-test-utils' +import connectPeers from './utils/connect-nodes.js' +import { identityKeys, signingKeys } from './fixtures/orbit-db-identity-keys.js' + +const { sync: rmrf } = rimraf +const { createIdentity } = IdentityProvider + +Object.keys(testAPIs).forEach((IPFS) => { + describe('Documents Database (' + IPFS + ')', function () { + this.timeout(config.timeout * 2) + + let ipfsd1, ipfsd2 + let ipfs1, ipfs2 + let keystore, signingKeystore + let peerId1, peerId2 + let testIdentity1, testIdentity2 + let db1, db2 + let accessController + + const databaseId = 'documents-AAA' + + before(async () => { + // Start two IPFS instances + ipfsd1 = await startIpfs(IPFS, config.daemon1) + ipfsd2 = await startIpfs(IPFS, config.daemon2) + ipfs1 = ipfsd1.api + ipfs2 = ipfsd2.api + + await connectPeers(ipfs1, ipfs2) + + // Get the peer IDs + peerId1 = await getIpfsPeerId(ipfs1) + peerId2 = await getIpfsPeerId(ipfs2) + + keystore = new Keystore('./keys_1') + await keystore.open() + for (const [key, value] of Object.entries(identityKeys)) { + await keystore.addKey(key, value) + } + + signingKeystore = new Keystore('./keys_2') + await signingKeystore.open() + for (const [key, value] of Object.entries(signingKeys)) { + await signingKeystore.addKey(key, value) + } + + // Create an identity for each peers + testIdentity1 = await createIdentity({ id: 'userA', keystore, signingKeystore }) + testIdentity2 = await createIdentity({ id: 'userB', keystore, signingKeystore }) + + const accessController = { + canAppend: (entry) => entry.identity.id === testIdentity1.id + } + }) + + beforeEach(async () => { + db1 = await Documents({ OpLog: Log, Database, ipfs: ipfs1, identity: testIdentity1, databaseId, accessController }) + }) + + afterEach(async () => { + if (db1) { + await db1.drop() + await db1.close() + } + if (db2) { + await db2.drop() + await db2.close() + } + }) + + after(async () => { + if (ipfsd1) { + await stopIpfs(ipfsd1) + } + if (ipfsd2) { + await stopIpfs(ipfsd2) + } + if (keystore) { + await keystore.close() + } + if (signingKeystore) { + await signingKeystore.close() + } + if (testIdentity1) { + rmrf(testIdentity1.id) + } + if (testIdentity2) { + rmrf(testIdentity2.id) + } + rmrf('./orbitdb') + rmrf('./keys_1') + rmrf('./keys_2') + }) + + describe('using database', () => { + it('gets a document', async () => { + const key = 'hello world 1' + + await db1.put({ _id: key, doc: 'writing 1 to db1' }) + + const doc = await db1.get(key) + strictEqual(doc._id, key) + }) + + it('deletes a document', async () => { + const key = 'hello world 1' + + await db1.put({ _id: key, doc: 'writing 1 to db1' }) + await db1.del(key) + + const doc = await db1.get(key) + strictEqual(doc, undefined) + }) + + it('queries a document', async () => { + const expected = { _id: 'hello world 1', doc: 'writing new 1 to db1', views: 10 } + + const doc3 = { _id: 'hello world 3', doc: 'writing 3 to db1', views: 12 } + + await db1.put({ _id: 'hello world 1', doc: 'writing 1 to db1', views: 10 }) + await db1.put({ _id: 'hello world 2', doc: 'writing 2 to db1', views: 5 }) + await db1.put({ _id: 'hello world 3', doc: 'writing 3 to db1', views: 12 }) + await db1.del('hello world 3') + await db1.put(expected) + + deepStrictEqual(await db1.query((e) => e.views > 5), [expected]) + }) + }) + }) +})