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