@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,685 +1,370 @@
1
- import { afterEach, beforeEach, describe, expect, jest, test } from "bun:test"
1
+ import { describe, expect, it, vi } from "bun:test"
2
+ import type { Hex } from "viem"
3
+ import { type Attributes, i32 } from "../attr"
2
4
  import type { ArkivClient } from "../clients/baseClient"
3
- import { Entity } from "../types/entity"
4
- import * as entitiesUtils from "../utils/entities"
5
- import * as engine from "./engine"
6
- import { and, eq, gt, gte, neq, or } from "./predicate"
7
- import { asc, QueryBuilder } from "./queryBuilder"
8
-
9
- describe("QueryBuilder", () => {
10
- let mockClient: ArkivClient
11
- let mockProcessQuery: any
12
- let mockEntityFromRpcResult: any
13
-
14
- beforeEach(() => {
15
- // Create a mock client
16
- mockClient = {
17
- request: jest.fn(),
18
- } as unknown as ArkivClient
19
-
20
- // Spy on the functions instead of mocking the module
21
- mockProcessQuery = jest.spyOn(engine, "processQuery")
22
- mockEntityFromRpcResult = jest.spyOn(entitiesUtils, "entityFromRpcResult")
5
+ import { NoMoreResultsError } from "../errors"
6
+ import type { RpcEntity } from "../types/rpcSchema"
7
+ import { MAX_LIMIT } from "./engine"
8
+ import { InvalidPredicateError, QueryError } from "./errors"
9
+ import { eq, gte, or } from "./expression"
10
+ import { SelectQueryBuilder } from "./queryBuilder"
11
+ import type { ProjectedEntity } from "./selection"
12
+
13
+ const ACCOUNT = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
14
+ const KEY = `0x${"ab".repeat(32)}` as const
15
+
16
+ type Page = { data: RpcEntity[]; blockNumber: string; cursor?: string }
17
+
18
+ /** A client whose `arkiv_query` serves the given pages in order. */
19
+ function makeClient(...pages: Page[]) {
20
+ const served = pages.length > 0 ? pages : [{ data: [], blockNumber: "0x1" }]
21
+ let index = 0
22
+ const request = vi.fn(async () => served[Math.min(index++, served.length - 1)])
23
+ return { client: { request } as unknown as ArkivClient, request }
24
+ }
25
+
26
+ /** The `[queryString, options]` of the nth `arkiv_query` call. */
27
+ function sent(request: ReturnType<typeof makeClient>["request"], call = 0) {
28
+ const params = request.mock.calls[call][0] as unknown as {
29
+ method: string
30
+ params: [string, Record<string, unknown>]
31
+ }
32
+ return { method: params.method, query: params.params[0], options: params.params[1] }
33
+ }
34
+
35
+ describe("the query it sends", () => {
36
+ it("ANDs everything across repeated where() calls", async () => {
37
+ const { client, request } = makeClient()
38
+ await new SelectQueryBuilder(client, { key: true })
39
+ .where(gte("level", i32(10)))
40
+ .where(eq("category", "docs"), or(eq("status", "open"), eq("status", "review")))
41
+ .fetch()
42
+
43
+ expect(sent(request).method).toBe("arkiv_query")
44
+ expect(sent(request).query).toBe(
45
+ "level >= i32(10) AND category = str('docs') AND " +
46
+ "(status = str('open') OR status = str('review'))",
47
+ )
23
48
  })
24
49
 
25
- afterEach(() => {
26
- // Restore all spies to prevent test interference
27
- jest.restoreAllMocks()
50
+ it("takes an array of expressions", async () => {
51
+ const { client, request } = makeClient()
52
+ await new SelectQueryBuilder(client, { key: true }).where([eq("a", 1), eq("b", 2)]).fetch()
53
+ expect(sent(request).query).toBe("a = i32(1) AND b = i32(2)")
28
54
  })
29
55
 
30
- test("creates QueryBuilder with empty predicates", () => {
31
- const builder = new QueryBuilder(mockClient)
32
- expect(builder).toBeDefined()
56
+ it("turns ownedBy and createdBy into typed system predicates", async () => {
57
+ const { client, request } = makeClient()
58
+ await new SelectQueryBuilder(client, { key: true })
59
+ .where(eq("category", "docs"))
60
+ .ownedBy(ACCOUNT)
61
+ .createdBy(ACCOUNT)
62
+ .fetch()
63
+
64
+ expect(sent(request).query).toBe(
65
+ `category = str('docs') AND $owner = addr(${ACCOUNT}) AND $creator = addr(${ACCOUNT})`,
66
+ )
33
67
  })
34
68
 
35
- describe("where() method", () => {
36
- test("adds single predicate", async () => {
37
- mockProcessQuery.mockResolvedValue({
38
- data: [],
39
- })
40
-
41
- const builder = new QueryBuilder(mockClient)
42
- await builder.where(eq("name", "test")).fetch()
43
-
44
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
45
- predicates: [eq("name", "test")],
46
- limit: undefined,
47
- offset: undefined,
48
- ownedBy: undefined,
49
- createdBy: undefined,
50
- orderBy: undefined,
51
- validBeforeBlock: undefined,
52
- withAttributes: undefined,
53
- withMetadata: undefined,
54
- withPayload: undefined,
55
- })
56
- })
57
-
58
- test("adds multiple predicates as array", async () => {
59
- mockProcessQuery.mockResolvedValue({
60
- data: [],
61
- })
62
-
63
- const builder = new QueryBuilder(mockClient)
64
- const predicates = [eq("name", "test"), gt("age", 18)]
65
- await builder.where(predicates).fetch()
66
-
67
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
68
- predicates: predicates,
69
- limit: undefined,
70
- offset: undefined,
71
- ownedBy: undefined,
72
- createdBy: undefined,
73
- orderBy: undefined,
74
- validBeforeBlock: undefined,
75
- withAttributes: undefined,
76
- withMetadata: undefined,
77
- withPayload: undefined,
78
- })
79
- })
80
-
81
- test("adds multiple predicates as varargs", async () => {
82
- mockProcessQuery.mockResolvedValue({
83
- data: [],
84
- })
85
-
86
- const builder = new QueryBuilder(mockClient)
87
- await builder.where(eq("name", "test"), gt("age", 18), neq("status", "inactive")).fetch()
88
-
89
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
90
- predicates: [eq("name", "test"), gt("age", 18), neq("status", "inactive")],
91
- limit: undefined,
92
- offset: undefined,
93
- ownedBy: undefined,
94
- createdBy: undefined,
95
- orderBy: undefined,
96
- validBeforeBlock: undefined,
97
- withAttributes: undefined,
98
- withMetadata: undefined,
99
- withPayload: undefined,
100
- })
101
- })
102
-
103
- test("chains multiple where() calls", async () => {
104
- mockProcessQuery.mockResolvedValue({
105
- data: [],
106
- })
69
+ it("replaces rather than ANDs a second ownedBy — two owners would match nothing", async () => {
70
+ const other = "0x1111111111111111111111111111111111111111"
71
+ const { client, request } = makeClient()
72
+ await new SelectQueryBuilder(client, { key: true }).ownedBy(ACCOUNT).ownedBy(other).fetch()
73
+ expect(sent(request).query).toBe(`$owner = addr(${other})`)
74
+ })
107
75
 
108
- const builder = new QueryBuilder(mockClient)
109
- await builder
110
- .where(eq("name", "test"))
111
- .where(gt("age", 18))
112
- .where([neq("status", "inactive"), eq("verified", 1)])
113
- .fetch()
76
+ it("is readable without running anything", () => {
77
+ const { client } = makeClient()
78
+ const builder = new SelectQueryBuilder(client, { key: true }).where(eq("a", 1))
79
+ expect(builder.toString()).toBe("a = i32(1)")
80
+ expect(`${builder}`).toBe("a = i32(1)")
81
+ })
114
82
 
115
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
116
- predicates: [
117
- eq("name", "test"),
118
- gt("age", 18),
119
- neq("status", "inactive"),
120
- eq("verified", 1),
121
- ],
122
- limit: undefined,
123
- offset: undefined,
124
- ownedBy: undefined,
125
- createdBy: undefined,
126
- orderBy: undefined,
127
- validBeforeBlock: undefined,
128
- withAttributes: undefined,
129
- withMetadata: undefined,
130
- withPayload: undefined,
131
- })
132
- })
83
+ it("refuses a query with no filter, which the language cannot spell", async () => {
84
+ const { client, request } = makeClient()
85
+ const builder = new SelectQueryBuilder(client, { key: true })
86
+ expect(() => builder.toString()).toThrow(InvalidPredicateError)
87
+ await expect(builder.fetch()).rejects.toThrow(/at least one filter/)
88
+ expect(request).not.toHaveBeenCalled()
89
+ })
90
+ })
133
91
 
134
- test("handles complex predicates with or/and", async () => {
135
- mockProcessQuery.mockResolvedValue({
136
- data: [],
137
- })
138
-
139
- const builder = new QueryBuilder(mockClient)
140
- const complexPredicate = or([eq("type", "admin"), eq("type", "moderator")])
141
- await builder.where(complexPredicate).fetch()
142
-
143
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
144
- predicates: [complexPredicate],
145
- limit: undefined,
146
- offset: undefined,
147
- ownedBy: undefined,
148
- createdBy: undefined,
149
- orderBy: undefined,
150
- validBeforeBlock: undefined,
151
- withAttributes: undefined,
152
- withMetadata: undefined,
153
- withPayload: undefined,
154
- })
155
- })
92
+ describe("the options it sends", () => {
93
+ it("sends the resolved selection, and nothing else by default", async () => {
94
+ const { client, request } = makeClient()
95
+ await new SelectQueryBuilder(client, { key: true, attributes: true }).where(eq("a", 1)).fetch()
156
96
 
157
- test("handles nested or/and predicates", async () => {
158
- mockProcessQuery.mockResolvedValue({
159
- data: [],
160
- })
161
-
162
- const builder = new QueryBuilder(mockClient)
163
- const nestedPredicate = and([
164
- eq("active", 1),
165
- or([eq("role", "admin"), eq("role", "moderator")]),
166
- ])
167
- await builder.where(nestedPredicate).fetch()
168
-
169
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
170
- predicates: [nestedPredicate],
171
- limit: undefined,
172
- offset: undefined,
173
- ownedBy: undefined,
174
- createdBy: undefined,
175
- orderBy: undefined,
176
- validBeforeBlock: undefined,
177
- withAttributes: undefined,
178
- withMetadata: undefined,
179
- withPayload: undefined,
180
- })
181
- })
97
+ const { options } = sent(request)
98
+ expect(options).toEqual({ select: expect.objectContaining({ key: true, attributes: true }) })
99
+ // No limit, cursor or atBlock unless asked for — the node picks its own page size.
100
+ expect(Object.keys(options).sort()).toEqual(["select"])
182
101
  })
