@arkiv-network/sdk 0.7.0 → 0.8.0-advanced.3

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 (240) 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-Ds2XRflQ.cjs +656 -0
  7. package/dist/advanced-FZy7WoYR.js +598 -0
  8. package/dist/advanced-FZy7WoYR.js.map +1 -0
  9. package/dist/arkivTransactions-BpARZOsH.d.cts +584 -0
  10. package/dist/arkivTransactions-BpARZOsH.d.cts.map +1 -0
  11. package/dist/arkivTransactions-DfZ_yVDD.d.ts +584 -0
  12. package/dist/arkivTransactions-DfZ_yVDD.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-BJGuhsqp.js +819 -0
  18. package/dist/attributes-BJGuhsqp.js.map +1 -0
  19. package/dist/attributes-CMJlp1wn.cjs +1039 -0
  20. package/dist/chains/index.cjs +2 -3
  21. package/dist/chains/index.d.cts +41 -76
  22. package/dist/chains/index.d.cts.map +1 -1
  23. package/dist/chains/index.d.ts +24 -59
  24. package/dist/chains/index.d.ts.map +1 -1
  25. package/dist/chains/index.js +2 -2
  26. package/dist/chains-BfNQliHU.cjs +76 -0
  27. package/dist/chains-Du8tql7I.js +60 -0
  28. package/dist/chains-Du8tql7I.js.map +1 -0
  29. package/dist/codec-M74UR1_0.d.ts +101 -0
  30. package/dist/codec-M74UR1_0.d.ts.map +1 -0
  31. package/dist/entity/index.cjs +21 -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-51znTSB1.cjs +56 -0
  41. package/dist/errors-CwPJ16bn.js +33 -0
  42. package/dist/errors-CwPJ16bn.js.map +1 -0
  43. package/dist/expiry-CZErShX6.js +95 -0
  44. package/dist/expiry-CZErShX6.js.map +1 -0
  45. package/dist/expiry-DRV29F1r.d.cts +79 -0
  46. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  47. package/dist/expiry-nDbCgZX4.d.ts +79 -0
  48. package/dist/expiry-nDbCgZX4.d.ts.map +1 -0
  49. package/dist/expiry-xr6mx4f7.cjs +135 -0
  50. package/dist/flags-5dTNRnow.d.ts +41 -0
  51. package/dist/flags-5dTNRnow.d.ts.map +1 -0
  52. package/dist/flags-Bh0HJldY.cjs +43 -0
  53. package/dist/flags-CRjPpoh3.d.cts +41 -0
  54. package/dist/flags-CRjPpoh3.d.cts.map +1 -0
  55. package/dist/flags-Cr76ldwF.js +33 -0
  56. package/dist/flags-Cr76ldwF.js.map +1 -0
  57. package/dist/index-CMeD8uEq.d.cts +280 -0
  58. package/dist/index-CMeD8uEq.d.cts.map +1 -0
  59. package/dist/index-CTDI5pXZ.d.ts +184 -0
  60. package/dist/index-CTDI5pXZ.d.ts.map +1 -0
  61. package/dist/index-CekGjkRx.d.cts +184 -0
  62. package/dist/index-CekGjkRx.d.cts.map +1 -0
  63. package/dist/index-D0IzFwMX.d.ts +328 -0
  64. package/dist/index-D0IzFwMX.d.ts.map +1 -0
  65. package/dist/index-DZxwz2Ky.d.cts +177 -0
  66. package/dist/index-DZxwz2Ky.d.cts.map +1 -0
  67. package/dist/index-Dz4SkQWg.d.ts +280 -0
  68. package/dist/index-Dz4SkQWg.d.ts.map +1 -0
  69. package/dist/index-JP31fjyM.d.ts +177 -0
  70. package/dist/index-JP31fjyM.d.ts.map +1 -0
  71. package/dist/index-NbfnXxgr.d.cts +424 -0
  72. package/dist/index-NbfnXxgr.d.cts.map +1 -0
  73. package/dist/index.cjs +434 -412
  74. package/dist/index.d.cts +401 -235
  75. package/dist/index.d.cts.map +1 -1
  76. package/dist/index.d.ts +402 -235
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +370 -404
  79. package/dist/index.js.map +1 -1
  80. package/dist/key-E7mv0BYf.cjs +116 -0
  81. package/dist/key-XQkpw2xh.js +82 -0
  82. package/dist/key-XQkpw2xh.js.map +1 -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-Cvrir3Q-.cjs +108 -0
  87. package/dist/payload-D53ms-Mg.d.ts +153 -0
  88. package/dist/payload-D53ms-Mg.d.ts.map +1 -0
  89. package/dist/payload-DaPzUvEh.js +92 -0
  90. package/dist/payload-DaPzUvEh.js.map +1 -0
  91. package/dist/payload-hluX1rc0.d.cts +153 -0
  92. package/dist/payload-hluX1rc0.d.cts.map +1 -0
  93. package/dist/query/index.cjs +26 -149
  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 -137
  97. package/dist/queryBuilder-BXR-HxD9.cjs +947 -0
  98. package/dist/queryBuilder-BwauPaGO.js +823 -0
  99. package/dist/queryBuilder-BwauPaGO.js.map +1 -0
  100. package/dist/selection-C6KeWJ7g.d.cts +441 -0
  101. package/dist/selection-C6KeWJ7g.d.cts.map +1 -0
  102. package/dist/selection-mjiiAGDO.d.ts +442 -0
  103. package/dist/selection-mjiiAGDO.d.ts.map +1 -0
  104. package/dist/types/index.cjs +4 -1
  105. package/dist/types/index.d.cts +7 -2
  106. package/dist/types/index.d.ts +8 -2
  107. package/dist/types/index.js +4 -1
  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 -9
  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 +28 -43
  122. package/src/actions/public/getEntityNonce.ts +30 -0
  123. package/src/actions/public/predictEntityKeys.test.ts +159 -0
  124. package/src/actions/public/predictEntityKeys.ts +152 -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 +8 -8
  129. package/src/actions/wallet/createEntity.ts +105 -26
  130. package/src/actions/wallet/deleteEntity.ts +2 -3
  131. package/src/actions/wallet/executeBatch.test.ts +76 -0
  132. package/src/actions/wallet/executeBatch.ts +51 -0
  133. package/src/actions/wallet/extendEntity.ts +40 -16
  134. package/src/actions/wallet/patchEntity.ts +90 -0
  135. package/src/attr/attributes.test.ts +120 -0
  136. package/src/attr/attributes.ts +196 -0
  137. package/src/attr/codec.test.ts +162 -0
  138. package/src/attr/codec.ts +220 -0
  139. package/src/attr/decimal.ts +86 -0
  140. package/src/attr/errors.ts +145 -0
  141. package/src/attr/index.ts +50 -0
  142. package/src/attr/names.ts +106 -0
  143. package/src/attr/types.ts +150 -0
  144. package/src/attr/values.test.ts +271 -0
  145. package/src/attr/values.ts +506 -0
  146. package/src/chains/cheesecake.ts +22 -0
  147. package/src/chains/index.ts +1 -2
  148. package/src/chains/localhost.ts +3 -0
  149. package/src/clients/createPublicClient.test.ts +4 -6
  150. package/src/clients/createPublicClient.ts +9 -8
  151. package/src/clients/createWalletClient.test.ts +4 -4
  152. package/src/clients/createWalletClient.ts +13 -9
  153. package/src/clients/decorators/arkivAdvanced.ts +144 -0
  154. package/src/clients/decorators/arkivPublic.ts +181 -147
  155. package/src/clients/decorators/arkivWallet.ts +141 -122
  156. package/src/consts.ts +2 -1
  157. package/src/entity/create.test.ts +203 -0
  158. package/src/entity/errors.ts +39 -0
  159. package/src/entity/events.ts +16 -0
  160. package/src/entity/expiry.test.ts +195 -0
  161. package/src/entity/expiry.ts +182 -0
  162. package/src/entity/flags.ts +65 -0
  163. package/src/entity/index.ts +16 -0
  164. package/src/entity/key.ts +88 -0
  165. package/src/entity/operations.ts +135 -0
  166. package/src/entity/params.ts +15 -0
  167. package/src/errors.ts +33 -28
  168. package/src/index.ts +19 -0
  169. package/src/query/engine.ts +68 -132
  170. package/src/query/errors.ts +151 -0
  171. package/src/query/expression.test.ts +286 -0
  172. package/src/query/expression.ts +500 -0
  173. package/src/query/index.ts +40 -4
  174. package/src/query/injection.test.ts +151 -0
  175. package/src/query/queryBuilder.test.ts +342 -637
  176. package/src/query/queryBuilder.ts +133 -359
  177. package/src/query/queryResult.ts +54 -51
  178. package/src/query/selection.test.ts +48 -98
  179. package/src/query/selection.ts +77 -65
  180. package/src/types/entity.test.ts +44 -155
  181. package/src/types/entity.ts +62 -44
  182. package/src/types/events.ts +64 -23
  183. package/src/types/index.ts +24 -20
  184. package/src/types/rpcSchema.ts +71 -33
  185. package/src/types/txParams.ts +8 -0
  186. package/src/utils/arkivTransactions.test.ts +687 -38
  187. package/src/utils/arkivTransactions.ts +400 -115
  188. package/src/utils/chains.test.ts +21 -24
  189. package/src/utils/compression.ts +1 -1
  190. package/src/utils/entities.test.ts +62 -0
  191. package/src/utils/entities.ts +111 -29
  192. package/src/utils/expirationTime.ts +153 -10
  193. package/src/utils/logger.ts +2 -2
  194. package/src/utils/payload.ts +29 -2
  195. package/src/utils/revert.test.ts +99 -0
  196. package/src/utils/revert.ts +145 -0
  197. package/dist/chains-BifaY04V.cjs +0 -108
  198. package/dist/chains-CrtkVbBh.js +0 -86
  199. package/dist/chains-CrtkVbBh.js.map +0 -1
  200. package/dist/consts-AUoL-v42.cjs +0 -18
  201. package/dist/consts-BV4PR-de.js +0 -7
  202. package/dist/consts-BV4PR-de.js.map +0 -1
  203. package/dist/entity-B6g_EPm1.d.cts +0 -382
  204. package/dist/entity-B6g_EPm1.d.cts.map +0 -1
  205. package/dist/entity-qoLMAuuk.d.ts +0 -382
  206. package/dist/entity-qoLMAuuk.d.ts.map +0 -1
  207. package/dist/index-DdspxNAA.d.ts +0 -481
  208. package/dist/index-DdspxNAA.d.ts.map +0 -1
  209. package/dist/index-Ds9dwqb5.d.cts +0 -481
  210. package/dist/index-Ds9dwqb5.d.cts.map +0 -1
  211. package/dist/payload-B2kFJyah.d.ts +0 -6
  212. package/dist/payload-B2kFJyah.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-Claf2jUO.js +0 -13
  216. package/dist/payload-Claf2jUO.js.map +0 -1
  217. package/dist/payload-DpA5PA-w.cjs +0 -23
  218. package/dist/query/index.js.map +0 -1
  219. package/dist/queryBuilder-D2uwP9jI.js +0 -704
  220. package/dist/queryBuilder-D2uwP9jI.js.map +0 -1
  221. package/dist/queryBuilder-UW2MCaX4.cjs +0 -792
  222. package/dist/utils/index.d.cts.map +0 -1
  223. package/dist/utils/index.d.ts.map +0 -1
  224. package/dist/utils/index.js.map +0 -1
  225. package/src/actions/public/buildQuery.ts +0 -10
  226. package/src/actions/public/subscribeEntityEvents.ts +0 -119
  227. package/src/actions/wallet/mutateEntities.ts +0 -111
  228. package/src/actions/wallet/updateEntity.ts +0 -60
  229. package/src/chains/braga.ts +0 -26
  230. package/src/chains/kaolin.ts +0 -29
  231. package/src/query/engine.test.ts +0 -526
  232. package/src/query/predicate.test.ts +0 -461
  233. package/src/query/predicate.ts +0 -144
  234. package/src/query/queryResult.test.ts +0 -270
  235. package/src/query/selectQueryBuilder.test.ts +0 -210
  236. package/src/types/attributes.ts +0 -16
  237. package/src/utils/validation.test.ts +0 -60
  238. package/src/utils/validation.ts +0 -32
  239. /package/dist/{types-H-TSmRpm.js → attr-DlmErkvR.cjs} +0 -0
  240. /package/dist/{types-o3zWarRp.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 {}