@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,493 @@
1
+ import {
2
+ type ArkivValue,
3
+ type StrValue,
4
+ str,
5
+ type TypeTag,
6
+ toValue,
7
+ type UserTypeTag,
8
+ type ValueInput,
9
+ validateAttributeName,
10
+ } from "../attr"
11
+ // Internal seam: the typeId table is the protocol's, not part of the package's public surface.
12
+ import { TYPE_IDS } from "../attr/types"
13
+ import { InvalidPredicateError, UnsupportedOperatorError } from "./errors"
14
+
15
+ export type ComparisonOperator = "=" | "!=" | "<" | "<=" | ">" | ">="
16
+
17
+ /** A comparison between an attribute and a typed literal. */
18
+ export type ComparisonNode = {
19
+ readonly kind: "comparison"
20
+ readonly name: string
21
+ readonly operator: ComparisonOperator
22
+ readonly value: ArkivValue
23
+ }
24
+
25
+ /** A `STARTSWITH` prefix test, the language's only pattern operator. */
26
+ export type StartsWithNode = {
27
+ readonly kind: "startsWith"
28
+ readonly name: string
29
+ readonly value: StrValue
30
+ }
31
+
32
+ /** An `EXISTS(name)` test — true when the attribute is set with any type. */
33
+ export type ExistsNode = {
34
+ readonly kind: "exists"
35
+ readonly name: string
36
+ }
37
+
38
+ /** A `TYPEOF(name) = tag` test. */
39
+ export type TypeOfNode = {
40
+ readonly kind: "typeOf"
41
+ readonly name: string
42
+ readonly tag: UserTypeTag
43
+ }
44
+
45
+ /** The complement of an expression, evaluated against the active-entity set. */
46
+ export type NotNode = {
47
+ readonly kind: "not"
48
+ readonly expression: Expression
49
+ }
50
+
51
+ /** A conjunction. */
52
+ export type AndNode = {
53
+ readonly kind: "and"
54
+ readonly expressions: readonly Expression[]
55
+ }
56
+
57
+ /** A disjunction. */
58
+ export type OrNode = {
59
+ readonly kind: "or"
60
+ readonly expressions: readonly Expression[]
61
+ }
62
+
63
+ /**
64
+ * A query expression: an immutable tree built by the combinators below, whose `toString()` is the
65
+ * exact query string sent over the wire.
66
+ *
67
+ * @example
68
+ * const q = and(gte("level", i32(10)), or(eq("status", str("open")), not(exists("closedAt"))))
69
+ * String(q) // level >= i32(10) AND (status = str('open') OR NOT EXISTS(closedAt))
70
+ */
71
+ export type Expression =
72
+ | ComparisonNode
73
+ | StartsWithNode
74
+ | ExistsNode
75
+ | TypeOfNode
76
+ | NotNode
77
+ | AndNode
78
+ | OrNode
79
+
80
+ /**
81
+ * The system attributes a query may filter on, and the type each one carries.
82
+ *
83
+ * Every other system attribute (`$createdAt`, `$updatedAt`, `$creationFlags`, `$contentType`,
84
+ * `$payload`) is returned in query results but is not indexed, so it cannot appear in a predicate.
85
+ */
86
+ const QUERYABLE_SYSTEM_ATTRIBUTES = {
87
+ $key: "key",
88
+ $owner: "addr",
89
+ $creator: "addr",
90
+ $expiresAt: "u64",
91
+ } as const satisfies Record<string, TypeTag>
92
+
93
+ type SystemAttributeName = keyof typeof QUERYABLE_SYSTEM_ATTRIBUTES
94
+
95
+ /** System attributes a query can ask for but not filter on — they carry no index. */
96
+ const RESULT_ONLY_SYSTEM_ATTRIBUTES = [
97
+ "$createdAt",
98
+ "$updatedAt",
99
+ "$creationFlags",
100
+ "$contentType",
101
+ "$payload",
102
+ ]
103
+
104
+ /** The types that carry an ordered index, and so accept `<` `<=` `>` `>=`. */
105
+ const ORDERED_TAGS: ReadonlySet<TypeTag> = new Set<TypeTag>(["i32", "u64", "u256", "dec"])
106
+
107
+ const RANGE_OPERATORS: ReadonlySet<ComparisonOperator> = new Set<ComparisonOperator>([
108
+ "<",
109
+ "<=",
110
+ ">",
111
+ ">=",
112
+ ])
113
+
114
+ /**
115
+ * Checks an attribute reference, returning its fixed type when it names a system attribute.
116
+ *
117
+ * User attribute names go through the same grammar the write path enforces, so a name that could
118
+ * never have been written is rejected here rather than matching nothing.
119
+ */
120
+ function resolveReference(name: string): TypeTag | undefined {
121
+ if (typeof name !== "string" || !name.startsWith("$")) {
122
+ validateAttributeName(name)
123
+ return undefined
124
+ }
125
+ const system = QUERYABLE_SYSTEM_ATTRIBUTES[name as SystemAttributeName]
126
+ if (system !== undefined) return system
127
+
128
+ if (RESULT_ONLY_SYSTEM_ATTRIBUTES.includes(name)) {
129
+ throw new InvalidPredicateError(
130
+ `"${name}" is returned in query results but carries no index, so it cannot be filtered ` +
131
+ "on. Select it and filter the results in JavaScript instead.",
132
+ name,
133
+ )
134
+ }
135
+ throw new InvalidPredicateError(
136
+ `"${name}" is not a system attribute. The queryable ones are ` +
137
+ `${Object.keys(QUERYABLE_SYSTEM_ATTRIBUTES).join(", ")}. ` +
138
+ '"$" is reserved for these, so a user attribute cannot be named this.',
139
+ name,
140
+ )
141
+ }
142
+
143
+ /**
144
+ * Whether a string names a type a client may set. `bytes` is excluded: it backs `$payload`, which
145
+ * is neither settable nor queryable.
146
+ *
147
+ * Takes a `string` rather than a `UserTypeTag` on purpose — the callers' static types already say
148
+ * as much, and this is the runtime check for the code that has no types.
149
+ */
150
+ function isUserTypeTag(tag: string): tag is UserTypeTag {
151
+ // `Object.hasOwn` rather than `in`, which would also accept every Object.prototype key.
152
+ return Object.hasOwn(TYPE_IDS, tag) && tag !== "bytes"
153
+ }
154
+
155
+ /** Freezes a node and gives it a (non-enumerable, so still `toEqual`-able) rendering. */
156
+ function node<T extends Expression>(value: T): T {
157
+ Object.defineProperty(value, "toString", {
158
+ value: () => render(value),
159
+ enumerable: false,
160
+ })
161
+ return Object.freeze(value)
162
+ }
163
+
164
+ function comparison(name: string, operator: ComparisonOperator, input: ValueInput): ComparisonNode {
165
+ const system = resolveReference(name)
166
+ const value = toValue(input, name)
167
+
168
+ if (system !== undefined && value.type !== system) {
169
+ throw new InvalidPredicateError(
170
+ `"${name}" is a ${system}, but the value is a ${value.type}. ` +
171
+ `System attributes have a fixed type — write ${system}(...) around the value.`,
172
+ name,
173
+ )
174
+ }
175
+ if (RANGE_OPERATORS.has(operator) && !ORDERED_TAGS.has(value.type)) {
176
+ throw new UnsupportedOperatorError(
177
+ name,
178
+ operator,
179
+ value.type,
180
+ `Only i32, u64, u256 and dec carry an ordered index; ${value.type} is equality-only.`,
181
+ )
182
+ }
183
+ return node({ kind: "comparison", name, operator, value })
184
+ }
185
+
186
+ /**
187
+ * Attribute equals value — set with this exact type, and holding this value.
188
+ *
189
+ * Bare booleans, numbers, bigints and strings take the same default types they take when written
190
+ * (`number` is an `i32`, `bigint` a `u256`, `string` a `str`); anything else names its type.
191
+ *
192
+ * @throws {InvalidPredicateError} If the name is not a queryable attribute, or a system attribute
193
+ * is compared against the wrong type.
194
+ *
195
+ * @example
196
+ * eq("status", str("open")) // status = str('open')
197
+ * eq("flagged", true) // flagged = true
198
+ * eq("$owner", addr(account)) // $owner = addr(0xd8dA…)
199
+ */
200
+ export function eq(name: string, value: ValueInput): ComparisonNode {
201
+ return comparison(name, "=", value)
202
+ }
203
+
204
+ /**
205
+ * Attribute is set with this type and holds a **different** value.
206
+ *
207
+ * This is typed value-negation, not the complement: an entity that never set the attribute, or set
208
+ * it with another type, does not match. Use `not(eq(...))` for the full complement.
209
+ *
210
+ * @example
211
+ * ne("status", str("closed")) // status != str('closed')
212
+ * not(eq("status", str("closed"))) // NOT status = str('closed') — also matches entities with no status
213
+ */
214
+ export function ne(name: string, value: ValueInput): ComparisonNode {
215
+ return comparison(name, "!=", value)
216
+ }
217
+
218
+ /**
219
+ * Attribute is greater than value. Ordered types only (`i32`, `u256`, `dec`).
220
+ *
221
+ * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.
222
+ *
223
+ * @example
224
+ * gt("level", i32(10)) // level > i32(10)
225
+ * gt("$expiresAt", u64(1_200_000n)) // $expiresAt > u64(1200000)
226
+ */
227
+ export function gt(name: string, value: ValueInput): ComparisonNode {
228
+ return comparison(name, ">", value)
229
+ }
230
+
231
+ /**
232
+ * Attribute is greater than or equal to value. Ordered types only (`i32`, `u256`, `dec`).
233
+ *
234
+ * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.
235
+ *
236
+ * @example
237
+ * gte("score", dec("3.5")) // score >= dec(3.5)
238
+ */
239
+ export function gte(name: string, value: ValueInput): ComparisonNode {
240
+ return comparison(name, ">=", value)
241
+ }
242
+
243
+ /**
244
+ * Attribute is less than value. Ordered types only (`i32`, `u256`, `dec`).
245
+ *
246
+ * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.
247
+ *
248
+ * @example
249
+ * lt("level", i32(10)) // level < i32(10)
250
+ */
251
+ export function lt(name: string, value: ValueInput): ComparisonNode {
252
+ return comparison(name, "<", value)
253
+ }
254
+
255
+ /**
256
+ * Attribute is less than or equal to value. Ordered types only (`i32`, `u256`, `dec`).
257
+ *
258
+ * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.
259
+ *
260
+ * @example
261
+ * lte("score", dec("5")) // score <= dec(5)
262
+ */
263
+ export function lte(name: string, value: ValueInput): ComparisonNode {
264
+ return comparison(name, "<=", value)
265
+ }
266
+
267
+ /**
268
+ * Attribute is a `str` beginning with this prefix.
269
+ *
270
+ * The match is on **raw UTF-8 bytes** with no normalization — it is the index encoding, so
271
+ * `startsWith("name", "é")` matches only the same byte sequence, not another normal form of it.
272
+ *
273
+ * @param prefix - The prefix, as a bare string or a `str` value.
274
+ * @throws {UnsupportedOperatorError} If given a value that is not a `str`.
275
+ *
276
+ * @example
277
+ * startsWith("desc", "ab") // desc STARTSWITH str('ab')
278
+ */
279
+ export function startsWith(name: string, prefix: string | StrValue): StartsWithNode {
280
+ const system = resolveReference(name)
281
+ const value = typeof prefix === "string" ? str(prefix) : toValue(prefix, name)
282
+ if (value.type !== "str") {
283
+ throw new UnsupportedOperatorError(
284
+ name,
285
+ "STARTSWITH",
286
+ value.type,
287
+ "Only str carries a prefix index.",
288
+ )
289
+ }
290
+ if (system !== undefined) {
291
+ throw new UnsupportedOperatorError(
292
+ name,
293
+ "STARTSWITH",
294
+ system,
295
+ "No queryable system attribute is a str.",
296
+ )
297
+ }
298
+ return node({ kind: "startsWith", name, value })
299
+ }
300
+
301
+ /**
302
+ * The attribute is set, with any type.
303
+ *
304
+ * @throws {InvalidPredicateError} If given a system attribute — every entity has all of them, so
305
+ * the test would be a constant.
306
+ *
307
+ * @example
308
+ * exists("reviewedBy") // EXISTS(reviewedBy)
309
+ * not(exists("closedAt")) // NOT EXISTS(closedAt)
310
+ */
311
+ export function exists(name: string): ExistsNode {
312
+ if (resolveReference(name) !== undefined) {
313
+ throw new InvalidPredicateError(
314
+ `"${name}" is a system attribute, so it is set on every entity and EXISTS(${name}) is ` +
315
+ "always true.",
316
+ name,
317
+ )
318
+ }
319
+ return node({ kind: "exists", name })
320
+ }
321
+
322
+ /**
323
+ * The attribute is set with exactly this type, whatever its value.
324
+ *
325
+ * Useful when the same name is written with different types across entities — a comparison already
326
+ * asserts the type, so this is for asking about the type alone.
327
+ *
328
+ * @param tag - The type tag: `bool`, `i32`, `u256`, `dec`, `bytes32`, `str`, `addr` or `key`.
329
+ * @throws {InvalidPredicateError} If the tag is not a settable type, or the name is a system
330
+ * attribute (whose type is fixed).
331
+ *
332
+ * @example
333
+ * hasType("age", "i32") // TYPEOF(age) = i32
334
+ */
335
+ export function hasType(name: string, tag: UserTypeTag): TypeOfNode {
336
+ const system = resolveReference(name)
337
+ if (system !== undefined) {
338
+ throw new InvalidPredicateError(
339
+ `"${name}" is always a ${system} — a system attribute has a fixed type, so ` +
340
+ `TYPEOF(${name}) is a constant.`,
341
+ name,
342
+ )
343
+ }
344
+ if (!isUserTypeTag(tag)) {
345
+ throw new InvalidPredicateError(
346
+ `"${tag}" is not a settable attribute type. Use one of ` +
347
+ "bool, i32, u256, dec, bytes32, str, addr, key.",
348
+ name,
349
+ )
350
+ }
351
+ return node({ kind: "typeOf", name, tag })
352
+ }
353
+
354
+ /**
355
+ * The complement of an expression, evaluated against the set of live entities — unlike `!=`, it
356
+ * matches entities that never set the attribute at all.
357
+ *
358
+ * @example
359
+ * not(exists("closedAt")) // NOT EXISTS(closedAt)
360
+ * not(and(eq("a", 1), eq("b", 2))) // NOT (a = i32(1) AND b = i32(2))
361
+ */
362
+ export function not(expression: Expression): NotNode {
363
+ return node({ kind: "not", expression })
364
+ }
365
+
366
+ /**
367
+ * All of these must hold.
368
+ *
369
+ * A single expression is returned unwrapped, so building a conjunction from a filtered list does
370
+ * not litter the query with redundant nesting.
371
+ *
372
+ * @param expressions - The expressions, as separate arguments or one array.
373
+ * @throws {InvalidPredicateError} If given nothing to combine.
374
+ *
375
+ * @example
376
+ * and(gte("level", i32(10)), eq("status", "open"))
377
+ * and(filters) // an array works too
378
+ */
379
+ export function and(expressions: readonly Expression[]): Expression
380
+ export function and(...expressions: Expression[]): Expression
381
+ export function and(...expressions: (Expression | readonly Expression[])[]): Expression {
382
+ return combine("and", expressions.flat())
383
+ }
384
+
385
+ /**
386
+ * At least one of these must hold.
387
+ *
388
+ * A single expression is returned unwrapped.
389
+ *
390
+ * @param expressions - The expressions, as separate arguments or one array.
391
+ * @throws {InvalidPredicateError} If given nothing to combine.
392
+ *
393
+ * @example
394
+ * or(eq("status", "open"), not(exists("closedAt")))
395
+ */
396
+ export function or(expressions: readonly Expression[]): Expression
397
+ export function or(...expressions: Expression[]): Expression
398
+ export function or(...expressions: (Expression | readonly Expression[])[]): Expression {
399
+ return combine("or", expressions.flat())
400
+ }
401
+
402
+ function combine(kind: "and" | "or", expressions: Expression[]): Expression {
403
+ if (expressions.length === 0) {
404
+ throw new InvalidPredicateError(
405
+ `${kind}() needs at least one expression. An empty ${kind === "and" ? "conjunction" : "disjunction"} ` +
406
+ `has no query-language spelling — leave the ${kind}() out, or omit the filter entirely to match everything.`,
407
+ )
408
+ }
409
+ // A one-element conjunction *is* its element; keeping the wrapper would only add parentheses.
410
+ const only = expressions[0]
411
+ if (expressions.length === 1 && only !== undefined) return only
412
+ return node(
413
+ kind === "and"
414
+ ? { kind: "and", expressions: Object.freeze(expressions) }
415
+ : { kind: "or", expressions: Object.freeze(expressions) },
416
+ )
417
+ }
418
+
419
+ /**
420
+ * Binding strength, tightest last — `NOT` binds tighter than `AND`, which binds tighter than `OR`.
421
+ * A leaf never needs parentheses, so it sits above them all.
422
+ */
423
+ const PRECEDENCE = { or: 1, and: 2, not: 3, leaf: 4 } as const
424
+
425
+ function precedenceOf(expression: Expression): number {
426
+ switch (expression.kind) {
427
+ case "or":
428
+ return PRECEDENCE.or
429
+ case "and":
430
+ return PRECEDENCE.and
431
+ case "not":
432
+ return PRECEDENCE.not
433
+ default:
434
+ return PRECEDENCE.leaf
435
+ }
436
+ }
437
+
438
+ /** Renders a subexpression, parenthesising it only where the precedence would otherwise change it. */
439
+ function nested(expression: Expression, minimum: number): string {
440
+ const rendered = render(expression)
441
+ return precedenceOf(expression) < minimum ? `(${rendered})` : rendered
442
+ }
443
+
444
+ /**
445
+ * Renders an expression as the query string the node parses. Equivalent to `String(expression)`.
446
+ *
447
+ * Literals are always written tagged, even where the language would infer them: an untagged number
448
+ * defaults to `i32`, but spelling it `i32(10)` keeps the exact-type assertion visible in the query
449
+ * that actually went over the wire.
450
+ */
451
+ export function render(expression: Expression): string {
452
+ switch (expression.kind) {
453
+ case "comparison":
454
+ return `${expression.name} ${expression.operator} ${renderLiteral(expression.value)}`
455
+ case "startsWith":
456
+ return `${expression.name} STARTSWITH ${renderLiteral(expression.value)}`
457
+ case "exists":
458
+ return `EXISTS(${expression.name})`
459
+ case "typeOf":
460
+ return `TYPEOF(${expression.name}) = ${expression.tag}`
461
+ case "not":
462
+ return `NOT ${nested(expression.expression, PRECEDENCE.not)}`
463
+ case "and":
464
+ return expression.expressions.map((sub) => nested(sub, PRECEDENCE.and)).join(" AND ")
465
+ case "or":
466
+ return expression.expressions.map((sub) => nested(sub, PRECEDENCE.or)).join(" OR ")
467
+ }
468
+ }
469
+
470
+ /** Renders a typed value as its query literal. `bool` is the one type written bare. */
471
+ function renderLiteral(value: ArkivValue): string {
472
+ switch (value.type) {
473
+ case "bool":
474
+ return value.value ? "true" : "false"
475
+ case "i32":
476
+ return `i32(${value.value})`
477
+ case "u64":
478
+ return `u64(${value.value})`
479
+ case "u256":
480
+ return `u256(${value.value})`
481
+ case "dec":
482
+ return `dec(${value.value})`
483
+ case "bytes32":
484
+ return `bytes32(${value.value})`
485
+ case "key":
486
+ return `key(${value.value})`
487
+ case "addr":
488
+ return `addr(${value.value})`
489
+ case "str":
490
+ // Single-quoted, with '' as the escape for a quote — the language has no backslash escapes.
491
+ return `str('${value.value.replaceAll("'", "''")}')`
492
+ }
493
+ }
@@ -1,4 +1,40 @@
1
- export * from "./predicate"
2
- export * from "./queryBuilder"
3
- export * from "./queryResult"
4
- export * from "./selection"
1
+ export type { QueryRequest, QueryResponse } from "./engine"
2
+ export { MAX_LIMIT, runQuery } from "./engine"
3
+ export type { QueryErrorKind } from "./errors"
4
+ export { InvalidPredicateError, QueryError, UnsupportedOperatorError } from "./errors"
5
+ export type {
6
+ AndNode,
7
+ ComparisonNode,
8
+ ComparisonOperator,
9
+ ExistsNode,
10
+ Expression,
11
+ NotNode,
12
+ OrNode,
13
+ StartsWithNode,
14
+ TypeOfNode,
15
+ } from "./expression"
16
+ export {
17
+ and,
18
+ eq,
19
+ exists,
20
+ gt,
21
+ gte,
22
+ hasType,
23
+ lt,
24
+ lte,
25
+ ne,
26
+ not,
27
+ or,
28
+ render,
29
+ startsWith,
30
+ } from "./expression"
31
+ export { SelectQueryBuilder } from "./queryBuilder"
32
+ export { QueryResult } from "./queryResult"
33
+ export type {
34
+ EntitySelection,
35
+ FullEntity,
36
+ ProjectedEntity,
37
+ SelectArg,
38
+ SelectionFields,
39
+ } from "./selection"
40
+ export { toRpcSelect } from "./selection"
@@ -0,0 +1,151 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { addr, bytes32, dec, i32, key, str, u256 } from "../attr"
3
+ import { InvalidAttributeNameError, InvalidValueError } from "../attr/errors"
4
+ import { and, eq, exists, gt, startsWith } from "./expression"
5
+ import { toRpcSelect } from "./selection"
6
+
7
+ const ACCOUNT = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
8
+ const ENTITY_KEY = `0x${"ab".repeat(32)}` as const
9
+
10
+ /**
11
+ * The query string is built by concatenation, so every value and name that reaches it is checked
12
+ * here against the shapes that would change what the node parses rather than what it matches.
13
+ */
14
+ describe("string literals cannot break out of their quotes", () => {
15
+ it("doubles every quote, which is the language's only escape", () => {
16
+ expect(String(eq("x", str("a' OR '1'='1")))).toBe("x = str('a'' OR ''1''=''1')")
17
+ expect(String(eq("x", str("')")))).toBe("x = str(''')')")
18
+ expect(String(eq("x", str("'")))).toBe("x = str('''')")
19
+ expect(String(startsWith("x", "pre' OR '1"))).toBe("x STARTSWITH str('pre'' OR ''1')")
20
+ })
21
+
22
+ it("leaves a backslash literal — the language has no backslash escape to subvert", () => {
23
+ // If `\` escaped anything, `\''` would close the string. It does not: `\` is an ordinary char,
24
+ // and the quote after it is still doubled.
25
+ expect(String(eq("x", str("a\\")))).toBe("x = str('a\\')")
26
+ // Backslash, then a doubled quote, then the terminator — the backslash buys nothing.
27
+ expect(String(eq("x", str("a\\'")))).toBe("x = str('a\\''')")
28
+ })
29
+
30
+ it("keeps a comment token harmless inside the quotes", () => {
31
+ expect(String(eq("x", str("a -- comment")))).toBe("x = str('a -- comment')")
32
+ })
33
+
34
+ it("rejects control characters, which have no spelling inside a literal", () => {
35
+ // The danger is a newline: `--` comments run to end of line, and a lexer that ends a string at
36
+ // a newline would read the rest of the value as query text. There is no escape for one, so a
37
+ // value carrying it could be stored but never queried — and might not fail loudly.
38
+ for (const code of [0x00, 0x09, 0x0a, 0x0d, 0x1f, 0x7f]) {
39
+ const hostile = `a${String.fromCharCode(code)}AND flagged = true`
40
+ expect(() => str(hostile)).toThrow(InvalidValueError)
41
+ expect(() => str(hostile)).toThrow(/control character U\+/)
42
+ // The bare-string path resolves through `str`, so it is closed too.
43
+ expect(() => eq("x", hostile)).toThrow(InvalidValueError)
44
+ expect(() => startsWith("x", hostile)).toThrow(InvalidValueError)
45
+ }
46
+ })
47
+
48
+ it("names the offending character and where it is", () => {
49
+ expect(() => str(`ab${String.fromCharCode(10)}c`)).toThrow(/U\+000A at index 2/)
50
+ })
51
+
52
+ it("still allows every printable character, including the awkward ones", () => {
53
+ for (const value of ["a b", "a\\b", "a--b", "a)b(", "a=b", "élan", "日本語", 'a"b']) {
54
+ expect(() => str(value)).not.toThrow()
55
+ }
56
+ })
57
+ })
58
+
59
+ describe("attribute names cannot smuggle a comment", () => {
60
+ it("rejects a name containing the comment token", () => {
61
+ // `-` is a legal name character, so `a--b` passes the grammar — and renders a query whose
62
+ // entire remainder is commented out. Verified before the fix: `and(eq("approved", true),
63
+ // eq("a--b", 1), eq("level", 5))` rendered `approved = true AND a--b = i32(1) AND level =
64
+ // i32(5)`, in which the node sees only `approved = true AND a`.
65
+ expect(() => eq("a--b", 1)).toThrow(InvalidAttributeNameError)
66
+ expect(() => eq("a--b", 1)).toThrow(/opens a comment in the query language/)
67
+ expect(() => exists("owner-name--x")).toThrow(InvalidAttributeNameError)
68
+ // Rejected on the write path too, so nothing unqueryable can be stored in the first place.
69
+ expect(() => str("fine")).not.toThrow()
70
+ })
71
+
72
+ it("keeps a single dash, which is unambiguous", () => {
73
+ expect(String(eq("a-b", 1))).toBe("a-b = i32(1)")
74
+ expect(String(eq("a-", 1))).toBe("a- = i32(1)")
75
+ })
76
+
77
+ it("rejects everything else outside the grammar", () => {
78
+ for (const name of ["x' OR '1", "x)", "a b", "x'", "x(", "1x", "x;", "a".repeat(33)]) {
79
+ expect(() => eq(name, 1)).toThrow(InvalidAttributeNameError)
80
+ }
81
+ })
82
+ })
83
+
84
+ describe("typed literals are closed by construction", () => {
85
+ it("renders numerics as digits and nothing else", () => {
86
+ expect(String(eq("x", i32(-2147483648)))).toBe("x = i32(-2147483648)")
87
+ expect(String(eq("x", u256(2n ** 256n - 1n)))).toBe(
88
+ "x = u256(115792089237316195423570985008687907853269984665640564039457584007913129639935)",
89
+ )
90
+ // Large and small decimals stay in positional notation — no exponent for a parser to choke on.
91
+ expect(String(eq("x", dec("1000000000000000000000")))).toBe("x = dec(1000000000000000000000)")
92
+ expect(String(eq("x", dec("-0.000000000000000001")))).toBe("x = dec(-0.000000000000000001)")
93
+ expect(String(eq("x", u256(10n ** 30n)))).not.toMatch(/[eE+]/)
94
+ })
95
+
96
+ it("renders the byte-shaped types as fixed-width hex", () => {
97
+ expect(String(eq("x", key(ENTITY_KEY)))).toMatch(/^x = key\(0x[0-9a-f]{64}\)$/)
98
+ expect(String(eq("x", bytes32(ENTITY_KEY)))).toMatch(/^x = bytes32\(0x[0-9a-f]{64}\)$/)
99
+ expect(String(eq("x", addr(ACCOUNT)))).toMatch(/^x = addr\(0x[0-9a-fA-F]{40}\)$/)
100
+ })
101
+
102
+ it("refuses hostile input at the constructor, before it can be rendered", () => {
103
+ expect(() => u256("1) OR true --" as string)).toThrow(InvalidValueError)
104
+ expect(() => addr("0x0') OR true --")).toThrow(InvalidValueError)
105
+ expect(() => key("0xab') OR true --" as `0x${string}`)).toThrow(InvalidValueError)
106
+ expect(() => dec("1) OR true")).toThrow(InvalidValueError)
107
+ expect(() => i32("1) OR true" as unknown as number)).toThrow(InvalidValueError)
108
+ })
109
+
110
+ it("cannot be handed a hostile value through an untyped object either", () => {
111
+ // toValue re-runs the constructor, so a look-alike carrying a string gets validated, not
112
+ // interpolated.
113
+ expect(() => eq("x", { type: "u256", value: "1) OR true --" } as unknown as string)).toThrow(
114
+ InvalidValueError,
115
+ )
116
+ })
117
+ })
118
+
119
+ describe("the assembled query", () => {
120
+ it("keeps every predicate intact through a hostile filter set", () => {
121
+ const query = String(
122
+ and(
123
+ eq("approved", true),
124
+ eq("note", str("a') OR true --")),
125
+ gt("level", i32(5)),
126
+ eq("$owner", addr(ACCOUNT)),
127
+ ),
128
+ )
129
+ expect(query).toBe(
130
+ "approved = true AND note = str('a'') OR true --') AND level > i32(5) AND " +
131
+ `$owner = addr(${ACCOUNT})`,
132
+ )
133
+ // The guard predicates survive the hostile one, in both directions.
134
+ expect(query).toContain("approved = true")
135
+ expect(query).toContain("level > i32(5)")
136
+ })
137
+ })
138
+
139
+ describe("the selection", () => {
140
+ it("checks named attributes rather than forwarding them", () => {
141
+ expect(() => toRpcSelect({ key: true, attributes: { "a' OR '1": true } })).toThrow(
142
+ InvalidAttributeNameError,
143
+ )
144
+ expect(() => toRpcSelect({ key: true, attributes: { "a--b": true } })).toThrow(
145
+ InvalidAttributeNameError,
146
+ )
147
+ expect(toRpcSelect({ key: true, attributes: { ok: true } })).toMatchObject({
148
+ attributes: { ok: true },
149
+ })
150
+ })
151
+ })