@arkiv-network/sdk 0.8.1-dev.1 → 0.9.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +180 -61
  2. package/dist/chains/index.d.ts +17 -17
  3. package/dist/changeOwnership-Cy9gfyjN.d.cts +1160 -0
  4. package/dist/changeOwnership-Cy9gfyjN.d.cts.map +1 -0
  5. package/dist/changeOwnership-gXf-lBqI.d.ts +1160 -0
  6. package/dist/changeOwnership-gXf-lBqI.d.ts.map +1 -0
  7. package/dist/entity/index.cjs +4 -10
  8. package/dist/entity/index.d.cts +3 -3
  9. package/dist/entity/index.d.ts +3 -3
  10. package/dist/entity/index.js +3 -4
  11. package/dist/entity-BM1unrQf.cjs +41 -0
  12. package/dist/{entity-DJ0Gml9c.cjs → entity-BgaFJSaV.cjs} +4 -4
  13. package/dist/entity-CFAI1KyG.js +36 -0
  14. package/dist/entity-CFAI1KyG.js.map +1 -0
  15. package/dist/{entity-CniBgBSd.js → entity-CGqx_AU0.js} +4 -4
  16. package/dist/entity-CGqx_AU0.js.map +1 -0
  17. package/dist/{payload-AfUfSctb.d.ts → expirationTime-C7PZK49K.d.ts} +8 -37
  18. package/dist/expirationTime-C7PZK49K.d.ts.map +1 -0
  19. package/dist/expirationTime-CkIGNAg8.cjs +140 -0
  20. package/dist/{payload-D167BlJR.d.cts → expirationTime-DOB8V4B8.d.cts} +8 -37
  21. package/dist/expirationTime-DOB8V4B8.d.cts.map +1 -0
  22. package/dist/{expiry-Cgdb_dbq.js → expirationTime-UKPuA-LM.js} +62 -27
  23. package/dist/expirationTime-UKPuA-LM.js.map +1 -0
  24. package/dist/expiry-B48I-yTA.d.cts +40 -0
  25. package/dist/expiry-B48I-yTA.d.cts.map +1 -0
  26. package/dist/expiry-S2PUL8Hg.d.ts +40 -0
  27. package/dist/expiry-S2PUL8Hg.d.ts.map +1 -0
  28. package/dist/index-C-mMF5WZ.d.cts +735 -0
  29. package/dist/index-C-mMF5WZ.d.cts.map +1 -0
  30. package/dist/{index-BkqSnGJH.d.ts → index-C4zZqJ9S.d.ts} +5 -27
  31. package/dist/index-C4zZqJ9S.d.ts.map +1 -0
  32. package/dist/{index-CLhsFyVH.d.cts → index-CcVWYB6v.d.cts} +5 -27
  33. package/dist/index-CcVWYB6v.d.cts.map +1 -0
  34. package/dist/index-DCx2XC6S.d.ts +489 -0
  35. package/dist/index-DCx2XC6S.d.ts.map +1 -0
  36. package/dist/index-DLb7BfIw.d.ts +735 -0
  37. package/dist/index-DLb7BfIw.d.ts.map +1 -0
  38. package/dist/index-DzriOUm8.d.cts +489 -0
  39. package/dist/index-DzriOUm8.d.cts.map +1 -0
  40. package/dist/index.cjs +162 -146
  41. package/dist/index.d.cts +180 -133
  42. package/dist/index.d.cts.map +1 -1
  43. package/dist/index.d.ts +180 -133
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +93 -83
  46. package/dist/index.js.map +1 -1
  47. package/dist/params-BuIrJA9a.cjs +45 -0
  48. package/dist/params-DDJOnsNi.js +22 -0
  49. package/dist/params-DDJOnsNi.js.map +1 -0
  50. package/dist/query/index.cjs +9 -4
  51. package/dist/query/index.d.cts +6 -6
  52. package/dist/query/index.d.ts +6 -6
  53. package/dist/query/index.js +4 -5
  54. package/dist/query-D0KtuFh2.cjs +1625 -0
  55. package/dist/query-DpRDozmj.js +1417 -0
  56. package/dist/query-DpRDozmj.js.map +1 -0
  57. package/dist/schema/index.cjs +46 -0
  58. package/dist/schema/index.d.cts +2 -0
  59. package/dist/schema/index.d.ts +2 -0
  60. package/dist/schema/index.js +3 -0
  61. package/dist/{attr-BXTkpnoc.cjs → schema-B9AyXcgU.cjs} +513 -246
  62. package/dist/{attr-tmyREHtw.js → schema-DY4sqehl.js} +427 -244
  63. package/dist/schema-DY4sqehl.js.map +1 -0
  64. package/dist/types/index.cjs +1 -1
  65. package/dist/types/index.d.cts +5 -5
  66. package/dist/types/index.d.ts +5 -5
  67. package/dist/types/index.js +1 -1
  68. package/dist/utils/index.cjs +3 -5
  69. package/dist/utils/index.d.cts +3 -3
  70. package/dist/utils/index.d.ts +3 -3
  71. package/dist/utils/index.js +3 -3
  72. package/package.json +9 -12
  73. package/src/actions/public/getEntity.test.ts +69 -0
  74. package/src/actions/public/getEntity.ts +34 -20
  75. package/src/actions/public/predictEntityKeys.ts +8 -7
  76. package/src/actions/public/query.test.ts +58 -0
  77. package/src/actions/public/query.ts +41 -13
  78. package/src/actions/wallet/createEntity.ts +34 -63
  79. package/src/actions/wallet/executeBatch.test.ts +38 -0
  80. package/src/actions/wallet/executeBatch.ts +26 -9
  81. package/src/actions/wallet/extendEntity.ts +9 -8
  82. package/src/actions/wallet/patchEntity.ts +43 -52
  83. package/src/clients/createPublicClient.test.ts +10 -2
  84. package/src/clients/createWalletClient.ts +4 -1
  85. package/src/clients/decorators/arkivPublic.ts +123 -80
  86. package/src/clients/decorators/arkivWallet.ts +113 -65
  87. package/src/entity/create.test.ts +25 -21
  88. package/src/entity/expiry.ts +8 -6
  89. package/src/entity/flags.ts +1 -1
  90. package/src/entity/index.ts +2 -4
  91. package/src/entity/key.ts +3 -3
  92. package/src/entity/operations.ts +1 -1
  93. package/src/errors.ts +2 -11
  94. package/src/index.ts +18 -6
  95. package/src/query/builderBase.ts +166 -0
  96. package/src/query/countBuilder.test.ts +91 -0
  97. package/src/query/countBuilder.ts +67 -0
  98. package/src/query/engine.ts +9 -3
  99. package/src/query/errors.ts +24 -14
  100. package/src/query/expression.test.ts +129 -21
  101. package/src/query/expression.ts +323 -94
  102. package/src/query/index.ts +26 -4
  103. package/src/query/injection.test.ts +5 -11
  104. package/src/query/queryBuilder.test.ts +467 -58
  105. package/src/query/queryBuilder.ts +191 -98
  106. package/src/query/queryResult.ts +35 -9
  107. package/src/query/selection.test.ts +278 -24
  108. package/src/query/selection.ts +427 -86
  109. package/src/schema/attributes.test.ts +266 -0
  110. package/src/schema/attributes.ts +212 -0
  111. package/src/{attr → schema}/codec.test.ts +37 -5
  112. package/src/{attr → schema}/codec.ts +40 -11
  113. package/src/schema/column.ts +193 -0
  114. package/src/{attr → schema}/errors.ts +67 -10
  115. package/src/schema/index.ts +76 -0
  116. package/src/{attr → schema}/names.ts +4 -2
  117. package/src/schema/schema.test.ts +171 -0
  118. package/src/schema/schema.ts +250 -0
  119. package/src/{attr → schema}/types.ts +36 -30
  120. package/src/{attr → schema}/values.test.ts +62 -3
  121. package/src/{attr → schema}/values.ts +246 -65
  122. package/src/types/entity.test.ts +1 -44
  123. package/src/types/entity.ts +9 -55
  124. package/src/types/events.ts +1 -1
  125. package/src/types/index.ts +11 -4
  126. package/src/types/rpcSchema.ts +22 -16
  127. package/src/utils/arkivTransactions.test.ts +356 -116
  128. package/src/utils/arkivTransactions.ts +30 -38
  129. package/src/utils/entities.ts +10 -9
  130. package/src/utils/expirationTime.ts +5 -5
  131. package/src/utils/index.ts +0 -1
  132. package/src/utils/logger.test.ts +45 -0
  133. package/src/utils/logger.ts +5 -39
  134. package/src/utils/revert.test.ts +25 -0
  135. package/src/utils/revert.ts +28 -3
  136. package/dist/attr/index.cjs +0 -34
  137. package/dist/attr/index.d.cts +0 -2
  138. package/dist/attr/index.d.ts +0 -2
  139. package/dist/attr/index.js +0 -3
  140. package/dist/attr-tmyREHtw.js.map +0 -1
  141. package/dist/entity-B0t6sN7F.cjs +0 -71
  142. package/dist/entity-BIp_df9u.js +0 -67
  143. package/dist/entity-BIp_df9u.js.map +0 -1
  144. package/dist/entity-CniBgBSd.js.map +0 -1
  145. package/dist/expiry-BxOlvVWC.d.ts +0 -79
  146. package/dist/expiry-BxOlvVWC.d.ts.map +0 -1
  147. package/dist/expiry-CG9GhVXv.cjs +0 -135
  148. package/dist/expiry-Cgdb_dbq.js.map +0 -1
  149. package/dist/expiry-DRV29F1r.d.cts +0 -79
  150. package/dist/expiry-DRV29F1r.d.cts.map +0 -1
  151. package/dist/flags-DCD26uhe.cjs +0 -43
  152. package/dist/flags-DUT2gHEU.js +0 -33
  153. package/dist/flags-DUT2gHEU.js.map +0 -1
  154. package/dist/index-BkqSnGJH.d.ts.map +0 -1
  155. package/dist/index-C7fbakoI.d.cts +0 -278
  156. package/dist/index-C7fbakoI.d.cts.map +0 -1
  157. package/dist/index-CIcHPx2j.d.cts +0 -424
  158. package/dist/index-CIcHPx2j.d.cts.map +0 -1
  159. package/dist/index-CLhsFyVH.d.cts.map +0 -1
  160. package/dist/index-Cd2aaWme.d.ts +0 -424
  161. package/dist/index-Cd2aaWme.d.ts.map +0 -1
  162. package/dist/index-Cq6fa8AI.d.ts +0 -278
  163. package/dist/index-Cq6fa8AI.d.ts.map +0 -1
  164. package/dist/index-DaNeJUw8.d.ts +0 -955
  165. package/dist/index-DaNeJUw8.d.ts.map +0 -1
  166. package/dist/index-Dv16LO2s.d.cts +0 -955
  167. package/dist/index-Dv16LO2s.d.cts.map +0 -1
  168. package/dist/payload-AfUfSctb.d.ts.map +0 -1
  169. package/dist/payload-D167BlJR.d.cts.map +0 -1
  170. package/dist/payload-DE7wMztL.js +0 -92
  171. package/dist/payload-DE7wMztL.js.map +0 -1
  172. package/dist/payload-DqFweqiO.cjs +0 -108
  173. package/dist/query-9oojUdM7.cjs +0 -1128
  174. package/dist/query-Ber-lOJQ.js +0 -968
  175. package/dist/query-Ber-lOJQ.js.map +0 -1
  176. package/src/actions/public/getEntityCount.ts +0 -10
  177. package/src/attr/attributes.test.ts +0 -120
  178. package/src/attr/attributes.ts +0 -196
  179. package/src/attr/index.ts +0 -50
  180. package/src/types/mimeTypes.ts +0 -27
  181. package/src/utils/compression.test.ts +0 -32
  182. package/src/utils/compression.ts +0 -102
  183. package/src/utils/payload.ts +0 -36
  184. /package/src/{attr → schema}/decimal.ts +0 -0
