diff --git a/src/client/base.ts b/src/client/base.ts index 72ab72d..e14ede4 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -22,6 +22,5 @@ export interface DocumentData { export interface DocumentEntry { id: string owner: string - tx: string doc: T } diff --git a/src/client/client_v2.ts b/src/client/client_v2.ts index 0347114..d9795b6 100644 --- a/src/client/client_v2.ts +++ b/src/client/client_v2.ts @@ -29,6 +29,7 @@ import type { DB3Account } from '../account/types' import type { Client } from './types' import { Index } from '../proto/db3_database_v2' import { StorageProviderV2 } from '../provider/storage_provider_v2' +import { IndexerProvider } from '../provider/indexer_provider' import { fromHEX } from '../crypto/crypto_utils' import { BSON } from 'db3-bson' @@ -54,8 +55,10 @@ export function createClient( account: DB3Account ) { const provider = new StorageProviderV2(rollup_node_url, account) + const indexer = new IndexerProvider(index_node_url) return { provider, + indexer, account, nonce: 0, } as Client diff --git a/src/client/types.ts b/src/client/types.ts index fa9a767..bb5ffc5 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -18,9 +18,11 @@ import type { DB3Account } from '../account/types' import { StorageProviderV2 } from '../provider/storage_provider_v2' +import { IndexerProvider } from '../provider/indexer_provider' export type Client = { provider: StorageProviderV2 + indexer: IndexerProvider account: DB3Account nonce: number } diff --git a/src/index.ts b/src/index.ts index 65bbe93..17a3198 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,34 +14,13 @@ * limitations under the License. */ -//export { Account } from './proto/db3_account' -//export { Index, Database } from './proto/db3_database' -//export { EventMessage, EventType } from './proto/db3_event' -//export { DB3BrowserWallet } from './wallet/db3_browser_wallet' -//export { MetamaskWallet } from './wallet/metamask' -//export { DB3Client } from './client/client' -//export { initializeDB3, listMyDatabases, listDatabases } from './store/app' -////export { DB3Store } from './store/database' -//export { collection, CollectionReference } from './store/collection' -//export { -// addDoc, -// getDocs, -// deleteDoc, -// updateDoc, -// DocumentReference, -//} from './store/document' -//export { limit, where, query } from './store/query' -//export { DB3Network } from './store/network' - -//===================v2====================== - export { createFromPrivateKey, createRandomAccount, signTypedData, } from './account/db3_account' -export { addDoc, updateDoc, deleteDoc } from './store/document_v2' +export { addDoc, updateDoc, deleteDoc, queryDoc } from './store/document_v2' export { createClient, diff --git a/src/provider/indexer_provider.ts b/src/provider/indexer_provider.ts new file mode 100644 index 0000000..60b629b --- /dev/null +++ b/src/provider/indexer_provider.ts @@ -0,0 +1,49 @@ +// +// indexer_provider.ts +// Copyright (C) 2023 db3.network Author imotai +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import { + GrpcWebFetchTransport, + GrpcWebOptions, +} from '@protobuf-ts/grpcweb-transport' +import { IndexerNodeClient } from '../proto/db3_indexer.client' +import { RunQueryRequest } from '../proto/db3_indexer' +import { Query } from '../proto/db3_database_v2' + +export class IndexerProvider { + readonly client: IndexerNodeClient + constructor(url: string) { + const goptions: GrpcWebOptions = { + baseUrl: url, + // simple example for how to add auth headers to each request + // see `RpcInterceptor` for documentation + interceptors: [], + // you can set global request headers here + meta: {}, + } + const transport = new GrpcWebFetchTransport(goptions) + this.client = new IndexerNodeClient(transport) + } + + async runQuery(db: string, colName: string, query: Query) { + const request: RunQueryRequest = { + db, + colName, + query, + } + const { response } = await this.client.runQuery(request) + return response + } +} diff --git a/src/store/database.ts b/src/store/database.ts index aec7dbd..ac964bc 100644 --- a/src/store/database.ts +++ b/src/store/database.ts @@ -31,31 +31,9 @@ export class DB3Store { } async getCollections(name: string) { - if (this._database && this._collections && this._collections[name]) { - return this._collections[name] - } - const database = await this.client.getDatabase(this.address) - if (database === undefined) { - return undefined - } - this._database = database - this._collections = Object.fromEntries( - database!.collections.map((e) => [e.name, e]) - ) - if (this._collections && this._collections[name]) { - return this._collections[name] - } return undefined } async getDatabase() { - if (!this._database) { - const database = await this.client.getDatabase(this.address) - this._database = database - this._collections = Object.fromEntries( - database!.collections.map((e) => [e.name, e]) - ) - } - return this._database } } diff --git a/src/store/document_v2.ts b/src/store/document_v2.ts index 8e68c54..429d55c 100644 --- a/src/store/document_v2.ts +++ b/src/store/document_v2.ts @@ -26,7 +26,62 @@ import { BSON } from 'db3-bson' import { fromHEX } from '../crypto/crypto_utils' -import type { Collection, DocumentData } from './types' +import type { Collection, QueryResult } from './types' +import type { Query, QueryParameter } from '../proto/db3_database_v2' +import type { DocumentData, DocumentEntry } from '../client/base' + +async function runQueryInternal(col: Collection, query: Query) { + const response = await col.db.client.indexer.runQuery( + col.db.addr, + col.name, + query + ) + const entries = response.documents.map((doc) => { + return { + doc: JSON.parse(doc.doc) as T, + id: doc.id, + owner: doc.owner, + } as DocumentEntry + }) + return { + docs: entries, + collection: col, + } as QueryResult +} + +/** + * + * Query document with a query language + * + * ```ts + * const queryStr = '/* | limit 1' + * const resultSet = await queryDoc(collection, queryStr) + * ``` + * @param col - the instance of collection + * @param queryStr - a document query string + * @param parameters - an optional query parameters + * @returns the {@link Queryresult} + * + **/ +export async function queryDoc( + col: Collection, + queryStr: string, + parameters?: QueryParameter[] +) { + if (!parameters) { + const query: Query = { + queryStr, + parameters: [], + } + return runQueryInternal(col, query) + } else { + const query: Query = { + queryStr, + parameters, + } + return runQueryInternal(col, query) + } +} export async function deleteDoc(col: Collection, ids: string[]) { const documentMutation: DocumentMutation = { @@ -61,6 +116,7 @@ export async function deleteDoc(col: Collection, ids: string[]) { throw new Error('fail to create collection') } } + export async function updateDoc( col: Collection, id: string, diff --git a/src/store/types.ts b/src/store/types.ts index 5604359..a38c756 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -16,6 +16,7 @@ // import { Client } from '../client/types' +import { DocumentEntry, DocumentData } from '../client/base' import { DatabaseMessage as InternalDatabase, Index, @@ -51,6 +52,7 @@ export type Collection = { internal: InternalCollection | undefined } -export interface DocumentData { - [field: string]: any +export type QueryResult = { + docs: Array> + collection: Collection } diff --git a/tests/client.test.ts b/tests/client.test.ts index 07d0f53..367fb0a 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -36,65 +36,7 @@ describe('test db3.js client module', () => { wallet ) const [dbId, txId] = await client.createDatabase() - await new Promise((r) => setTimeout(r, 2000)) - const db = await client.getDatabase(dbId) - expect(dbId).toEqual(`0x${toHEX(db!.address)}`) - const dbs = await client.listDatabases(wallet.getAddress()) - console.log(`dbs length ${dbs.length}`) - expect(dbs.length == 1).toEqual(true) }) - test('create database smoke test', async () => { - const mnemonic = - 'result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss' - const wallet = DB3BrowserWallet.createNew(mnemonic, 'DB3_SECP256K1') - const client = new DB3Client( - 'http://127.0.0.1:26659', - 'http://127.0.0.1:26639', - wallet - ) - const [dbId, txId] = await client.createDatabase() - await new Promise((r) => setTimeout(r, 2000)) - try { - const db = await client.getDatabase(dbId) - expect(dbId).toEqual(`0x${toHEX(db!.address)}`) - const dbs = await client.listDatabases(wallet.getAddress()) - expect(dbs.length > 0).toEqual(true) - const indexList: Index[] = [ - { - name: 'idx1', - id: 1, - fields: [ - { - fieldPath: 'name', - valueMode: { - oneofKind: 'order', - order: Index_IndexField_Order.ASCENDING, - }, - }, - ], - }, - ] - await client.createCollection(dbId, 'books', indexList) - await new Promise((r) => setTimeout(r, 2000)) - const collections = await client.listCollection(dbId) - expect(collections.length).toEqual(1) - await client.createDocument(dbId, 'books', { - name: 'book1', - author: 'db3 developers', - }) - await new Promise((r) => setTimeout(r, 2000)) - const query: StructuredQuery = { - collectionName: 'books', - } - const books = await client.runQuery(dbId, query) - expect(books.length).toBe(1) - expect(books[0].doc['name']).toBe('book1') - const bookId = books[0].id - const result = await client.deleteDocument(dbId, 'books', [bookId]) - expect(result).toBeDefined() - } catch (e) { - console.log(e) - } - }) + }) diff --git a/tests/client_v2.test.ts b/tests/client_v2.test.ts index 792ebc8..0ca6237 100644 --- a/tests/client_v2.test.ts +++ b/tests/client_v2.test.ts @@ -24,7 +24,12 @@ import { getMutationBody, scanMutationHeaders, } from '../src/client/client_v2' -import { addDoc, deleteDoc, updateDoc } from '../src/store/document_v2' +import { + addDoc, + deleteDoc, + updateDoc, + queryDoc, +} from '../src/store/document_v2' import { createRandomAccount } from '../src/account/db3_account' import { createDocumentDatabase, @@ -33,10 +38,20 @@ import { } from '../src/store/database_v2' import { Index, IndexType } from '../src/proto/db3_database_v2' +interface Profile { + city: string + author: string + age: number +} + describe('test db3.js client module', () => { async function createTestClient() { const db3_account = createRandomAccount() - const client = createClient('http://127.0.0.1:26619', '', db3_account) + const client = createClient( + 'http://127.0.0.1:26619', + 'http://127.0.0.1:26639', + db3_account + ) const nonce = await syncAccountNonce(client) return client } @@ -46,6 +61,93 @@ describe('test db3.js client module', () => { expect(1).toBe(client.nonce) }) + test('test query document', async () => { + const client = await createTestClient() + try { + const { db, result } = await createDocumentDatabase(client, 'db1') + const index: Index = { + path: '/city', + indexType: IndexType.StringKey, + } + { + const { collection, result } = await createCollection( + db, + 'col', + [index] + ) + await new Promise((r) => setTimeout(r, 3000)) + const [txId2, block2, order2] = await addDoc(collection, { + city: 'beijing', + author: 'imotai', + age: 10, + }) + + const [txId3, block3, order3] = await addDoc(collection, { + city: 'beijing2', + author: 'imotai1', + age: 1, + }) + await new Promise((r) => setTimeout(r, 3000)) + { + const queryStr = '/[city = beijing]' + const resultSet = await queryDoc( + collection, + queryStr + ) + expect(1).toBe(resultSet.docs.length) + expect(resultSet.docs[0].doc.city).toBe('beijing') + expect(resultSet.docs[0].doc.author).toBe('imotai') + expect(resultSet.docs[0].doc.age).toBe(10) + } + { + const queryStr = '/* | limit 1' + const resultSet = await queryDoc( + collection, + queryStr + ) + expect(1).toBe(resultSet.docs.length) + expect(resultSet.docs[0].doc.city).toBe('beijing2') + expect(resultSet.docs[0].doc.author).toBe('imotai1') + expect(resultSet.docs[0].doc.age).toBe(1) + } + + { + const queryStr = '/[age = :age]' + const parameter: QueryParameter = { + name: 'age', + parameter: { + oneofKind: 'int64Value', + int64Value: 10, + }, + idx:0 + } + const resultSet = await queryDoc( + collection, + queryStr, + [parameter] + ) + console.log(resultSet) + expect(1).toBe(resultSet.docs.length) + expect(resultSet.docs[0].doc.city).toBe('beijing') + expect(resultSet.docs[0].doc.author).toBe('imotai') + expect(resultSet.docs[0].doc.age).toBe(10) + } + { + const queryStr = '/{age}' + const resultSet = await queryDoc( + collection, + queryStr, + [] + ) + console.log(resultSet.docs) + } + + } + } catch (e) { + console.log(e) + expect(1).toBe(0) + } + }) test('create database v2', async () => { const client = await createTestClient() try { @@ -102,19 +204,9 @@ describe('test db3.js client module', () => { name: 'book1', author: 'db3 developers', id: '0x10b1b560b2fd9a66ae5bce29e5050ffcef6bcc9663d5d116e9877b6a4dda13aa', - time: 1686285013, - fee: 0.069781, + time: "1686285013", + fee: "0.069781", }) - await updateDoc( - collection, - 'id111', - { - name: 'book1', - author: 'db3 developers', - }, - ['name', 'author'] - ) - await deleteDoc(collection, ['id1']) } } } catch (e) { diff --git a/thirdparty/db3 b/thirdparty/db3 index 76d441d..330d0db 160000 --- a/thirdparty/db3 +++ b/thirdparty/db3 @@ -1 +1 @@ -Subproject commit 76d441d184d6690fafe93b3f2bb1794f8839e7d2 +Subproject commit 330d0dbbabe82f4b7582b2acf81abd8511346f32