183
102
 
184
- describe("builder methods chain correctly", () => {
185
- test("ownedBy() sets owner", async () => {
186
- mockProcessQuery.mockResolvedValue({
187
- data: [],
188
- })
189
-
190
- const builder = new QueryBuilder(mockClient)
191
- const owner = "0x1234567890123456789012345678901234567890" as const
192
- await builder.ownedBy(owner).fetch()
193
-
194
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
195
- predicates: [],
196
- limit: undefined,
197
- offset: undefined,
198
- ownedBy: owner,
199
- createdBy: undefined,
200
- orderBy: undefined,
201
- validBeforeBlock: undefined,
202
- withAttributes: undefined,
203
- withMetadata: undefined,
204
- withPayload: undefined,
205
- })
103
+ it("sends limit, cursor and atBlock as the RPC spells them", async () => {
104
+ const { client, request } = makeClient()
105
+ await new SelectQueryBuilder(client, { key: true })
106
+ .where(eq("a", 1))
107
+ .limit(100)
108
+ .cursor("b64:abc")
109
+ .atBlock(1_297_000n)
110
+ .fetch()
111
+
112
+ const { options } = sent(request)
113
+ expect(options).toMatchObject({
114
+ limit: "0x64",
115
+ cursor: "b64:abc",
116
+ atBlock: "0x13ca68",
206
117
  })
118
+ })
207
119
 
208
- test("createdBy() sets creator", async () => {
209
- mockProcessQuery.mockResolvedValue({
210
- data: [],
211
- })
212
-
213
- const builder = new QueryBuilder(mockClient)
214
- const creator = "0x1234567890123456789012345678901234567890" as const
215
- await builder.createdBy(creator).fetch()
216
-
217
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
218
- predicates: [],
219
- limit: undefined,
220
- offset: undefined,
221
- ownedBy: undefined,
222
- createdBy: creator,
223
- orderBy: undefined,
224
- validBeforeBlock: undefined,
225
- withAttributes: undefined,
226
- withMetadata: undefined,
227
- withPayload: undefined,
228
- })
229
- })
120
+ it("rejects a page size the node would reject", async () => {
121
+ const { client, request } = makeClient()
122
+ const query = () => new SelectQueryBuilder(client, { key: true }).where(eq("a", 1))
123
+ await expect(
124
+ query()
125
+ .limit(MAX_LIMIT + 1)
126
+ .fetch(),
127
+ ).rejects.toThrow(/exceeds the node maximum/)
128
+ await expect(query().limit(0).fetch()).rejects.toThrow(/positive integer/)
129
+ await expect(query().limit(1.5).fetch()).rejects.toThrow(/positive integer/)
130
+ expect(query().limit(MAX_LIMIT)).toBeDefined()
131
+ expect(request).not.toHaveBeenCalled()
132
+ })
133
+ })
230
134
 