@@ -1,65 +1,107 @@
1
1
  import {
2
- type ArkivValue,
2
+ type ColumnsOf,
3
+ describeColumns,
4
+ type IndexableTypeTag,
5
+ type IndexableValue,
6
+ type IndexedColumnName,
7
+ lookupColumn,
8
+ type Schema,
3
9
  type StrValue,
4
10
  str,
11
+ type TagOf,
5
12
  type TypeTag,
6
13
  toValue,
7
- type UserTypeTag,
8
- type ValueInput,
9
14
  validateAttributeName,
10
- } from "../attr"
15
+ } from "../schema"
11
16
  // Internal seam: the typeId table is the protocol's, not part of the package's public surface.
12
- import { TYPE_IDS } from "../attr/types"
17
+ import { isIndexable, TYPE_IDS } from "../schema/types"
13
18
  import { InvalidPredicateError, UnsupportedOperatorError } from "./errors"
14
19
 
15
20
  /**
16
- * The comparison operators the query language defines. `<` `<=` `>` `>=` need an ordered type —
17
- * `i32`, `u64`, `u256` or `dec`; every type accepts `=` and `!=`.
21
+ * The comparison operators the query language defines. Every indexable type accepts all of them;
22
+ * `<` `<=` `>` `>=` compare in index order — numerically for `i32`, `u64`, `u256` and `dec`,
23
+ * `false < true` for `bool`, and by unsigned bytes for `str`, `bytes32`, `addr` and `key`.
18
24
  */
