From aba695991b9898f93b7680d7362b2787e4835b41 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 4 Sep 2026 20:51:43 +0200 Subject: [PATCH] feat(wasm-sdk): composite document queries on the JS surface - wasm-sdk: `getCompositeDocuments` and `getCompositeDocumentsWithProofInfo`. The query is the page plus `subQueries` (optional contract, document type, `documents` or `counts`, fixed clauses, per-value limit, and a `bind` naming the page or an earlier documents sub-query); the result is the page and one discriminated sub-result per sub-query, counts keyed by the bound value's base58 identifier. Sub-query contracts go through the same cache as the page's. - js-evo-sdk: `documents.composite` / `documents.compositeWithProof` and a README section with the feed-page example. - platform-test-suite: a composite case next to the chained one (page, like counts from the countable index, the viewer's likes through the byLiker terminal). Co-Authored-By: Claude Fable 5.1 --- packages/js-evo-sdk/README.md | 49 +++ packages/js-evo-sdk/src/documents/facade.ts | 25 ++ .../platform/IndexOnlyDocument.spec.js | 47 ++ .../src/queries/composite_document.rs | 415 ++++++++++++++++++ packages/wasm-sdk/src/queries/document.rs | 2 +- packages/wasm-sdk/src/queries/mod.rs | 1 + 6 files changed, 538 insertions(+), 1 deletion(-) create mode 100644 packages/wasm-sdk/src/queries/composite_document.rs diff --git a/packages/js-evo-sdk/README.md b/packages/js-evo-sdk/README.md index a87c8c3b8a0..b8dd0e6aad9 100644 --- a/packages/js-evo-sdk/README.md +++ b/packages/js-evo-sdk/README.md @@ -17,6 +17,7 @@ Evo SDK provides a high-level, strongly-typed interface for interacting with [Da - [Ranked queries](#ranked-queries) - [Document references (`refersTo`)](#document-references-refersto) - [Chained queries (provable semi-join)](#chained-queries-provable-semi-join) +- [Composite queries (a page plus its sub-queries)](#composite-queries-a-page-plus-its-sub-queries) - [Contributing](#contributing) - [License](#license) @@ -223,6 +224,54 @@ const next = await sdk.documents.chained({ The inner query must target an indexOnly document type and resolve to an index carrying `joinProperty`, and `joinProperty` must declare a same-contract `refersTo: permanentDocument` targeting `outerDocumentType`. `innerLimit` is required — it bounds the derived outer fetch, so there is no server-default fallback. There are no outer-side clauses by design; filter `outerDocuments` locally. `sdk.documents.chainedWithProof(...)` returns the same result with the metadata and proof envelope attached. +## Composite queries (a page plus its sub-queries) + +A **composite query** answers a page and everything a UI needs to render it in ONE verified round trip: the page documents, plus up to ten sub-queries whose `IN` clause the node derives from the proven page (or from an earlier `documents` sub-query). The request never names the derived values. Four sub-query shapes exist: + +- a **by-id join** (`bind.field: '$id'`): the documents a page property refers to (the property must declare `refersTo: permanentDocument` targeting the sub-query's type, so a missing document fails verification); +- an **indexed lookup** (`bind.field` an indexed property or `$ownerId`): documents keyed by a page value, in this or any other contract, with a `limit` on the rows it returns in total unless the index already bounds them (a unique index, or an indexOnly terminal with every prefix fixed); +- a **count** (`kind: 'counts'`): one count per page value from a `countable` index covering the fixed clauses plus the bound field; +- a **sibling** (no `bind`): an independent documents query proven under the same root. + +The node returns everything under ONE merged proof, a single quorum-signed state root by construction, and the SDK bootstraps the page from the proof, re-derives every sub-query itself and verifies the whole composition: the node cannot substitute, omit, or inject a sub-result. + +```ts +// A feed page: the dash posts, their like counts, the posts they quote, +// their authors' profiles, and which of them I liked. +const page = await sdk.documents.composite({ + dataContractId: YAPPR, + documentType: 'post', + where: [['hashtag', '==', 'dash']], + orderBy: [['$createdAt', 'desc']], + limit: 20, + subQueries: [ + { documentType: 'like', kind: 'counts', where: [['hashtag', '==', 'dash']], bind: { sourceProperty: '$id', field: 'postId' } }, + { documentType: 'post', bind: { sourceProperty: 'quotedPostId', field: '$id' } }, + { dataContractId: DASHPAY, documentType: 'profile', bind: { sourceProperty: '$ownerId', field: '$ownerId' } }, + { documentType: 'like', where: [['$ownerId', '==', me]], bind: { sourceProperty: '$id', field: 'postId' } }, + ], +}); + +const [likeCounts, quotedPosts, profiles, myLikes] = page.subResults; +for (const post of page.pageDocuments) { + const likes = likeCounts.kind === 'counts' ? likeCounts.counts.get(post.id.toBase58()) ?? 0n : 0n; + console.log(post.properties.message, likes); +} + +// Next page: continue past the last proven page document. +const cursor = page.pageDocuments.at(-1)?.createdAt; +const next = await sdk.documents.composite({ + dataContractId: YAPPR, + documentType: 'post', + where: [['hashtag', '==', 'dash'], ['$createdAt', '<', cursor]], + orderBy: [['$createdAt', 'desc']], + limit: 20, + subQueries: [/* the same */], +}); +``` + +`limit` on the page is required and bounds every derived clause (at most 100 values reach a sub-query). A sub-query may bind the page (`bind.source: 'page'`, the default) or an earlier `documents` sub-query by index (`bind.source: 1`), so quoted posts can in turn pull their authors' profiles. Every sub-query walks in the page's direction: leave a lookup's ordering out and it inherits that direction, while an ordering that disagrees with the page is refused. Sub-results come back in request order as `{ kind: 'documents', documents }` (a join in first-appearance order of the page's ids, a lookup or sibling in query order) or `{ kind: 'counts', counts }` (a `Map` keyed by the bound value's base58 identifier; a value with no entry counts zero). There is no cursor on this surface; paginate with a range clause on the page's ordering property. `sdk.documents.compositeWithProof(...)` returns the same result with the metadata and proof envelope attached. + ## Contributing Feel free to dive in! [Open an issue](https://github.com/dashpay/platform/issues/new/choose) or submit PRs. diff --git a/packages/js-evo-sdk/src/documents/facade.ts b/packages/js-evo-sdk/src/documents/facade.ts index 2a3b09793c0..1910b77058b 100644 --- a/packages/js-evo-sdk/src/documents/facade.ts +++ b/packages/js-evo-sdk/src/documents/facade.ts @@ -48,6 +48,31 @@ export class DocumentsFacade { return w.getChainedDocumentsWithProofInfo(query); } + /** + * Composite document query: a page plus the sub-queries derived from + * it (by-id joins, indexed lookups, grouped counts, siblings), in ONE + * verified round trip. + * + * A feed page in a single call: the posts, their like counts, the + * posts they quote, their authors' profiles, and the viewer's own + * likes on them. Everything rides ONE merged proof under one + * quorum-signed state root, and every sub-query is re-derived from + * the proven page, so the responding node cannot substitute, omit, + * or inject a sub-result. Paginate with a range clause on the page's + * ordering property. + */ + async composite(query: wasm.CompositeDocumentsQuery): Promise { + const w = await this.sdk.getWasmSdkConnected(); + return w.getCompositeDocuments(query); + } + + async compositeWithProof( + query: wasm.CompositeDocumentsQuery, + ): Promise> { + const w = await this.sdk.getWasmSdkConnected(); + return w.getCompositeDocumentsWithProofInfo(query); + } + async history(query: wasm.DocumentHistoryQuery): Promise> { const w = await this.sdk.getWasmSdkConnected(); return w.getDocumentHistory(query); diff --git a/packages/platform-test-suite/test/functional/platform/IndexOnlyDocument.spec.js b/packages/platform-test-suite/test/functional/platform/IndexOnlyDocument.spec.js index c1198d7ee9e..413a1f4e197 100644 --- a/packages/platform-test-suite/test/functional/platform/IndexOnlyDocument.spec.js +++ b/packages/platform-test-suite/test/functional/platform/IndexOnlyDocument.spec.js @@ -343,6 +343,53 @@ describe('Platform', () => { .to.equal(post.getId().toString()); }); + it('should fetch a feed page with its like counts and my likes through a composite query', async () => { + // A page plus the sub-queries derived from it, ONE merged proof: + // the dash posts, one like count per post (from the countable + // [hashtag, postId] index with hashtag fixed), and which of them + // I liked (the byLiker index with $ownerId fixed, its postId + // terminal bound to the page ids: value-bounded, so no limit). + // The WASM SDK bootstraps the page from the proof, re-derives + // every sub-query, and verifies the composition against the + // quorum-signed root. + const { sdk: evoSdk } = await createPlatformProofVerifier + .getEvoSdkForNetwork(process.env.NETWORK); + + const page = await evoSdk.documents.composite({ + dataContractId: dataContract.getId().toString(), + documentType: 'post', + where: [['hashtag', '==', POST_HASHTAG]], + limit: 10, + subQueries: [ + { + documentType: 'like', + kind: 'counts', + where: [['hashtag', '==', POST_HASHTAG]], + bind: { sourceProperty: '$id', field: 'postId' }, + }, + { + documentType: 'like', + where: [['$ownerId', '==', identity.getId().toString()]], + bind: { sourceProperty: '$id', field: 'postId' }, + }, + ], + }); + + expect(page.pageDocuments).to.have.lengthOf(1); + expect(page.subResults).to.have.lengthOf(2); + + const [pagePost] = page.pageDocuments; + expect(pagePost.id.toBase58()).to.equal(post.getId().toString()); + + const [likeCounts, myLikes] = page.subResults; + expect(likeCounts.kind).to.equal('counts'); + expect(likeCounts.counts.get(post.getId().toString())).to.equal(1n); + + expect(myLikes.kind).to.equal('documents'); + expect(myLikes.documents).to.have.lengthOf(1); + expect(myLikes.documents[0].ownerId.toBase58()).to.equal(identity.getId().toString()); + }); + it('should fail to query a subset-index projection without proofs', async () => { // The subset index [postId] synthesizes a projection without the // hashtag — and with hashtag optional, serializing it would assert diff --git a/packages/wasm-sdk/src/queries/composite_document.rs b/packages/wasm-sdk/src/queries/composite_document.rs new file mode 100644 index 00000000000..189d8fc963c --- /dev/null +++ b/packages/wasm-sdk/src/queries/composite_document.rs @@ -0,0 +1,415 @@ +//! Composite document queries — a page plus the sub-queries derived +//! from it, answered as ONE merged proof. +//! +//! The page is an ordinary documents query with an explicit limit; +//! each sub-query is a by-id join, an indexed lookup, a grouped count, +//! or an independent sibling, whose `IN` clause the node derives from +//! the proven page (or an earlier documents sub-query). Everything +//! rides the typed `getDocuments` V1 wire (the request's `subQueries`) +//! and comes back as one merged grovedb proof — a single quorum-signed +//! state root by construction. The SDK bootstraps the page from the +//! proof, re-derives every sub-query itself, and verifies the whole +//! composition, so a substituted, omitted or injected sub-result fails +//! verification. +//! +//! Pagination lives on the page alone: order by a property and +//! continue with a range clause past the last proven page document. + +use crate::error::WasmSdkError; +use crate::queries::document::{ + build_documents_query, parse_order_clause, parse_where_clause, DocumentsQueryInput, +}; +use crate::queries::utils::deserialize_required_query; +use crate::queries::ProofMetadataResponseWasm; +use crate::sdk::WasmSdk; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::dpp::prelude::Identifier; +use dash_sdk::platform::documents::composite_document_query::{ + CompositeBindingSource, CompositeDocumentQuery, CompositeSubQuery, +}; +use dash_sdk::platform::{CompositeDocuments, CompositeSubQueryResult, Fetch}; +use js_sys::{Array, Map, Object, Reflect}; +use serde::Deserialize; +use serde_json::Value as JsonValue; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; +use wasm_dpp2::data_contract::document::DocumentWasm; +use wasm_dpp2::identifier::IdentifierWasm; + +#[wasm_bindgen(typescript_custom_section)] +const COMPOSITE_DOCUMENTS_QUERY_TS: &'static str = r#" +/** + * Where a sub-query's derived values come from: `'page'` for the page's + * proven documents, or the index of an earlier `documents` sub-query. + */ +export type CompositeBindSource = 'page' | number; + +/** + * The derived clause of a sub-query: ` IN `, the values + * read off the source's proven documents. The request never names them. + */ +export interface CompositeBind { + /** Defaults to `'page'`. */ + source?: CompositeBindSource; + /** + * The source property read off each document: `$id`, `$ownerId`, or an + * identifier-typed property (dotted paths reach nested properties). + */ + sourceProperty: string; + /** + * The sub-query field receiving the `IN` clause. `$id` makes this a + * by-id JOIN (the source property must declare `refersTo: + * permanentDocument` targeting the sub-query's document type, so a + * missing document is a verification error); otherwise `$ownerId` or an + * indexed property (a LOOKUP, where absence is a proven fact). + */ + field: string; +} + +/** + * One sub-query of a composite request. + */ +export interface CompositeSubQuery { + /** Defaults to the page's contract. Any contract works (profiles keyed by owner, names by identity). */ + dataContractId?: string | Uint8Array; + documentType: string; + /** `'documents'` (default) returns the matching documents; `'counts'` one count per derived value. */ + kind?: 'documents' | 'counts'; + /** The FIXED clauses, same shape as a documents query's `where`. Must not name the bound field. */ + where?: any[]; + /** + * Ordering (documents only), same shape as a documents query's `orderBy`. + * Every component walks in the page's direction: leave the bound field + * unordered and it inherits that direction; an ordering that disagrees + * with the page's direction is refused. + */ + orderBy?: any[]; + /** + * Required for a documents lookup on a non-unique index: caps the rows the + * lookup returns in total, in walk order, like an ordinary query's limit + * (at most 100). Forbidden for a lookup already bounded by its values, a + * by-id join, and a count. + */ + limit?: number; + /** The derived clause. Omit for a SIBLING: an independent documents query proven under the same root. */ + bind?: CompositeBind; +} + +/** + * A composite document query: the page plus its sub-queries, in binding + * order (a sub-query may bind only the page or an earlier `documents` + * sub-query). Paginate with a range clause on the page's ordering + * property; there is no cursor on this surface. + */ +export interface CompositeDocumentsQuery { + dataContractId: string | Uint8Array; + documentType: string; + /** Page where clauses, same shape as a documents query's `where`. */ + where?: any[]; + /** Page ordering, same shape as a documents query's `orderBy`. */ + orderBy?: any[]; + /** REQUIRED page size — it bounds every derived clause, so there is no server-default fallback. */ + limit: number; + /** At most 10. */ + subQueries: CompositeSubQuery[]; +} + +/** + * A verified `documents` sub-result: a by-id join in first-appearance + * order of the derived ids among the source documents; a lookup or + * sibling in query order. + */ +export interface CompositeDocumentsSubResult { + kind: 'documents'; + documents: Document[]; +} + +/** + * A verified `counts` sub-result: one entry per derived value that has a + * count, keyed by the value's base58 identifier. A value with no entry + * counts zero. + */ +export interface CompositeCountsSubResult { + kind: 'counts'; + counts: Map; +} + +export type CompositeSubResult = CompositeDocumentsSubResult | CompositeCountsSubResult; + +/** + * A verified composite result. + */ +export interface CompositeDocumentsResult { + /** The page, exactly as the page query alone would return it. */ + pageDocuments: Document[]; + /** One result per sub-query, in request order. */ + subResults: CompositeSubResult[]; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "CompositeDocumentsQuery")] + pub type CompositeDocumentsQueryJs; +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum BindSourceInput { + Index(usize), + Named(String), +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct CompositeBindInput { + #[serde(default)] + source: Option, + source_property: String, + field: String, +} + +#[derive(Deserialize, Default)] +#[serde(rename_all = "lowercase")] +enum SubQueryKindInput { + #[default] + Documents, + Counts, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct CompositeSubQueryInput { + #[serde(default)] + data_contract_id: Option, + document_type: String, + #[serde(default)] + kind: SubQueryKindInput, + #[serde(rename = "where", default)] + where_clauses: Option>, + #[serde(default)] + order_by: Option>, + #[serde(default)] + limit: Option, + #[serde(default)] + bind: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct CompositeDocumentsQueryInput { + data_contract_id: IdentifierWasm, + document_type: String, + #[serde(rename = "where", default)] + where_clauses: Option>, + #[serde(default)] + order_by: Option>, + limit: u32, + sub_queries: Vec, +} + +async fn parse_composite_documents_query( + sdk: &WasmSdk, + query: CompositeDocumentsQueryJs, +) -> Result { + let input: CompositeDocumentsQueryInput = deserialize_required_query( + query, + "Query object is required", + "composite documents query", + )?; + + let page_limit = input.limit; + let page = build_documents_query( + sdk, + DocumentsQueryInput { + data_contract_id: input.data_contract_id, + document_type_name: input.document_type, + where_clauses: input.where_clauses, + order_by: input.order_by, + limit: Some(page_limit), + start_after: None, + start_at: None, + group_by: None, + time_range: None, + }, + ) + .await? + .with_limit(page_limit); + + let mut composite = CompositeDocumentQuery::new(page); + for (index, sub_input) in input.sub_queries.into_iter().enumerate() { + let contract = match sub_input.data_contract_id { + Some(id) => std::sync::Arc::new(sdk.get_or_fetch_contract(id.into()).await?), + None => composite.page.data_contract.clone(), + }; + let mut sub_query = match sub_input.kind { + SubQueryKindInput::Documents => { + CompositeSubQuery::documents(contract, &sub_input.document_type)? + } + SubQueryKindInput::Counts => { + CompositeSubQuery::count(contract, &sub_input.document_type)? + } + }; + if let Some(where_values) = sub_input.where_clauses { + for clause_json in where_values.iter() { + sub_query = sub_query.with_where(parse_where_clause(clause_json)?); + } + } + if let Some(order_values) = sub_input.order_by { + for clause_json in order_values.iter() { + sub_query = sub_query.with_order_by(parse_order_clause(clause_json)?); + } + } + if let Some(limit) = sub_input.limit { + sub_query = sub_query.with_limit(limit); + } + if let Some(bind) = sub_input.bind { + let source = match bind.source { + None => CompositeBindingSource::Page, + Some(BindSourceInput::Named(name)) if name == "page" => { + CompositeBindingSource::Page + } + Some(BindSourceInput::Named(name)) => { + return Err(WasmSdkError::invalid_argument(format!( + "subQueries[{index}].bind.source must be 'page' or the index of an \ + earlier documents sub-query, got '{name}'" + ))); + } + Some(BindSourceInput::Index(source_index)) => { + if source_index >= index { + return Err(WasmSdkError::invalid_argument(format!( + "subQueries[{index}].bind.source must name an EARLIER sub-query, \ + got {source_index}" + ))); + } + CompositeBindingSource::SubQuery(source_index) + } + }; + sub_query = sub_query.bound_to(source, bind.source_property, bind.field); + } + composite = composite.with_sub_query(sub_query); + } + + Ok(composite) +} + +/// A count entry's key is the bound value's index-key bytes; derived +/// values are identifiers, so a 32-byte key renders as base58 (what the +/// page documents' ids render as). Anything else falls back to hex. +fn count_key_to_js(key: &[u8]) -> JsValue { + match Identifier::from_bytes(key) { + Ok(identifier) => identifier.to_string(Encoding::Base58).into(), + Err(_) => hex::encode(key).into(), + } +} + +fn set_field(target: &Object, key: &str, value: &JsValue) -> Result<(), WasmSdkError> { + Reflect::set(target, &JsValue::from_str(key), value) + .map(|_| ()) + .map_err(|_| WasmSdkError::generic("failed to build composite result object")) +} + +fn composite_result_to_js( + composite: &CompositeDocuments, + query: &CompositeDocumentQuery, +) -> Result { + let to_array = + |documents: &[dash_sdk::platform::Document], contract_id: Identifier, type_name: &str| { + let array = Array::new(); + for document in documents { + let wasm_doc = + DocumentWasm::new(document.clone(), contract_id, type_name.to_string(), None); + array.push(&JsValue::from(wasm_doc)); + } + array + }; + + let page_documents = to_array( + &composite.page_documents, + query.page.data_contract.id(), + &query.page.document_type_name, + ); + + let sub_results = Array::new(); + for (sub_query, result) in query.sub_queries.iter().zip(composite.sub_results.iter()) { + let entry = Object::new(); + match result { + CompositeSubQueryResult::Documents(documents) => { + set_field(&entry, "kind", &JsValue::from_str("documents"))?; + set_field( + &entry, + "documents", + &to_array( + documents, + sub_query.data_contract.id(), + &sub_query.document_type_name, + ), + )?; + } + CompositeSubQueryResult::Counts(entries) => { + let counts = Map::new(); + for count_entry in entries { + if let Some(count) = count_entry.count { + counts.set(&count_key_to_js(&count_entry.key), &JsValue::from(count)); + } + } + set_field(&entry, "kind", &JsValue::from_str("counts"))?; + set_field(&entry, "counts", &counts)?; + } + } + sub_results.push(&entry); + } + + let result = Object::new(); + set_field(&result, "pageDocuments", &page_documents)?; + set_field(&result, "subResults", &sub_results)?; + Ok(result) +} + +#[wasm_bindgen] +impl WasmSdk { + /// Run a composite document query (a page plus the sub-queries + /// derived from it) and return the verified page and every + /// sub-result. + /// + /// The composition is always proof-verified: one merged grovedb + /// proof commits to one quorum-signed root, every sub-query is + /// re-derived from the proven page, and a by-id join whose + /// referenced document is missing is a verification error, not an + /// absence. + #[wasm_bindgen( + js_name = "getCompositeDocuments", + unchecked_return_type = "CompositeDocumentsResult" + )] + pub async fn get_composite_documents( + &self, + query: CompositeDocumentsQueryJs, + ) -> Result { + let query = parse_composite_documents_query(self, query).await?; + let composite = CompositeDocuments::fetch(self.as_ref(), query.clone()) + .await? + .unwrap_or_default(); + composite_result_to_js(&composite, &query) + } + + /// [`Self::get_composite_documents`] with the response metadata and + /// proof envelope attached. + #[wasm_bindgen( + js_name = "getCompositeDocumentsWithProofInfo", + unchecked_return_type = "ProofMetadataResponseTyped" + )] + pub async fn get_composite_documents_with_proof_info( + &self, + query: CompositeDocumentsQueryJs, + ) -> Result { + let query = parse_composite_documents_query(self, query).await?; + let (composite, metadata, proof) = + CompositeDocuments::fetch_with_metadata_and_proof(self.as_ref(), query.clone(), None) + .await?; + let result = composite_result_to_js(&composite.unwrap_or_default(), &query)?; + Ok(ProofMetadataResponseWasm::from_sdk_parts( + result, metadata, proof, + )) + } +} diff --git a/packages/wasm-sdk/src/queries/document.rs b/packages/wasm-sdk/src/queries/document.rs index 1aec8924c8e..09548da0f2a 100644 --- a/packages/wasm-sdk/src/queries/document.rs +++ b/packages/wasm-sdk/src/queries/document.rs @@ -616,7 +616,7 @@ pub(super) fn parse_time_range_clause( } /// Parse JSON order by clause into OrderClause -fn parse_order_clause(json_clause: &JsonValue) -> Result { +pub(super) fn parse_order_clause(json_clause: &JsonValue) -> Result { let clause_array = json_clause .as_array() .ok_or_else(|| WasmSdkError::invalid_argument("order by clause must be an array"))?; diff --git a/packages/wasm-sdk/src/queries/mod.rs b/packages/wasm-sdk/src/queries/mod.rs index 7c178750e0a..5bacbbdaeea 100644 --- a/packages/wasm-sdk/src/queries/mod.rs +++ b/packages/wasm-sdk/src/queries/mod.rs @@ -1,5 +1,6 @@ pub mod address; pub mod chained_document; +pub mod composite_document; pub mod data_contract; pub mod document; pub mod document_ranked;