231
- test("orderBy() sets orderBy ascending", async () => {
232
- mockProcessQuery.mockResolvedValue({
233
- data: [],
234
- })
235
-
236
- const builder = new QueryBuilder(mockClient)
237
- await builder.orderBy("name", "string", "asc").fetch()
238
-
239
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
240
- predicates: [],
241
- limit: undefined,
242
- offset: undefined,
243
- ownedBy: undefined,
244
- createdBy: undefined,
245
- orderBy: [{ name: "name", type: "string", desc: false }],
246
- validAtBlock: undefined,
247
- withAttributes: undefined,
248
- withMetadata: undefined,
249
- withPayload: undefined,
250
- })
135
+ describe("decoding", () => {
136
+ it("builds entities from exactly the fields the row carries", async () => {
137
+ const { client } = makeClient({
138
+ blockNumber: "0x8e1ff",
139
+ data: [
140
+ {
141
+ key: KEY,
142
+ owner: ACCOUNT,
143
+ expiresAt: "0x92e21",
144
+ creationFlags: { readonly: false, permissionlessExtension: true, raw: 2 },
145
+ contentType: "application/json",
146
+ payload: "0x7b7d",
147
+ attributeSchema: [{ name: "version", type: "i32" }],
148
+ attributes: [
149
+ { name: "projectId", type: "str", value: "alice-123" },
150
+ { name: "version", type: "i32", value: 4 },
151
+ { name: "balance", type: "u256", value: "0xf4240" },
152
+ { name: "score", type: "dec", value: "3.5" },
153
+ ],
154
+ },
155
+ ],
251
156
  })
252
157
 
253
- test("orderBy() sets orderBy descending", async () => {
254
- mockProcessQuery.mockResolvedValue({
255
- data: [],
256
- })
257
-
258
- const builder = new QueryBuilder(mockClient)
259
- await builder.orderBy("name", "string", "desc").fetch()
260
-
261
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
262
- predicates: [],
263
- limit: undefined,
264
- offset: undefined,
265
- ownedBy: undefined,
266
- createdBy: undefined,
267
- orderBy: [{ name: "name", type: "string", desc: true }],
268
- validAtBlock: undefined,
269
- withAttributes: undefined,
270
- withMetadata: undefined,
271
- withPayload: undefined,
272
- })
273
- })
158
+ const page = await new SelectQueryBuilder(client, "*").where(eq("a", 1)).fetch()
159
+ const entity = page.entities[0]
274
160
 
275
- test("orderBy() with helper function", async () => {
276
- mockProcessQuery.mockResolvedValue({
277
- data: [],
278
- })
279
-
280
- const builder = new QueryBuilder(mockClient)
281
- await builder.orderBy(asc("name", "string")).fetch()
282
-
283
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
284
- predicates: [],
285
- limit: undefined,
286
- offset: undefined,
287
- ownedBy: undefined,
288
- createdBy: undefined,
289
- orderBy: [{ name: "name", type: "string", desc: false }],
290
- validAtBlock: undefined,
291
- withAttributes: undefined,
292
- withMetadata: undefined,
293
- withPayload: undefined,
294
- })
161
+ expect(entity.key).toBe(KEY)
162
+ expect(entity.expiresAt).toBe(0x92e21n)
163
+ expect(entity.creationFlags).toEqual({
164
+ readonly: false,
165
+ permissionlessExtension: true,
166
+ raw: 2,
295
167
  })
296
- test("limit() sets limit", async () => {
297
- mockProcessQuery.mockResolvedValue({
298
- data: [],
299
- })
300
-
301
- const builder = new QueryBuilder(mockClient)
302
- await builder.limit(10).fetch()
303
-
304
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
305
- predicates: [],
306
- limit: 10,
307
- offset: undefined,
308
- ownedBy: undefined,
309
- createdBy: undefined,
310
- orderBy: undefined,
311
- validBeforeBlock: undefined,
312
- withAttributes: undefined,
313
- withMetadata: undefined,
314
- withPayload: undefined,
315
- })
316
- })
317
-
318
- test("cursor() sets cursor", async () => {
319
- mockProcessQuery.mockResolvedValue({
320
- data: [],
321
- })
322
-
323
- const builder = new QueryBuilder(mockClient)
324
- await builder.cursor("0xABC123").fetch()
325
-
326
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
327
- predicates: [],
328
- limit: undefined,
329
- cursor: "0xABC123",
330
- ownedBy: undefined,
331
- createdBy: undefined,
332
- orderBy: undefined,
333
- validAtBlock: undefined,
334
- withAttributes: undefined,
335
- withMetadata: undefined,
336
- withPayload: undefined,
337
- })
338
- })
339
-
340
- test("withAttributes() sets withAttributes to true by default", async () => {
341
- mockProcessQuery.mockResolvedValue({
342
- data: [],
343
- })
344
-
345
- const builder = new QueryBuilder(mockClient)
346
- await builder.withAttributes().fetch()
347
-
348
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
349
- predicates: [],
350
- limit: undefined,
351
- offset: undefined,
352
- ownedBy: undefined,
353
- createdBy: undefined,
354
- orderBy: undefined,
355
- validBeforeBlock: undefined,
356
- withAttributes: true,
357
- withMetadata: undefined,
358
- withPayload: undefined,
359
- })
168
+ expect(entity.attributes).toEqual({
169
+ projectId: { type: "str", value: "alice-123" },
170
+ version: { type: "i32", value: 4 },
171
+ balance: { type: "u256", value: 1_000_000n },
172
+ score: { type: "dec", value: "3.5" },
360
173
  })