19
25
  export type ComparisonOperator = "=" | "!=" | "<" | "<=" | ">" | ">="
20
26
 
27
+ /**
28
+ * A value a predicate can compare against: a value of any indexable type, or a bare boolean,
29
+ * number, bigint or string. `bytes` is excluded — it is never indexed, so never filterable.
30
+ */
31
+ export type PredicateValue = IndexableValue | boolean | number | bigint | string
32
+
33
+ /**
34
+ * `between`'s check that both bounds have one type — `unknown` when they do, and otherwise an
35
+ * object whose one property is named after the problem. No value has that property, so the
36
+ * compiler reports the sentence as the property it is missing.
37
+ */
38
+ type SameTypeCheck<L, H> = [TagOf<L>, TagOf<H>] extends [TagOf<H>, TagOf<L>]
39
+ ? unknown
40
+ : { [K in `BETWEEN bounds must have one type, got ${TagOf<L>} and ${TagOf<H>}`]: never }
41
+
42
+ /**
43
+ * The attribute a predicate names, and the type it asserts — `never` when it asserts none, as
44
+ * `EXISTS` does. Carried by an {@link Expression} at the type level only, so a query against a
45
+ * schema can be checked at compile time: every name must be an indexed column, and every type the
46
+ * column's.
47
+ */
48
+ export type AttributeRef = { readonly name: string; readonly type: IndexableTypeTag }
49
+
50
+ declare const refs: unique symbol
51
+
52
+ /** The type-level record of the attributes an expression names. Never present at runtime. */
53
+ type Refs<R extends AttributeRef> = { readonly [refs]?: R }
54
+
21
55
  /** A comparison between an attribute and a typed literal. */
