@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.
- package/README.md +29 -16
- package/dist/attr/index.cjs +34 -0
- package/dist/attr/index.d.cts +2 -0
- package/dist/attr/index.d.ts +2 -0
- package/dist/attr/index.js +3 -0
- package/dist/attr-BDZ-APg5.js +809 -0
- package/dist/attr-BDZ-APg5.js.map +1 -0
- package/dist/attr-BvxHN99W.cjs +1029 -0
- package/dist/chains/index.cjs +2 -3
- package/dist/chains/index.d.cts +22 -60
- package/dist/chains/index.d.cts.map +1 -1
- package/dist/chains/index.d.ts +22 -60
- package/dist/chains/index.d.ts.map +1 -1
- package/dist/chains/index.js +2 -2
- package/dist/chains-BJrZs6sG.js +57 -0
- package/dist/chains-BJrZs6sG.js.map +1 -0
- package/dist/chains-BYRTTipa.cjs +73 -0
- package/dist/changeOwnership-B7bEgla4.d.cts +509 -0
- package/dist/changeOwnership-B7bEgla4.d.cts.map +1 -0
- package/dist/changeOwnership-DwgtdSFc.d.ts +509 -0
- package/dist/changeOwnership-DwgtdSFc.d.ts.map +1 -0
- package/dist/entity/index.cjs +19 -0
- package/dist/entity/index.d.cts +4 -0
- package/dist/entity/index.d.ts +4 -0
- package/dist/entity/index.js +6 -0
- package/dist/entity-B0t6sN7F.cjs +71 -0
- package/dist/entity-BIp_df9u.js +67 -0
- package/dist/entity-BIp_df9u.js.map +1 -0
- package/dist/entity-CFhbAv0R.js +70 -0
- package/dist/entity-CFhbAv0R.js.map +1 -0
- package/dist/entity-wfBnZd91.cjs +98 -0
- package/dist/errors-D-eJkGVc.cjs +56 -0
- package/dist/errors-DGB8AgT2.js +33 -0
- package/dist/errors-DGB8AgT2.js.map +1 -0
- package/dist/expiry-BUYIXTGb.cjs +135 -0
- package/dist/expiry-BxOlvVWC.d.ts +79 -0
- package/dist/expiry-BxOlvVWC.d.ts.map +1 -0
- package/dist/expiry-DGRzrSan.js +95 -0
- package/dist/expiry-DGRzrSan.js.map +1 -0
- package/dist/expiry-DRV29F1r.d.cts +79 -0
- package/dist/expiry-DRV29F1r.d.cts.map +1 -0
- package/dist/flags-0Vl5rrHq.d.cts +41 -0
- package/dist/flags-0Vl5rrHq.d.cts.map +1 -0
- package/dist/flags-Ct8k0gpB.cjs +43 -0
- package/dist/flags-ZtlvOIIk.js +33 -0
- package/dist/flags-ZtlvOIIk.js.map +1 -0
- package/dist/flags-wmcMoAnN.d.ts +41 -0
- package/dist/flags-wmcMoAnN.d.ts.map +1 -0
- package/dist/index-Cgw1u5aY.d.cts +278 -0
- package/dist/index-Cgw1u5aY.d.cts.map +1 -0
- package/dist/index-ClgIuxJ8.d.ts +159 -0
- package/dist/index-ClgIuxJ8.d.ts.map +1 -0
- package/dist/index-D0CmtiWT.d.cts +159 -0
- package/dist/index-D0CmtiWT.d.cts.map +1 -0
- package/dist/index-D8LZt-3J.d.cts +417 -0
- package/dist/index-D8LZt-3J.d.cts.map +1 -0
- package/dist/index-DVYFaXNV.d.ts +417 -0
- package/dist/index-DVYFaXNV.d.ts.map +1 -0
- package/dist/index-VCqgn29D.d.ts +278 -0
- package/dist/index-VCqgn29D.d.ts.map +1 -0
- package/dist/index.cjs +676 -375
- package/dist/index.d.cts +247 -208
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +247 -208
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +626 -368
- package/dist/index.js.map +1 -1
- package/dist/payload-Bnw1hrvb.cjs +83 -0
- package/dist/payload-CIxSn_DW.js +67 -0
- package/dist/payload-CIxSn_DW.js.map +1 -0
- package/dist/payload-Dgx3PQeK.d.ts +94 -0
- package/dist/payload-Dgx3PQeK.d.ts.map +1 -0
- package/dist/payload-P0nWyjLg.d.cts +94 -0
- package/dist/payload-P0nWyjLg.d.cts.map +1 -0
- package/dist/query/index.cjs +24 -150
- package/dist/query/index.d.cts +5 -3
- package/dist/query/index.d.ts +5 -3
- package/dist/query/index.js +6 -138
- package/dist/queryBuilder-BXDvX1K_.cjs +1123 -0
- package/dist/queryBuilder-CbLL4pG5.js +963 -0
- package/dist/queryBuilder-CbLL4pG5.js.map +1 -0
- package/dist/selection-BvK6yX0g.d.cts +433 -0
- package/dist/selection-BvK6yX0g.d.cts.map +1 -0
- package/dist/selection-fpmI3ADQ.d.ts +433 -0
- package/dist/selection-fpmI3ADQ.d.ts.map +1 -0
- package/dist/types/index.cjs +3 -3
- package/dist/types/index.d.cts +7 -2
- package/dist/types/index.d.ts +7 -2
- package/dist/types/index.js +3 -3
- package/dist/utils/index.cjs +4 -17
- package/dist/utils/index.d.cts +3 -17
- package/dist/utils/index.d.ts +3 -17
- package/dist/utils/index.js +4 -18
- package/package.json +26 -5
- package/src/actions/public/getEntity.ts +27 -43
- package/src/actions/public/getEntityNonce.ts +30 -0
- package/src/actions/public/predictEntityKeys.test.ts +143 -0
- package/src/actions/public/predictEntityKeys.ts +138 -0
- package/src/actions/public/query.ts +33 -69
- package/src/actions/public/watchEntityEvents.test.ts +414 -0
- package/src/actions/public/watchEntityEvents.ts +225 -0
- package/src/actions/wallet/changeOwnership.ts +2 -2
- package/src/actions/wallet/createEntity.ts +99 -24
- package/src/actions/wallet/extendEntity.ts +37 -13
- package/src/actions/wallet/mutateEntities.test.ts +76 -0
- package/src/actions/wallet/mutateEntities.ts +19 -21
- package/src/actions/wallet/patchEntity.ts +90 -0
- package/src/attr/attributes.test.ts +120 -0
- package/src/attr/attributes.ts +190 -0
- package/src/attr/codec.test.ts +162 -0
- package/src/attr/codec.ts +220 -0
- package/src/attr/decimal.ts +86 -0
- package/src/attr/errors.ts +145 -0
- package/src/attr/index.ts +50 -0
- package/src/attr/names.ts +106 -0
- package/src/attr/types.ts +150 -0
- package/src/attr/values.test.ts +271 -0
- package/src/attr/values.ts +502 -0
- package/src/chains/cheesecake.ts +22 -0
- package/src/chains/index.ts +1 -2
- package/src/clients/createPublicClient.test.ts +3 -5
- package/src/clients/createPublicClient.ts +5 -4
- package/src/clients/createWalletClient.test.ts +2 -2
- package/src/clients/createWalletClient.ts +13 -7
- package/src/clients/decorators/arkivPublic.ts +160 -139
- package/src/clients/decorators/arkivWallet.ts +86 -94
- package/src/entity/create.test.ts +184 -0
- package/src/entity/errors.ts +38 -0
- package/src/entity/events.ts +16 -0
- package/src/entity/expiry.test.ts +195 -0
- package/src/entity/expiry.ts +181 -0
- package/src/entity/flags.ts +65 -0
- package/src/entity/index.ts +14 -0
- package/src/entity/key.ts +68 -0
- package/src/entity/operations.ts +134 -0
- package/src/entity/params.ts +15 -0
- package/src/errors.ts +26 -49
- package/src/index.ts +6 -0
- package/src/query/engine.ts +59 -139
- package/src/query/errors.ts +151 -0
- package/src/query/expression.test.ts +286 -0
- package/src/query/expression.ts +493 -0
- package/src/query/index.ts +40 -4
- package/src/query/injection.test.ts +151 -0
- package/src/query/queryBuilder.test.ts +322 -637
- package/src/query/queryBuilder.ts +126 -359
- package/src/query/queryResult.ts +54 -51
- package/src/query/selection.test.ts +48 -98
- package/src/query/selection.ts +77 -65
- package/src/types/entity.test.ts +44 -155
- package/src/types/entity.ts +60 -52
- package/src/types/events.ts +59 -16
- package/src/types/index.ts +20 -17
- package/src/types/rpcSchema.ts +69 -37
- package/src/utils/arkivTransactions.test.ts +600 -155
- package/src/utils/arkivTransactions.ts +181 -190
- package/src/utils/chains.test.ts +21 -24
- package/src/utils/compression.ts +1 -1
- package/src/utils/entities.test.ts +62 -0
- package/src/utils/entities.ts +109 -37
- package/src/utils/expirationTime.ts +114 -10
- package/src/utils/logger.ts +2 -2
- package/src/utils/revert.test.ts +99 -0
- package/src/utils/revert.ts +145 -0
- package/dist/chains-BifaY04V.cjs +0 -108
- package/dist/chains-CrtkVbBh.js +0 -86
- package/dist/chains-CrtkVbBh.js.map +0 -1
- package/dist/consts-Bz0LZQZY.js +0 -7
- package/dist/consts-Bz0LZQZY.js.map +0 -1
- package/dist/consts-CxT1-RXU.cjs +0 -18
- package/dist/entity-BhSXaB23.cjs +0 -81
- package/dist/entity-CN41IOrq.d.cts +0 -401
- package/dist/entity-CN41IOrq.d.cts.map +0 -1
- package/dist/entity-DGfVyB-a.js +0 -71
- package/dist/entity-DGfVyB-a.js.map +0 -1
- package/dist/entity-NnEVlePN.d.ts +0 -401
- package/dist/entity-NnEVlePN.d.ts.map +0 -1
- package/dist/index-ClkpS6s_.d.ts +0 -481
- package/dist/index-ClkpS6s_.d.ts.map +0 -1
- package/dist/index-w4qop0Pe.d.cts +0 -481
- package/dist/index-w4qop0Pe.d.cts.map +0 -1
- package/dist/payload-BpNUPOnY.js +0 -13
- package/dist/payload-BpNUPOnY.js.map +0 -1
- package/dist/payload-C2KHOUoj.d.ts +0 -6
- package/dist/payload-C2KHOUoj.d.ts.map +0 -1
- package/dist/payload-CJN8fTvX.d.cts +0 -6
- package/dist/payload-CJN8fTvX.d.cts.map +0 -1
- package/dist/payload-D8YLKcJj.cjs +0 -23
- package/dist/query/index.js.map +0 -1
- package/dist/queryBuilder-BE_XhLLa.js +0 -735
- package/dist/queryBuilder-BE_XhLLa.js.map +0 -1
- package/dist/queryBuilder-J37buGCi.cjs +0 -865
- package/dist/utils/index.d.cts.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/src/actions/public/buildQuery.ts +0 -10
- package/src/actions/public/subscribeEntityEvents.ts +0 -93
- package/src/actions/wallet/updateEntity.ts +0 -59
- package/src/chains/braga.ts +0 -26
- package/src/chains/kaolin.ts +0 -29
- package/src/query/engine.test.ts +0 -571
- package/src/query/predicate.test.ts +0 -461
- package/src/query/predicate.ts +0 -144
- package/src/query/queryResult.test.ts +0 -270
- package/src/query/selectQueryBuilder.test.ts +0 -213
- package/src/types/attributes.ts +0 -28
- package/src/utils/validation.test.ts +0 -106
- package/src/utils/validation.ts +0 -80
- /package/dist/{types-Ci4BRClw.js → types-Bx1EKe0v.js} +0 -0
- /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
|
+
}
|
package/src/query/index.ts
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
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
|
+
})
|