@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.
Files changed (210) hide show
  1. package/README.md +29 -16
  2. package/dist/attr/index.cjs +34 -0
  3. package/dist/attr/index.d.cts +2 -0
  4. package/dist/attr/index.d.ts +2 -0
  5. package/dist/attr/index.js +3 -0
  6. package/dist/attr-BDZ-APg5.js +809 -0
  7. package/dist/attr-BDZ-APg5.js.map +1 -0
  8. package/dist/attr-BvxHN99W.cjs +1029 -0
  9. package/dist/chains/index.cjs +2 -3
  10. package/dist/chains/index.d.cts +39 -77
  11. package/dist/chains/index.d.cts.map +1 -1
  12. package/dist/chains/index.d.ts +22 -60
  13. package/dist/chains/index.d.ts.map +1 -1
  14. package/dist/chains/index.js +2 -2
  15. package/dist/chains-BJrZs6sG.js +57 -0
  16. package/dist/chains-BJrZs6sG.js.map +1 -0
  17. package/dist/chains-BYRTTipa.cjs +73 -0
  18. package/dist/changeOwnership-B7bEgla4.d.cts +509 -0
  19. package/dist/changeOwnership-B7bEgla4.d.cts.map +1 -0
  20. package/dist/changeOwnership-DwgtdSFc.d.ts +509 -0
  21. package/dist/changeOwnership-DwgtdSFc.d.ts.map +1 -0
  22. package/dist/entity/index.cjs +19 -0
  23. package/dist/entity/index.d.cts +4 -0
  24. package/dist/entity/index.d.ts +4 -0
  25. package/dist/entity/index.js +6 -0
  26. package/dist/entity-B0t6sN7F.cjs +71 -0
  27. package/dist/entity-BIp_df9u.js +67 -0
  28. package/dist/entity-BIp_df9u.js.map +1 -0
  29. package/dist/entity-CFhbAv0R.js +70 -0
  30. package/dist/entity-CFhbAv0R.js.map +1 -0
  31. package/dist/entity-wfBnZd91.cjs +98 -0
  32. package/dist/errors-D-eJkGVc.cjs +56 -0
  33. package/dist/errors-DGB8AgT2.js +33 -0
  34. package/dist/errors-DGB8AgT2.js.map +1 -0
  35. package/dist/expiry-BUYIXTGb.cjs +135 -0
  36. package/dist/expiry-BxOlvVWC.d.ts +79 -0
  37. package/dist/expiry-BxOlvVWC.d.ts.map +1 -0
  38. package/dist/expiry-DGRzrSan.js +95 -0
  39. package/dist/expiry-DGRzrSan.js.map +1 -0
  40. package/dist/expiry-DRV29F1r.d.cts +79 -0
  41. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  42. package/dist/flags-0Vl5rrHq.d.cts +41 -0
  43. package/dist/flags-0Vl5rrHq.d.cts.map +1 -0
  44. package/dist/flags-Ct8k0gpB.cjs +43 -0
  45. package/dist/flags-ZtlvOIIk.js +33 -0
  46. package/dist/flags-ZtlvOIIk.js.map +1 -0
  47. package/dist/flags-wmcMoAnN.d.ts +41 -0
  48. package/dist/flags-wmcMoAnN.d.ts.map +1 -0
  49. package/dist/index-Cgw1u5aY.d.cts +278 -0
  50. package/dist/index-Cgw1u5aY.d.cts.map +1 -0
  51. package/dist/index-ClgIuxJ8.d.ts +159 -0
  52. package/dist/index-ClgIuxJ8.d.ts.map +1 -0
  53. package/dist/index-D0CmtiWT.d.cts +159 -0
  54. package/dist/index-D0CmtiWT.d.cts.map +1 -0
  55. package/dist/index-D8LZt-3J.d.cts +417 -0
  56. package/dist/index-D8LZt-3J.d.cts.map +1 -0
  57. package/dist/index-DVYFaXNV.d.ts +417 -0
  58. package/dist/index-DVYFaXNV.d.ts.map +1 -0
  59. package/dist/index-VCqgn29D.d.ts +278 -0
  60. package/dist/index-VCqgn29D.d.ts.map +1 -0
  61. package/dist/index.cjs +675 -357
  62. package/dist/index.d.cts +251 -203
  63. package/dist/index.d.cts.map +1 -1
  64. package/dist/index.d.ts +251 -203
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +626 -355
  67. package/dist/index.js.map +1 -1
  68. package/dist/payload-Bnw1hrvb.cjs +83 -0
  69. package/dist/payload-CIxSn_DW.js +67 -0
  70. package/dist/payload-CIxSn_DW.js.map +1 -0
  71. package/dist/payload-Dgx3PQeK.d.ts +94 -0
  72. package/dist/payload-Dgx3PQeK.d.ts.map +1 -0
  73. package/dist/payload-P0nWyjLg.d.cts +94 -0
  74. package/dist/payload-P0nWyjLg.d.cts.map +1 -0
  75. package/dist/query/index.cjs +24 -149
  76. package/dist/query/index.d.cts +5 -3
  77. package/dist/query/index.d.ts +5 -3
  78. package/dist/query/index.js +6 -137
  79. package/dist/queryBuilder-BXDvX1K_.cjs +1123 -0
  80. package/dist/queryBuilder-CbLL4pG5.js +963 -0
  81. package/dist/queryBuilder-CbLL4pG5.js.map +1 -0
  82. package/dist/selection-BvK6yX0g.d.cts +433 -0
  83. package/dist/selection-BvK6yX0g.d.cts.map +1 -0
  84. package/dist/selection-fpmI3ADQ.d.ts +433 -0
  85. package/dist/selection-fpmI3ADQ.d.ts.map +1 -0
  86. package/dist/types/index.cjs +4 -1
  87. package/dist/types/index.d.cts +7 -2
  88. package/dist/types/index.d.ts +7 -2
  89. package/dist/types/index.js +4 -1
  90. package/dist/utils/index.cjs +4 -17
  91. package/dist/utils/index.d.cts +3 -17
  92. package/dist/utils/index.d.ts +3 -17
  93. package/dist/utils/index.js +4 -18
  94. package/package.json +26 -5
  95. package/src/actions/public/getEntity.ts +27 -43
  96. package/src/actions/public/getEntityNonce.ts +30 -0
  97. package/src/actions/public/predictEntityKeys.test.ts +143 -0
  98. package/src/actions/public/predictEntityKeys.ts +138 -0
  99. package/src/actions/public/query.ts +33 -69
  100. package/src/actions/public/watchEntityEvents.test.ts +414 -0
  101. package/src/actions/public/watchEntityEvents.ts +225 -0
  102. package/src/actions/wallet/changeOwnership.ts +8 -8
  103. package/src/actions/wallet/createEntity.ts +104 -26
  104. package/src/actions/wallet/deleteEntity.ts +2 -3
  105. package/src/actions/wallet/extendEntity.ts +39 -16
  106. package/src/actions/wallet/mutateEntities.test.ts +76 -0
  107. package/src/actions/wallet/mutateEntities.ts +29 -74
  108. package/src/actions/wallet/patchEntity.ts +90 -0
  109. package/src/attr/attributes.test.ts +120 -0
  110. package/src/attr/attributes.ts +190 -0
  111. package/src/attr/codec.test.ts +162 -0
  112. package/src/attr/codec.ts +220 -0
  113. package/src/attr/decimal.ts +86 -0
  114. package/src/attr/errors.ts +145 -0
  115. package/src/attr/index.ts +50 -0
  116. package/src/attr/names.ts +106 -0
  117. package/src/attr/types.ts +150 -0
  118. package/src/attr/values.test.ts +271 -0
  119. package/src/attr/values.ts +502 -0
  120. package/src/chains/cheesecake.ts +22 -0
  121. package/src/chains/index.ts +1 -2
  122. package/src/clients/createPublicClient.test.ts +3 -5
  123. package/src/clients/createPublicClient.ts +5 -4
  124. package/src/clients/createWalletClient.test.ts +2 -2
  125. package/src/clients/createWalletClient.ts +13 -7
  126. package/src/clients/decorators/arkivPublic.ts +160 -139
  127. package/src/clients/decorators/arkivWallet.ts +91 -95
  128. package/src/consts.ts +2 -1
  129. package/src/entity/create.test.ts +184 -0
  130. package/src/entity/errors.ts +38 -0
  131. package/src/entity/events.ts +16 -0
  132. package/src/entity/expiry.test.ts +195 -0
  133. package/src/entity/expiry.ts +181 -0
  134. package/src/entity/flags.ts +65 -0
  135. package/src/entity/index.ts +14 -0
  136. package/src/entity/key.ts +68 -0
  137. package/src/entity/operations.ts +134 -0
  138. package/src/entity/params.ts +15 -0
  139. package/src/errors.ts +33 -28
  140. package/src/index.ts +6 -0
  141. package/src/query/engine.ts +59 -134
  142. package/src/query/errors.ts +151 -0
  143. package/src/query/expression.test.ts +286 -0
  144. package/src/query/expression.ts +493 -0
  145. package/src/query/index.ts +40 -4
  146. package/src/query/injection.test.ts +151 -0
  147. package/src/query/queryBuilder.test.ts +322 -637
  148. package/src/query/queryBuilder.ts +126 -359
  149. package/src/query/queryResult.ts +54 -51
  150. package/src/query/selection.test.ts +48 -98
  151. package/src/query/selection.ts +77 -65
  152. package/src/types/entity.test.ts +44 -155
  153. package/src/types/entity.ts +62 -44
  154. package/src/types/events.ts +64 -23
  155. package/src/types/index.ts +20 -16
  156. package/src/types/rpcSchema.ts +70 -33
  157. package/src/utils/arkivTransactions.test.ts +681 -38
  158. package/src/utils/arkivTransactions.ts +249 -104
  159. package/src/utils/chains.test.ts +21 -24
  160. package/src/utils/compression.ts +1 -1
  161. package/src/utils/entities.test.ts +62 -0
  162. package/src/utils/entities.ts +111 -29
  163. package/src/utils/expirationTime.ts +114 -10
  164. package/src/utils/logger.ts +2 -2
  165. package/src/utils/payload.ts +4 -2
  166. package/src/utils/revert.test.ts +99 -0
  167. package/src/utils/revert.ts +145 -0
  168. package/dist/chains-BifaY04V.cjs +0 -108
  169. package/dist/chains-CrtkVbBh.js +0 -86
  170. package/dist/chains-CrtkVbBh.js.map +0 -1
  171. package/dist/consts-AUoL-v42.cjs +0 -18
  172. package/dist/consts-BV4PR-de.js +0 -7
  173. package/dist/consts-BV4PR-de.js.map +0 -1
  174. package/dist/entity-B6g_EPm1.d.cts +0 -382
  175. package/dist/entity-B6g_EPm1.d.cts.map +0 -1
  176. package/dist/entity-qoLMAuuk.d.ts +0 -382
  177. package/dist/entity-qoLMAuuk.d.ts.map +0 -1
  178. package/dist/index-DdspxNAA.d.ts +0 -481
  179. package/dist/index-DdspxNAA.d.ts.map +0 -1
  180. package/dist/index-Ds9dwqb5.d.cts +0 -481
  181. package/dist/index-Ds9dwqb5.d.cts.map +0 -1
  182. package/dist/payload-B2kFJyah.d.ts +0 -6
  183. package/dist/payload-B2kFJyah.d.ts.map +0 -1
  184. package/dist/payload-CJN8fTvX.d.cts +0 -6
  185. package/dist/payload-CJN8fTvX.d.cts.map +0 -1
  186. package/dist/payload-Claf2jUO.js +0 -13
  187. package/dist/payload-Claf2jUO.js.map +0 -1
  188. package/dist/payload-DpA5PA-w.cjs +0 -23
  189. package/dist/query/index.js.map +0 -1
  190. package/dist/queryBuilder-D2uwP9jI.js +0 -704
  191. package/dist/queryBuilder-D2uwP9jI.js.map +0 -1
  192. package/dist/queryBuilder-UW2MCaX4.cjs +0 -792
  193. package/dist/utils/index.d.cts.map +0 -1
  194. package/dist/utils/index.d.ts.map +0 -1
  195. package/dist/utils/index.js.map +0 -1
  196. package/src/actions/public/buildQuery.ts +0 -10
  197. package/src/actions/public/subscribeEntityEvents.ts +0 -119
  198. package/src/actions/wallet/updateEntity.ts +0 -60
  199. package/src/chains/braga.ts +0 -26
  200. package/src/chains/kaolin.ts +0 -29
  201. package/src/query/engine.test.ts +0 -526
  202. package/src/query/predicate.test.ts +0 -461
  203. package/src/query/predicate.ts +0 -144
  204. package/src/query/queryResult.test.ts +0 -270
  205. package/src/query/selectQueryBuilder.test.ts +0 -210
  206. package/src/types/attributes.ts +0 -16
  207. package/src/utils/validation.test.ts +0 -60
  208. package/src/utils/validation.ts +0 -32
  209. /package/dist/{types-H-TSmRpm.js → types-Bx1EKe0v.js} +0 -0
  210. /package/dist/{types-o3zWarRp.cjs → types-CKPddlfS.cjs} +0 -0
