@arkiv-network/sdk 0.7.0 → 0.8.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -16
- package/dist/attr/index.cjs +34 -0
- package/dist/attr/index.d.cts +2 -0
- package/dist/attr/index.d.ts +2 -0
- package/dist/attr/index.js +3 -0
- package/dist/attr-BDZ-APg5.js +809 -0
- package/dist/attr-BDZ-APg5.js.map +1 -0
- package/dist/attr-BvxHN99W.cjs +1029 -0
- package/dist/chains/index.cjs +2 -3
- package/dist/chains/index.d.cts +39 -77
- package/dist/chains/index.d.cts.map +1 -1
- package/dist/chains/index.d.ts +22 -60
- package/dist/chains/index.d.ts.map +1 -1
- package/dist/chains/index.js +2 -2
- package/dist/chains-BJrZs6sG.js +57 -0
- package/dist/chains-BJrZs6sG.js.map +1 -0
- package/dist/chains-BYRTTipa.cjs +73 -0
- package/dist/changeOwnership-B7bEgla4.d.cts +509 -0
- package/dist/changeOwnership-B7bEgla4.d.cts.map +1 -0
- package/dist/changeOwnership-DwgtdSFc.d.ts +509 -0
- package/dist/changeOwnership-DwgtdSFc.d.ts.map +1 -0
- package/dist/entity/index.cjs +19 -0
- package/dist/entity/index.d.cts +4 -0
- package/dist/entity/index.d.ts +4 -0
- package/dist/entity/index.js +6 -0
- package/dist/entity-B0t6sN7F.cjs +71 -0
- package/dist/entity-BIp_df9u.js +67 -0
- package/dist/entity-BIp_df9u.js.map +1 -0
- package/dist/entity-CFhbAv0R.js +70 -0
- package/dist/entity-CFhbAv0R.js.map +1 -0
- package/dist/entity-wfBnZd91.cjs +98 -0
- package/dist/errors-D-eJkGVc.cjs +56 -0
- package/dist/errors-DGB8AgT2.js +33 -0
- package/dist/errors-DGB8AgT2.js.map +1 -0
- package/dist/expiry-BUYIXTGb.cjs +135 -0
- package/dist/expiry-BxOlvVWC.d.ts +79 -0
- package/dist/expiry-BxOlvVWC.d.ts.map +1 -0
- package/dist/expiry-DGRzrSan.js +95 -0
- package/dist/expiry-DGRzrSan.js.map +1 -0
- package/dist/expiry-DRV29F1r.d.cts +79 -0
- package/dist/expiry-DRV29F1r.d.cts.map +1 -0
- package/dist/flags-0Vl5rrHq.d.cts +41 -0
- package/dist/flags-0Vl5rrHq.d.cts.map +1 -0
- package/dist/flags-Ct8k0gpB.cjs +43 -0
- package/dist/flags-ZtlvOIIk.js +33 -0
- package/dist/flags-ZtlvOIIk.js.map +1 -0
- package/dist/flags-wmcMoAnN.d.ts +41 -0
- package/dist/flags-wmcMoAnN.d.ts.map +1 -0
- package/dist/index-Cgw1u5aY.d.cts +278 -0
- package/dist/index-Cgw1u5aY.d.cts.map +1 -0
- package/dist/index-ClgIuxJ8.d.ts +159 -0
- package/dist/index-ClgIuxJ8.d.ts.map +1 -0
- package/dist/index-D0CmtiWT.d.cts +159 -0
- package/dist/index-D0CmtiWT.d.cts.map +1 -0
- package/dist/index-D8LZt-3J.d.cts +417 -0
- package/dist/index-D8LZt-3J.d.cts.map +1 -0
- package/dist/index-DVYFaXNV.d.ts +417 -0
- package/dist/index-DVYFaXNV.d.ts.map +1 -0
- package/dist/index-VCqgn29D.d.ts +278 -0
- package/dist/index-VCqgn29D.d.ts.map +1 -0
- package/dist/index.cjs +675 -357
- package/dist/index.d.cts +251 -203
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +251 -203
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +626 -355
- package/dist/index.js.map +1 -1
- package/dist/payload-Bnw1hrvb.cjs +83 -0
- package/dist/payload-CIxSn_DW.js +67 -0
- package/dist/payload-CIxSn_DW.js.map +1 -0
- package/dist/payload-Dgx3PQeK.d.ts +94 -0
- package/dist/payload-Dgx3PQeK.d.ts.map +1 -0
- package/dist/payload-P0nWyjLg.d.cts +94 -0
- package/dist/payload-P0nWyjLg.d.cts.map +1 -0
- package/dist/query/index.cjs +24 -149
- package/dist/query/index.d.cts +5 -3
- package/dist/query/index.d.ts +5 -3
- package/dist/query/index.js +6 -137
- package/dist/queryBuilder-BXDvX1K_.cjs +1123 -0
- package/dist/queryBuilder-CbLL4pG5.js +963 -0
- package/dist/queryBuilder-CbLL4pG5.js.map +1 -0
- package/dist/selection-BvK6yX0g.d.cts +433 -0
- package/dist/selection-BvK6yX0g.d.cts.map +1 -0
- package/dist/selection-fpmI3ADQ.d.ts +433 -0
- package/dist/selection-fpmI3ADQ.d.ts.map +1 -0
- package/dist/types/index.cjs +4 -1
- package/dist/types/index.d.cts +7 -2
- package/dist/types/index.d.ts +7 -2
- package/dist/types/index.js +4 -1
- package/dist/utils/index.cjs +4 -17
- package/dist/utils/index.d.cts +3 -17
- package/dist/utils/index.d.ts +3 -17
- package/dist/utils/index.js +4 -18
- package/package.json +26 -5
- package/src/actions/public/getEntity.ts +27 -43
- package/src/actions/public/getEntityNonce.ts +30 -0
- package/src/actions/public/predictEntityKeys.test.ts +143 -0
- package/src/actions/public/predictEntityKeys.ts +138 -0
- package/src/actions/public/query.ts +33 -69
- package/src/actions/public/watchEntityEvents.test.ts +414 -0
- package/src/actions/public/watchEntityEvents.ts +225 -0
- package/src/actions/wallet/changeOwnership.ts +8 -8
- package/src/actions/wallet/createEntity.ts +104 -26
- package/src/actions/wallet/deleteEntity.ts +2 -3
- package/src/actions/wallet/extendEntity.ts +39 -16
- package/src/actions/wallet/mutateEntities.test.ts +76 -0
- package/src/actions/wallet/mutateEntities.ts +29 -74
- package/src/actions/wallet/patchEntity.ts +90 -0
- package/src/attr/attributes.test.ts +120 -0
- package/src/attr/attributes.ts +190 -0
- package/src/attr/codec.test.ts +162 -0
- package/src/attr/codec.ts +220 -0
- package/src/attr/decimal.ts +86 -0
- package/src/attr/errors.ts +145 -0
- package/src/attr/index.ts +50 -0
- package/src/attr/names.ts +106 -0
- package/src/attr/types.ts +150 -0
- package/src/attr/values.test.ts +271 -0
- package/src/attr/values.ts +502 -0
- package/src/chains/cheesecake.ts +22 -0
- package/src/chains/index.ts +1 -2
- package/src/clients/createPublicClient.test.ts +3 -5
- package/src/clients/createPublicClient.ts +5 -4
- package/src/clients/createWalletClient.test.ts +2 -2
- package/src/clients/createWalletClient.ts +13 -7
- package/src/clients/decorators/arkivPublic.ts +160 -139
- package/src/clients/decorators/arkivWallet.ts +91 -95
- package/src/consts.ts +2 -1
- package/src/entity/create.test.ts +184 -0
- package/src/entity/errors.ts +38 -0
- package/src/entity/events.ts +16 -0
- package/src/entity/expiry.test.ts +195 -0
- package/src/entity/expiry.ts +181 -0
- package/src/entity/flags.ts +65 -0
- package/src/entity/index.ts +14 -0
- package/src/entity/key.ts +68 -0
- package/src/entity/operations.ts +134 -0
- package/src/entity/params.ts +15 -0
- package/src/errors.ts +33 -28
- package/src/index.ts +6 -0
- package/src/query/engine.ts +59 -134
- package/src/query/errors.ts +151 -0
- package/src/query/expression.test.ts +286 -0
- package/src/query/expression.ts +493 -0
- package/src/query/index.ts +40 -4
- package/src/query/injection.test.ts +151 -0
- package/src/query/queryBuilder.test.ts +322 -637
- package/src/query/queryBuilder.ts +126 -359
- package/src/query/queryResult.ts +54 -51
- package/src/query/selection.test.ts +48 -98
- package/src/query/selection.ts +77 -65
- package/src/types/entity.test.ts +44 -155
- package/src/types/entity.ts +62 -44
- package/src/types/events.ts +64 -23
- package/src/types/index.ts +20 -16
- package/src/types/rpcSchema.ts +70 -33
- package/src/utils/arkivTransactions.test.ts +681 -38
- package/src/utils/arkivTransactions.ts +249 -104
- package/src/utils/chains.test.ts +21 -24
- package/src/utils/compression.ts +1 -1
- package/src/utils/entities.test.ts +62 -0
- package/src/utils/entities.ts +111 -29
- package/src/utils/expirationTime.ts +114 -10
- package/src/utils/logger.ts +2 -2
- package/src/utils/payload.ts +4 -2
- package/src/utils/revert.test.ts +99 -0
- package/src/utils/revert.ts +145 -0
- package/dist/chains-BifaY04V.cjs +0 -108
- package/dist/chains-CrtkVbBh.js +0 -86
- package/dist/chains-CrtkVbBh.js.map +0 -1
- package/dist/consts-AUoL-v42.cjs +0 -18
- package/dist/consts-BV4PR-de.js +0 -7
- package/dist/consts-BV4PR-de.js.map +0 -1
- package/dist/entity-B6g_EPm1.d.cts +0 -382
- package/dist/entity-B6g_EPm1.d.cts.map +0 -1
- package/dist/entity-qoLMAuuk.d.ts +0 -382
- package/dist/entity-qoLMAuuk.d.ts.map +0 -1
- package/dist/index-DdspxNAA.d.ts +0 -481
- package/dist/index-DdspxNAA.d.ts.map +0 -1
- package/dist/index-Ds9dwqb5.d.cts +0 -481
- package/dist/index-Ds9dwqb5.d.cts.map +0 -1
- package/dist/payload-B2kFJyah.d.ts +0 -6
- package/dist/payload-B2kFJyah.d.ts.map +0 -1
- package/dist/payload-CJN8fTvX.d.cts +0 -6
- package/dist/payload-CJN8fTvX.d.cts.map +0 -1
- package/dist/payload-Claf2jUO.js +0 -13
- package/dist/payload-Claf2jUO.js.map +0 -1
- package/dist/payload-DpA5PA-w.cjs +0 -23
- package/dist/query/index.js.map +0 -1
- package/dist/queryBuilder-D2uwP9jI.js +0 -704
- package/dist/queryBuilder-D2uwP9jI.js.map +0 -1
- package/dist/queryBuilder-UW2MCaX4.cjs +0 -792
- package/dist/utils/index.d.cts.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/src/actions/public/buildQuery.ts +0 -10
- package/src/actions/public/subscribeEntityEvents.ts +0 -119
- package/src/actions/wallet/updateEntity.ts +0 -60
- package/src/chains/braga.ts +0 -26
- package/src/chains/kaolin.ts +0 -29
- package/src/query/engine.test.ts +0 -526
- package/src/query/predicate.test.ts +0 -461
- package/src/query/predicate.ts +0 -144
- package/src/query/queryResult.test.ts +0 -270
- package/src/query/selectQueryBuilder.test.ts +0 -210
- package/src/types/attributes.ts +0 -16
- package/src/utils/validation.test.ts +0 -60
- package/src/utils/validation.ts +0 -32
- /package/dist/{types-H-TSmRpm.js → types-Bx1EKe0v.js} +0 -0
- /package/dist/{types-o3zWarRp.cjs → types-CKPddlfS.cjs} +0 -0
package/src/query/queryResult.ts
CHANGED
|
@@ -1,70 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NoMoreResultsError } from "../errors"
|
|
2
2
|
import type { Entity } from "../types/entity"
|
|
3
|
-
import { getLogger } from "../utils/logger"
|
|
4
|
-
import type { BaseQueryBuilder } from "./queryBuilder"
|
|
5
3
|
|
|
6
|
-
|
|
4
|
+
/** Fetches the page a cursor points at. */
|
|
5
|
+
type FetchPage<TEntity> = (cursor: string) => Promise<QueryResult<TEntity>>
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
8
|
+
* One page of query results.
|
|
10
9
|
*
|
|
11
|
-
* @
|
|
12
|
-
*
|
|
10
|
+
* A page is immutable: {@link next} returns the following page rather than mutating this one, so a
|
|
11
|
+
* page that has been read stays readable.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam TEntity - The shape of each entity, inferred from the selection by `client.select()`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* let page = await client.select({ key: true }).where(eq("category", "docs")).limit(100).fetch()
|
|
17
|
+
* while (page.hasNextPage()) {
|
|
18
|
+
* page = await page.next()
|
|
19
|
+
* }
|
|
13
20
|
*/
|
|
14
21
|
export class QueryResult<TEntity = Entity> {
|
|
15
|
-
entities
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
/** The entities on this page. */
|
|
23
|
+
readonly entities: readonly TEntity[]
|
|
24
|
+
/**
|
|
25
|
+
* The block this page was read at.
|
|
26
|
+
*
|
|
27
|
+
* Every page of one walk reads the same block — the cursor is bound to it — so results cannot
|
|
28
|
+
* shift under a paginating reader.
|
|
29
|
+
*/
|
|
30
|
+
readonly blockNumber: bigint
|
|
31
|
+
/** The cursor for the next page, or `undefined` when this was the last one. */
|
|
32
|
+
readonly cursor: string | undefined
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
return this._cursor
|
|
29
|
-
}
|
|
34
|
+
private readonly fetchPage: FetchPage<TEntity>
|
|
30
35
|
|
|
31
36
|
constructor(
|
|
32
|
-
entities: TEntity[],
|
|
33
|
-
|
|
37
|
+
entities: readonly TEntity[],
|
|
38
|
+
blockNumber: bigint,
|
|
34
39
|
cursor: string | undefined,
|
|
35
|
-
|
|
36
|
-
validAtBlock: bigint | undefined,
|
|
40
|
+
fetchPage: FetchPage<TEntity>,
|
|
37
41
|
) {
|
|
38
42
|
this.entities = entities
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
this._limit = limit
|
|
43
|
-
this._validAtBlock = validAtBlock
|
|
43
|
+
this.blockNumber = blockNumber
|
|
44
|
+
this.cursor = cursor
|
|
45
|
+
this.fetchPage = fetchPage
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
54
|
-
const result = await this._queryBuilder.fetch()
|
|
55
|
-
this.entities = result.entities
|
|
56
|
-
// Update the query builder reference
|
|
57
|
-
this._queryBuilder = result.queryBuilder
|
|
58
|
-
// Check if we've reached the end (no more cursor or we got fewer entities than limit)
|
|
59
|
-
this._endOfIteration = !result.cursor || result.entities.length < this._limit
|
|
60
|
-
// Update the cursor
|
|
61
|
-
this._cursor = result.cursor
|
|
62
|
-
|
|
63
|
-
// TODO check current block height and if it is not too old
|
|
64
|
-
logger("Current block height for next page %s", this._validAtBlock?.toString() ?? "unknown")
|
|
48
|
+
/**
|
|
49
|
+
* Whether another page follows.
|
|
50
|
+
*
|
|
51
|
+
* The node omits the cursor once nothing remains, so this is an answer rather than the guess a
|
|
52
|
+
* short final page would be — a full last page is reported correctly.
|
|
53
|
+
*/
|
|
54
|
+
hasNextPage(): boolean {
|
|
55
|
+
return this.cursor !== undefined
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Fetches the next page.
|
|
60
|
+
*
|
|
61
|
+
* @throws {NoMoreResultsError} If this was the last page — check {@link hasNextPage} first.
|
|
62
|
+
* @throws {QueryError} If the node rejects the request; `kind === "cursor"` means the cursor
|
|
63
|
+
* expired and the walk has to start again.
|
|
64
|
+
*/
|
|
65
|
+
// Async so that "no more pages" arrives as a rejection like every other failure here, rather
|
|
66
|
+
// than as a synchronous throw the caller's `.catch` would miss.
|
|
67
|
+
async next(): Promise<QueryResult<TEntity>> {
|
|
68
|
+
if (this.cursor === undefined) {
|
|
69
|
+
throw new NoMoreResultsError()
|
|
70
|
+
}
|
|
71
|
+
return this.fetchPage(this.cursor)
|
|
69
72
|
}
|
|
70
73
|
}
|
|
@@ -1,120 +1,70 @@
|
|
|
1
|
-
import { describe, expect,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import { toRpcSelect } from "./selection"
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const EVERY_FIELD = {
|
|
5
5
|
key: true,
|
|
6
|
-
attributes: true,
|
|
7
|
-
payload: true,
|
|
8
|
-
contentType: true,
|
|
9
|
-
expiration: true,
|
|
10
6
|
owner: true,
|
|
11
7
|
creator: true,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
attributes: false,
|
|
21
|
-
payload: false,
|
|
22
|
-
contentType: false,
|
|
23
|
-
expiration: false,
|
|
24
|
-
owner: false,
|
|
25
|
-
creator: false,
|
|
26
|
-
createdAtBlock: false,
|
|
27
|
-
lastModifiedAtBlock: false,
|
|
28
|
-
transactionIndexInBlock: false,
|
|
29
|
-
operationIndexInTransaction: false,
|
|
8
|
+
createdAt: true,
|
|
9
|
+
updatedAt: true,
|
|
10
|
+
expiresAt: true,
|
|
11
|
+
creationFlags: true,
|
|
12
|
+
contentType: true,
|
|
13
|
+
payload: true,
|
|
14
|
+
attributeSchema: true,
|
|
15
|
+
attributes: true,
|
|
30
16
|
}
|
|
31
17
|
|
|
32
|
-
describe("
|
|
33
|
-
|
|
34
|
-
expect(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
test('select("*") selects everything', () => {
|
|
38
|
-
expect(selectionToIncludeData("*")).toEqual(ALL)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test("empty object throws", () => {
|
|
42
|
-
// @ts-expect-error – select requires at least one field
|
|
43
|
-
expect(() => selectionToIncludeData({})).toThrow()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test("an all-false selection throws (no field is actually selected)", () => {
|
|
47
|
-
expect(() => selectionToIncludeData({ key: false })).toThrow()
|
|
48
|
-
expect(() => selectionToIncludeData({ owner: false, payload: false })).toThrow()
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test("key: true selects only the key", () => {
|
|
52
|
-
expect(selectionToIncludeData({ key: true })).toEqual({ ...EMPTY, key: true })
|
|
18
|
+
describe("toRpcSelect", () => {
|
|
19
|
+
it("selects everything for no argument or a star", () => {
|
|
20
|
+
expect(toRpcSelect()).toEqual(EVERY_FIELD)
|
|
21
|
+
expect(toRpcSelect("*")).toEqual(EVERY_FIELD)
|
|
53
22
|
})
|
|
54
23
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
expect(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
test("attributes only (no key)", () => {
|
|
62
|
-
expect(selectionToIncludeData({ attributes: true })).toEqual({
|
|
63
|
-
...EMPTY,
|
|
64
|
-
attributes: true,
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test("payload only (no key)", () => {
|
|
69
|
-
expect(selectionToIncludeData({ payload: true })).toEqual({
|
|
70
|
-
...EMPTY,
|
|
71
|
-
payload: true,
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test("a single metadata field, selected flat (no key)", () => {
|
|
76
|
-
expect(selectionToIncludeData({ owner: true })).toEqual({
|
|
77
|
-
...EMPTY,
|
|
24
|
+
it("sends every field, so what was asked for is legible in the request", () => {
|
|
25
|
+
// The node defaults everything but `key` to off. Spelling out the false ones costs nothing and
|
|
26
|
+
// pins the answer even if those defaults move.
|
|
27
|
+
expect(toRpcSelect({ owner: true })).toEqual({
|
|
28
|
+
key: false,
|
|
78
29
|
owner: true,
|
|
30
|
+
creator: false,
|
|
31
|
+
createdAt: false,
|
|
32
|
+
updatedAt: false,
|
|
33
|
+
expiresAt: false,
|
|
34
|
+
creationFlags: false,
|
|
35
|
+
contentType: false,
|
|
36
|
+
payload: false,
|
|
37
|
+
attributeSchema: false,
|
|
38
|
+
attributes: false,
|
|
79
39
|
})
|
|
80
40
|
})
|
|
81
41
|
|
|
82
|
-
|
|
83
|
-
expect(
|
|
84
|
-
|
|
85
|
-
|
|
42
|
+
it("passes a named attribute subset through", () => {
|
|
43
|
+
expect(
|
|
44
|
+
toRpcSelect({ key: true, attributes: { projectId: true, version: true } }),
|
|
45
|
+
).toMatchObject({
|
|
46
|
+
key: true,
|
|
47
|
+
attributes: { projectId: true, version: true },
|
|
86
48
|
})
|
|
87
49
|
})
|
|
88
50
|
|
|
89
|
-
|
|
51
|
+
it("drops names the subset turned off, and falls back to false when none are left", () => {
|
|
90
52
|
expect(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
attributes: true,
|
|
96
|
-
}),
|
|
97
|
-
).toEqual({
|
|
98
|
-
...EMPTY,
|
|
99
|
-
owner: true,
|
|
100
|
-
creator: true,
|
|
101
|
-
createdAtBlock: true,
|
|
102
|
-
attributes: true,
|
|
103
|
-
})
|
|
53
|
+
toRpcSelect({ key: true, attributes: { projectId: true, version: false } }),
|
|
54
|
+
).toMatchObject({ attributes: { projectId: true } })
|
|
55
|
+
// An empty subset asks for no attributes, which is what `false` already means.
|
|
56
|
+
expect(toRpcSelect({ key: true, attributes: {} })).toMatchObject({ attributes: false })
|
|
104
57
|
})
|
|
105
58
|
|
|
106
|
-
|
|
107
|
-
expect
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
})
|
|
59
|
+
it("rejects a selection that asks for nothing", () => {
|
|
60
|
+
// @ts-expect-error - AtLeastOne rejects this at compile time too
|
|
61
|
+
expect(() => toRpcSelect({})).toThrow(/at least one field/)
|
|
62
|
+
// The type system permits an explicit `false`; the node would answer it with empty rows.
|
|
63
|
+
expect(() => toRpcSelect({ key: false })).toThrow(/at least one field/)
|
|
64
|
+
expect(() => toRpcSelect({ key: false, attributes: {} })).toThrow(/at least one field/)
|
|
112
65
|
})
|
|
113
66
|
|
|
114
|
-
|
|
115
|
-
expect(
|
|
116
|
-
...EMPTY,
|
|
117
|
-
owner: true,
|
|
118
|
-
})
|
|
67
|
+
it("counts a named attribute subset as a selection in its own right", () => {
|
|
68
|
+
expect(() => toRpcSelect({ attributes: { projectId: true } })).not.toThrow()
|
|
119
69
|
})
|
|
120
70
|
})
|
package/src/query/selection.ts
CHANGED
|
@@ -1,25 +1,42 @@
|
|
|
1
|
+
import { validateAttributeName } from "../attr"
|
|
1
2
|
import type { Entity } from "../types/entity"
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
/** The entity fields that can be selected. These match the fields of {@link Entity} one-to-one. */
|
|
5
|
-
type DataKey =
|
|
6
|
-
| "key"
|
|
7
|
-
| "contentType"
|
|
8
|
-
| "owner"
|
|
9
|
-
| "creator"
|
|
10
|
-
| "expiresAtBlock"
|
|
11
|
-
| "createdAtBlock"
|
|
12
|
-
| "lastModifiedAtBlock"
|
|
13
|
-
| "transactionIndexInBlock"
|
|
14
|
-
| "operationIndexInTransaction"
|
|
15
|
-
| "attributes"
|
|
16
|
-
| "payload"
|
|
3
|
+
import type { RpcSelect } from "../types/rpcSchema"
|
|
17
4
|
|
|
18
5
|
/**
|
|
19
|
-
* The
|
|
20
|
-
*
|
|
6
|
+
* The entity fields a query can select. Each maps one-to-one onto a field of {@link Entity}.
|
|
7
|
+
*
|
|
8
|
+
* `attributes` takes a third form beyond `true`/`false`: a map of names, to fetch only those.
|
|
21
9
|
*/
|
|
22
|
-
export type SelectionFields = {
|
|
10
|
+
export type SelectionFields = {
|
|
11
|
+
key?: boolean
|
|
12
|
+
owner?: boolean
|
|
13
|
+
creator?: boolean
|
|
14
|
+
createdAt?: boolean
|
|
15
|
+
updatedAt?: boolean
|
|
16
|
+
expiresAt?: boolean
|
|
17
|
+
creationFlags?: boolean
|
|
18
|
+
contentType?: boolean
|
|
19
|
+
payload?: boolean
|
|
20
|
+
/** Attribute names and types, without their values. */
|
|
21
|
+
attributeSchema?: boolean
|
|
22
|
+
/** `true` for every attribute, or a map of names for a subset. */
|
|
23
|
+
attributes?: boolean | Readonly<Record<string, boolean>>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The selection keys, in the order they are written into the RPC `select` object. */
|
|
27
|
+
const SELECTION_KEYS = [
|
|
28
|
+
"key",
|
|
29
|
+
"owner",
|
|
30
|
+
"creator",
|
|
31
|
+
"createdAt",
|
|
32
|
+
"updatedAt",
|
|
33
|
+
"expiresAt",
|
|
34
|
+
"creationFlags",
|
|
35
|
+
"contentType",
|
|
36
|
+
"payload",
|
|
37
|
+
"attributeSchema",
|
|
38
|
+
"attributes",
|
|
39
|
+
] as const satisfies readonly (keyof SelectionFields)[]
|
|
23
40
|
|
|
24
41
|
/**
|
|
25
42
|
* Requires at least one property of `T` to be present.
|
|
@@ -38,79 +55,74 @@ export type EntitySelection = AtLeastOne<SelectionFields>
|
|
|
38
55
|
/** Argument accepted by `select()` — a non-empty {@link EntitySelection}, or `"*"` for everything. */
|
|
39
56
|
export type SelectArg = EntitySelection | "*"
|
|
40
57
|
|
|
58
|
+
/** Whether a selection value asks for the field: `true`, or a non-empty map of attribute names. */
|
|
59
|
+
type Selected<V> = V extends false | undefined ? false : true
|
|
60
|
+
|
|
41
61
|
/**
|
|
42
62
|
* The result of a selection `S`: an {@link Entity} narrowed to exactly the selected fields (each
|
|
43
|
-
* non-`undefined`). The shape is flat
|
|
44
|
-
*
|
|
45
|
-
* error.
|
|
63
|
+
* non-`undefined`). The shape is flat — `select({ owner: true })` yields `{ owner: Hex }`, read as
|
|
64
|
+
* `entity.owner`. Accessing an unselected field is a compile error.
|
|
46
65
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
66
|
+
* `toText()` / `toJson()` operate on the payload, so they appear only when `payload` was selected —
|
|
67
|
+
* calling them otherwise would always throw.
|
|
49
68
|
*/
|
|
50
69
|
export type ProjectedEntity<S> = {
|
|
51
|
-
readonly [K in keyof S as S[K] extends true ? K : never]: K extends keyof Entity
|
|
70
|
+
readonly [K in keyof S as Selected<S[K]> extends true ? K : never]: K extends keyof Entity
|
|
52
71
|
? NonNullable<Entity[K]>
|
|
53
72
|
: never
|
|
54
73
|
} & (S extends { payload: true } ? Pick<Entity, "toText" | "toJson"> : unknown)
|
|
55
74
|
|
|
56
75
|
/** A selection of every field, used to type the result of `select()` / `select("*")`. */
|
|
57
|
-
type AllSelected = { [K in
|
|
76
|
+
type AllSelected = { [K in (typeof SELECTION_KEYS)[number]]: true }
|
|
58
77
|
|
|
59
78
|
/** The projected entity type when everything is selected — every field, plus the methods. */
|
|
60
79
|
export type FullEntity = ProjectedEntity<AllSelected>
|
|
61
80
|
|
|
62
81
|
/**
|
|
63
|
-
* Converts a {@link SelectArg} into the
|
|
64
|
-
*
|
|
82
|
+
* Converts a {@link SelectArg} into the RPC `select` object.
|
|
83
|
+
*
|
|
84
|
+
* Every field is opt-in — anything not explicitly selected is sent as `false`, including the key.
|
|
85
|
+
* Sending the full object rather than omitting the `false`s keeps what the SDK asked for legible in
|
|
86
|
+
* a request log, and pins the answer even if a node's defaults change.
|
|
87
|
+
*
|
|
65
88
|
* `undefined` or `"*"` selects everything.
|
|
66
89
|
*
|
|
67
|
-
* @
|
|
68
|
-
* @returns The fully-resolved include-data object for the RPC query
|
|
69
|
-
* @throws If an empty selection object is provided
|
|
90
|
+
* @throws If the selection asks for nothing.
|
|
70
91
|
*
|
|
71
92
|
* @example
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* selectionToIncludeData({ owner: true }) // only the owner
|
|
93
|
+
* toRpcSelect() // everything
|
|
94
|
+
* toRpcSelect({ key: true }) // only the key
|
|
95
|
+
* toRpcSelect({ attributes: { projectId: true } }) // one named attribute
|
|
76
96
|
*/
|
|
77
|
-
export function
|
|
97
|
+
export function toRpcSelect(selection?: SelectArg): RpcSelect {
|
|
78
98
|
if (selection === undefined || selection === "*") {
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
return Object.fromEntries(SELECTION_KEYS.map((field) => [field, true])) as RpcSelect
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const select: RpcSelect = {}
|
|
103
|
+
let any = false
|
|
104
|
+
for (const field of SELECTION_KEYS) {
|
|
105
|
+
const value = selection[field]
|
|
106
|
+
if (field === "attributes" && typeof value === "object" && value !== null) {
|
|
107
|
+
// A named subset. An empty map asks for no attributes at all, which is what `false` means.
|
|
108
|
+
const names = Object.entries(value).filter(([, wanted]) => wanted)
|
|
109
|
+
// Checked rather than forwarded: a name outside the grammar cannot exist on any entity, so
|
|
110
|
+
// asking for one is a typo that would otherwise come back as a silently absent attribute.
|
|
111
|
+
for (const [name] of names) validateAttributeName(name)
|
|
112
|
+
select.attributes = names.length > 0 ? Object.fromEntries(names) : false
|
|
113
|
+
any ||= names.length > 0
|
|
114
|
+
continue
|
|
91
115
|
}
|
|
116
|
+
select[field] = value === true
|
|
117
|
+
any ||= value === true
|
|
92
118
|
}
|
|
93
119
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
if (!
|
|
120
|
+
// Rejects both `{}` and a selection whose fields are all explicitly `false` — the type system
|
|
121
|
+
// permits `select({ key: false })`, and the node would answer it with a page of empty objects.
|
|
122
|
+
if (!any) {
|
|
97
123
|
throw new Error(
|
|
98
124
|
'select() requires at least one field to be selected. Pass select() or select("*") to select everything.',
|
|
99
125
|
)
|
|
100
126
|
}
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
key: selection.key ?? false,
|
|
104
|
-
attributes: selection.attributes ?? false,
|
|
105
|
-
payload: selection.payload ?? false,
|
|
106
|
-
contentType: selection.contentType ?? false,
|
|
107
|
-
// The selection uses the entity field name `expiresAtBlock`; the RPC calls it `expiration`.
|
|
108
|
-
expiration: selection.expiresAtBlock ?? false,
|
|
109
|
-
owner: selection.owner ?? false,
|
|
110
|
-
creator: selection.creator ?? false,
|
|
111
|
-
createdAtBlock: selection.createdAtBlock ?? false,
|
|
112
|
-
lastModifiedAtBlock: selection.lastModifiedAtBlock ?? false,
|
|
113
|
-
transactionIndexInBlock: selection.transactionIndexInBlock ?? false,
|
|
114
|
-
operationIndexInTransaction: selection.operationIndexInTransaction ?? false,
|
|
115
|
-
}
|
|
127
|
+
return select
|
|
116
128
|
}
|