22
- export type ComparisonNode = {
56
+ export interface ComparisonNode<R extends AttributeRef = AttributeRef> extends Refs<R> {
23
57
  readonly kind: "comparison"
24
58
  readonly name: string
25
59
  readonly operator: ComparisonOperator
26
- readonly value: ArkivValue
60
+ readonly value: IndexableValue
61
+ }
62
+
63
+ /** An inclusive range test, `name BETWEEN low AND high`. Both bounds have one type. */
64
+ export interface BetweenNode<R extends AttributeRef = AttributeRef> extends Refs<R> {
65
+ readonly kind: "between"
66
+ readonly name: string
67
+ readonly low: IndexableValue
68
+ readonly high: IndexableValue
27
69
  }
28
70
 
29
71
  /** A `STARTSWITH` prefix test, the language's only pattern operator. */
30
- export type StartsWithNode = {
72
+ export interface StartsWithNode<R extends AttributeRef = AttributeRef> extends Refs<R> {
31
73
  readonly kind: "startsWith"
32
74
  readonly name: string
33
75
  readonly value: StrValue
34
76
  }
35
77
 
36
78
  /** An `EXISTS(name)` test — true when the attribute is set with any type. */
37
- export type ExistsNode = {
79
+ export interface ExistsNode<R extends AttributeRef = AttributeRef> extends Refs<R> {
38
80
  readonly kind: "exists"
39
81
  readonly name: string
40
82
  }
41
83
 
42
84
  /** A `TYPEOF(name) = tag` test. */
43
- export type TypeOfNode = {
85
+ export interface TypeOfNode<R extends AttributeRef = AttributeRef> extends Refs<R> {
44
86
  readonly kind: "typeOf"
45
87
  readonly name: string
46
- readonly tag: UserTypeTag
88
+ readonly tag: IndexableTypeTag
47
89
  }
48
90
 
49
91
  /** The complement of an expression, evaluated against the active-entity set. */
50
- export type NotNode = {
92
+ export interface NotNode<R extends AttributeRef = AttributeRef> extends Refs<R> {
51
93
  readonly kind: "not"
52
94
  readonly expression: Expression
53
95
  }
54
96
 
55
97
  /** A conjunction. */
56
- export type AndNode = {
98
+ export interface AndNode<R extends AttributeRef = AttributeRef> extends Refs<R> {
57
99
  readonly kind: "and"
58
100
  readonly expressions: readonly Expression[]
59
101
  }
60
102
 
61
103
  /** A disjunction. */
62
- export type OrNode = {
104
+ export interface OrNode<R extends AttributeRef = AttributeRef> extends Refs<R> {
63
105
  readonly kind: "or"
64
106
  readonly expressions: readonly Expression[]
65
107
  }
@@ -68,24 +110,38 @@ export type OrNode = {
68
110
  * A query expression: an immutable tree built by the combinators below, whose `toString()` is the
69
111
  * exact query string sent over the wire.
70
112
  *
113
+ * @typeParam R - The attributes the expression names, as {@link AttributeRef}s — inferred by the
114
+ * combinators, and checked against the schema by `select(...).from(schema).where(...)`.
115
+ *
71
116
  * @example
72
117
  * const q = and(gte("level", i32(10)), or(eq("status", str("open")), not(exists("closedAt"))))
73
118
  * String(q) // level >= i32(10) AND (status = str('open') OR NOT EXISTS(closedAt))
74
119
  */
75
- export type Expression =
76
- | ComparisonNode
77
- | StartsWithNode
78
- | ExistsNode
79
- | TypeOfNode
80
- | NotNode
81
- | AndNode
82
- | OrNode
120
+ export type Expression<R extends AttributeRef = AttributeRef> =
121
+ | ComparisonNode<R>
122
+ | BetweenNode<R>
123
+ | StartsWithNode<R>
124
+ | ExistsNode<R>
125
+ | TypeOfNode<R>
126
+ | NotNode<R>
127
+ | AndNode<R>
128
+ | OrNode<R>
129
+
130
+ /** The attributes an expression names — the `R` of an {@link Expression}. */
131
+ export type RefsOf<E> = E extends Refs<infer R> ? Exclude<R, undefined> : never
132
+
133
+ /** The reference a comparison against `V` makes to `N`. */
134
+ type ValueRef<N extends string, V> = {
135
+ readonly name: N
136
+ readonly type: TagOf<V> & IndexableTypeTag
137
+ }
83
138
 
84
139
  /**
85
140
  * The system attributes a query may filter on, and the type each one carries.
86
141
  *
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.
142
+ * Every other system attribute (`$createdAt`, `$updatedAt`, `$creationFlags`) is returned in
143
+ * query results — selected and read back as `createdAt`, `updatedAt` and `creationFlags` — but is
144
+ * not indexed, so it cannot appear in a predicate.
89
145
  */
90
146
  const QUERYABLE_SYSTEM_ATTRIBUTES = {
91
147
  $key: "key",
@@ -96,24 +152,30 @@ const QUERYABLE_SYSTEM_ATTRIBUTES = {
96
152
 
97
153
  type SystemAttributeName = keyof typeof QUERYABLE_SYSTEM_ATTRIBUTES
98
154
 
155
+ /** The references a filter on a system attribute makes — each with its fixed type. */
156
+ export type SystemAttributeRef = {
157
+ [N in SystemAttributeName]: {
158
+ readonly name: N
159
+ readonly type: (typeof QUERYABLE_SYSTEM_ATTRIBUTES)[N]
160
+ }
161
+ }[SystemAttributeName]
162
+
163
+ /**
164
+ * The references a filter through schema `S` may make: each indexed column with its declared
165
+ * type, and the queryable system attributes with theirs. An {@link Expression} naming anything else
166
+ * is a compile error in `select(...).from(S).where(...)`.
167
+ */
168
+ export type SchemaRef<S extends Schema> =
169
+ | {
170
+ [K in IndexedColumnName<S>]: {
171
+ readonly name: K
172
+ readonly type: ColumnsOf<S>[K]["type"] & IndexableTypeTag
173
+ }
174
+ }[IndexedColumnName<S>]
175
+ | SystemAttributeRef
176
+
99
177
  /** 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
- ])
178
+ const RESULT_ONLY_SYSTEM_ATTRIBUTES = ["$createdAt", "$updatedAt", "$creationFlags"]
117
179
 
118
180
  /**
119
181
  * Checks an attribute reference, returning its fixed type when it names a system attribute.
@@ -130,9 +192,11 @@ function resolveReference(name: string): TypeTag | undefined {
130
192
  if (system !== undefined) return system
131
193
 
132
194
  if (RESULT_ONLY_SYSTEM_ATTRIBUTES.includes(name)) {
195
+ // Selected, and read back, as the entity field of the same name without the `$`.
196
+ const field = name.slice(1)
133
197
  throw new InvalidPredicateError(
134
198
  `"${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.",
199
+ `on. Select it as \`${field}\` and filter the results in JavaScript instead.`,
136
200
  name,
137
201
  )
138
202
  }
@@ -145,15 +209,14 @@ function resolveReference(name: string): TypeTag | undefined {
145
209
  }
146
210
 
147
211
  /**
148
- * Whether a string names a type a client may set. `bytes` is excluded: it backs `$payload`, which
149
- * is neither settable nor queryable.
212
+ * Whether a string names a type that can be indexed — every type but `bytes`.
150
213
  *
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.
214
+ * Takes a `string` rather than an `IndexableTypeTag` on purpose — the callers' static types
215
+ * already say as much, and this is the runtime check for the code that has no types.
153
216
  */
154
- function isUserTypeTag(tag: string): tag is UserTypeTag {
217
+ function isIndexableTag(tag: string): tag is IndexableTypeTag {
155
218
  // `Object.hasOwn` rather than `in`, which would also accept every Object.prototype key.
156
- return Object.hasOwn(TYPE_IDS, tag) && tag !== "bytes"
219
+ return Object.hasOwn(TYPE_IDS, tag) && isIndexable(tag as TypeTag)
157
220
  }
158
221
 
159
222
  /** Freezes a node and gives it a (non-enumerable, so still `toEqual`-able) rendering. */
@@ -165,25 +228,39 @@ function node<T extends Expression>(value: T): T {
165
228
  return Object.freeze(value)
166
229
  }
167
230
 
168
- function comparison(name: string, operator: ComparisonOperator, input: ValueInput): ComparisonNode {
169
- const system = resolveReference(name)
231
+ /**
232
+ * Resolves a value a predicate compares `name` against, and checks it can be: not `bytes`, which is
233
+ * never indexed, and of the fixed type when `name` is a system attribute.
234
+ */
235
+ function predicateValue(
236
+ name: string,
237
+ system: TypeTag | undefined,
238
+ input: PredicateValue,
239
+ ): IndexableValue {
170
240
  const value = toValue(input, name)
171
-
172
- if (system !== undefined && value.type !== system) {
241
+ if (value.type === "bytes") {
173
242
  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.`,
243
+ `"${name}" is compared against a bytes value, and bytes are stored but never indexed, so ` +
244
+ "they cannot be filtered on. Index a str, bytes32 or key derived from them instead.",
176
245
  name,
177
246
  )
178
247
  }
179
- if (RANGE_OPERATORS.has(operator) && !ORDERED_TAGS.has(value.type)) {
180
- throw new UnsupportedOperatorError(
248
+ if (system !== undefined && value.type !== system) {
249
+ throw new InvalidPredicateError(
250
+ `"${name}" is a ${system}, but the value is a ${value.type}. ` +
251
+ `System attributes have a fixed type — write ${system}(...) around the value.`,
181
252
  name,
182
- operator,
183
- value.type,
184
- `Only i32, u64, u256 and dec carry an ordered index; ${value.type} is equality-only.`,
185
253
  )
186
254
  }
255
+ return value
256
+ }
257
+
258
+ function comparison<R extends AttributeRef>(
259
+ name: string,
260
+ operator: ComparisonOperator,
261
+ input: PredicateValue,
262
+ ): ComparisonNode<R> {
263
+ const value = predicateValue(name, resolveReference(name), input)
187
264
  return node({ kind: "comparison", name, operator, value })
188
265
  }
189
266
 
@@ -193,15 +270,19 @@ function comparison(name: string, operator: ComparisonOperator, input: ValueInpu
193
270
  * Bare booleans, numbers, bigints and strings take the same default types they take when written
194
271
  * (`number` is an `i32`, `bigint` a `u256`, `string` a `str`); anything else names its type.
195
272
  *
196
- * @throws {InvalidPredicateError} If the name is not a queryable attribute, or a system attribute
197
- * is compared against the wrong type.
273
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
274
+ * @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
275
+ * attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
198
276
  *
199
277
  * @example
200
278
  * eq("status", str("open")) // status = str('open')
201
279
  * eq("flagged", true) // flagged = true
202
280
  * eq("$owner", addr(account)) // $owner = addr(0xd8dA…)
203
281
  */
204
- export function eq(name: string, value: ValueInput): ComparisonNode {
282
+ export function eq<N extends string, V extends PredicateValue>(
283
+ name: N,
284
+ value: V,
285
+ ): ComparisonNode<ValueRef<N, V>> {
205
286
  return comparison(name, "=", value)
206
287
  }
207
288
 
@@ -211,63 +292,122 @@ export function eq(name: string, value: ValueInput): ComparisonNode {
211
292
  * This is typed value-negation, not the complement: an entity that never set the attribute, or set
212
293
  * it with another type, does not match. Use `not(eq(...))` for the full complement.
213
294
  *
295
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
296
+ * @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
297
+ * attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
298
+ *
214
299
  * @example
215
300
  * ne("status", str("closed")) // status != str('closed')
216
301
  * not(eq("status", str("closed"))) // NOT status = str('closed') — also matches entities with no status
217
302
  */
218
- export function ne(name: string, value: ValueInput): ComparisonNode {
303
+ export function ne<N extends string, V extends PredicateValue>(
304
+ name: N,
305
+ value: V,
306
+ ): ComparisonNode<ValueRef<N, V>> {
219
307
  return comparison(name, "!=", value)
220
308
  }
221
309
 
222
310
  /**
223
- * Attribute is greater than value. Ordered types only (`i32`, `u64`, `u256`, `dec`).
311
+ * Attribute is greater than value, in index order — see {@link ComparisonOperator}.
224
312
  *
225
- * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.
313
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
314
+ * @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
315
+ * attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
226
316
  *
227
317
  * @example
228
318
  * gt("level", i32(10)) // level > i32(10)
229
319
  * gt("$expiresAt", u64(1_200_000n)) // $expiresAt > u64(1200000)
230
320
  */
231
- export function gt(name: string, value: ValueInput): ComparisonNode {
321
+ export function gt<N extends string, V extends PredicateValue>(
322
+ name: N,
323
+ value: V,
324
+ ): ComparisonNode<ValueRef<N, V>> {
232
325
  return comparison(name, ">", value)
233
326
  }
234
327
 
235
328
  /**
236
- * Attribute is greater than or equal to value. Ordered types only (`i32`, `u64`, `u256`, `dec`).
329
+ * Attribute is greater than or equal to value, in index order — see {@link ComparisonOperator}.
237
330
  *
238
- * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.
331
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
332
+ * @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
333
+ * attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
239
334
  *
240
335
  * @example
241
336
  * gte("score", dec("3.5")) // score >= dec(3.5)
242
337
  */
243
- export function gte(name: string, value: ValueInput): ComparisonNode {
338
+ export function gte<N extends string, V extends PredicateValue>(
339
+ name: N,
340
+ value: V,
341
+ ): ComparisonNode<ValueRef<N, V>> {
244
342
  return comparison(name, ">=", value)
245
343
  }
246
344
 
247
345
  /**
248
- * Attribute is less than value. Ordered types only (`i32`, `u64`, `u256`, `dec`).
346
+ * Attribute is less than value, in index order — see {@link ComparisonOperator}.
249
347
  *
250
- * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.
348
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
349
+ * @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
350
+ * attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
251
351
  *
252
352
  * @example
253
353
  * lt("level", i32(10)) // level < i32(10)
254
354
  */
255
- export function lt(name: string, value: ValueInput): ComparisonNode {
355
+ export function lt<N extends string, V extends PredicateValue>(
356
+ name: N,
357
+ value: V,
358
+ ): ComparisonNode<ValueRef<N, V>> {
256
359
  return comparison(name, "<", value)
257
360
  }
258
361
 
259
362
  /**
260
- * Attribute is less than or equal to value. Ordered types only (`i32`, `u64`, `u256`, `dec`).
363
+ * Attribute is less than or equal to value, in index order — see {@link ComparisonOperator}.
261
364
  *
262
- * @throws {UnsupportedOperatorError} If the value's type carries no ordered index.
365
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
366
+ * @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
367
+ * attribute, a system attribute is compared against the wrong type, or the value is `bytes`.
263
368
  *
264
369
  * @example
265
370
  * lte("score", dec("5")) // score <= dec(5)
266
371
  */
267
- export function lte(name: string, value: ValueInput): ComparisonNode {
372
+ export function lte<N extends string, V extends PredicateValue>(
373
+ name: N,
374
+ value: V,
375
+ ): ComparisonNode<ValueRef<N, V>> {
268
376
  return comparison(name, "<=", value)
269
377
  }
270
378
 
379
+ /**
380
+ * Attribute lies between two values, **inclusive** at both ends: it matches exactly what
381
+ * `and(gte(name, low), lte(name, high))` matches, in one predicate. Both bounds must have one type,
382
+ * and a `low` above `high` matches nothing.
383
+ *
384
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
385
+ * @throws {InvalidPredicateError} If the bounds have different types, the name starts with `$` but
386
+ * is not a queryable system attribute, a system attribute is compared against the wrong type, or a
387
+ * bound is `bytes`.
388
+ *
389
+ * @example
390
+ * between("score", dec("3.5"), dec("5")) // score BETWEEN dec(3.5) AND dec(5)
391
+ * between("$expiresAt", u64(100n), u64(200n))
392
+ */
393
+ export function between<N extends string, L extends PredicateValue, H extends PredicateValue>(
394
+ name: N,
395
+ low: L,
396
+ high: H & SameTypeCheck<L, H>,
397
+ ): BetweenNode<ValueRef<N, L>> {
398
+ const system = resolveReference(name)
399
+ const lowValue = predicateValue(name, system, low)
400
+ const highValue = predicateValue(name, system, high)
401
+ if (lowValue.type !== highValue.type) {
402
+ throw new InvalidPredicateError(
403
+ `BETWEEN on "${name}" has a ${lowValue.type} and a ${highValue.type} bound. Both bounds ` +
404
+ "must have one type, the type the attribute is compared as.",
405
+ name,
406
+ )
407
+ }
408
+ return node({ kind: "between", name, low: lowValue, high: highValue })
409
+ }
410
+
271
411
  /**
272
412
  * Attribute is a `str` beginning with this prefix.
273
413
  *
@@ -275,14 +415,19 @@ export function lte(name: string, value: ValueInput): ComparisonNode {
275
415
  * `startsWith("name", "é")` matches only the same byte sequence, not another normal form of it.
276
416
  *
277
417
  * @param prefix - The prefix, as a bare string or a `str` value.
278
- * @throws {InvalidPredicateError} If the name is not a queryable attribute.
418
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
419
+ * @throws {InvalidPredicateError} If the name starts with `$` but is not a queryable system
420
+ * attribute.
279
421
  * @throws {UnsupportedOperatorError} If given a value that is not a `str`, or a system attribute
280
422
  * (none of which is a `str`).
281
423
  *
282
424
  * @example
283
425
  * startsWith("desc", "ab") // desc STARTSWITH str('ab')
284
426
  */
285
- export function startsWith(name: string, prefix: string | StrValue): StartsWithNode {
427
+ export function startsWith<N extends string>(
428
+ name: N,
429
+ prefix: string | StrValue,
430
+ ): StartsWithNode<{ readonly name: N; readonly type: "str" }> {
286
431
  const system = resolveReference(name)
287
432
  const value = typeof prefix === "string" ? str(prefix) : toValue(prefix, name)
288
433
  if (value.type !== "str") {
@@ -307,14 +452,17 @@ export function startsWith(name: string, prefix: string | StrValue): StartsWithN
307
452
  /**
308
453
  * The attribute is set, with any type.
309
454
  *
455
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
310
456
  * @throws {InvalidPredicateError} If given a system attribute — every entity has all of them, so
311
- * the test would be a constant.
457
+ * the test would be a constant — or a `$` name that is not one.
312
458
  *
313
459
  * @example
314
460
  * exists("reviewedBy") // EXISTS(reviewedBy)
315
461
  * not(exists("closedAt")) // NOT EXISTS(closedAt)
316
462
  */
317
- export function exists(name: string): ExistsNode {
463
+ export function exists<N extends string>(
464
+ name: N,
465
+ ): ExistsNode<{ readonly name: N; readonly type: never }> {
318
466
  if (resolveReference(name) !== undefined) {
319
467
  throw new InvalidPredicateError(
320
468
  `"${name}" is a system attribute, so it is set on every entity and EXISTS(${name}) is ` +
@@ -332,14 +480,18 @@ export function exists(name: string): ExistsNode {
332
480
  * asserts the type, so this is for asking about the type alone.
333
481
  *
334
482
  * @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).
483
+ * `key` — any type but `bytes`, which is never indexed.
484
+ * @throws {InvalidAttributeNameError} If the name is outside the attribute name grammar.
485
+ * @throws {InvalidPredicateError} If the tag is not an indexable type, the name is a system
486
+ * attribute (whose type is fixed), or the name starts with `$` but is not a system attribute.
338
487
  *
339
488
  * @example
340
489
  * hasType("age", "i32") // TYPEOF(age) = i32
341
490
  */
342
- export function hasType(name: string, tag: UserTypeTag): TypeOfNode {
491
+ export function hasType<N extends string, T extends IndexableTypeTag>(
492
+ name: N,
493
+ tag: T,
494
+ ): TypeOfNode<{ readonly name: N; readonly type: T }> {
343
495
  const system = resolveReference(name)
344
496
  if (system !== undefined) {
345
497
  throw new InvalidPredicateError(
@@ -348,9 +500,9 @@ export function hasType(name: string, tag: UserTypeTag): TypeOfNode {
348
500
  name,
349
501
  )
350
502
  }
351
- if (!isUserTypeTag(tag)) {
503
+ if (!isIndexableTag(tag)) {
352
504
  throw new InvalidPredicateError(
353
- `"${tag}" is not a settable attribute type. Use one of ` +
505
+ `"${tag}" is not an indexable attribute type. Use one of ` +
354
506
  "bool, i32, u64, u256, dec, bytes32, str, addr, key.",
355
507
  name,
356
508
  )
@@ -366,7 +518,7 @@ export function hasType(name: string, tag: UserTypeTag): TypeOfNode {
366
518
  * not(exists("closedAt")) // NOT EXISTS(closedAt)
367
519
  * not(and(eq("a", 1), eq("b", 2))) // NOT (a = i32(1) AND b = i32(2))
368
520
  */
369
- export function not(expression: Expression): NotNode {
521
+ export function not<R extends AttributeRef>(expression: Expression<R>): NotNode<R> {
370
522
  return node({ kind: "not", expression })
371
523
  }
372
524
 
@@ -383,8 +535,12 @@ export function not(expression: Expression): NotNode {
383
535
  * and(gte("level", i32(10)), eq("status", "open"))
384
536
  * and(filters) // an array works too
385
537
  */
386
- export function and(expressions: readonly Expression[]): Expression
387
- export function and(...expressions: Expression[]): Expression
538
+ export function and<const E extends readonly Expression[]>(
539
+ expressions: E,
540
+ ): Expression<RefsOf<E[number]>>
541
+ export function and<const E extends readonly Expression[]>(
542
+ ...expressions: E
543
+ ): Expression<RefsOf<E[number]>>
388
544
  export function and(...expressions: (Expression | readonly Expression[])[]): Expression {
389
545
  return combine("and", expressions.flat())
390
546
  }
@@ -400,8 +556,12 @@ export function and(...expressions: (Expression | readonly Expression[])[]): Exp
400
556
  * @example
401
557
  * or(eq("status", "open"), not(exists("closedAt")))
402
558
  */
403
- export function or(expressions: readonly Expression[]): Expression
404
- export function or(...expressions: Expression[]): Expression
559
+ export function or<const E extends readonly Expression[]>(
560
+ expressions: E,
561
+ ): Expression<RefsOf<E[number]>>
562
+ export function or<const E extends readonly Expression[]>(
563
+ ...expressions: E
564
+ ): Expression<RefsOf<E[number]>>
405
565
  export function or(...expressions: (Expression | readonly Expression[])[]): Expression {
406
566
  return combine("or", expressions.flat())
407
567
  }
@@ -410,7 +570,9 @@ function combine(kind: "and" | "or", expressions: Expression[]): Expression {
410
570
  if (expressions.length === 0) {
411
571
  throw new InvalidPredicateError(
412
572
  `${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.`,
573
+ `has no query-language spelling — leave the ${kind}() out. A query still needs at least one ` +
574
+ "filter, so to match broadly narrow by owner or creator, or read through a named schema, " +
575
+ "whose tag counts as one.",
414
576
  )
415
577
  }
416
578
  // A one-element conjunction *is* its element; keeping the wrapper would only add parentheses.
@@ -423,6 +585,69 @@ function combine(kind: "and" | "or", expressions: Expression[]): Expression {
423
585
  )
424
586
  }
425
587
 
588
+ /**
589
+ * Checks every attribute an expression names against a schema — the runtime half of the check
590
+ * {@link SchemaRef} makes at compile time, for the callers the type system does not reach.
591
+ *
592
+ * @throws {InvalidPredicateError} If a predicate names something that is not a column of the
593
+ * schema, a column that is not indexed, or compares a column against a value of another type.
594
+ */
595
+ export function checkAgainstSchema(expression: Expression, schema: Schema): void {
596
+ switch (expression.kind) {
597
+ case "not":
598
+ checkAgainstSchema(expression.expression, schema)
599
+ return
600
+ case "and":
601
+ case "or":
602
+ for (const sub of expression.expressions) checkAgainstSchema(sub, schema)
603
+ return
604
+ }
605
+
606
+ const { name } = expression
607
+ // System attributes were checked against their fixed types when the predicate was built.
608
+ if (name.startsWith("$")) return
609
+
610
+ const column = lookupColumn(schema, name)
611
+ if (column === undefined) {
612
+ throw new InvalidPredicateError(
613
+ `"${name}" is not a column of the schema, so a predicate on it can only match entities ` +
614
+ `written some other way. Its columns are ${describeColumns(schema)}.`,
615
+ name,
616
+ )
617
+ }
618
+ if (!column.isIndexed) {
619
+ if (column.type === "bytes") {
620
+ throw new InvalidPredicateError(
621
+ `"${name}" is a bytes column, and bytes are stored but never indexed, so it cannot be ` +
622
+ "filtered on. Index a str, bytes32 or key derived from it instead, or select it and " +
623
+ "filter the results in JavaScript.",
624
+ name,
625
+ )
626
+ }
627
+ throw new InvalidPredicateError(
628
+ `"${name}" is not indexed, and only indexed attributes can be filtered on. Declare it ` +
629
+ `${column.type}().indexed() — entities written before that are indexed by their next ` +
630
+ "patch of it — or select it and filter the results in JavaScript.",
631
+ name,
632
+ )
633
+ }
634
+ const asserted =
635
+ expression.kind === "comparison" || expression.kind === "startsWith"
636
+ ? expression.value.type
637
+ : expression.kind === "between"
638
+ ? expression.low.type
639
+ : expression.kind === "typeOf"
640
+ ? expression.tag
641
+ : undefined
642
+ if (asserted !== undefined && asserted !== column.type) {
643
+ throw new InvalidPredicateError(
644
+ `"${name}" is a ${column.type} column, but the predicate asserts a ${asserted}, so it could ` +
645
+ `never match. Write ${column.type}(...) around the value.`,
646
+ name,
647
+ )
648
+ }
649
+ }
650
+
426
651
  /**
427
652
  * Binding strength, tightest last — `NOT` binds tighter than `AND`, which binds tighter than `OR`.
428
653
  * A leaf never needs parentheses, so it sits above them all.
@@ -459,6 +684,10 @@ export function render(expression: Expression): string {
459
684
  switch (expression.kind) {
460
685
  case "comparison":
461
686
  return `${expression.name} ${expression.operator} ${renderLiteral(expression.value)}`
687
+ case "between":
688
+ // A leaf, never parenthesised: the AND inside BETWEEN belongs to it, so
689
+ // `a BETWEEN 1 AND 5 AND b = 2` already reads as `(a BETWEEN 1 AND 5) AND b = 2`.
690
+ return `${expression.name} BETWEEN ${renderLiteral(expression.low)} AND ${renderLiteral(expression.high)}`
462
691
  case "startsWith":
463
692
  return `${expression.name} STARTSWITH ${renderLiteral(expression.value)}`
464
693
  case "exists":
@@ -475,7 +704,7 @@ export function render(expression: Expression): string {
475
704
  }
476
705
 
477
706
  /** Renders a typed value as its query literal. `bool` is the one type written bare. */
478
- function renderLiteral(value: ArkivValue): string {
707
+ function renderLiteral(value: IndexableValue): string {
479
708
  switch (value.type) {
480
709
  case "bool":
481
710
  return value.value ? "true" : "false"