174
+ expect(entity.attributeSchema).toEqual({ version: "i32" })
175
+ expect(entity.toJson()).toEqual({})
176
+ // Unselected stays undefined rather than becoming an empty value.
177
+ expect(entity.creator).toBeUndefined()
178
+ expect(entity.createdAt).toBeUndefined()
179
+ expect(page.blockNumber).toBe(0x8e1ffn)
180
+ })
181
+ })
361
182
 
362
- test("withAttributes(false) sets withAttributes to false", async () => {
363
- mockProcessQuery.mockResolvedValue({
364
- data: [],
365
- })
366
-
367
- const builder = new QueryBuilder(mockClient)
368
- await builder.withAttributes(false).fetch()
369
-
370
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
371
- predicates: [],
372
- limit: undefined,
373
- cursor: undefined,
374
- ownedBy: undefined,
375
- createdBy: undefined,
376
- orderBy: undefined,
377
- validBeforeBlock: undefined,
378
- withAttributes: false,
379
- withMetadata: undefined,
380
- withPayload: undefined,
381
- })
382
- })
183
+ describe("pagination", () => {
184
+ const page = (cursor: string | undefined, ...keys: string[]): Page => ({
185
+ blockNumber: "0x8e1ff",
186
+ data: keys.map((k) => ({ key: k as `0x${string}` })),
187
+ ...(cursor !== undefined && { cursor }),
188
+ })
383
189
 
384
- test("withMetadata() sets withMetadata to true by default", async () => {
385
- mockProcessQuery.mockResolvedValue({
386
- data: [],
387
- })
388
-
389
- const builder = new QueryBuilder(mockClient)
390
- await builder.withMetadata().fetch()
391
-
392
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
393
- predicates: [],
394
- limit: undefined,
395
- cursor: undefined,
396
- ownedBy: undefined,
397
- createdBy: undefined,
398
- orderBy: undefined,
399
- validAtBlock: undefined,
400
- withAttributes: undefined,
401
- withMetadata: true,
402
- withPayload: undefined,
403
- })
404
- })
190
+ it("follows the cursor and stops when the node omits it", async () => {
191
+ const { client, request } = makeClient(
192
+ page("b64:1", "0x1", "0x2"),
193
+ page("b64:2", "0x3"),
194
+ page(undefined, "0x4"),
195
+ )
196
+ const builder = new SelectQueryBuilder(client, { key: true }).where(eq("a", 1)).limit(2)
197
+
198
+ const first = await builder.fetch()
199
+ expect(first.entities).toHaveLength(2)
200
+ expect(first.hasNextPage()).toBe(true)
201
+
202
+ const second = await first.next()
203
+ expect(sent(request, 1).options).toMatchObject({ cursor: "b64:1" })
204
+
205
+ const third = await second.next()
206
+ expect(sent(request, 2).options).toMatchObject({ cursor: "b64:2" })
207
+ expect(third.hasNextPage()).toBe(false)
208
+ await expect(third.next()).rejects.toThrow(NoMoreResultsError)
209
+ })
405
210
 
