@arkiv-network/sdk 0.7.0-dev.1 → 0.8.0-advanced.2

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 (237) hide show
  1. package/README.md +71 -16
  2. package/dist/actions/advanced/index.cjs +13 -0
  3. package/dist/actions/advanced/index.d.cts +8 -0
  4. package/dist/actions/advanced/index.d.ts +9 -0
  5. package/dist/actions/advanced/index.js +9 -0
  6. package/dist/advanced-8ZPTmmaN.js +598 -0
  7. package/dist/advanced-8ZPTmmaN.js.map +1 -0
  8. package/dist/advanced-fPImL6lW.cjs +656 -0
  9. package/dist/arkivTransactions-D74gERHG.d.cts +571 -0
  10. package/dist/arkivTransactions-D74gERHG.d.cts.map +1 -0
  11. package/dist/arkivTransactions-NtGD6uk5.d.ts +571 -0
  12. package/dist/arkivTransactions-NtGD6uk5.d.ts.map +1 -0
  13. package/dist/attr/index.cjs +35 -0
  14. package/dist/attr/index.d.cts +2 -0
  15. package/dist/attr/index.d.ts +3 -0
  16. package/dist/attr/index.js +4 -0
  17. package/dist/attributes-BhY5DwJp.js +809 -0
  18. package/dist/attributes-BhY5DwJp.js.map +1 -0
  19. package/dist/attributes-Df9IsCVs.cjs +1029 -0
  20. package/dist/chains/index.cjs +2 -3
  21. package/dist/chains/index.d.cts +22 -60
  22. package/dist/chains/index.d.cts.map +1 -1
  23. package/dist/chains/index.d.ts +22 -60
  24. package/dist/chains/index.d.ts.map +1 -1
  25. package/dist/chains/index.js +2 -2
  26. package/dist/chains-BJrZs6sG.js +57 -0
  27. package/dist/chains-BJrZs6sG.js.map +1 -0
  28. package/dist/chains-BYRTTipa.cjs +73 -0
  29. package/dist/codec-Car3KkFF.d.ts +102 -0
  30. package/dist/codec-Car3KkFF.d.ts.map +1 -0
  31. package/dist/entity/index.cjs +20 -0
  32. package/dist/entity/index.d.cts +4 -0
  33. package/dist/entity/index.d.ts +5 -0
  34. package/dist/entity/index.js +7 -0
  35. package/dist/entity-B00Li04j.cjs +71 -0
  36. package/dist/entity-B7G8q_Eo.cjs +0 -0
  37. package/dist/entity-BYQR1uoe.js +67 -0
  38. package/dist/entity-BYQR1uoe.js.map +1 -0
  39. package/dist/entity-CD_qFF2M.js +0 -0
  40. package/dist/errors-BVbjKVMN.cjs +56 -0
  41. package/dist/errors-LTe7bOYS.js +33 -0
  42. package/dist/errors-LTe7bOYS.js.map +1 -0
  43. package/dist/expiry-BELY2Kpw.cjs +135 -0
  44. package/dist/expiry-BjpxZZQU.js +95 -0
  45. package/dist/expiry-BjpxZZQU.js.map +1 -0
  46. package/dist/expiry-DRV29F1r.d.cts +79 -0
  47. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  48. package/dist/expiry-nDbCgZX4.d.ts +79 -0
  49. package/dist/expiry-nDbCgZX4.d.ts.map +1 -0
  50. package/dist/flags-5dTNRnow.d.ts +41 -0
  51. package/dist/flags-5dTNRnow.d.ts.map +1 -0
  52. package/dist/flags-CRjPpoh3.d.cts +41 -0
  53. package/dist/flags-CRjPpoh3.d.cts.map +1 -0
  54. package/dist/flags-DFBs2J94.js +33 -0
  55. package/dist/flags-DFBs2J94.js.map +1 -0
  56. package/dist/flags-kr276q6i.cjs +43 -0
  57. package/dist/index-B5j76711.d.ts +184 -0
  58. package/dist/index-B5j76711.d.ts.map +1 -0
  59. package/dist/index-BF7Mq9Is.d.cts +280 -0
  60. package/dist/index-BF7Mq9Is.d.cts.map +1 -0
  61. package/dist/index-CA4V7Pdh.d.cts +167 -0
  62. package/dist/index-CA4V7Pdh.d.cts.map +1 -0
  63. package/dist/index-CV5zCdpa.d.cts +417 -0
  64. package/dist/index-CV5zCdpa.d.cts.map +1 -0
  65. package/dist/index-DHDud2hC.d.ts +280 -0
  66. package/dist/index-DHDud2hC.d.ts.map +1 -0
  67. package/dist/index-DvYvrkdk.d.ts +320 -0
  68. package/dist/index-DvYvrkdk.d.ts.map +1 -0
  69. package/dist/index-EeUO0nnJ.d.cts +184 -0
  70. package/dist/index-EeUO0nnJ.d.cts.map +1 -0
  71. package/dist/index-fX_XyqLp.d.ts +167 -0
  72. package/dist/index-fX_XyqLp.d.ts.map +1 -0
  73. package/dist/index.cjs +411 -405
  74. package/dist/index.d.cts +360 -209
  75. package/dist/index.d.cts.map +1 -1
  76. package/dist/index.d.ts +361 -209
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +342 -387
  79. package/dist/index.js.map +1 -1
  80. package/dist/key-D1Bh7MF0.js +70 -0
  81. package/dist/key-D1Bh7MF0.js.map +1 -0
  82. package/dist/key-WKhXnXuU.cjs +98 -0
  83. package/dist/logger-CVpvU8ik.js +153 -0
  84. package/dist/logger-CVpvU8ik.js.map +1 -0
  85. package/dist/logger-Cr15lPAQ.cjs +188 -0
  86. package/dist/payload-BNoud2KZ.d.cts +94 -0
  87. package/dist/payload-BNoud2KZ.d.cts.map +1 -0
  88. package/dist/payload-Bz6651ax.js +67 -0
  89. package/dist/payload-Bz6651ax.js.map +1 -0
  90. package/dist/payload-CcUj85ys.d.ts +94 -0
  91. package/dist/payload-CcUj85ys.d.ts.map +1 -0
  92. package/dist/payload-KC14SDlN.cjs +83 -0
  93. package/dist/query/index.cjs +26 -150
  94. package/dist/query/index.d.cts +5 -3
  95. package/dist/query/index.d.ts +6 -3
  96. package/dist/query/index.js +8 -138
  97. package/dist/queryBuilder-DM6AWCBa.js +820 -0
  98. package/dist/queryBuilder-DM6AWCBa.js.map +1 -0
  99. package/dist/queryBuilder-DbUMEZln.cjs +944 -0
  100. package/dist/selection-C4UhsMOO.d.ts +434 -0
  101. package/dist/selection-C4UhsMOO.d.ts.map +1 -0
  102. package/dist/selection-DPg0uyJf.d.cts +433 -0
  103. package/dist/selection-DPg0uyJf.d.cts.map +1 -0
  104. package/dist/types/index.cjs +3 -3
  105. package/dist/types/index.d.cts +7 -2
  106. package/dist/types/index.d.ts +8 -2
  107. package/dist/types/index.js +3 -3
  108. package/dist/types-Bu_ZwMbZ.js +0 -0
  109. package/dist/types-NRqymi6S.cjs +0 -0
  110. package/dist/utils/index.cjs +4 -17
  111. package/dist/utils/index.d.cts +3 -17
  112. package/dist/utils/index.d.ts +3 -17
  113. package/dist/utils/index.js +4 -18
  114. package/package.json +26 -5
  115. package/src/actions/advanced/advanced.test.ts +258 -0
  116. package/src/actions/advanced/buildMutation.ts +104 -0
  117. package/src/actions/advanced/getMutationResult.ts +112 -0
  118. package/src/actions/advanced/index.ts +21 -0
  119. package/src/actions/advanced/pingTransaction.ts +43 -0
  120. package/src/actions/advanced/sendMutation.ts +59 -0
  121. package/src/actions/public/getEntity.ts +27 -43
  122. package/src/actions/public/getEntityNonce.ts +30 -0
  123. package/src/actions/public/predictEntityKeys.test.ts +143 -0
  124. package/src/actions/public/predictEntityKeys.ts +138 -0
  125. package/src/actions/public/query.ts +35 -67
  126. package/src/actions/public/watchEntityEvents.test.ts +414 -0
  127. package/src/actions/public/watchEntityEvents.ts +225 -0
  128. package/src/actions/wallet/changeOwnership.ts +2 -2
  129. package/src/actions/wallet/createEntity.ts +99 -24
  130. package/src/actions/wallet/extendEntity.ts +37 -13
  131. package/src/actions/wallet/mutateEntities.test.ts +76 -0
  132. package/src/actions/wallet/mutateEntities.ts +16 -33
  133. package/src/actions/wallet/patchEntity.ts +90 -0
  134. package/src/attr/attributes.test.ts +120 -0
  135. package/src/attr/attributes.ts +190 -0
  136. package/src/attr/codec.test.ts +162 -0
  137. package/src/attr/codec.ts +220 -0
  138. package/src/attr/decimal.ts +86 -0
  139. package/src/attr/errors.ts +145 -0
  140. package/src/attr/index.ts +50 -0
  141. package/src/attr/names.ts +106 -0
  142. package/src/attr/types.ts +150 -0
  143. package/src/attr/values.test.ts +271 -0
  144. package/src/attr/values.ts +502 -0
  145. package/src/chains/cheesecake.ts +22 -0
  146. package/src/chains/index.ts +1 -2
  147. package/src/clients/createPublicClient.test.ts +3 -5
  148. package/src/clients/createPublicClient.ts +5 -4
  149. package/src/clients/createWalletClient.test.ts +2 -2
  150. package/src/clients/createWalletClient.ts +13 -7
  151. package/src/clients/decorators/arkivAdvanced.ts +144 -0
  152. package/src/clients/decorators/arkivPublic.ts +172 -139
  153. package/src/clients/decorators/arkivWallet.ts +101 -94
  154. package/src/entity/create.test.ts +184 -0
  155. package/src/entity/errors.ts +38 -0
  156. package/src/entity/events.ts +16 -0
  157. package/src/entity/expiry.test.ts +195 -0
  158. package/src/entity/expiry.ts +181 -0
  159. package/src/entity/flags.ts +65 -0
  160. package/src/entity/index.ts +15 -0
  161. package/src/entity/key.ts +68 -0
  162. package/src/entity/operations.ts +134 -0
  163. package/src/entity/params.ts +15 -0
  164. package/src/errors.ts +26 -49
  165. package/src/index.ts +19 -0
  166. package/src/query/engine.ts +68 -137
  167. package/src/query/errors.ts +151 -0
  168. package/src/query/expression.test.ts +286 -0
  169. package/src/query/expression.ts +493 -0
  170. package/src/query/index.ts +40 -4
  171. package/src/query/injection.test.ts +151 -0
  172. package/src/query/queryBuilder.test.ts +342 -637
  173. package/src/query/queryBuilder.ts +133 -359
  174. package/src/query/queryResult.ts +54 -51
  175. package/src/query/selection.test.ts +48 -98
  176. package/src/query/selection.ts +77 -65
  177. package/src/types/entity.test.ts +44 -155
  178. package/src/types/entity.ts +60 -52
  179. package/src/types/events.ts +59 -16
  180. package/src/types/index.ts +20 -17
  181. package/src/types/rpcSchema.ts +69 -37
  182. package/src/utils/arkivTransactions.test.ts +600 -155
  183. package/src/utils/arkivTransactions.ts +350 -219
  184. package/src/utils/chains.test.ts +21 -24
  185. package/src/utils/compression.ts +1 -1
  186. package/src/utils/entities.test.ts +62 -0
  187. package/src/utils/entities.ts +109 -37
  188. package/src/utils/expirationTime.ts +114 -10
  189. package/src/utils/logger.ts +2 -2
  190. package/src/utils/revert.test.ts +99 -0
  191. package/src/utils/revert.ts +145 -0
  192. package/dist/chains-BifaY04V.cjs +0 -108
  193. package/dist/chains-CrtkVbBh.js +0 -86
  194. package/dist/chains-CrtkVbBh.js.map +0 -1
  195. package/dist/consts-Bz0LZQZY.js +0 -7
  196. package/dist/consts-Bz0LZQZY.js.map +0 -1
  197. package/dist/consts-CxT1-RXU.cjs +0 -18
  198. package/dist/entity-BhSXaB23.cjs +0 -81
  199. package/dist/entity-CN41IOrq.d.cts +0 -401
  200. package/dist/entity-CN41IOrq.d.cts.map +0 -1
  201. package/dist/entity-DGfVyB-a.js +0 -71
  202. package/dist/entity-DGfVyB-a.js.map +0 -1
  203. package/dist/entity-NnEVlePN.d.ts +0 -401
  204. package/dist/entity-NnEVlePN.d.ts.map +0 -1
  205. package/dist/index-ClkpS6s_.d.ts +0 -481
  206. package/dist/index-ClkpS6s_.d.ts.map +0 -1
  207. package/dist/index-w4qop0Pe.d.cts +0 -481
  208. package/dist/index-w4qop0Pe.d.cts.map +0 -1
  209. package/dist/payload-BpNUPOnY.js +0 -13
  210. package/dist/payload-BpNUPOnY.js.map +0 -1
  211. package/dist/payload-C2KHOUoj.d.ts +0 -6
  212. package/dist/payload-C2KHOUoj.d.ts.map +0 -1
  213. package/dist/payload-CJN8fTvX.d.cts +0 -6
  214. package/dist/payload-CJN8fTvX.d.cts.map +0 -1
  215. package/dist/payload-D8YLKcJj.cjs +0 -23
  216. package/dist/query/index.js.map +0 -1
  217. package/dist/queryBuilder-BE_XhLLa.js +0 -735
  218. package/dist/queryBuilder-BE_XhLLa.js.map +0 -1
  219. package/dist/queryBuilder-J37buGCi.cjs +0 -865
  220. package/dist/utils/index.d.cts.map +0 -1
  221. package/dist/utils/index.d.ts.map +0 -1
  222. package/dist/utils/index.js.map +0 -1
  223. package/src/actions/public/buildQuery.ts +0 -10
  224. package/src/actions/public/subscribeEntityEvents.ts +0 -93
  225. package/src/actions/wallet/updateEntity.ts +0 -59
  226. package/src/chains/braga.ts +0 -26
  227. package/src/chains/kaolin.ts +0 -29
  228. package/src/query/engine.test.ts +0 -571
  229. package/src/query/predicate.test.ts +0 -461
  230. package/src/query/predicate.ts +0 -144
  231. package/src/query/queryResult.test.ts +0 -270
  232. package/src/query/selectQueryBuilder.test.ts +0 -213
  233. package/src/types/attributes.ts +0 -28
  234. package/src/utils/validation.test.ts +0 -106
  235. package/src/utils/validation.ts +0 -80
  236. /package/dist/{types-Ci4BRClw.js → attr-DlmErkvR.cjs} +0 -0
  237. /package/dist/{types-a8gm_IaQ.cjs → attr-GcQKatub.js} +0 -0