@@ -1,70 +1,73 @@
1
- import { NoCursorOrLimitError, NoMoreResultsError } from "../errors"
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
- const logger = getLogger("query:result")
4
+ /** Fetches the page a cursor points at. */
5
+ type FetchPage<TEntity> = (cursor: string) => Promise<QueryResult<TEntity>>
7
6
 
8
7
  /**
9
- * The result of a query. Holds the fetched entities and supports cursor-based pagination.
8
+ * One page of query results.
10
9
  *
11
- * @typeParam TEntity - The shape of each entity, inferred from the query builder
12
- * (a full {@link Entity}, or a projected object inferred from a `select()` selection).
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: TEntity[]
16
- private _endOfIteration: boolean
17
- private _cursor: string | undefined
18
- private _limit: number | undefined
19
- private _validAtBlock: bigint | undefined
20
- private _queryBuilder: BaseQueryBuilder<TEntity>
21
-
22
- // Public getters for internal state
23
- get queryBuilder(): BaseQueryBuilder<TEntity> {
24
- return this._queryBuilder
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
- get cursor(): string | undefined {
28
- return this._cursor
29
- }
34
+ private readonly fetchPage: FetchPage<TEntity>
30
35
 
31
36
  constructor(
32
- entities: TEntity[],
33
- queryBuilder: BaseQueryBuilder<TEntity>,
37
+ entities: readonly TEntity[],
38
+ blockNumber: bigint,
34
39
  cursor: string | undefined,
35
- limit: number | undefined,
36
- validAtBlock: bigint | undefined,
40
+ fetchPage: FetchPage<TEntity>,
37
41
  ) {
38
42
  this.entities = entities
39
- this._queryBuilder = queryBuilder
40
- this._endOfIteration = !limit || entities.length < limit
41
- this._cursor = cursor
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
- async next() {
47
- if (this._cursor === undefined || this._limit === undefined) {
48
- throw new NoCursorOrLimitError()
49
- }
50
- if (this._endOfIteration) {
51
- throw new NoMoreResultsError()
52
- }
53
- this._queryBuilder.cursor(this._cursor)
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
- hasNextPage() {
68
- return !this._endOfIteration
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, test } from "bun:test"
2
- import { selectionToIncludeData } from "./selection"
1
+ import { describe, expect, it } from "bun:test"
2
+ import { toRpcSelect } from "./selection"
3
3
 
4
- const ALL = {
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
- createdAtBlock: true,
13
- lastModifiedAtBlock: true,
14
- transactionIndexInBlock: true,
15
- operationIndexInTransaction: true,
16
- }
17
-
18
- const EMPTY = {
19
- key: false,
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("selectionToIncludeData", () => {
33
- test("select() with no argument selects everything", () => {
34
- expect(selectionToIncludeData()).toEqual(ALL)
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
- test("key is opt-in like every other field", () => {
56
- expect(selectionToIncludeData({ key: true }).key).toBe(true)
57
- expect(selectionToIncludeData({ attributes: true }).key).toBe(false)
58
- expect(selectionToIncludeData({ owner: true }).key).toBe(false)
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
- test("expiresAtBlock maps to the RPC `expiration` field", () => {
83
- expect(selectionToIncludeData({ expiresAtBlock: true })).toEqual({
84
- ...EMPTY,
85
- expiration: true,
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
- test("several fields selected together", () => {
51
+ it("drops names the subset turned off, and falls back to false when none are left", () => {
90
52
  expect(
91
- selectionToIncludeData({
92
- owner: true,
93
- creator: true,
94
- createdAtBlock: true,
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
- test("key can be combined with other fields", () => {
107
- expect(selectionToIncludeData({ key: true, attributes: true })).toEqual({
108
- ...EMPTY,
109
- key: true,
110
- attributes: true,
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
- test("a field set to false is not selected", () => {
115
- expect(selectionToIncludeData({ owner: true, creator: false })).toEqual({
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
  })
@@ -1,25 +1,42 @@
1
+ import { validateAttributeName } from "../attr"
1
2
  import type { Entity } from "../types/entity"
2
- import type { RpcIncludeData } from "../types/rpcSchema"
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 parts of an entity that can be selected. Every field is opt-in (including the `key`) and maps
20
- * directly to a field of the returned {@link Entity} — the selection is flat.
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 = { [K in DataKey]?: boolean }
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 and backwards compatible — e.g. `select({ owner: true })`
44
- * yields `{ owner: Hex }` accessed as `entity.owner`. Accessing an unselected field is a compile
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
- * The `toText()` / `toJson()` helpers operate on the payload, so they are only present when
48
- * `payload` was selected — calling them otherwise would always throw.
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 DataKey]: true }
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 {@link RpcIncludeData} understood by the query engine.
64
- * Every field is opt-in — anything not explicitly selected defaults to `false`, including the key.
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
- * @param selection - The selection to convert
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
- * selectionToIncludeData() // everything
73
- * selectionToIncludeData("*") // everything
74
- * selectionToIncludeData({ key: true }) // only the key
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 selectionToIncludeData(selection?: SelectArg): RpcIncludeData {
97
+ export function toRpcSelect(selection?: SelectArg): RpcSelect {
78
98
  if (selection === undefined || selection === "*") {
79
- return {
80
- key: true,
81
- attributes: true,
82
- payload: true,
83
- contentType: true,
84
- expiration: true,
85
- owner: true,
86
- creator: true,
87
- createdAtBlock: true,
88
- lastModifiedAtBlock: true,
89
- transactionIndexInBlock: true,
90
- operationIndexInTransaction: true,
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
- // At least one field must be opted in. This rejects both `{}` and selections whose fields are
95
- // all explicitly `false` (e.g. `select({ key: false })`), which the type system permits.
96
- if (!Object.values(selection).some((selected) => selected === true)) {
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
  }