406
- test("withMetadata(false) sets withMetadata to false", async () => {
407
- mockProcessQuery.mockResolvedValue({
408
- data: [],
409
- })
410
-
411
- const builder = new QueryBuilder(mockClient)
412
- await builder.withMetadata(false).fetch()
413
-
414
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
415
- predicates: [],
416
- limit: undefined,
417
- cursor: undefined,
418
- ownedBy: undefined,
419
- createdBy: undefined,
420
- orderBy: undefined,
421
- validAtBlock: undefined,
422
- withAttributes: undefined,
423
- withMetadata: false,
424
- withPayload: undefined,
425
- })
426
- })
211
+ it("leaves an already-read page readable", async () => {
212
+ const { client } = makeClient(page("b64:1", "0x1"), page(undefined, "0x2"))
213
+ const first = await new SelectQueryBuilder(client, { key: true }).where(eq("a", 1)).fetch()
214
+ await first.next()
215
+ // next() returns the following page rather than mutating this one.
216
+ expect(first.entities).toHaveLength(1)
217
+ expect(first.cursor).toBe("b64:1")
218
+ })
427
219
 
428
- test("withPayload() sets withPayload to true by default", async () => {
429
- mockProcessQuery.mockResolvedValue({
430
- data: [],
431
- })
432
-
433
- const builder = new QueryBuilder(mockClient)
434
- await builder.withPayload().fetch()
435
-
436
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
437
- predicates: [],
438
- limit: undefined,
439
- cursor: undefined,
440
- ownedBy: undefined,
441
- createdBy: undefined,
442
- orderBy: undefined,
443
- validAtBlock: undefined,
444
- withAttributes: undefined,
445
- withMetadata: undefined,
446
- withPayload: true,
447
- })
448
- })
220
+ it("reports a full last page correctly", async () => {
221
+ // The old heuristic was "fewer rows than the limit means the end", which mistakes a final page
222
+ // that happens to be exactly full for a middle one. The node omits the cursor instead.
223
+ const { client } = makeClient(page(undefined, "0x1", "0x2"))
224
+ const result = await new SelectQueryBuilder(client, { key: true })
225
+ .where(eq("a", 1))
226
+ .limit(2)
227
+ .fetch()
228
+ expect(result.entities).toHaveLength(2)
229
+ expect(result.hasNextPage()).toBe(false)
230
+ })
449
231
 