@@ -1,685 +1,390 @@
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
- })
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
+ })
80
75
 
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
- })
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
+ })
102
82
 
103
- test("chains multiple where() calls", async () => {
104
- mockProcessQuery.mockResolvedValue({
105
- data: [],
106
- })
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
+ })
107
91
 
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()
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()
114
96
 
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
- })
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"])
101
+ })
133
102
 
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
- })
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",
155
117
  })
118
+ })
156
119
 
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
- })
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()
182
132
  })
133
+ })
183
134
 
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
- })
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
+ ],
206
156
  })
207
157
 
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
- })
158
+ const page = await new SelectQueryBuilder(client, "*").where(eq("a", 1)).fetch()
159
+ const entity = page.entities[0]
230
160
 
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
- })
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,
251
167
  })
252
-
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
- })
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" },
273
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
+ })
274
182
 
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
- })
295
- })
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
- })
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
+ })
317
189
 
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
- })
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
+ })
339
210
 
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
- })
360
- })
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
+ })
361
219
 
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
- })
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
+ })
383
231
 
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
- })
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))
235
+
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()
242
+
243
+ expect(sent(request, 1).query).toBe("a = i32(1)")
244
+ expect(sent(request, 1).options).not.toHaveProperty("limit")
245
+ // The block page 2 reads at is the one page 1 was served from, not the builder's later edit.
246
+ expect(sent(request, 1).options).toMatchObject({ atBlock: "0x8e1ff" })
247
+ })
405
248
 
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
- })
249
+ it("pins the rest of the walk to the block the first page was read at", async () => {
250
+ const { client, request } = makeClient(
251
+ page("b64:1", "0x1"),
252
+ page("b64:2", "0x2"),
253
+ page(undefined, "0x3"),
254
+ )
255
+ // No atBlock on the builder: the first page reads at the head, and every page after it reads
256
+ // at the block that page came back with. Otherwise a block mined mid-walk moves the head on
257
+ // and the node rejects a cursor bound to the earlier one.
258
+ const first = await new SelectQueryBuilder(client, { key: true }).where(eq("a", 1)).fetch()
259
+ expect(sent(request, 0).options).not.toHaveProperty("atBlock")
260
+
261
+ const second = await first.next()
262
+ expect(sent(request, 1).options).toMatchObject({ cursor: "b64:1", atBlock: "0x8e1ff" })
263
+
264
+ await second.next()
265
+ expect(sent(request, 2).options).toMatchObject({ cursor: "b64:2", atBlock: "0x8e1ff" })
266
+ })
427
267
 
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
- })
268
+ it("walks every page through the async iterator", async () => {
269
+ const { client } = makeClient(page("b64:1", "0x1", "0x2"), page(undefined, "0x3"))
270
+ const keys: unknown[] = []
271
+ for await (const entity of new SelectQueryBuilder(client, { key: true }).where(eq("a", 1))) {
272
+ keys.push((entity as { key: string }).key)
273
+ }
274
+ expect(keys).toEqual(["0x1", "0x2", "0x3"])
275
+ })
276
+ })
449
277
 
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
- })
278
+ describe("node rejections", () => {
279
+ /** viem wraps the arkiv codes in its own EIP-1474 classes, so the code sits down the chain. */
280
+ function rpcFailure(code: number, data?: unknown) {
281
+ const inner = Object.assign(new Error("RPC Request failed."), { code, data })
282
+ return Object.assign(new Error("Something went wrong."), {
283
+ shortMessage: "Something went wrong.",
284
+ cause: inner,
469
285
  })
286
+ }
287
+
288
+ it("names the failure and keeps the node's detail", async () => {
289
+ const { client } = makeClient()
290
+ const data = { position: 14, expected: "typed literal", got: "3.5" }
291
+ ;(client.request as ReturnType<typeof vi.fn>).mockRejectedValue(rpcFailure(-32001, data))
292
+
293
+ const promise = new SelectQueryBuilder(client, { key: true }).where(eq("a", 1)).fetch()
294
+ await expect(promise).rejects.toThrow(QueryError)
295
+
296
+ const error = (await promise.catch((e) => e)) as QueryError
297
+ expect(error.kind).toBe("parse")
298
+ expect(error.code).toBe(-32001)
299
+ expect(error.data).toEqual(data)
300
+ expect(error.query).toBe("a = i32(1)")
301
+ expect(error.message).toContain("Query: a = i32(1)")
302
+ })
470
303
 
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)
304
+ it("maps each code in the frozen range", async () => {
305
+ const kinds = {
306
+ [-32001]: "parse",
307
+ [-32002]: "type",
308
+ [-32003]: "literal",
309
+ [-32004]: "limits",
310
+ [-32005]: "cursor",
311
+ [-32006]: "block",
312
+ }
313
+ for (const [code, kind] of Object.entries(kinds)) {
314
+ const { client } = makeClient()
315
+ ;(client.request as ReturnType<typeof vi.fn>).mockRejectedValue(rpcFailure(Number(code)))
316
+ const error = await new SelectQueryBuilder(client, { key: true })
317
+ .where(eq("a", 1))
487
318
  .fetch()
488
-
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
- })
319
+ .catch((e) => e)
320
+ expect((error as QueryError).kind).toBe(kind as QueryError["kind"])
321
+ }
501
322
  })
