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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/README.md +29 -16
  2. package/dist/attr/index.cjs +34 -0
  3. package/dist/attr/index.d.cts +2 -0
  4. package/dist/attr/index.d.ts +2 -0
  5. package/dist/attr/index.js +3 -0
  6. package/dist/attr-BDZ-APg5.js +809 -0
  7. package/dist/attr-BDZ-APg5.js.map +1 -0
  8. package/dist/attr-BvxHN99W.cjs +1029 -0
  9. package/dist/chains/index.cjs +2 -3
  10. package/dist/chains/index.d.cts +22 -60
  11. package/dist/chains/index.d.cts.map +1 -1
  12. package/dist/chains/index.d.ts +22 -60
  13. package/dist/chains/index.d.ts.map +1 -1
  14. package/dist/chains/index.js +2 -2
  15. package/dist/chains-BJrZs6sG.js +57 -0
  16. package/dist/chains-BJrZs6sG.js.map +1 -0
  17. package/dist/chains-BYRTTipa.cjs +73 -0
  18. package/dist/changeOwnership-B7bEgla4.d.cts +509 -0
  19. package/dist/changeOwnership-B7bEgla4.d.cts.map +1 -0
  20. package/dist/changeOwnership-DwgtdSFc.d.ts +509 -0
  21. package/dist/changeOwnership-DwgtdSFc.d.ts.map +1 -0
  22. package/dist/entity/index.cjs +19 -0
  23. package/dist/entity/index.d.cts +4 -0
  24. package/dist/entity/index.d.ts +4 -0
  25. package/dist/entity/index.js +6 -0
  26. package/dist/entity-B0t6sN7F.cjs +71 -0
  27. package/dist/entity-BIp_df9u.js +67 -0
  28. package/dist/entity-BIp_df9u.js.map +1 -0
  29. package/dist/entity-CFhbAv0R.js +70 -0
  30. package/dist/entity-CFhbAv0R.js.map +1 -0
  31. package/dist/entity-wfBnZd91.cjs +98 -0
  32. package/dist/errors-D-eJkGVc.cjs +56 -0
  33. package/dist/errors-DGB8AgT2.js +33 -0
  34. package/dist/errors-DGB8AgT2.js.map +1 -0
  35. package/dist/expiry-BUYIXTGb.cjs +135 -0
  36. package/dist/expiry-BxOlvVWC.d.ts +79 -0
  37. package/dist/expiry-BxOlvVWC.d.ts.map +1 -0
  38. package/dist/expiry-DGRzrSan.js +95 -0
  39. package/dist/expiry-DGRzrSan.js.map +1 -0
  40. package/dist/expiry-DRV29F1r.d.cts +79 -0
  41. package/dist/expiry-DRV29F1r.d.cts.map +1 -0
  42. package/dist/flags-0Vl5rrHq.d.cts +41 -0
  43. package/dist/flags-0Vl5rrHq.d.cts.map +1 -0
  44. package/dist/flags-Ct8k0gpB.cjs +43 -0
  45. package/dist/flags-ZtlvOIIk.js +33 -0
  46. package/dist/flags-ZtlvOIIk.js.map +1 -0
  47. package/dist/flags-wmcMoAnN.d.ts +41 -0
  48. package/dist/flags-wmcMoAnN.d.ts.map +1 -0
  49. package/dist/index-Cgw1u5aY.d.cts +278 -0
  50. package/dist/index-Cgw1u5aY.d.cts.map +1 -0
  51. package/dist/index-ClgIuxJ8.d.ts +159 -0
  52. package/dist/index-ClgIuxJ8.d.ts.map +1 -0
  53. package/dist/index-D0CmtiWT.d.cts +159 -0
  54. package/dist/index-D0CmtiWT.d.cts.map +1 -0
  55. package/dist/index-D8LZt-3J.d.cts +417 -0
  56. package/dist/index-D8LZt-3J.d.cts.map +1 -0
  57. package/dist/index-DVYFaXNV.d.ts +417 -0
  58. package/dist/index-DVYFaXNV.d.ts.map +1 -0
  59. package/dist/index-VCqgn29D.d.ts +278 -0
  60. package/dist/index-VCqgn29D.d.ts.map +1 -0
  61. package/dist/index.cjs +676 -375
  62. package/dist/index.d.cts +247 -208
  63. package/dist/index.d.cts.map +1 -1
  64. package/dist/index.d.ts +247 -208
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +626 -368
  67. package/dist/index.js.map +1 -1
  68. package/dist/payload-Bnw1hrvb.cjs +83 -0
  69. package/dist/payload-CIxSn_DW.js +67 -0
  70. package/dist/payload-CIxSn_DW.js.map +1 -0
  71. package/dist/payload-Dgx3PQeK.d.ts +94 -0
  72. package/dist/payload-Dgx3PQeK.d.ts.map +1 -0
  73. package/dist/payload-P0nWyjLg.d.cts +94 -0
  74. package/dist/payload-P0nWyjLg.d.cts.map +1 -0
  75. package/dist/query/index.cjs +24 -150
  76. package/dist/query/index.d.cts +5 -3
  77. package/dist/query/index.d.ts +5 -3
  78. package/dist/query/index.js +6 -138
  79. package/dist/queryBuilder-BXDvX1K_.cjs +1123 -0
  80. package/dist/queryBuilder-CbLL4pG5.js +963 -0
  81. package/dist/queryBuilder-CbLL4pG5.js.map +1 -0
  82. package/dist/selection-BvK6yX0g.d.cts +433 -0
  83. package/dist/selection-BvK6yX0g.d.cts.map +1 -0
  84. package/dist/selection-fpmI3ADQ.d.ts +433 -0
  85. package/dist/selection-fpmI3ADQ.d.ts.map +1 -0
  86. package/dist/types/index.cjs +3 -3
  87. package/dist/types/index.d.cts +7 -2
  88. package/dist/types/index.d.ts +7 -2
  89. package/dist/types/index.js +3 -3
  90. package/dist/utils/index.cjs +4 -17
  91. package/dist/utils/index.d.cts +3 -17
  92. package/dist/utils/index.d.ts +3 -17
  93. package/dist/utils/index.js +4 -18
  94. package/package.json +26 -5
  95. package/src/actions/public/getEntity.ts +27 -43
  96. package/src/actions/public/getEntityNonce.ts +30 -0
  97. package/src/actions/public/predictEntityKeys.test.ts +143 -0
  98. package/src/actions/public/predictEntityKeys.ts +138 -0
  99. package/src/actions/public/query.ts +33 -69
  100. package/src/actions/public/watchEntityEvents.test.ts +414 -0
  101. package/src/actions/public/watchEntityEvents.ts +225 -0
  102. package/src/actions/wallet/changeOwnership.ts +2 -2
  103. package/src/actions/wallet/createEntity.ts +99 -24
  104. package/src/actions/wallet/extendEntity.ts +37 -13
  105. package/src/actions/wallet/mutateEntities.test.ts +76 -0
  106. package/src/actions/wallet/mutateEntities.ts +19 -21
  107. package/src/actions/wallet/patchEntity.ts +90 -0
  108. package/src/attr/attributes.test.ts +120 -0
  109. package/src/attr/attributes.ts +190 -0
  110. package/src/attr/codec.test.ts +162 -0
  111. package/src/attr/codec.ts +220 -0
  112. package/src/attr/decimal.ts +86 -0
  113. package/src/attr/errors.ts +145 -0
  114. package/src/attr/index.ts +50 -0
  115. package/src/attr/names.ts +106 -0
  116. package/src/attr/types.ts +150 -0
  117. package/src/attr/values.test.ts +271 -0
  118. package/src/attr/values.ts +502 -0
  119. package/src/chains/cheesecake.ts +22 -0
  120. package/src/chains/index.ts +1 -2
  121. package/src/clients/createPublicClient.test.ts +3 -5
  122. package/src/clients/createPublicClient.ts +5 -4
  123. package/src/clients/createWalletClient.test.ts +2 -2
  124. package/src/clients/createWalletClient.ts +13 -7
  125. package/src/clients/decorators/arkivPublic.ts +160 -139
  126. package/src/clients/decorators/arkivWallet.ts +86 -94
  127. package/src/entity/create.test.ts +184 -0
  128. package/src/entity/errors.ts +38 -0
  129. package/src/entity/events.ts +16 -0
  130. package/src/entity/expiry.test.ts +195 -0
  131. package/src/entity/expiry.ts +181 -0
  132. package/src/entity/flags.ts +65 -0
  133. package/src/entity/index.ts +14 -0
  134. package/src/entity/key.ts +68 -0
  135. package/src/entity/operations.ts +134 -0
  136. package/src/entity/params.ts +15 -0
  137. package/src/errors.ts +26 -49
  138. package/src/index.ts +6 -0
  139. package/src/query/engine.ts +59 -139
  140. package/src/query/errors.ts +151 -0
  141. package/src/query/expression.test.ts +286 -0
  142. package/src/query/expression.ts +493 -0
  143. package/src/query/index.ts +40 -4
  144. package/src/query/injection.test.ts +151 -0
  145. package/src/query/queryBuilder.test.ts +322 -637
  146. package/src/query/queryBuilder.ts +126 -359
  147. package/src/query/queryResult.ts +54 -51
  148. package/src/query/selection.test.ts +48 -98
  149. package/src/query/selection.ts +77 -65
  150. package/src/types/entity.test.ts +44 -155
  151. package/src/types/entity.ts +60 -52
  152. package/src/types/events.ts +59 -16
  153. package/src/types/index.ts +20 -17
  154. package/src/types/rpcSchema.ts +69 -37
  155. package/src/utils/arkivTransactions.test.ts +600 -155
  156. package/src/utils/arkivTransactions.ts +181 -190
  157. package/src/utils/chains.test.ts +21 -24
  158. package/src/utils/compression.ts +1 -1
  159. package/src/utils/entities.test.ts +62 -0
  160. package/src/utils/entities.ts +109 -37
  161. package/src/utils/expirationTime.ts +114 -10
  162. package/src/utils/logger.ts +2 -2
  163. package/src/utils/revert.test.ts +99 -0
  164. package/src/utils/revert.ts +145 -0
  165. package/dist/chains-BifaY04V.cjs +0 -108
  166. package/dist/chains-CrtkVbBh.js +0 -86
  167. package/dist/chains-CrtkVbBh.js.map +0 -1
  168. package/dist/consts-Bz0LZQZY.js +0 -7
  169. package/dist/consts-Bz0LZQZY.js.map +0 -1
  170. package/dist/consts-CxT1-RXU.cjs +0 -18
  171. package/dist/entity-BhSXaB23.cjs +0 -81
  172. package/dist/entity-CN41IOrq.d.cts +0 -401
  173. package/dist/entity-CN41IOrq.d.cts.map +0 -1
  174. package/dist/entity-DGfVyB-a.js +0 -71
  175. package/dist/entity-DGfVyB-a.js.map +0 -1
  176. package/dist/entity-NnEVlePN.d.ts +0 -401
  177. package/dist/entity-NnEVlePN.d.ts.map +0 -1
  178. package/dist/index-ClkpS6s_.d.ts +0 -481
  179. package/dist/index-ClkpS6s_.d.ts.map +0 -1
  180. package/dist/index-w4qop0Pe.d.cts +0 -481
  181. package/dist/index-w4qop0Pe.d.cts.map +0 -1
  182. package/dist/payload-BpNUPOnY.js +0 -13
  183. package/dist/payload-BpNUPOnY.js.map +0 -1
  184. package/dist/payload-C2KHOUoj.d.ts +0 -6
  185. package/dist/payload-C2KHOUoj.d.ts.map +0 -1
  186. package/dist/payload-CJN8fTvX.d.cts +0 -6
  187. package/dist/payload-CJN8fTvX.d.cts.map +0 -1
  188. package/dist/payload-D8YLKcJj.cjs +0 -23
  189. package/dist/query/index.js.map +0 -1
  190. package/dist/queryBuilder-BE_XhLLa.js +0 -735
  191. package/dist/queryBuilder-BE_XhLLa.js.map +0 -1
  192. package/dist/queryBuilder-J37buGCi.cjs +0 -865
  193. package/dist/utils/index.d.cts.map +0 -1
  194. package/dist/utils/index.d.ts.map +0 -1
  195. package/dist/utils/index.js.map +0 -1
  196. package/src/actions/public/buildQuery.ts +0 -10
  197. package/src/actions/public/subscribeEntityEvents.ts +0 -93
  198. package/src/actions/wallet/updateEntity.ts +0 -59
  199. package/src/chains/braga.ts +0 -26
  200. package/src/chains/kaolin.ts +0 -29
  201. package/src/query/engine.test.ts +0 -571
  202. package/src/query/predicate.test.ts +0 -461
  203. package/src/query/predicate.ts +0 -144
  204. package/src/query/queryResult.test.ts +0 -270
  205. package/src/query/selectQueryBuilder.test.ts +0 -213
  206. package/src/types/attributes.ts +0 -28
  207. package/src/utils/validation.test.ts +0 -106
  208. package/src/utils/validation.ts +0 -80
  209. /package/dist/{types-Ci4BRClw.js → types-Bx1EKe0v.js} +0 -0
  210. /package/dist/{types-a8gm_IaQ.cjs → types-CKPddlfS.cjs} +0 -0