450
- test("withPayload(false) sets withPayload to false", async () => {
451
- mockProcessQuery.mockResolvedValue({
452
- data: [],
453
- })
454
-
455
- const builder = new QueryBuilder(mockClient)
456
- await builder.withPayload(false).fetch()
457
-
458
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
459
- predicates: [],
460
- limit: undefined,
461
- cursor: undefined,
462
- ownedBy: undefined,
463
- createdBy: undefined,
464
- validAtBlock: undefined,
465
- withAttributes: undefined,
466
- withMetadata: undefined,
467
- withPayload: false,
468
- })
469
- })
232
+ it("pages the query it started with, not whatever the builder says later", async () => {
233
+ const { client, request } = makeClient(page("b64:1", "0x1"), page(undefined, "0x2"))
234
+ const builder = new SelectQueryBuilder(client, { key: true }).where(eq("a", 1))
470
235
 
471
- test("all methods chain together correctly", async () => {
472
- mockProcessQuery.mockResolvedValue({
473
- data: [],
474
- })
475
-
476
- const builder = new QueryBuilder(mockClient)
477
- const owner = "0x1234567890123456789012345678901234567890" as const
478
- await builder
479
- .where(eq("name", "test"))
480
- .where(gte("age", 18))
481
- .ownedBy(owner)
482
- .limit(10)
483
- .cursor("0xABC123")
484
- .withAttributes(true)
485
- .withMetadata(true)
486
- .withPayload(true)
487
- .fetch()
236
+ const first = await builder.fetch()
237
+ // The builder stays mutable and reusable. Re-reading it per page would send page 2 under a
238
+ // different query than the cursor was issued for, which the node answers with a "cursor" error
239
+ // — or, worse, with rows from another result set.
240
+ builder.where(eq("b", 2)).limit(50).atBlock(9n)
241
+ await first.next()
488
242
 
489
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
490
- predicates: [eq("name", "test"), gte("age", 18)],
491
- limit: 10,
492
- cursor: "0xABC123",
493
- ownedBy: owner,
494
- createdBy: undefined,
495
- validAtBlock: undefined,
496
- withAttributes: true,
497
- withMetadata: true,
498
- withPayload: true,
499
- })
500
- })
243
+ expect(sent(request, 1).query).toBe("a = i32(1)")
244
+ expect(sent(request, 1).options).not.toHaveProperty("limit")
245
+ expect(sent(request, 1).options).not.toHaveProperty("atBlock")
501
246
  })
502
247
 
