Skip to content

Commit cf11e97

Browse files
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 <noreply@anthropic.com>
1 parent 2e24461 commit cf11e97

6 files changed

Lines changed: 538 additions & 1 deletion

File tree

packages/js-evo-sdk/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Evo SDK provides a high-level, strongly-typed interface for interacting with [Da
1717
- [Ranked queries](#ranked-queries)
1818
- [Document references (`refersTo`)](#document-references-refersto)
1919
- [Chained queries (provable semi-join)](#chained-queries-provable-semi-join)
20+
- [Composite queries (a page plus its sub-queries)](#composite-queries-a-page-plus-its-sub-queries)
2021
- [Contributing](#contributing)
2122
- [License](#license)
2223

@@ -223,6 +224,54 @@ const next = await sdk.documents.chained({
223224

224225
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.
225226

227+
## Composite queries (a page plus its sub-queries)
228+
229+
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:
230+
231+
- 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);
232+
- 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);
233+
- a **count** (`kind: 'counts'`): one count per page value from a `countable` index covering the fixed clauses plus the bound field;
234+
- a **sibling** (no `bind`): an independent documents query proven under the same root.
235+
236+
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.
237+
238+
```ts
239+
// A feed page: the dash posts, their like counts, the posts they quote,
240+
// their authors' profiles, and which of them I liked.
241+
const page = await sdk.documents.composite({
242+
dataContractId: YAPPR,
243+
documentType: 'post',
244+
where: [['hashtag', '==', 'dash']],
245+
orderBy: [['$createdAt', 'desc']],
246+
limit: 20,
247+
subQueries: [
248+
{ documentType: 'like', kind: 'counts', where: [['hashtag', '==', 'dash']], bind: { sourceProperty: '$id', field: 'postId' } },
249+
{ documentType: 'post', bind: { sourceProperty: 'quotedPostId', field: '$id' } },
250+
{ dataContractId: DASHPAY, documentType: 'profile', bind: { sourceProperty: '$ownerId', field: '$ownerId' } },
251+
{ documentType: 'like', where: [['$ownerId', '==', me]], bind: { sourceProperty: '$id', field: 'postId' } },
252+
],
253+
});
254+
255+
const [likeCounts, quotedPosts, profiles, myLikes] = page.subResults;
256+
for (const post of page.pageDocuments) {
257+
const likes = likeCounts.kind === 'counts' ? likeCounts.counts.get(post.id.toBase58()) ?? 0n : 0n;
258+
console.log(post.properties.message, likes);
259+
}
260+
261+
// Next page: continue past the last proven page document.
262+
const cursor = page.pageDocuments.at(-1)?.createdAt;
263+
const next = await sdk.documents.composite({
264+
dataContractId: YAPPR,
265+
documentType: 'post',
266+
where: [['hashtag', '==', 'dash'], ['$createdAt', '<', cursor]],
267+
orderBy: [['$createdAt', 'desc']],
268+
limit: 20,
269+
subQueries: [/* the same */],
270+
});
271+
```
272+
273+
`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.
274+
226275
## Contributing
227276

228277
Feel free to dive in! [Open an issue](https://github.com/dashpay/platform/issues/new/choose) or submit PRs.

packages/js-evo-sdk/src/documents/facade.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@ export class DocumentsFacade {
4848
return w.getChainedDocumentsWithProofInfo(query);
4949
}
5050

51+
/**
52+
* Composite document query: a page plus the sub-queries derived from
53+
* it (by-id joins, indexed lookups, grouped counts, siblings), in ONE
54+
* verified round trip.
55+
*
56+
* A feed page in a single call: the posts, their like counts, the
57+
* posts they quote, their authors' profiles, and the viewer's own
58+
* likes on them. Everything rides ONE merged proof under one
59+
* quorum-signed state root, and every sub-query is re-derived from
60+
* the proven page, so the responding node cannot substitute, omit,
61+
* or inject a sub-result. Paginate with a range clause on the page's
62+
* ordering property.
63+
*/
64+
async composite(query: wasm.CompositeDocumentsQuery): Promise<wasm.CompositeDocumentsResult> {
65+
const w = await this.sdk.getWasmSdkConnected();
66+
return w.getCompositeDocuments(query);
67+
}
68+
69+
async compositeWithProof(
70+
query: wasm.CompositeDocumentsQuery,
71+
): Promise<wasm.ProofMetadataResponseTyped<wasm.CompositeDocumentsResult>> {
72+
const w = await this.sdk.getWasmSdkConnected();
73+
return w.getCompositeDocumentsWithProofInfo(query);
74+
}
75+
5176
async history(query: wasm.DocumentHistoryQuery): Promise<Map<bigint, wasm.Document>> {
5277
const w = await this.sdk.getWasmSdkConnected();
5378
return w.getDocumentHistory(query);

packages/platform-test-suite/test/functional/platform/IndexOnlyDocument.spec.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,53 @@ describe('Platform', () => {
343343
.to.equal(post.getId().toString());
344344
});
345345

346+
it('should fetch a feed page with its like counts and my likes through a composite query', async () => {
347+
// A page plus the sub-queries derived from it, ONE merged proof:
348+
// the dash posts, one like count per post (from the countable
349+
// [hashtag, postId] index with hashtag fixed), and which of them
350+
// I liked (the byLiker index with $ownerId fixed, its postId
351+
// terminal bound to the page ids: value-bounded, so no limit).
352+
// The WASM SDK bootstraps the page from the proof, re-derives
353+
// every sub-query, and verifies the composition against the
354+
// quorum-signed root.
355+
const { sdk: evoSdk } = await createPlatformProofVerifier
356+
.getEvoSdkForNetwork(process.env.NETWORK);
357+
358+
const page = await evoSdk.documents.composite({
359+
dataContractId: dataContract.getId().toString(),
360+
documentType: 'post',
361+
where: [['hashtag', '==', POST_HASHTAG]],
362+
limit: 10,
363+
subQueries: [
364+
{
365+
documentType: 'like',
366+
kind: 'counts',
367+
where: [['hashtag', '==', POST_HASHTAG]],
368+
bind: { sourceProperty: '$id', field: 'postId' },
369+
},
370+
{
371+
documentType: 'like',
372+
where: [['$ownerId', '==', identity.getId().toString()]],
373+
bind: { sourceProperty: '$id', field: 'postId' },
374+
},
375+
],
376+
});
377+
378+
expect(page.pageDocuments).to.have.lengthOf(1);
379+
expect(page.subResults).to.have.lengthOf(2);
380+
381+
const [pagePost] = page.pageDocuments;
382+
expect(pagePost.id.toBase58()).to.equal(post.getId().toString());
383+
384+
const [likeCounts, myLikes] = page.subResults;
385+
expect(likeCounts.kind).to.equal('counts');
386+
expect(likeCounts.counts.get(post.getId().toString())).to.equal(1n);
387+
388+
expect(myLikes.kind).to.equal('documents');
389+
expect(myLikes.documents).to.have.lengthOf(1);
390+
expect(myLikes.documents[0].ownerId.toBase58()).to.equal(identity.getId().toString());
391+
});
392+
346393
it('should fail to query a subset-index projection without proofs', async () => {
347394
// The subset index [postId] synthesizes a projection without the
348395
// hashtag — and with hashtag optional, serializing it would assert

0 commit comments

Comments
 (0)