@@ -0,0 +1,286 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { addr, bytes32, dec, i32, key, str, u64, u256 } from "../attr"
3
+ import { InvalidAttributeNameError } from "../attr/errors"
4
+ import { InvalidPredicateError, UnsupportedOperatorError } from "./errors"
5
+ import {
6
+ and,
7
+ eq,
8
+ exists,
9
+ gt,
10
+ gte,
11
+ hasType,
12
+ lt,
13
+ lte,
14
+ ne,
15
+ not,
16
+ or,
17
+ render,
18
+ startsWith,
19
+ } from "./expression"
20
+
21
+ const ENTITY_KEY = `0x${"ab".repeat(32)}` as const
22
+ const ACCOUNT = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
23
+
24
+ describe("literals", () => {
25
+ it("writes every type in its query spelling", () => {
26
+ expect(String(eq("level", i32(10)))).toBe("level = i32(10)")
27
+ expect(String(eq("level", i32(-42)))).toBe("level = i32(-42)")
28
+ expect(String(eq("balance", u256(1_000_000n)))).toBe("balance = u256(1000000)")
29
+ expect(String(eq("score", dec("3.5")))).toBe("score = dec(3.5)")
30
+ expect(String(eq("name", str("Bob")))).toBe("name = str('Bob')")
31
+ expect(String(eq("parent", key(ENTITY_KEY)))).toBe(`parent = key(${ENTITY_KEY})`)
32
+ expect(String(eq("hash", bytes32(ENTITY_KEY)))).toBe(`hash = bytes32(${ENTITY_KEY})`)
33
+ expect(String(eq("who", addr(ACCOUNT)))).toBe(`who = addr(${ACCOUNT})`)
34
+ })
35
+
36
+ it("writes bool bare — it is the one type with no wrapper", () => {
37
+ expect(String(eq("flagged", true))).toBe("flagged = true")
38
+ expect(String(eq("flagged", false))).toBe("flagged = false")
39
+ })
40
+
41
+ it("tags what the language would have inferred", () => {
42
+ // An untagged number literal defaults to i32, so `level = 10` would parse the same. Writing it
43
+ // out keeps the exact-type assertion — the thing that decides whether a u256 `level` matches —
44
+ // visible in the query that actually went over the wire.
45
+ expect(String(eq("level", 10))).toBe("level = i32(10)")
46
+ })
47
+
48
+ it("gives bare values the same types they take when written", () => {
49
+ expect(String(eq("level", 10))).toBe("level = i32(10)")
50
+ expect(String(eq("balance", 10n))).toBe("balance = u256(10)")
51
+ expect(String(eq("name", "Bob"))).toBe("name = str('Bob')")
52
+ expect(String(eq("flagged", true))).toBe("flagged = true")
53
+ })
54
+
55
+ it("escapes a quote by doubling it", () => {
56
+ expect(String(eq("name", "O'Brien"))).toBe("name = str('O''Brien')")
57
+ expect(String(eq("name", "''"))).toBe("name = str('''''')")
58
+ })
59
+
60
+ it("keeps the checksummed form of an address", () => {
61
+ expect(String(eq("$owner", addr(ACCOUNT.toLowerCase())))).toBe(`$owner = addr(${ACCOUNT})`)
62
+ })
63
+ })
64
+
65
+ describe("comparisons", () => {
66
+ it("renders each operator", () => {
67
+ expect(String(eq("a", 1))).toBe("a = i32(1)")
68
+ expect(String(ne("a", 1))).toBe("a != i32(1)")
69
+ expect(String(gt("a", 1))).toBe("a > i32(1)")
70
+ expect(String(gte("a", 1))).toBe("a >= i32(1)")
71
+ expect(String(lt("a", 1))).toBe("a < i32(1)")
72
+ expect(String(lte("a", 1))).toBe("a <= i32(1)")
73
+ })
74
+
75
+ it("allows range operators only on the ordered types", () => {
76
+ expect(String(gt("level", i32(1)))).toBe("level > i32(1)")
77
+ expect(String(gt("balance", u256(1n)))).toBe("balance > u256(1)")
78
+ expect(String(gt("score", dec("1.5")))).toBe("score > dec(1.5)")
79
+
80
+ // The node treats a range operator on an equality-only type as a parse error rather than an
81
+ // empty result, so catching it here saves a round trip for an error code.
82
+ for (const value of [str("a"), addr(ACCOUNT), key(ENTITY_KEY), bytes32(ENTITY_KEY), true]) {
83
+ expect(() => gt("a", value)).toThrow(UnsupportedOperatorError)
84
+ }
85
+ expect(() => lte("name", "Bob")).toThrow(/does not define <= for a str/)
86
+ })
87
+
88
+ it("carries the offending operator and type on the error", () => {
89
+ try {
90
+ gte("name", "Bob")
91
+ expect.unreachable()
92
+ } catch (error) {
93
+ expect(error).toBeInstanceOf(UnsupportedOperatorError)
94
+ const { attributeName, operator, tag } = error as UnsupportedOperatorError
95
+ expect({ attributeName, operator, tag }).toEqual({
96
+ attributeName: "name",
97
+ operator: ">=",
98
+ tag: "str",
99
+ })
100
+ }
101
+ })
102
+
103
+ it("validates the attribute name against the write-path grammar", () => {
104
+ expect(() => eq("2fast", 1)).toThrow(InvalidAttributeNameError)
105
+ expect(() => eq("has space", 1)).toThrow(InvalidAttributeNameError)
106
+ // Reserved by the query language: `not = i32(1)` could never be parsed as a comparison.
107
+ expect(() => eq("not", 1)).toThrow(InvalidAttributeNameError)
108
+ expect(() => eq("str", 1)).toThrow(InvalidAttributeNameError)
109
+ expect(() => eq("a".repeat(33), 1)).toThrow(InvalidAttributeNameError)
110
+ })
111
+ })
112
+
113
+ describe("system attributes", () => {
114
+ it("filters on the four that carry an index", () => {
115
+ expect(String(eq("$key", key(ENTITY_KEY)))).toBe(`$key = key(${ENTITY_KEY})`)
116
+ expect(String(eq("$owner", addr(ACCOUNT)))).toBe(`$owner = addr(${ACCOUNT})`)
117
+ expect(String(ne("$creator", addr(ACCOUNT)))).toBe(`$creator != addr(${ACCOUNT})`)
118
+ expect(String(lt("$expiresAt", u64(1_200_000n)))).toBe("$expiresAt < u64(1200000)")
119
+ })
120
+
121
+ it("holds each one to its fixed type", () => {
122
+ // The trap this closes: a bare string is a `str`, so `eq("$owner", "0xabc…")` would otherwise
123
+ // send `$owner = str('0xabc…')` and match nothing at all.
124
+ expect(() => eq("$owner", ACCOUNT)).toThrow(InvalidPredicateError)
125
+ expect(() => eq("$owner", ACCOUNT)).toThrow(/is a addr, but the value is a str/)
126
+ expect(() => eq("$key", ENTITY_KEY)).toThrow(/write key\(\.\.\.\) around the value/)
127
+ expect(() => eq("$expiresAt", 1_200_000)).toThrow(/is a u64, but the value is a i32/)
128
+ expect(() => eq("$expiresAt", 1_200_000n)).toThrow(/write u64\(\.\.\.\) around the value/)
129
+ })
130
+
131
+ it("allows range comparisons only on $expiresAt", () => {
132
+ expect(() => gt("$owner", addr(ACCOUNT))).toThrow(UnsupportedOperatorError)
133
+ expect(() => gt("$key", key(ENTITY_KEY))).toThrow(UnsupportedOperatorError)
134
+ })
135
+
136
+ it("rejects the ones that are returned but not indexed", () => {
137
+ for (const name of ["$createdAt", "$updatedAt", "$creationFlags", "$contentType", "$payload"]) {
138
+ expect(() => eq(name, 1)).toThrow(/carries no index, so it cannot be filtered on/)
139
+ }
140
+ })
141
+
142
+ it("rejects a $ name that is not a system attribute at all", () => {
143
+ expect(() => eq("$nope", 1)).toThrow(/is not a system attribute/)
144
+ expect(() => eq("$nope", 1)).toThrow(/\$key, \$owner, \$creator, \$expiresAt/)
145
+ })
146
+
147
+ it("rejects EXISTS and TYPEOF on them, which would be constants", () => {
148
+ expect(() => exists("$owner")).toThrow(/set on every entity/)
149
+ expect(() => hasType("$owner", "addr")).toThrow(/always a addr/)
150
+ })
151
+
152
+ it("rejects STARTSWITH — none of them is a str", () => {
153
+ expect(() => startsWith("$owner", "0xd8")).toThrow(UnsupportedOperatorError)
154
+ })
155
+ })
156
+
157
+ describe("startsWith", () => {
158
+ it("takes a bare string or a str value", () => {
159
+ expect(String(startsWith("desc", "ab"))).toBe("desc STARTSWITH str('ab')")
160
+ expect(String(startsWith("desc", str("ab")))).toBe("desc STARTSWITH str('ab')")
161
+ })
162
+
163
+ it("escapes the prefix like any other string literal", () => {
164
+ expect(String(startsWith("desc", "O'"))).toBe("desc STARTSWITH str('O''')")
165
+ })
166
+
167
+ it("rejects a non-str value", () => {
168
+ // @ts-expect-error - only a str has a prefix index
169
+ expect(() => startsWith("level", i32(1))).toThrow(UnsupportedOperatorError)
170
+ })
171
+ })
172
+
173
+ describe("exists and hasType", () => {
174
+ it("renders the function-call forms", () => {
175
+ expect(String(exists("reviewedBy"))).toBe("EXISTS(reviewedBy)")
176
+ expect(String(hasType("age", "i32"))).toBe("TYPEOF(age) = i32")
177
+ })
178
+
179
+ it("rejects a type that cannot be set on an attribute", () => {
180
+ // @ts-expect-error - bytes backs $payload and is never a user attribute
181
+ expect(() => hasType("blob", "bytes")).toThrow(/is not a settable attribute type/)
182
+ // @ts-expect-error - not a type at all
183
+ expect(() => hasType("age", "int")).toThrow(InvalidPredicateError)
184
+ })
185
+
186
+ it("does not mistake an inherited Object key for a type tag", () => {
187
+ // `tag in TYPE_IDS` walks the prototype chain, so `hasType("a", "toString")` rendered
188
+ // `TYPEOF(a) = toString` — a query the node can only answer with a parse error.
189
+ for (const inherited of ["constructor", "toString", "valueOf", "hasOwnProperty"]) {
190
+ // @ts-expect-error - not a type tag
191
+ expect(() => hasType("age", inherited)).toThrow(/is not a settable attribute type/)
192
+ }
193
+ })
194
+
195
+ it("validates the name", () => {
196
+ expect(() => exists("2fast")).toThrow(InvalidAttributeNameError)
197
+ expect(() => hasType("2fast", "i32")).toThrow(InvalidAttributeNameError)
198
+ })
199
+ })
200
+
201
+ describe("combinators", () => {
202
+ it("joins with the keyword operators", () => {
203
+ expect(String(and(eq("a", 1), eq("b", 2)))).toBe("a = i32(1) AND b = i32(2)")
204
+ expect(String(or(eq("a", 1), eq("b", 2)))).toBe("a = i32(1) OR b = i32(2)")
205
+ expect(String(not(exists("closedAt")))).toBe("NOT EXISTS(closedAt)")
206
+ })
207
+
208
+ it("takes an array as well as separate arguments", () => {
209
+ expect(String(and([eq("a", 1), eq("b", 2)]))).toBe("a = i32(1) AND b = i32(2)")
210
+ expect(String(or([eq("a", 1), eq("b", 2)]))).toBe("a = i32(1) OR b = i32(2)")
211
+ })
212
+
213
+ it("returns a lone expression unwrapped", () => {
214
+ const one = eq("a", 1)
215
+ expect(and(one)).toBe(one)
216
+ expect(or([one])).toBe(one)
217
+ })
218
+
219
+ it("refuses to combine nothing", () => {
220
+ expect(() => and()).toThrow(InvalidPredicateError)
221
+ expect(() => or([])).toThrow(/needs at least one expression/)
222
+ })
223
+ })
224
+
225
+ describe("precedence", () => {
226
+ it("parenthesises an OR inside an AND, and nothing else", () => {
227
+ expect(String(and(eq("a", 1), or(eq("b", 2), eq("c", 3))))).toBe(
228
+ "a = i32(1) AND (b = i32(2) OR c = i32(3))",
229
+ )
230
+ // AND binds tighter than OR, so the nesting the other way round needs no parentheses.
231
+ expect(String(or(eq("a", 1), and(eq("b", 2), eq("c", 3))))).toBe(
232
+ "a = i32(1) OR b = i32(2) AND c = i32(3)",
233
+ )
234
+ })
235
+
236
+ it("flattens same-operator nesting rather than bracketing it", () => {
237
+ expect(String(and(eq("a", 1), and(eq("b", 2), eq("c", 3))))).toBe(
238
+ "a = i32(1) AND b = i32(2) AND c = i32(3)",
239
+ )
240
+ })
241
+
242
+ it("parenthesises anything under NOT that is not a leaf", () => {
243
+ expect(String(not(and(eq("a", 1), eq("b", 2))))).toBe("NOT (a = i32(1) AND b = i32(2))")
244
+ expect(String(not(or(eq("a", 1), eq("b", 2))))).toBe("NOT (a = i32(1) OR b = i32(2))")
245
+ // A comparison is a primary, so it needs none.
246
+ expect(String(not(eq("a", 1)))).toBe("NOT a = i32(1)")
247
+ expect(String(not(not(eq("a", 1))))).toBe("NOT NOT a = i32(1)")
248
+ })
249
+
250
+ it("renders the spec's worked example", () => {
251
+ const q = and(
252
+ gte("level", i32(10)),
253
+ gte("score", dec("3.5")),
254
+ lte("score", dec("5")),
255
+ eq("parent", key(ENTITY_KEY)),
256
+ eq("flagged", true),
257
+ startsWith("desc", str("ab")),
258
+ or(eq("status", str("open")), not(exists("closedAt"))),
259
+ lt("$expiresAt", u64(1_200_000n)),
260
+ )
261
+ expect(q.toString()).toBe(
262
+ "level >= i32(10) AND score >= dec(3.5) AND score <= dec(5) AND " +
263
+ `parent = key(${ENTITY_KEY}) AND flagged = true AND desc STARTSWITH str('ab') AND ` +
264
+ "(status = str('open') OR NOT EXISTS(closedAt)) AND $expiresAt < u64(1200000)",
265
+ )
266
+ })
267
+ })
268
+
269
+ describe("the expression tree", () => {
270
+ it("is inspectable, frozen data", () => {
271
+ const expression = eq("level", i32(10))
272
+ expect(expression).toEqual({
273
+ kind: "comparison",
274
+ name: "level",
275
+ operator: "=",
276
+ value: i32(10),
277
+ })
278
+ expect(Object.isFrozen(expression)).toBe(true)
279
+ })
280
+
281
+ it("renders the same string through toString, String() and render()", () => {
282
+ const expression = and(eq("a", 1), not(exists("b")))
283
+ expect(expression.toString()).toBe(render(expression))
284
+ expect(`${expression}`).toBe(render(expression))
285
+ })
286
+ })