503
- describe("fetch() method", () => {
504
- test("calls processQuery and entityFromRpcResult", async () => {
505
- const mockQueryResult = {
506
- data: [
507
- { key: "0xabc" as const, value: "encodedData1" },
508
- { key: "0xdef" as const, value: "encodedData2" },
509
- ],
510
- }
511
- const mockEntity1 = new Entity(
512
- "0xabc" as const,
513
- "application/json" as const,
514
- "0x123" as const,
515
- 1000n,
516
- 1000n,
517
- 1000n,
518
- 1000n,
519
- 1000n,
520
- new Uint8Array(),
521
- [],
522
- )
523
- const mockEntity2 = new Entity(
524
- "0xdef" as const,
525
- "application/json" as const,
526
- "0x456" as const,
527
- 2000n,
528
- 2000n,
529
- 2000n,
530
- 2000n,
531
- 2000n,
532
- new Uint8Array(),
533
- [],
534
- )
535
-
536
- mockProcessQuery.mockResolvedValue(mockQueryResult)
537
- mockEntityFromRpcResult.mockResolvedValueOnce(mockEntity1).mockResolvedValueOnce(mockEntity2)
538
-
539
- const builder = new QueryBuilder(mockClient)
540
- const result = await builder.where(eq("name", "test")).fetch()
541
-
542
- expect(mockProcessQuery).toHaveBeenCalled()
543
- expect(mockEntityFromRpcResult).toHaveBeenCalled()
544
- expect(mockEntityFromRpcResult).toHaveBeenCalledWith({ key: "0xabc", value: "encodedData1" })
545
- expect(mockEntityFromRpcResult).toHaveBeenCalledWith({ key: "0xdef", value: "encodedData2" })
546
- expect(result.entities).toHaveLength(2)
547
- expect(result.entities[0]).toBe(mockEntity1)
548
- expect(result.entities[1]).toBe(mockEntity2)
549
- })
550
-
551
- test("returns QueryResult with correct properties", async () => {
552
- mockProcessQuery.mockResolvedValue({
553
- data: [],
554
- })
555
-
556
- const builder = new QueryBuilder(mockClient)
557
- const result = await builder.limit(10).cursor("0xABC123").fetch()
248
+ it("walks every page through the async iterator", async () => {
249
+ const { client } = makeClient(page("b64:1", "0x1", "0x2"), page(undefined, "0x3"))
250
+ const keys: unknown[] = []
251
+ for await (const entity of new SelectQueryBuilder(client, { key: true }).where(eq("a", 1))) {
252
+ keys.push((entity as { key: string }).key)
253
+ }
254
+ expect(keys).toEqual(["0x1", "0x2", "0x3"])
255
+ })
256
+ })
558
257
 
559
- expect(result).toBeDefined()
560
- expect(result.entities).toEqual([])
258
+ describe("node rejections", () => {
259
+ /** viem wraps the arkiv codes in its own EIP-1474 classes, so the code sits down the chain. */
260
+ function rpcFailure(code: number, data?: unknown) {
261
+ const inner = Object.assign(new Error("RPC Request failed."), { code, data })
262
+ return Object.assign(new Error("Something went wrong."), {
263
+ shortMessage: "Something went wrong.",
264
+ cause: inner,
561
265
  })
266
+ }
267
+
268
+ it("names the failure and keeps the node's detail", async () => {
269
+ const { client } = makeClient()
270
+ const data = { position: 14, expected: "typed literal", got: "3.5" }
271
+ ;(client.request as ReturnType<typeof vi.fn>).mockRejectedValue(rpcFailure(-32001, data))
272
+
273
+ const promise = new SelectQueryBuilder(client, { key: true }).where(eq("a", 1)).fetch()
274
+ await expect(promise).rejects.toThrow(QueryError)
275
+
276
+ const error = (await promise.catch((e) => e)) as QueryError
277
+ expect(error.kind).toBe("parse")
278
+ expect(error.code).toBe(-32001)
279
+ expect(error.data).toEqual(data)
280
+ expect(error.query).toBe("a = i32(1)")
281
+ expect(error.message).toContain("Query: a = i32(1)")
562
282
  })
563
283
 
564
- describe("count() method", () => {
565
- test("calls processQuery and returns count", async () => {
566
- const mockQueryResult = {
567
- data: [
568
- { key: "0xabc", value: "data1" },
569
- { key: "0xdef", value: "data2" },
570
- { key: "0xghi", value: "data3" },
571
- ],
572
- }
573
- mockProcessQuery.mockResolvedValue(mockQueryResult)
574
-
575
- const builder = new QueryBuilder(mockClient)
576
- const count = await builder.where(eq("status", "active")).count()
577
-
578
- expect(mockProcessQuery).toHaveBeenCalled()
579
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
580
- predicates: [eq("status", "active")],
581
- limit: undefined,
582
- cursor: undefined,
583
- ownedBy: undefined,
584
- createdBy: undefined,
585
- validAtBlock: undefined,
586
- withAttributes: false,
587
- withMetadata: false,
588
- withPayload: false,
589
- })
590
- expect(count as number).toEqual(3 as number)
591
- // Verify entityFromRpcResult is NOT called for count()
592
- expect(mockEntityFromRpcResult).not.toHaveBeenCalled()
593
- })
284
+ it("maps each code in the frozen range", async () => {
285
+ const kinds = {
286
+ [-32001]: "parse",
287
+ [-32002]: "type",
288
+ [-32003]: "literal",
289
+ [-32004]: "limits",
290
+ [-32005]: "cursor",
291
+ [-32006]: "block",
292
+ }
293
+ for (const [code, kind] of Object.entries(kinds)) {
294
+ const { client } = makeClient()
295
+ ;(client.request as ReturnType<typeof vi.fn>).mockRejectedValue(rpcFailure(Number(code)))
296
+ const error = await new SelectQueryBuilder(client, { key: true })
297
+ .where(eq("a", 1))
298
+ .fetch()
299
+ .catch((e) => e)
300
+ expect((error as QueryError).kind).toBe(kind as QueryError["kind"])
301
+ }
302
+ })
594
303
 
595
- test("returns 0 for empty results", async () => {
596
- mockProcessQuery.mockResolvedValue({
597
- data: [],
598
- })
304
+ it("leaves anything else alone", async () => {
305
+ const { client } = makeClient()
306
+ const transportFailure = new Error("fetch failed")
307
+ ;(client.request as ReturnType<typeof vi.fn>).mockRejectedValue(transportFailure)
599
308
 
600
- const builder = new QueryBuilder(mockClient)
601
- const count = await builder.count()
309
+ const error = await new SelectQueryBuilder(client, { key: true })
310
+ .where(eq("a", 1))
311
+ .fetch()
312
+ .catch((e) => e)
313
+ expect(error).toBe(transportFailure)
314
+ })
315
+ })
602
316
 
603
- expect(count as number).toEqual(0 as number)
317
+ describe("the projected type", () => {
318
+ // Compile-time only: these assertions fail the build, not the run.
319
+ it("narrows the entity to exactly the selected fields", async () => {
320
+ const { client } = makeClient({
321
+ blockNumber: "0x1",
322
+ data: [{ key: KEY, owner: ACCOUNT, payload: "0x7b7d" }],
604
323
  })
605
324
 
606
- test("count() uses all builder parameters", async () => {
607
- mockProcessQuery.mockResolvedValue({
608
- data: [],
609
- })
610
-
611
- const builder = new QueryBuilder(mockClient)
612
- const owner = "0x1234567890123456789012345678901234567890" as const
613
- await builder
614
- .where([eq("active", 1), gte("score", 100)])
615
- .ownedBy(owner)
616
- .limit(50)
617
- .cursor("0xABC123")
618
- .validAtBlock(123n)
619
- .count()
620
-
621
- expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
622
- predicates: [eq("active", 1), gte("score", 100)],
623
- limit: 50,
624
- cursor: "0xABC123",
625
- ownedBy: owner,
626
- createdBy: undefined,
627
- validAtBlock: 123n,
628
- withAttributes: false,
629
- withMetadata: false,
630
- withPayload: false,
631
- })
325
+ const page = await new SelectQueryBuilder<ProjectedEntity<{ key: true; owner: true }>>(client, {
326
+ key: true,
327
+ owner: true,
632
328
  })
329
+ .where(eq("a", 1))
330
+ .fetch()
331
+
332
+ const entity = page.entities[0]
333
+ expectType<Hex>(entity.key)
334
+ expectType<Hex>(entity.owner)
335
+ // @ts-expect-error - creator was not selected
336
+ entity.creator
337
+ // @ts-expect-error - toJson comes with the payload, which was not selected
338
+ entity.toJson()
633
339
  })
634
340
 
635
- describe("predicate types", () => {
636
- test("passes string values correctly", async () => {
637
- mockProcessQuery.mockResolvedValue({
638
- data: [],
639
- })
640
-
641
- const builder = new QueryBuilder(mockClient)
642
- await builder.where(eq("name", "john")).fetch()
643
-
644
- expect(mockProcessQuery).toHaveBeenCalledWith(
645
- mockClient,
646
- expect.objectContaining({
647
- predicates: [eq("name", "john")],
648
- }),
649
- )
341
+ it("keeps the payload helpers when the payload is selected", async () => {
342
+ const { client } = makeClient({ blockNumber: "0x1", data: [{ payload: "0x7b7d" }] })
343
+ const page = await new SelectQueryBuilder<ProjectedEntity<{ payload: true }>>(client, {
344
+ payload: true,
650
345
  })
346
+ .where(eq("a", 1))
347
+ .fetch()
651
348
 
652
- test("passes numeric values correctly", async () => {
653
- mockProcessQuery.mockResolvedValue({
654
- data: [],
655
- })
656
-
657
- const builder = new QueryBuilder(mockClient)
658
- await builder.where(gt("age", 25)).fetch()
659
-
660
- expect(mockProcessQuery).toHaveBeenCalledWith(
661
- mockClient,
662
- expect.objectContaining({
663
- predicates: [gt("age", 25)],
664
- }),
665
- )
666
- })
349
+ expect(page.entities[0].toJson()).toEqual({})
350
+ expectType<Uint8Array>(page.entities[0].payload)
351
+ })
667
352
 
668
- test("passes mixed predicates correctly", async () => {
669
- mockProcessQuery.mockResolvedValue({
670
- data: [],
671
- })
672
-
673
- const builder = new QueryBuilder(mockClient)
674
- const predicates = [eq("name", "john"), gt("age", 25), neq("status", "inactive")]
675
- await builder.where(predicates).fetch()
676
-
677
- expect(mockProcessQuery).toHaveBeenCalledWith(
678
- mockClient,
679
- expect.objectContaining({
680
- predicates: predicates,
681
- }),
682
- )
353
+ it("treats a named attribute subset as a selection of attributes", async () => {
354
+ const { client } = makeClient({
355
+ blockNumber: "0x1",
356
+ data: [{ key: KEY, attributes: [{ name: "version", type: "i32", value: 4 }] }],
683
357
  })
358
+ const page = await new SelectQueryBuilder<
359
+ ProjectedEntity<{ key: true; attributes: { version: true } }>
360
+ >(client, { key: true, attributes: { version: true } })
361
+ .where(eq("a", 1))
362
+ .fetch()
363
+
364
+ expectType<Attributes>(page.entities[0].attributes)
365
+ expect(page.entities[0].attributes).toEqual({ version: { type: "i32", value: 4 } })
684
366
  })
685
367
  })
368
+
369
+ /** Asserts the static type of an expression without touching it at runtime. */
370
+ function expectType<T>(_value: T): void {}