502
323
 
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
- })
324
+ it("leaves anything else alone", async () => {
325
+ const { client } = makeClient()
326
+ const transportFailure = new Error("fetch failed")
327
+ ;(client.request as ReturnType<typeof vi.fn>).mockRejectedValue(transportFailure)
555
328
 
556
- const builder = new QueryBuilder(mockClient)
557
- const result = await builder.limit(10).cursor("0xABC123").fetch()
558
-
559
- expect(result).toBeDefined()
560
- expect(result.entities).toEqual([])
561
- })
329
+ const error = await new SelectQueryBuilder(client, { key: true })
330
+ .where(eq("a", 1))
331
+ .fetch()
332
+ .catch((e) => e)
333
+ expect(error).toBe(transportFailure)
562
334
  })
335
+ })
563
336
 
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
- })
594
-
595
- test("returns 0 for empty results", async () => {
596
- mockProcessQuery.mockResolvedValue({
597
- data: [],
598
- })
599
-
600
- const builder = new QueryBuilder(mockClient)
601
- const count = await builder.count()
602
-
603
- expect(count as number).toEqual(0 as number)
337
+ describe("the projected type", () => {
338
+ // Compile-time only: these assertions fail the build, not the run.
339
+ it("narrows the entity to exactly the selected fields", async () => {
340
+ const { client } = makeClient({
341
+ blockNumber: "0x1",
342
+ data: [{ key: KEY, owner: ACCOUNT, payload: "0x7b7d" }],
604
343
  })
605
344
 
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
- })
345
+ const page = await new SelectQueryBuilder<ProjectedEntity<{ key: true; owner: true }>>(client, {
346
+ key: true,
347
+ owner: true,
632
348
  })
349
+ .where(eq("a", 1))
350
+ .fetch()
351
+
352
+ const entity = page.entities[0]
353
+ expectType<Hex>(entity.key)
354
+ expectType<Hex>(entity.owner)
355
+ // @ts-expect-error - creator was not selected
356
+ entity.creator
357
+ // @ts-expect-error - toJson comes with the payload, which was not selected
358
+ entity.toJson()
633
359
  })
634
360
 
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
- )
361
+ it("keeps the payload helpers when the payload is selected", async () => {
362
+ const { client } = makeClient({ blockNumber: "0x1", data: [{ payload: "0x7b7d" }] })
363
+ const page = await new SelectQueryBuilder<ProjectedEntity<{ payload: true }>>(client, {
364
+ payload: true,
650
365
  })
366
+ .where(eq("a", 1))
367
+ .fetch()
651
368
 
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
- })
369
+ expect(page.entities[0].toJson()).toEqual({})
370
+ expectType<Uint8Array>(page.entities[0].payload)
371
+ })
667
372
 
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
- )
373
+ it("treats a named attribute subset as a selection of attributes", async () => {
374
+ const { client } = makeClient({
375
+ blockNumber: "0x1",
376
+ data: [{ key: KEY, attributes: [{ name: "version", type: "i32", value: 4 }] }],
683
377
  })
378
+ const page = await new SelectQueryBuilder<
379
+ ProjectedEntity<{ key: true; attributes: { version: true } }>
380
+ >(client, { key: true, attributes: { version: true } })
381
+ .where(eq("a", 1))
382
+ .fetch()
383
+
384
+ expectType<Attributes>(page.entities[0].attributes)
385
+ expect(page.entities[0].attributes).toEqual({ version: { type: "i32", value: 4 } })
684
386
  })
685
387
  })
388
+
389
+ /** Asserts the static type of an expression without touching it at runtime. */
390
+ function expectType<T>(_value: T): void {}